diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 1cfa351..7f64691 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -2,9 +2,9 @@ name: Build and Test
on:
push:
- branches: [ master ]
+ branches: [ master, develop ]
pull_request:
- branches: [ master ]
+ branches: [ master, develop ]
workflow_dispatch:
jobs:
diff --git a/ExCodable.podspec b/ExCodable.podspec
index 070e872..336bd9c 100644
--- a/ExCodable.podspec
+++ b/ExCodable.podspec
@@ -3,14 +3,14 @@ Pod::Spec.new do |s|
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
s.name = "ExCodable"
# export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)
- s.version = ENV["LIB_VERSION"] || "0.5.0"
+ s.version = ENV["LIB_VERSION"] || "1.0.0-alpha"
s.summary = "Key-Mapping Extensions for Swift Codable"
# s.description = "Key-Mapping Extensions for Swift Codable."
s.homepage = "https://github.com/iwill/ExCodable"
# ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
s.license = "MIT"
- s.author = { "Mr. Ming" => "i+ExCodable@iwill.im" }
+ s.author = { "Mr. Míng" => "i+ExCodable@iwill.im" }
s.social_media_url = "https://iwill.im/about/"
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
diff --git a/LICENSE b/LICENSE
index f66b212..d4a2b48 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2021 Mr. Ming
+Copyright (c) 2022 Mr. Míng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index fdb81b7..699e31f 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,12 @@
[](https://swift.org/)
[](https://swift.org/package-manager/)
[](#readme)
+
[](https://github.com/iwill/ExCodable/actions/workflows/build-and-test.yml)
[](https://github.com/iwill/ExCodable/releases)
[](https://github.com/iwill/ExCodable/actions/workflows/deploy_to_cocoapods.yml)
[](https://cocoapods.org/pods/ExCodable)
+
[](https://github.com/iwill/ExCodable/blob/master/LICENSE)
[](https://twitter.com/minglq)
@@ -17,6 +19,7 @@ En | [中文](https://iwill.im/ExCodable/)
- [Features](#features)
- [Usage](#usage)
- [Requirements](#requirements)
+- [Migration Guides](#migration-guides)
- [Installation](#installation)
- [Credits](#credits)
- [License](#license)
@@ -24,7 +27,7 @@ En | [中文](https://iwill.im/ExCodable/)
## Features
- Extends Swift `Codable` - `Encodable & Decodable`;
-- Supports Key-Mapping via `KeyPath` and Coding-Key:
+- Supports Key-Mapping via Property-Wrapper `ExCodable` + `String`:
- `ExCodable` did not read/write memory via unsafe pointers;
- No need to encode/decode properties one by one;
- Just requires using `var` to declare properties and provide default values;
@@ -57,7 +60,7 @@ struct TestAutoCodable: Codable, Equatable {
```
-But, if you have to encode/decode manually for some reason, e.g. Alternative-Keys and Nested-Keys ...
+But, if you have to encode/decode manually for some reason, e.g. Default-Value, Alternative-Keys, Nested-Keys or Type-Conversions ...
```swift
struct TestManualCodable: Equatable {
@@ -414,7 +417,7 @@ pod 'ExCodable', '~> 0.5.0'
- John Sundell ([@JohnSundell](https://github.com/JohnSundell)) and the ideas from his [Codextended](https://github.com/JohnSundell/Codextended)
- ibireme ([@ibireme](https://github.com/ibireme)) and the features from his [YYModel](https://github.com/ibireme/YYModel)
-- Mr. Ming ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im
+- Mr. Míng ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im
## License
diff --git a/Sources/ExCodable/DecodingContainer+AnyCollection.swift b/Sources/ExCodable/DecodingContainer+AnyCollection.swift
new file mode 100644
index 0000000..1be33d6
--- /dev/null
+++ b/Sources/ExCodable/DecodingContainer+AnyCollection.swift
@@ -0,0 +1,151 @@
+//
+// DecodingContainer+AnyCollection.swift
+// AnyDecodable
+//
+// Created by levantAJ on 1/18/19.
+// Copyright © 2019 levantAJ. All rights reserved.
+//
+// https://github.com/levantAJ/AnyCodable
+import Foundation
+
+struct AnyCodingKey: CodingKey {
+ var stringValue: String
+ var intValue: Int?
+
+ init?(stringValue: String) {
+ self.stringValue = stringValue
+ }
+
+ init?(intValue: Int) {
+ self.intValue = intValue
+ self.stringValue = String(intValue)
+ }
+}
+
+extension KeyedDecodingContainer {
+ /// Decodes a value of the given type for the given key.
+ ///
+ /// - parameter type: The type of value to decode.
+ /// - parameter key: The key that the decoded value is associated with.
+ /// - returns: A value of the requested type, if present for the given key
+ /// and convertible to the requested type.
+ /// - throws: `DecodingError.typeMismatch` if the encountered encoded value
+ /// is not convertible to the requested type.
+ /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry
+ /// for the given key.
+ /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for
+ /// the given key.
+ public func decode(_ type: [Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [Any] {
+ var values = try nestedUnkeyedContainer(forKey: key)
+ return try values.decode(type)
+ }
+
+ /// Decodes a value of the given type for the given key.
+ ///
+ /// - parameter type: The type of value to decode.
+ /// - parameter key: The key that the decoded value is associated with.
+ /// - returns: A value of the requested type, if present for the given key
+ /// and convertible to the requested type.
+ /// - throws: `DecodingError.typeMismatch` if the encountered encoded value
+ /// is not convertible to the requested type.
+ /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry
+ /// for the given key.
+ /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for
+ /// the given key.
+ public func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [String: Any] {
+ let values = try nestedContainer(keyedBy: AnyCodingKey.self, forKey: key)
+ return try values.decode(type)
+ }
+
+ /// Decodes a value of the given type for the given key, if present.
+ ///
+ /// This method returns `nil` if the container does not have a value
+ /// associated with `key`, or if the value is null. The difference between
+ /// these states can be distinguished with a `contains(_:)` call.
+ ///
+ /// - parameter type: The type of value to decode.
+ /// - parameter key: The key that the decoded value is associated with.
+ /// - returns: A decoded value of the requested type, or `nil` if the
+ /// `Decoder` does not have an entry associated with the given key, or if
+ /// the value is a null value.
+ /// - throws: `DecodingError.typeMismatch` if the encountered encoded value
+ /// is not convertible to the requested type.
+ public func decodeIfPresent(_ type: [Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [Any]? {
+ guard contains(key),
+ try decodeNil(forKey: key) == false else { return nil }
+ return try decode(type, forKey: key)
+ }
+
+ /// Decodes a value of the given type for the given key, if present.
+ ///
+ /// This method returns `nil` if the container does not have a value
+ /// associated with `key`, or if the value is null. The difference between
+ /// these states can be distinguished with a `contains(_:)` call.
+ ///
+ /// - parameter type: The type of value to decode.
+ /// - parameter key: The key that the decoded value is associated with.
+ /// - returns: A decoded value of the requested type, or `nil` if the
+ /// `Decoder` does not have an entry associated with the given key, or if
+ /// the value is a null value.
+ /// - throws: `DecodingError.typeMismatch` if the encountered encoded value
+ /// is not convertible to the requested type.
+ public func decodeIfPresent(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [String: Any]? {
+ guard contains(key),
+ try decodeNil(forKey: key) == false else { return nil }
+ return try decode(type, forKey: key)
+ }
+}
+
+private extension KeyedDecodingContainer {
+ func decode(_ type: [String: Any].Type) throws -> [String: Any] {
+ var dictionary: [String: Any] = [:]
+ for key in allKeys {
+ if try decodeNil(forKey: key) {
+ dictionary[key.stringValue] = NSNull()
+ } else if let bool = try? decode(Bool.self, forKey: key) {
+ dictionary[key.stringValue] = bool
+ } else if let string = try? decode(String.self, forKey: key) {
+ dictionary[key.stringValue] = string
+ } else if let int = try? decode(Int.self, forKey: key) {
+ dictionary[key.stringValue] = int
+ } else if let double = try? decode(Double.self, forKey: key) {
+ dictionary[key.stringValue] = double
+ } else if let dict = try? decode([String: Any].self, forKey: key) {
+ dictionary[key.stringValue] = dict
+ } else if let array = try? decode([Any].self, forKey: key) {
+ dictionary[key.stringValue] = array
+ }
+ }
+ return dictionary
+ }
+}
+
+private extension UnkeyedDecodingContainer {
+ mutating func decode(_ type: [Any].Type) throws -> [Any] {
+ var elements: [Any] = []
+ while !isAtEnd {
+ if try decodeNil() {
+ elements.append(NSNull())
+ } else if let int = try? decode(Int.self) {
+ elements.append(int)
+ } else if let bool = try? decode(Bool.self) {
+ elements.append(bool)
+ } else if let double = try? decode(Double.self) {
+ elements.append(double)
+ } else if let string = try? decode(String.self) {
+ elements.append(string)
+ } else if let values = try? nestedContainer(keyedBy: AnyCodingKey.self),
+ let element = try? values.decode([String: Any].self) {
+ elements.append(element)
+ } else if var values = try? nestedUnkeyedContainer(),
+ let element = try? values.decode([Any].self) {
+ elements.append(element)
+ }
+ }
+ return elements
+ }
+ mutating func decode(_ type: Dictionary.Type) throws -> Dictionary {
+ let nestedContainer = try self.nestedContainer(keyedBy: AnyCodingKey.self)
+ return try nestedContainer.decode(type)
+ }
+}
diff --git a/Sources/ExCodable/EncodingContainer+AnyCollection.swift b/Sources/ExCodable/EncodingContainer+AnyCollection.swift
new file mode 100644
index 0000000..faf1e01
--- /dev/null
+++ b/Sources/ExCodable/EncodingContainer+AnyCollection.swift
@@ -0,0 +1,131 @@
+//
+// EncodingContainer+AnyCollection.swift
+// AnyDecodable
+//
+// Created by ShopBack on 1/19/19.
+// Copyright © 2019 levantAJ. All rights reserved.
+//
+// https://github.com/levantAJ/AnyCodable
+import Foundation
+
+extension KeyedEncodingContainer {
+ /// Encodes the given value for the given key.
+ ///
+ /// - parameter value: The value to encode.
+ /// - parameter key: The key to associate the value with.
+ /// - throws: `EncodingError.invalidValue` if the given value is invalid in
+ /// the current context for this format.
+ public mutating func encode(_ value: [String: Any], forKey key: KeyedEncodingContainer.Key) throws {
+ var container = nestedContainer(keyedBy: AnyCodingKey.self, forKey: key)
+ try container.encode(value)
+ }
+
+ /// Encodes the given value for the given key.
+ ///
+ /// - parameter value: The value to encode.
+ /// - parameter key: The key to associate the value with.
+ /// - throws: `EncodingError.invalidValue` if the given value is invalid in
+ /// the current context for this format.
+ public mutating func encode(_ value: [Any], forKey key: KeyedEncodingContainer.Key) throws {
+ var container = nestedUnkeyedContainer(forKey: key)
+ try container.encode(value)
+ }
+
+ /// Encodes the given value for the given key if it is not `nil`.
+ ///
+ /// - parameter value: The value to encode.
+ /// - parameter key: The key to associate the value with.
+ /// - throws: `EncodingError.invalidValue` if the given value is invalid in
+ /// the current context for this format.
+ public mutating func encodeIfPresent(_ value: [String: Any]?, forKey key: KeyedEncodingContainer.Key) throws {
+ if let value = value {
+ var container = nestedContainer(keyedBy: AnyCodingKey.self, forKey: key)
+ try container.encode(value)
+ } else {
+ try encodeNil(forKey: key)
+ }
+ }
+
+ /// Encodes the given value for the given key if it is not `nil`.
+ ///
+ /// - parameter value: The value to encode.
+ /// - parameter key: The key to associate the value with.
+ /// - throws: `EncodingError.invalidValue` if the given value is invalid in
+ /// the current context for this format.
+ public mutating func encodeIfPresent(_ value: [Any]?, forKey key: KeyedEncodingContainer.Key) throws {
+ if let value = value {
+ var container = nestedUnkeyedContainer(forKey: key)
+ try container.encode(value)
+ } else {
+ try encodeNil(forKey: key)
+ }
+ }
+}
+
+private extension KeyedEncodingContainer where K == AnyCodingKey {
+ mutating func encode(_ value: [String: Any]) throws {
+ for (k, v) in value {
+ let key = AnyCodingKey(stringValue: k)!
+ switch v {
+ case is NSNull:
+ try encodeNil(forKey: key)
+ case let string as String:
+ try encode(string, forKey: key)
+ case let int as Int:
+ try encode(int, forKey: key)
+ case let bool as Bool:
+ try encode(bool, forKey: key)
+ case let double as Double:
+ try encode(double, forKey: key)
+ case let dict as [String: Any]:
+ try encode(dict, forKey: key)
+ case let array as [Any]:
+ try encode(array, forKey: key)
+ default:
+ debugPrint("⚠️ Unsuported type!", v)
+ continue
+ }
+ }
+ }
+}
+
+private extension UnkeyedEncodingContainer {
+ /// Encodes the given value.
+ ///
+ /// - parameter value: The value to encode.
+ /// - throws: `EncodingError.invalidValue` if the given value is invalid in
+ /// the current context for this format.
+ mutating func encode(_ value: [Any]) throws {
+ for v in value {
+ switch v {
+ case is NSNull:
+ try encodeNil()
+ case let string as String:
+ try encode(string)
+ case let int as Int:
+ try encode(int)
+ case let bool as Bool:
+ try encode(bool)
+ case let double as Double:
+ try encode(double)
+ case let dict as [String: Any]:
+ try encode(dict)
+ case let array as [Any]:
+ var values = nestedUnkeyedContainer()
+ try values.encode(array)
+ default:
+ debugPrint("⚠️ Unsuported type!", v)
+ }
+ }
+ }
+
+ /// Encodes the given value.
+ ///
+ /// - parameter value: The value to encode.
+ /// - throws: `EncodingError.invalidValue` if the given value is invalid in
+ /// the current context for this format.
+ mutating func encode(_ value: [String: Any]) throws {
+ var container = self.nestedContainer(keyedBy: AnyCodingKey.self)
+ try container.encode(value)
+ }
+}
diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift
new file mode 100644
index 0000000..4cd2657
--- /dev/null
+++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift
@@ -0,0 +1,125 @@
+//
+// ExCodable.swift
+// ExCodable
+//
+// Created by Mr. Míng on 2021-07-01.
+// Copyright (c) 2022 Mr. Míng . Released under the MIT license.
+//
+
+import Foundation
+
+// MARK: - keyMapping
+
+@available(*, deprecated, message: "use `@ExCodable` property wrapper instead")
+public protocol ExCodableProtocol: Codable {
+ associatedtype Root = Self where Root: ExCodableProtocol
+ static var keyMapping: [KeyMap] { get }
+}
+
+@available(*, deprecated)
+public extension ExCodableProtocol where Root == Self {
+ // default implementation of ExCodableProtocol
+ static var keyMapping: [KeyMap] { [] }
+ // default implementation of Encodable
+ func encode(to encoder: Encoder) throws {
+ try encode(to: encoder, with: Self.keyMapping)
+ try encode(to: encoder, nonnull: false, throws: false)
+ }
+ func decode(from decoder: Decoder) throws {
+ try decode(from: decoder, nonnull: false, throws: false)
+ }
+}
+
+@available(*, deprecated)
+public extension ExCodableProtocol {
+ func encode(to encoder: Encoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws {
+ try keyMapping.forEach { try $0.encode(self, encoder, nonnull, `throws`) }
+ }
+ mutating func decode(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws {
+ try keyMapping.forEach { try $0.decode?(&self, decoder, nonnull, `throws`) }
+ }
+ func decodeReference(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws {
+ try keyMapping.forEach { try $0.decodeReference?(self, decoder, nonnull, `throws`) }
+ }
+}
+
+@available(*, deprecated, message: "use `@ExCodable` property wrapper instead")
+public final class KeyMap {
+ fileprivate let encode: (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void
+ fileprivate let decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?
+ fileprivate let decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?
+ private init(encode: @escaping (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void,
+ decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?,
+ decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?) {
+ (self.encode, self.decode, self.decodeReference) = (encode, decode, decodeReference)
+ }
+}
+
+@available(*, deprecated)
+public extension KeyMap {
+ convenience init(_ keyPath: WritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) {
+ self.init(encode: { root, encoder, nonnullAll, throwsAll in
+ try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
+ }, decode: { root, decoder, nonnullAll, throwsAll in
+ if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
+ root[keyPath: keyPath] = value
+ }
+ }, decodeReference: nil)
+ }
+ convenience init(_ keyPath: WritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) {
+ self.init(encode: { root, encoder, nonnullAll, throwsAll in
+ try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
+ }, decode: { root, decoder, nonnullAll, throwsAll in
+ if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
+ root[keyPath: keyPath] = value
+ }
+ }, decodeReference: nil)
+ }
+ convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) {
+ self.init(encode: { root, encoder, nonnullAll, throwsAll in
+ try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
+ }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in
+ if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
+ root[keyPath: keyPath] = value
+ }
+ })
+ }
+ convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) {
+ self.init(encode: { root, encoder, nonnullAll, throwsAll in
+ try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
+ }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in
+ if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
+ root[keyPath: keyPath] = value
+ }
+ })
+ }
+}
+
+// MARK: -
+
+@available(*, deprecated)
+public extension ExCodableProtocol {
+ @available(*, deprecated, renamed: "encode(to:with:nonnull:throws:)")
+ func encode(with keyMapping: [KeyMap], using encoder: Encoder) {
+ try? encode(to: encoder, with: keyMapping)
+ }
+ @available(*, deprecated, renamed: "decode(from:with:nonnull:throws:)")
+ mutating func decode(with keyMapping: [KeyMap], using decoder: Decoder) {
+ try? decode(from: decoder, with: keyMapping)
+ }
+ @available(*, deprecated, renamed: "decodeReference(from:with:nonnull:throws:)")
+ func decodeReference(with keyMapping: [KeyMap], using decoder: Decoder) {
+ try? decodeReference(from: decoder, with: keyMapping)
+ }
+}
+
+@available(*, deprecated, renamed: "append(decodingTypeConverter:)")
+public protocol KeyedDecodingContainerCustomTypeConversion: ExCodableDecodingTypeConverter {
+ func decodeForTypeConversion(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T?
+}
+@available(*, deprecated)
+public extension KeyedDecodingContainerCustomTypeConversion {
+ func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? {
+ return decodeForTypeConversion(container, codingKey: codingKey, as: type)
+ }
+}
diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift
index 9e873eb..f4c63f9 100644
--- a/Sources/ExCodable/ExCodable.swift
+++ b/Sources/ExCodable/ExCodable.swift
@@ -2,8 +2,8 @@
// ExCodable.swift
// ExCodable
//
-// Created by Mr. Ming on 2021-02-10.
-// Copyright (c) 2021 Mr. Ming . Released under the MIT license.
+// Created by Mr. Míng on 2021-02-10.
+// Copyright (c) 2022 Mr. Míng . Released under the MIT license.
//
import Foundation
@@ -11,89 +11,177 @@ import Foundation
/**
* # ExCodable
*
- * A protocol extends `Encodable` & `Decodable` with `keyMapping`
+ * - `ExCodable`: A property-wrapper for mapping properties to JSON keys.
+ * - `ExAutoEncodable` & `ExAutoDecodable`: Protocols with default implementation for Encodable & Decodable.
+ * - `ExAutoCodable`: A typealias for `ExAutoEncodable & ExAutoDecodable`.
+ * - `Encodable` & `Decodable` extensions for encode/decode-ing from internal/external.
+ * - `Encoder` & `Encoder` extensions for encode/decode-ing properties one by one.
+ * - Supports Alternative-Keys, Nested-Keys, Type-Conversions and Default-Values.
+ *
* <#swift#> <#codable#> <#json#> <#model#> <#type-inference#>
- * <#key-mapping#> <#keypath#> <#codingkey#> <#subscript#>
- * <#alternative-keys#> <#nested-keys#> <#type-conversion#>
+ * <#key-mapping#> <#property-wrapper#> <#coding-key#> <#subscript#>
+ * <#alternative-keys#> <#nested-keys#> <#type-conversions#>
*
- * - seealso: [Usage](https://github.com/iwill/ExCodable#usage) from GitGub
- * - seealso: `ExCodableTests.swift` form the source code
+ * - seealso: [Usage](https://github.com/iwill/ExCodable#usage) from the `README.md`
+ * - seealso: `ExCodableTests.swift` from the `Tests`
+ * - seealso: [Decoding and overriding](https://www.swiftbysundell.com/articles/property-wrappers-in-swift/#decoding-and-overriding) and [Useful Codable extensions](https://www.swiftbysundell.com/tips/useful-codable-extensions/), by John Sundell.
*/
-public protocol ExCodable: Codable {
- associatedtype Root = Self where Root: ExCodable
- static var keyMapping: [KeyMap] { get }
-}
-public extension ExCodable {
- func encode(to encoder: Encoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws {
- try keyMapping.forEach { try $0.encode(self, encoder, nonnull, `throws`) }
+@propertyWrapper
+public final class ExCodable {
+ fileprivate let stringKeys: [String]?
+ fileprivate let nonnull, `throws`: Bool?
+ fileprivate let encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)?
+ public var wrappedValue: Value
+ private init(wrappedValue: Value, stringKeys: [String]? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)?) {
+ (self.wrappedValue, self.stringKeys, self.nonnull, self.throws, self.encode, self.decode) = (wrappedValue, stringKeys, nonnull, `throws`, encode, decode)
}
- mutating func decode(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws {
- try keyMapping.forEach { try $0.decode?(&self, decoder, nonnull, `throws`) }
+ public convenience init(wrappedValue: Value, _ stringKey: String? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) {
+ self.init(wrappedValue: wrappedValue, stringKeys: stringKey.map { [$0] }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode)
}
- func decodeReference(from decoder: Decoder, with keyMapping: [KeyMap], nonnull: Bool = false, throws: Bool = false) throws {
- try keyMapping.forEach { try $0.decodeReference?(self, decoder, nonnull, `throws`) }
+ public convenience init(wrappedValue: Value, _ stringKeys: String..., nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) {
+ self.init(wrappedValue: wrappedValue, stringKeys: stringKeys, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode)
+ }
+ public convenience init(wrappedValue: Value, _ codingKeys: CodingKey..., nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) {
+ self.init(wrappedValue: wrappedValue, stringKeys: codingKeys.map { $0.stringValue }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode)
}
}
-public extension ExCodable where Root == Self {
- func encode(to encoder: Encoder) throws {
- try encode(to: encoder, with: Self.keyMapping)
+extension ExCodable: Equatable where Value: Equatable {
+ public static func == (lhs: ExCodable, rhs: ExCodable) -> Bool {
+ return lhs.wrappedValue == rhs.wrappedValue
}
}
+extension ExCodable: CustomStringConvertible { // CustomDebugStringConvertible
+ public var description: String { String(describing: wrappedValue) }
+ // public var debugDescription: String { "\(type(of: self))(\(wrappedValue))" }
+}
-// MARK: -
+fileprivate protocol EncodablePropertyWrapper {
+ func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws
+}
+extension ExCodable: EncodablePropertyWrapper where Value: Encodable {
+ fileprivate func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws {
+ if encode != nil { try encode!(encoder, wrappedValue) }
+ else { try encoder.encode(wrappedValue, for: stringKeys?.first ?? String(label), nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`) }
+ }
+}
-public final class KeyMap {
- fileprivate let encode: (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void
- fileprivate let decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?
- fileprivate let decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?
- private init(encode: @escaping (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void,
- decode: ((_ root: inout Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?,
- decodeReference: ((_ root: Root, _ decoder: Decoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void)?) {
- (self.encode, self.decode, self.decodeReference) = (encode, decode, decodeReference)
+fileprivate protocol DecodablePropertyWrapper {
+ func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws
+}
+extension ExCodable: DecodablePropertyWrapper where Value: Decodable {
+ fileprivate func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws {
+ if let value = decode != nil
+ ? try decode!(decoder)
+ : try decoder.decode(stringKeys ?? [String(label)], nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`) {
+ wrappedValue = value
+ }
}
}
-public extension KeyMap {
- convenience init(_ keyPath: WritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) {
- self.init(encode: { root, encoder, nonnullAll, throwsAll in
- try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
- }, decode: { root, decoder, nonnullAll, throwsAll in
- if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
- root[keyPath: keyPath] = value
+// Make `Any` support Codable, like: [String: Any], [Any]
+fileprivate protocol EncodableAnyPropertyWrapper {
+ func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws
+}
+extension ExCodable: EncodableAnyPropertyWrapper {
+ fileprivate func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws {
+ if encode != nil { try encode!(encoder, wrappedValue) }
+ else {
+ let t = type(of: wrappedValue)
+ if let key = AnyCodingKey(stringValue: String(label)) {
+ if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
+ var container = try encoder.container(keyedBy: AnyCodingKey.self)
+ try container.encodeIfPresent(wrappedValue as? [String: Any], forKey: key)
+ } else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
+ var container = try encoder.container(keyedBy: AnyCodingKey.self)
+ try container.encodeIfPresent(wrappedValue as? [Any], forKey: key)
+ }
}
- }, decodeReference: nil)
- }
- convenience init(_ keyPath: WritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) {
- self.init(encode: { root, encoder, nonnullAll, throwsAll in
- try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
- }, decode: { root, decoder, nonnullAll, throwsAll in
- if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
- root[keyPath: keyPath] = value
+ }
+ }
+}
+fileprivate protocol DecodableAnyPropertyWrapper {
+ func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws
+}
+extension ExCodable: DecodableAnyPropertyWrapper {
+ fileprivate func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws {
+ if let decode = decode {
+ if let value = try decode(decoder) {
+ wrappedValue = value
}
- }, decodeReference: nil)
- }
- convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: String ..., nonnull: Bool? = nil, throws: Bool? = nil) {
- self.init(encode: { root, encoder, nonnullAll, throwsAll in
- try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
- }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in
- if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
- root[keyPath: keyPath] = value
+ } else {
+ let t = type(of: wrappedValue)
+ if let key = AnyCodingKey(stringValue: String(label)) {
+ if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
+ let container = try decoder.container(keyedBy: AnyCodingKey.self)
+ if let value = try container.decodeIfPresent([String: Any].self, forKey: key) as? Value {
+ wrappedValue = value
+ }
+ } else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
+ let container = try decoder.container(keyedBy: AnyCodingKey.self)
+ if let value = try container.decodeIfPresent([Any].self, forKey: key) as? Value {
+ wrappedValue = value
+ }
+ }
}
- })
- }
- convenience init(ref keyPath: ReferenceWritableKeyPath, to codingKeys: Key ..., nonnull: Bool? = nil, throws: Bool? = nil) {
- self.init(encode: { root, encoder, nonnullAll, throwsAll in
- try encoder.encode(root[keyPath: keyPath], for: codingKeys.first!, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll)
- }, decode: nil, decodeReference: { root, decoder, nonnullAll, throwsAll in
- if let value: Value = try decoder.decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) {
- root[keyPath: keyPath] = value
+ }
+ }
+}
+
+// MARK: - Auto implementation for Encodable & Decodable
+
+public protocol ExAutoEncodable: Encodable {}
+public extension ExAutoEncodable {
+ func encode(to encoder: Encoder) throws {
+ try encode(to: encoder, nonnull: false, throws: false)
+ }
+}
+
+public protocol ExAutoDecodable: Decodable { init() }
+public extension ExAutoDecodable {
+ init(from decoder: Decoder) throws {
+ self.init()
+ try decode(from: decoder, nonnull: false, throws: false)
+ }
+}
+
+public typealias ExAutoCodable = ExAutoEncodable & ExAutoDecodable
+
+// MARK: - Encodable & Decodable - internal
+
+public extension Encodable {
+ func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws {
+ var mirror: Mirror! = Mirror(reflecting: self)
+ while mirror != nil {
+ for child in mirror.children where child.label != nil {
+ if let wrapper = (child.value as? EncodablePropertyWrapper) {
+ try wrapper.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
+ } else {
+ try (child.value as? EncodableAnyPropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
+ }
+ }
+ mirror = mirror.superclassMirror
+ }
+ }
+}
+
+public extension Decodable {
+ func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws {
+ var mirror: Mirror! = Mirror(reflecting: self)
+ while mirror != nil {
+ for child in mirror.children where child.label != nil {
+ if let wrapper = (child.value as? DecodablePropertyWrapper) {
+ try wrapper.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
+ } else {
+ try (child.value as? DecodableAnyPropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
+ }
}
- })
+ mirror = mirror.superclassMirror
+ }
}
}
-// MARK: - subscript
+// MARK: - Encoder & Decoder
public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false
subscript(stringKey: String) -> T? { get { return nil }
@@ -119,8 +207,6 @@ public extension Decoder { // , abortIfNull nonnull: Bool = false, abortOnError
}
}
-// MARK: -
-
public extension Encoder {
func encodeNonnullThrows(_ value: T, for stringKey: String) throws {
@@ -132,7 +218,7 @@ public extension Encoder {
func encode(_ value: T?, for stringKey: String) {
try? encode(value, for: stringKey, nonnull: false, throws: false)
}
- fileprivate func encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws {
+ internal/* fileprivate */ func encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws {
let dot: Character = "."
guard stringKey.contains(dot), stringKey.count > 1 else {
@@ -163,7 +249,7 @@ public extension Encoder {
func encode(_ value: T?, for codingKey: K) {
try? encode(value, for: codingKey, nonnull: false, throws: false)
}
- fileprivate func encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws {
+ internal/* fileprivate */ func encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws {
var container = self.container(keyedBy: K.self)
do {
if nonnull { try container.encode(value, forKey: codingKey) }
@@ -193,7 +279,7 @@ public extension Decoder {
func decode(_ stringKeys: [String], as type: T.Type = T.self) -> T? {
return try? decode(stringKeys, as: type, nonnull: false, throws: false)
}
- fileprivate func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? {
+ internal/* fileprivate */ func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? {
return try decode(stringKeys.map { ExCodingKey($0) }, as: type, nonnull: nonnull, throws: `throws`)
}
@@ -215,7 +301,7 @@ public extension Decoder {
func decode(_ codingKeys: [K], as type: T.Type = T.self) -> T? {
return try? decode(codingKeys, as: type, nonnull: false, throws: false)
}
- fileprivate func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? {
+ internal/* fileprivate */ func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? {
do {
let container = try self.container(keyedBy: K.self)
return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`)
@@ -225,18 +311,21 @@ public extension Decoder {
}
}
-private struct ExCodingKey: CodingKey {
- let stringValue: String, intValue: Int?
- init(_ stringValue: String) { (self.stringValue, self.intValue) = (stringValue, nil) }
- init(_ stringValue: Substring) { self.init(String(stringValue)) }
- init?(stringValue: String) { self.init(stringValue) }
- init(_ intValue: Int) { (self.intValue, self.stringValue) = (intValue, String(intValue)) }
- init?(intValue: Int) { self.init(intValue) }
+// MARK: - ExCodingKey
+
+private struct ExCodingKey {
+ public let stringValue: String, intValue: Int?
+ init(_ stringValue: S) { (self.stringValue, self.intValue) = (stringValue as? String ?? String(stringValue), nil) }
+}
+
+extension ExCodingKey: CodingKey {
+ public init?(stringValue: String) { self.init(stringValue) }
+ public init?(intValue: Int) { self.init(intValue) }
}
-// MARK: - alternative-keys + nested-keys + type-conversion
+// MARK: - KeyedDecodingContainer - alternative-keys + nested-keys + type-conversions
-private extension KeyedDecodingContainer {
+fileprivate extension KeyedDecodingContainer {
func decodeForAlternativeKeys(_ codingKeys: [Self.Key], as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? {
@@ -395,13 +484,13 @@ private extension KeyedDecodingContainer {
else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return String(describing: double) as? T } // include Float
}
- for conversion in _decodingTypeConverters {
- if let value = try? conversion.decode(self, codingKey: codingKey, as: type) {
+ for converter in _decodingTypeConverters {
+ if let value = try? converter.decode(self, codingKey: codingKey, as: type) {
return value
}
}
- if let custom = self as? ExCodableDecodingTypeConverter,
- let value = try? custom.decode(self, codingKey: codingKey, as: type) {
+ if let customConverter = self as? ExCodableDecodingTypeConverter,
+ let value = try? customConverter.decode(self, codingKey: codingKey, as: type) {
return value
}
@@ -413,13 +502,12 @@ public protocol ExCodableDecodingTypeConverter {
func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T?
}
-private var _decodingTypeConverters: [ExCodableDecodingTypeConverter] = []
+fileprivate var _decodingTypeConverters: [ExCodableDecodingTypeConverter] = []
public func register(_ decodingTypeConverter: ExCodableDecodingTypeConverter) {
_decodingTypeConverters.append(decodingTypeConverter)
}
-// MARK: - Encodable/Decodable
-// - seealso: [Codextended](https://github.com/JohnSundell/Codextended)
+// MARK: - Encodable & Decodable - external
// Encodable.encode() -> Data?
public extension Encodable {
@@ -494,35 +582,7 @@ public protocol DataDecoder {
extension JSONEncoder: DataEncoder {}
extension JSONDecoder: DataDecoder {}
-#if canImport(ObjectiveC) || swift(>=5.1)
+@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension PropertyListEncoder: DataEncoder {}
+@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension PropertyListDecoder: DataDecoder {}
-#endif
-
-// MARK: - #### DEPRECATED ####
-
-public extension ExCodable {
- @available(*, deprecated, renamed: "encode(to:with:nonnull:throws:)")
- func encode(with keyMapping: [KeyMap], using encoder: Encoder) {
- try? encode(to: encoder, with: keyMapping)
- }
- @available(*, deprecated, renamed: "decode(from:with:nonnull:throws:)")
- mutating func decode(with keyMapping: [KeyMap], using decoder: Decoder) {
- try? decode(from: decoder, with: keyMapping)
- }
- @available(*, deprecated, renamed: "decodeReference(from:with:nonnull:throws:)")
- func decodeReference(with keyMapping: [KeyMap], using decoder: Decoder) {
- try? decodeReference(from: decoder, with: keyMapping)
- }
-}
-
-@available(*, deprecated, renamed: "append(decodingTypeConverter:)")
-public protocol KeyedDecodingContainerCustomTypeConversion: ExCodableDecodingTypeConverter {
- func decodeForTypeConversion(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T?
-}
-@available(*, deprecated)
-public extension KeyedDecodingContainerCustomTypeConversion {
- func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? {
- return decodeForTypeConversion(container, codingKey: codingKey, as: type)
- }
-}
diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift
index ce7976d..d9ee993 100644
--- a/Tests/ExCodableTests/ExCodableTests.swift
+++ b/Tests/ExCodableTests/ExCodableTests.swift
@@ -2,8 +2,8 @@
// ExCodableTests.swift
// ExCodable
//
-// Created by Mr. Ming on 2021-02-10.
-// Copyright (c) 2021 Mr. Ming . Released under the MIT license.
+// Created by Mr. Míng on 2021-02-10.
+// Copyright (c) 2022 Mr. Míng . Released under the MIT license.
//
import XCTest
@@ -59,93 +59,52 @@ extension TestManualCodable: Codable {
// MARK: struct
struct TestStruct: Equatable {
+ @ExCodable("int")
private(set) var int: Int = 0
- private(set) var string: String?
+ @ExCodable("string")
+ private(set) var string: String? = nil
var bool: Bool!
}
-
-extension TestStruct: ExCodable {
-
- static let keyMapping: [KeyMap] = [
- KeyMap(\.int, to: "int"),
- KeyMap(\.string, to: "string"),
- ]
-
- init(from decoder: Decoder) throws {
- try decode(from: decoder, with: Self.keyMapping)
- }
- // `encode` with default implementation can be omitted
- // func encode(to encoder: Encoder) throws {
- // try encode(to: encoder, with: Self.keyMapping)
- // }
-}
+extension TestStruct: ExAutoCodable {}
// MARK: alternative-keys & alternative-keyMapping
struct TestAlternativeKeys: Equatable {
+ @ExCodable("int", "i")
var int: Int = 0
- var string: String!
-}
-
-extension TestAlternativeKeys: ExCodable {
-
- static let keyMapping: [KeyMap] = [
- KeyMap(\.int, to: "int", "i"),
- KeyMap(\.string, to: "string", "str", "s")
- ]
-
- static let keyMappingFromLocal: [KeyMap] = [
- KeyMap(\.int, to: "INT"),
- KeyMap(\.string, to: "STRING")
- ]
-
- enum LocalKeys: String, CodingKey {
- case isLocal = "_IS_LOCAL_"
- }
-
- init(from decoder: Decoder) throws {
- let isLocal = decoder[LocalKeys.isLocal] ?? false
- try decode(from: decoder, with: isLocal ? Self.keyMappingFromLocal : Self.keyMapping)
- }
- func encode(to encoder: Encoder) throws {
- try encode(to: encoder, with: Self.keyMappingFromLocal)
- encoder[LocalKeys.isLocal] = true
- }
+ @ExCodable("string", "str", "s")
+ var string: String! = nil
}
+extension TestAlternativeKeys: ExAutoCodable {}
// MARK: nested-keys
struct TestNestedKeys: Equatable {
+ @ExCodable
var int: Int = 0
- var string: String!
-}
-
-extension TestNestedKeys: ExCodable {
-
- static let keyMapping: [KeyMap] = [
- KeyMap(\.int, to: "int"),
- KeyMap(\.string, to: "nested.string")
- ]
-
- init(from decoder: Decoder) throws {
- try decode(from: decoder, with: Self.keyMapping)
- }
- // func encode(to encoder: Encoder) throws {
- // try encode(to: encoder, with: Self.keyMapping)
- // }
+ @ExCodable("nested.nested.string")
+ var string: String! = nil
}
+extension TestNestedKeys: ExAutoCodable {}
// MARK: custom encode/decode
struct TestCustomEncodeDecode: Equatable {
+
+ @ExCodable(Keys.int)
var int: Int = 0
+
var string: String?
+
+ @ExCodable(encode: { encoder, value in encoder[Keys.bool] = value },
+ decode: { decoder in return decoder[Keys.bool] })
+ var bool: Bool = false
}
-extension TestCustomEncodeDecode: ExCodable {
+extension TestCustomEncodeDecode: Codable {
private enum Keys: CodingKey {
- case int, string
+ case int, string, bool
}
private static let dddd = "dddd"
private func string(for int: Int) -> String {
@@ -162,19 +121,15 @@ extension TestCustomEncodeDecode: ExCodable {
}
}
- static let keyMapping: [KeyMap] = [
- KeyMap(\.int, to: Keys.int),
- ]
-
init(from decoder: Decoder) throws {
- try decode(from: decoder, with: Self.keyMapping)
+ try decode(from: decoder, nonnull: false, throws: false)
string = decoder[Keys.string]
if string == nil || string == Self.dddd {
string = string(for: int)
}
}
func encode(to encoder: Encoder) throws {
- try encode(to: encoder, with: Self.keyMapping)
+ try encode(to: encoder, nonnull: false, throws: false)
encoder[Keys.string] = Self.dddd
}
}
@@ -309,44 +264,29 @@ struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter {
}
struct TestCustomTypeConverter: Equatable {
+ @ExCodable("doubleFromBool")
var doubleFromBool: Double? = nil
}
-
-extension TestCustomTypeConverter: ExCodable {
-
- static let keyMapping: [KeyMap] = [
- KeyMap(\.doubleFromBool, to: "doubleFromBool")
- ]
-
- init(from decoder: Decoder) throws {
- try decode(from: decoder, with: Self.keyMapping)
- }
- // func encode(to encoder: Encoder) throws {
- // try encode(to: encoder, with: Self.keyMapping)
- // }
-}
+extension TestCustomTypeConverter: ExAutoCodable {}
// MARK: class
-class TestClass: ExCodable, Equatable {
+class TestClass: Codable, Equatable {
+ @ExCodable("int")
var int: Int = 0
+ @ExCodable("string")
var string: String? = nil
init(int: Int, string: String?) {
(self.int, self.string) = (int, string)
}
- static let keyMapping: [KeyMap] = [
- KeyMap(ref: \.int, to: "int"),
- KeyMap(ref: \.string, to: "string")
- ]
-
required init(from decoder: Decoder) throws {
- try decodeReference(from: decoder, with: Self.keyMapping)
+ try decode(from: decoder, nonnull: false, throws: false)
+ }
+ func encode(to encoder: Encoder) throws {
+ try encode(to: encoder, nonnull: false, throws: false)
}
- // func encode(to encoder: Encoder) throws {
- // try encode(to: encoder, with: Self.keyMapping)
- // }
static func == (lhs: TestClass, rhs: TestClass) -> Bool {
return lhs.int == rhs.int && lhs.string == rhs.string
@@ -356,23 +296,16 @@ class TestClass: ExCodable, Equatable {
// MARK: subclass
class TestSubclass: TestClass {
+
+ @ExCodable("bool")
var bool: Bool = false
required init(int: Int, string: String, bool: Bool) {
self.bool = bool
super.init(int: int, string: string)
}
- static let keyMappingForTestSubclass: [KeyMap] = [
- KeyMap(ref: \.bool, to: "bool")
- ]
-
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
- try decodeReference(from: decoder, with: Self.keyMappingForTestSubclass)
- }
- override func encode(to encoder: Encoder) throws {
- try super.encode(to: encoder)
- try encode(to: encoder, with: Self.keyMappingForTestSubclass)
}
static func == (lhs: TestSubclass, rhs: TestSubclass) -> Bool {
@@ -385,24 +318,12 @@ class TestSubclass: TestClass {
// MARK: ExCodable
struct TestExCodable: Equatable {
+ @ExCodable("int")
private(set) var int: Int = 0
- private(set) var string: String?
-}
-
-extension TestExCodable: ExCodable {
-
- static let keyMapping: [KeyMap] = [
- KeyMap(\.int, to: "int"),
- KeyMap(\.string, to: "string")
- ]
-
- init(from decoder: Decoder) throws {
- try decode(from: decoder, with: Self.keyMapping)
- }
- // func encode(to encoder: Encoder) throws {
- // try encode(to: encoder, with: Self.keyMapping)
- // }
+ @ExCodable("string")
+ private(set) var string: String? = nil
}
+extension TestExCodable: ExAutoCodable {}
// MARK: - Tests
@@ -412,7 +333,7 @@ final class ExCodableTests: XCTestCase {
let test = TestAutoCodable(int: 100, string: "Continue")
if let data = try? test.encoded() as Data,
let copy = try? data.decoded() as TestAutoCodable,
- let json: [String: Any] = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
XCTAssertEqual(copy, test)
XCTAssertEqual(NSDictionary(dictionary: json), ["i": 100, "s": "Continue"])
}
@@ -454,11 +375,10 @@ final class ExCodableTests: XCTestCase {
let copy = try? data.decoded() as TestAlternativeKeys {
XCTAssertEqual(test, TestAlternativeKeys(int: 403, string: "Forbidden"))
XCTAssertEqual(copy, test)
- let localJSON: [String: Any] = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
+ let localJSON = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
XCTAssertEqual(NSDictionary(dictionary: localJSON), [
- "_IS_LOCAL_": true,
- "INT": 403,
- "STRING": "Forbidden"
+ "int": 403,
+ "string": "Forbidden"
])
}
else {
@@ -471,12 +391,14 @@ final class ExCodableTests: XCTestCase {
if let data = try? test.encoded() as Data,
let copy = try? data.decoded() as TestNestedKeys {
XCTAssertEqual(copy, test)
- let json: [String: Any] = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
+ let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
debugPrint(json)
XCTAssertEqual(NSDictionary(dictionary: json), [
"int": 404,
"nested": [
- "string": "Not Found"
+ "nested": [
+ "string": "Not Found"
+ ]
]
])
}
@@ -486,15 +408,16 @@ final class ExCodableTests: XCTestCase {
}
func testCustomEncodeDecode() {
- let test = TestCustomEncodeDecode(int: 418, string: "I'm a teapot")
+ let test = TestCustomEncodeDecode(int: 418, string: "I'm a teapot", bool: true)
if let data = try? test.encoded() as Data,
let copy = try? data.decoded() as TestCustomEncodeDecode {
XCTAssertEqual(copy, test)
- let json: [String: Any] = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
+ let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
debugPrint(json)
XCTAssertEqual(NSDictionary(dictionary: json), [
"int": 418,
- "string": "dddd"
+ "string": "dddd",
+ "bool": true
])
}
else {