From c4975003b76564c14b36b96f10177f6571fb3bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mr=2E=20M=C3=ADng?= Date: Fri, 17 Feb 2023 14:49:59 +0800 Subject: [PATCH 01/43] Star Chart --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fdb81b7..9eff24d 100644 --- a/README.md +++ b/README.md @@ -410,12 +410,20 @@ pod 'ExCodable', '~> 0.5.0' ``` +## Like it? + +Hope you like this project, don't forget to give it a star [⭐](https://github.com/iwill/ExCodable#repository-container-header) + +[![Stargazers over time](https://starchart.cc/iwill/ExCodable.svg)](https://starchart.cc/iwill/ExCodable) + + ## Credits - 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 **ExCodable** is released under the MIT license. See [LICENSE](./LICENSE) for details. + From 83428211ee27a36c11c39c8d44275436d6560bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mr=2E=20M=C3=ADng?= Date: Fri, 17 Feb 2023 14:51:51 +0800 Subject: [PATCH 02/43] Star Chart --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9eff24d..6890377 100644 --- a/README.md +++ b/README.md @@ -414,7 +414,9 @@ pod 'ExCodable', '~> 0.5.0' Hope you like this project, don't forget to give it a star [⭐](https://github.com/iwill/ExCodable#repository-container-header) -[![Stargazers over time](https://starchart.cc/iwill/ExCodable.svg)](https://starchart.cc/iwill/ExCodable) + + Star Chart + ## Credits From 70d1a0fdc4c8dd3c8eaf91b90f3908d1021783c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mr=2E=20M=C3=ADng?= Date: Fri, 17 Feb 2023 14:56:49 +0800 Subject: [PATCH 03/43] Connect with me --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6890377..fe2145d 100644 --- a/README.md +++ b/README.md @@ -418,14 +418,15 @@ Hope you like this project, don't forget to give it a star [⭐](https://github. Star Chart - -## Credits +## Thanks to - 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) + +## Connect with me + - Mr. Míng ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im ## License **ExCodable** is released under the MIT license. See [LICENSE](./LICENSE) for details. - From 584d191565eeddb397b2d00895c99e6d37cea12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Wed, 25 Oct 2023 23:47:10 +0800 Subject: [PATCH 04/43] fix: warning in Swift 5.9 or earlier --- Sources/ExCodable/ExCodable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 9e873eb..8ef6c24 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -288,7 +288,7 @@ private extension KeyedDecodingContainer { private func nestedContainer(with keys: [ExCodingKey]) -> Self? { var container: Self? = self for key in keys { - container = try? container?.nestedContainer(keyedBy: Self.Key, forKey: key as! Self.Key) + container = try? container?.nestedContainer(keyedBy: Self.Key.self, forKey: key as! Self.Key) if container == nil { return nil } } return container From e5e053aa4abc6dcf65e210c0556384e9a8002b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Thu, 26 Oct 2023 00:02:24 +0800 Subject: [PATCH 05/43] opt: unit test --- Tests/ExCodableTests/ExCodableTests.swift | 50 ++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index ce7976d..333f83f 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -422,12 +422,13 @@ final class ExCodableTests: XCTestCase { } func testManualCodable() { - let json = Data(#"{"i":200,"nested":{"string":"OK"}}"#.utf8) + let json = Data(#"{"int":200,"nested":{"string":"OK"}}"#.utf8) if let test = try? json.decoded() as TestManualCodable, let data = try? test.encoded() as Data, let copy = try? data.decoded() as TestManualCodable { XCTAssertEqual(copy, test) - XCTAssertEqual(data, Data(#"{"int":200,"nested":{"string":"OK"}}"#.utf8)) + let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + XCTAssertEqual(NSDictionary(dictionary: json), ["int":200,"nested":["string":"OK"]]) } else { XCTFail() @@ -454,7 +455,7 @@ 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, @@ -471,7 +472,7 @@ 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, @@ -490,7 +491,7 @@ final class ExCodableTests: XCTestCase { 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, @@ -710,6 +711,44 @@ final class ExCodableTests: XCTestCase { } } + func testElapsed() { + let start = DispatchTime.now().uptimeNanoseconds + + for _ in 0..<1_0000 { + let test = TestStruct(int: 304, string: "Not Modified") + if let data = try? test.encoded() as Data, + let copy = try? TestStruct.decoded(from: data) { + XCTAssertEqual(copy, test) + } + else { + XCTFail() + } + + let test2 = TestClass(int: 502, string: "Bad Gateway") + if let data = try? test2.encoded() as Data, + let copy = try? data.decoded() as TestClass { + XCTAssertEqual(copy, test2) + } + else { + XCTFail() + } + + let test3 = TestSubclass(int: 504, string: "Gateway Timeout", bool: true) + if let data = try? test3.encoded() as Data, + let copy = try? data.decoded() as TestSubclass { + XCTAssertEqual(copy, test3) + } + else { + XCTFail() + } + } + + let elapsed = DispatchTime.now().uptimeNanoseconds - start + // let seconds = elapsed / 1_000_000_000 + let milliseconds = elapsed / 1_000_000 + print("elapsed: \(milliseconds) ms") + } + static var allTests = [ ("testAutoCodable", testAutoCodable), ("testManualCodable", testManualCodable), @@ -723,5 +762,6 @@ final class ExCodableTests: XCTestCase { ("testClass", testClass), ("testSubclass", testSubclass), ("testExCodable", testExCodable), + ("testElapsed", testElapsed) ] } From 68494130e8f68babc46af2af3aae7a742b1aaf4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Thu, 26 Oct 2023 00:47:15 +0800 Subject: [PATCH 06/43] meta: version 0.6.0 --- ExCodable.podspec | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index 070e872..b595796 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -3,7 +3,7 @@ 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"] || "0.6.0" s.summary = "Key-Mapping Extensions for Swift Codable" # s.description = "Key-Mapping Extensions for Swift Codable." s.homepage = "https://github.com/iwill/ExCodable" diff --git a/README.md b/README.md index fe2145d..0154806 100644 --- a/README.md +++ b/README.md @@ -374,14 +374,14 @@ XCTAssertEqual(copy2, test) - [Swift Package Manager](https://swift.org/package-manager/): ```swift -.package(url: "https://github.com/iwill/ExCodable", from: "0.5.0") +.package(url: "https://github.com/iwill/ExCodable", from: "0.6.0") ``` - [CocoaPods](http://cocoapods.org): ```ruby -pod 'ExCodable', '~> 0.5.0' +pod 'ExCodable', '~> 0.6.0' ``` From 628e79f2fc1e3d5ab90eb7c20ccee387283fe600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Wed, 24 Jan 2024 11:22:35 +0800 Subject: [PATCH 07/43] Update build-and-test.yml update Swift version to 5.9 --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1cfa351..bd71cb3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -11,7 +11,7 @@ jobs: build: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build run: swift build -v - name: Run tests From 1c3676160824448c74c0f5971b8553991aecce20 Mon Sep 17 00:00:00 2001 From: iwill Date: Thu, 1 Jul 2021 11:30:58 +0800 Subject: [PATCH 08/43] sty: comments --- Sources/ExCodable/ExCodable+DEPRECATED.swift | 35 +++++++++++ Sources/ExCodable/ExCodable.swift | 61 +++++++------------- 2 files changed, 56 insertions(+), 40 deletions(-) create mode 100644 Sources/ExCodable/ExCodable+DEPRECATED.swift diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift new file mode 100644 index 0000000..6666bb6 --- /dev/null +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -0,0 +1,35 @@ +// +// ExCodable.swift +// ExCodable +// +// Created by Mr. Ming on 2021-07-01. +// Copyright (c) 2021 Mr. Ming . Released under the MIT license. +// + +import Foundation + +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/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 8ef6c24..e5f69ad 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -24,7 +24,18 @@ public protocol ExCodable: Codable { static var keyMapping: [KeyMap] { get } } +// default implementation for Encodable +public extension ExCodable where Root == Self { + func encode(to encoder: Encoder) throws { + try encode(to: encoder, with: Self.keyMapping) + } +} + +// MARK: - keyMapping + +// encode/decode public extension ExCodable { + static var keyMapping: [KeyMap] { [] } // default implementation for optional property 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`) } } @@ -35,13 +46,6 @@ public extension ExCodable { try keyMapping.forEach { try $0.decodeReference?(self, decoder, nonnull, `throws`) } } } -public extension ExCodable where Root == Self { - func encode(to encoder: Encoder) throws { - try encode(to: encoder, with: Self.keyMapping) - } -} - -// MARK: - public final class KeyMap { fileprivate let encode: (_ root: Root, _ encoder: Encoder, _ nonnullAll: Bool, _ throwsAll: Bool) throws -> Void @@ -119,7 +123,7 @@ public extension Decoder { // , abortIfNull nonnull: Bool = false, abortOnError } } -// MARK: - +// MARK: - Encoder&Decoder public extension Encoder { @@ -225,13 +229,18 @@ public extension Decoder { } } -private struct ExCodingKey: CodingKey { - let stringValue: String, intValue: Int? +// MARK: - ExCodingKey + +private struct ExCodingKey { + public 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) } +} + +extension ExCodingKey: CodingKey { + public init?(stringValue: String) { self.init(stringValue) } + public init?(intValue: Int) { self.init(intValue) } } // MARK: - alternative-keys + nested-keys + type-conversion @@ -498,31 +507,3 @@ extension JSONDecoder: DataDecoder {} extension PropertyListEncoder: DataEncoder {} 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) - } -} From 64bf846cd9490435e0a03690447a87f03c31bbee Mon Sep 17 00:00:00 2001 From: iwill Date: Fri, 2 Jul 2021 00:01:15 +0800 Subject: [PATCH 09/43] !!!: 1.x, keyMapping -> propertyWrapper --- ExCodable.podspec | 2 +- Sources/ExCodable/ExCodable+DEPRECATED.swift | 95 ++++++++++- Sources/ExCodable/ExCodable.swift | 167 ++++++++++--------- Tests/ExCodableTests/ExCodableTests.swift | 165 +++++------------- 4 files changed, 228 insertions(+), 201 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index b595796..fa839ab 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -3,7 +3,7 @@ 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.6.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" diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index 6666bb6..7046def 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -8,7 +8,100 @@ import Foundation -public extension ExCodable { +// 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) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index e5f69ad..4b14cd2 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -18,86 +18,104 @@ import Foundation * * - seealso: [Usage](https://github.com/iwill/ExCodable#usage) from GitGub * - seealso: `ExCodableTests.swift` form the source code + * - seealso: Idea from [Decoding and overriding](https://www.swiftbysundell.com/articles/property-wrappers-in-swift/#decoding-and-overriding), by John Sundell. */ -public protocol ExCodable: Codable { - associatedtype Root = Self where Root: ExCodable - static var keyMapping: [KeyMap] { get } -} -// default implementation for Encodable -public extension ExCodable where Root == Self { - func encode(to encoder: Encoder) throws { - try encode(to: encoder, with: Self.keyMapping) +@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) + } + 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) + } + 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) + } +} +extension ExCodable: Equatable where Value: Equatable { + public static func == (lhs: ExCodable, rhs: ExCodable) -> Bool { + return lhs.wrappedValue == rhs.wrappedValue } } -// MARK: - keyMapping +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`) } + + } +} -// encode/decode -public extension ExCodable { - static var keyMapping: [KeyMap] { [] } // default implementation for optional property - 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`) } +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 + } } - 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`) } +} + +// MARK: Codable + +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 { + try (child.value as? EncodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false) + } + mirror = mirror.superclassMirror + } } - 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 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 { + try (child.value as? DecodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false) + } + mirror = mirror.superclassMirror + } } } -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) +// MARK: auto implementation for Encodable and Decodable + +public protocol ExAutoEncodable: Encodable {} +public extension ExAutoEncodable { + func encode(to encoder: Encoder) throws { + try encode(to: encoder, nonnull: false, throws: false) } } -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 - } - }) +public protocol ExAutoDecodable: Decodable { init() } +public extension ExAutoDecodable { + init(from decoder: Decoder) throws { + self.init() + try decode(from: decoder, nonnull: false, throws: false) } } -// MARK: - subscript +public protocol ExAutoCodable: ExAutoEncodable, ExAutoDecodable {} + +// MARK: - Encoder&Decoder public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false subscript(stringKey: String) -> T? { get { return nil } @@ -123,8 +141,6 @@ public extension Decoder { // , abortIfNull nonnull: Bool = false, abortOnError } } -// MARK: - Encoder&Decoder - public extension Encoder { func encodeNonnullThrows(_ value: T, for stringKey: String) throws { @@ -136,7 +152,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 { @@ -167,7 +183,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) } @@ -197,7 +213,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`) } @@ -219,7 +235,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`) @@ -233,9 +249,7 @@ public extension Decoder { private struct ExCodingKey { public let stringValue: String, intValue: Int? - init(_ stringValue: String) { (self.stringValue, self.intValue) = (stringValue, nil) } - init(_ stringValue: Substring) { self.init(String(stringValue)) } - init(_ intValue: Int) { (self.intValue, self.stringValue) = (intValue, String(intValue)) } + init(_ stringValue: S) { (self.stringValue, self.intValue) = (stringValue as? String ?? String(stringValue), nil) } } extension ExCodingKey: CodingKey { @@ -245,7 +259,7 @@ extension ExCodingKey: CodingKey { // MARK: - alternative-keys + nested-keys + type-conversion -private extension KeyedDecodingContainer { +fileprivate extension KeyedDecodingContainer { func decodeForAlternativeKeys(_ codingKeys: [Self.Key], as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? { @@ -422,13 +436,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) // Encodable.encode() -> Data? public extension Encodable { @@ -503,7 +516,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 diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 333f83f..6f8d2f4 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -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.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 @@ -457,9 +378,8 @@ final class ExCodableTests: XCTestCase { XCTAssertEqual(copy, test) 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 { @@ -487,7 +407,7 @@ 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) @@ -495,7 +415,8 @@ final class ExCodableTests: XCTestCase { debugPrint(json) XCTAssertEqual(NSDictionary(dictionary: json), [ "int": 418, - "string": "dddd" + "string": "dddd", + "bool": true ]) } else { From c17839c6e4164c25f0408e9eefaec415d17954f2 Mon Sep 17 00:00:00 2001 From: iwill Date: Fri, 2 Jul 2021 10:38:33 +0800 Subject: [PATCH 10/43] proj: enable build for develop --- .github/workflows/build-and-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index bd71cb3..c3ae7fb 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: From 9f8324d47457e6a8cb2bb4d2721884b3b55990be Mon Sep 17 00:00:00 2001 From: iwill Date: Fri, 2 Jul 2021 16:33:13 +0800 Subject: [PATCH 11/43] sty: coding style --- Sources/ExCodable/ExCodable+DEPRECATED.swift | 3 - Sources/ExCodable/ExCodable.swift | 64 +++++++++++--------- Tests/ExCodableTests/ExCodableTests.swift | 10 +-- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index 7046def..ccb795f 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -18,16 +18,13 @@ public protocol ExCodableProtocol: Codable { @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) } diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 4b14cd2..66044f2 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -11,9 +11,15 @@ import Foundation /** * # ExCodable * - * A protocol extends `Encodable` & `Decodable` with `keyMapping` + * - `ExCodable`: A property-wrapper for mapping property to json-key. + * - `ExAutoEncodable` and `ExAutoDecodable`: Protocols with default implementation for Encodable & Decodable. + * - `ExAutoCodable`: A typealias for `ExAutoEncodable & ExAutoDecodable`. + * - Extensions of `Encodable & Decodable`, for encode/decode-ing from internal/external. + * - Extensions of `Encoder & Encoder`, for encode/decode-ing properties one-by-one. + * - Supports alternative-keys, nested-keys and type-conversion + * * <#swift#> <#codable#> <#json#> <#model#> <#type-inference#> - * <#key-mapping#> <#keypath#> <#codingkey#> <#subscript#> + * <#property-wrapper#> <#key-mapping#> <#codingkey#> <#subscript#> * <#alternative-keys#> <#nested-keys#> <#type-conversion#> * * - seealso: [Usage](https://github.com/iwill/ExCodable#usage) from GitGub @@ -70,7 +76,26 @@ extension ExCodable: DecodablePropertyWrapper where Value: Decodable { } } -// MARK: Codable +// 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 { @@ -96,26 +121,7 @@ public extension Decodable { } } -// MARK: auto implementation for Encodable and 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 protocol ExAutoCodable: ExAutoEncodable, ExAutoDecodable {} - -// MARK: - Encoder&Decoder +// MARK: - Encoder & Decoder public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false subscript(stringKey: String) -> T? { get { return nil } @@ -257,7 +263,7 @@ extension ExCodingKey: CodingKey { public init?(intValue: Int) { self.init(intValue) } } -// MARK: - alternative-keys + nested-keys + type-conversion +// MARK: - KeyedDecodingContainer - alternative-keys + nested-keys + type-conversion fileprivate extension KeyedDecodingContainer { @@ -418,13 +424,13 @@ fileprivate 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 } @@ -441,7 +447,7 @@ public func register(_ decodingTypeConverter: ExCodableDecodingTypeConverter) { _decodingTypeConverters.append(decodingTypeConverter) } -// MARK: - Encodable/Decodable +// MARK: - Encodable & Decodable - external // Encodable.encode() -> Data? public extension Encodable { diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 6f8d2f4..0542f76 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -90,14 +90,14 @@ 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] - }) + + @ExCodable(encode: { encoder, value in encoder[Keys.bool] = value }, + decode: { decoder in return decoder[Keys.bool] }) var bool: Bool = false } From 47c8f5d48b7159d97083faf41d06b3ec0c880bf4 Mon Sep 17 00:00:00 2001 From: "Mr. Ming" Date: Mon, 16 Aug 2021 16:56:19 +0800 Subject: [PATCH 12/43] sty: comments & readme --- README.md | 7 +++++-- Sources/ExCodable/ExCodable.swift | 23 +++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0154806..b9fce2b 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,12 @@ [![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg)](https://swift.org/) [![Swift Package Manager](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) [![Platforms](https://img.shields.io/cocoapods/p/ExCodable.svg)](#readme) +
[![Build and Test](https://github.com/iwill/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/iwill/ExCodable/actions/workflows/build-and-test.yml) [![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/iwill/ExCodable.svg?sort=semver)](https://github.com/iwill/ExCodable/releases) [![Deploy to CocoaPods](https://github.com/iwill/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/iwill/ExCodable/actions/workflows/deploy_to_cocoapods.yml) [![Cocoapods](https://img.shields.io/cocoapods/v/ExCodable.svg)](https://cocoapods.org/pods/ExCodable) +
[![LICENSE](https://img.shields.io/github/license/iwill/ExCodable.svg)](https://github.com/iwill/ExCodable/blob/master/LICENSE) [![@minglq](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fiwill%2FExCodable)](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 { diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 66044f2..3fe5912 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -11,20 +11,20 @@ import Foundation /** * # ExCodable * - * - `ExCodable`: A property-wrapper for mapping property to json-key. - * - `ExAutoEncodable` and `ExAutoDecodable`: Protocols with default implementation for Encodable & Decodable. + * - `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`. - * - Extensions of `Encodable & Decodable`, for encode/decode-ing from internal/external. - * - Extensions of `Encoder & Encoder`, for encode/decode-ing properties one-by-one. - * - Supports alternative-keys, nested-keys and type-conversion + * - `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#> - * <#property-wrapper#> <#key-mapping#> <#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: Idea from [Decoding and overriding](https://www.swiftbysundell.com/articles/property-wrappers-in-swift/#decoding-and-overriding), by John Sundell. + * - 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. */ @propertyWrapper @@ -59,7 +59,6 @@ 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`) } - } } @@ -263,7 +262,7 @@ extension ExCodingKey: CodingKey { public init?(intValue: Int) { self.init(intValue) } } -// MARK: - KeyedDecodingContainer - alternative-keys + nested-keys + type-conversion +// MARK: - KeyedDecodingContainer - alternative-keys + nested-keys + type-conversions fileprivate extension KeyedDecodingContainer { From d1cb480c06f0634649be2a0bb8c1ee577068ddd6 Mon Sep 17 00:00:00 2001 From: iwill Date: Fri, 4 Feb 2022 15:23:37 +0800 Subject: [PATCH 13/43] opt: type inference in test --- Tests/ExCodableTests/ExCodableTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 0542f76..fa59bf7 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -333,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"]) } From 12bd1322c15e3c44977ef3b2ecfd8eab358c286e Mon Sep 17 00:00:00 2001 From: iwill Date: Fri, 4 Mar 2022 01:05:30 +0800 Subject: [PATCH 14/43] opt: test --- Tests/ExCodableTests/ExCodableTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index fa59bf7..4245be9 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -82,7 +82,7 @@ extension TestAlternativeKeys: ExAutoCodable {} struct TestNestedKeys: Equatable { @ExCodable var int: Int = 0 - @ExCodable("nested.string") + @ExCodable("nested.nested.string") var string: String! = nil } extension TestNestedKeys: ExAutoCodable {} @@ -397,7 +397,9 @@ final class ExCodableTests: XCTestCase { XCTAssertEqual(NSDictionary(dictionary: json), [ "int": 404, "nested": [ - "string": "Not Found" + "nested": [ + "string": "Not Found" + ] ] ]) } From dc93ab022caca91adb28cd3e662405fb3425a659 Mon Sep 17 00:00:00 2001 From: iwill Date: Fri, 4 Mar 2022 01:07:37 +0800 Subject: [PATCH 15/43] =?UTF-8?q?opt:=20Mr.=20Ming=20>=20Mr.=20M=C3=ADng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ExCodable.podspec | 2 +- LICENSE | 2 +- Sources/ExCodable/ExCodable+DEPRECATED.swift | 4 ++-- Sources/ExCodable/ExCodable.swift | 4 ++-- Tests/ExCodableTests/ExCodableTests.swift | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index fa839ab..336bd9c 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| # ――― 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..2c34e9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Mr. Ming +Copyright (c) 2021 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/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index ccb795f..8efb99b 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -2,8 +2,8 @@ // ExCodable.swift // ExCodable // -// Created by Mr. Ming on 2021-07-01. -// Copyright (c) 2021 Mr. Ming . Released under the MIT license. +// Created by Mr. Míng on 2021-07-01. +// Copyright (c) 2021 Mr. Míng . Released under the MIT license. // import Foundation diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 3fe5912..0529899 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) 2021 Mr. Míng . Released under the MIT license. // import Foundation diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 4245be9..5e47bcc 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) 2021 Mr. Míng . Released under the MIT license. // import XCTest From 19076055c060fc1f0f5eddfac8961dbe9f9447b0 Mon Sep 17 00:00:00 2001 From: iwill Date: Fri, 4 Mar 2022 01:08:41 +0800 Subject: [PATCH 16/43] proj: copyright --- LICENSE | 2 +- Sources/ExCodable/ExCodable+DEPRECATED.swift | 2 +- Sources/ExCodable/ExCodable.swift | 2 +- Tests/ExCodableTests/ExCodableTests.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 2c34e9a..d4a2b48 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Mr. Míng +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/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index 8efb99b..4cd2657 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -3,7 +3,7 @@ // ExCodable // // Created by Mr. Míng on 2021-07-01. -// Copyright (c) 2021 Mr. Míng . Released under the MIT license. +// Copyright (c) 2022 Mr. Míng . Released under the MIT license. // import Foundation diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 0529899..7f40e4c 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -3,7 +3,7 @@ // ExCodable // // Created by Mr. Míng on 2021-02-10. -// Copyright (c) 2021 Mr. Míng . Released under the MIT license. +// Copyright (c) 2022 Mr. Míng . Released under the MIT license. // import Foundation diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 5e47bcc..13f57d5 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -3,7 +3,7 @@ // ExCodable // // Created by Mr. Míng on 2021-02-10. -// Copyright (c) 2021 Mr. Míng . Released under the MIT license. +// Copyright (c) 2022 Mr. Míng . Released under the MIT license. // import XCTest From 804885f82e10088c611e7ea6583b279a95ccb94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Wed, 1 Jun 2022 15:38:09 +0800 Subject: [PATCH 17/43] fix: #5 print `wrappedValue` instead of `ExCodable` --- Sources/ExCodable/ExCodable.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 7f40e4c..3cdde7f 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -51,6 +51,10 @@ extension ExCodable: Equatable where Value: Equatable { return lhs.wrappedValue == rhs.wrappedValue } } +extension ExCodable: CustomStringConvertible { // CustomDebugStringConvertible + public var description: String { String(describing: wrappedValue) } + // public var debugDescription: String { "\(type(of: self))(\(wrappedValue))" } +} fileprivate protocol EncodablePropertyWrapper { func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws From 725768361a0bd6b9a69eb48c584646aa97084c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Mon, 23 Oct 2023 18:43:23 +0800 Subject: [PATCH 18/43] opt: code and test --- Sources/ExCodable/ExCodable+DEPRECATED.swift | 4 +-- Sources/ExCodable/ExCodable.swift | 15 ++++++----- Tests/ExCodableTests/ExCodableTests.swift | 27 ++++++++++++++------ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index 4cd2657..c23e974 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -10,7 +10,7 @@ import Foundation // MARK: - keyMapping -@available(*, deprecated, message: "use `@ExCodable` property wrapper instead") +@available(*, deprecated, message: "Use `@ExCodable` property wrapper instead.") public protocol ExCodableProtocol: Codable { associatedtype Root = Self where Root: ExCodableProtocol static var keyMapping: [KeyMap] { get } @@ -43,7 +43,7 @@ public extension ExCodableProtocol { } } -@available(*, deprecated, message: "use `@ExCodable` property wrapper instead") +@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)? diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 3cdde7f..00a1da0 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -24,7 +24,8 @@ import Foundation * * - 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. + * - 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. */ @propertyWrapper @@ -71,9 +72,9 @@ fileprivate protocol DecodablePropertyWrapper { } 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`) { + if let value = (decode != nil + ? try decode!(decoder) + : try decoder.decode(stringKeys ?? [String(label)], nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`)) { wrappedValue = value } } @@ -330,9 +331,9 @@ fileprivate extension KeyedDecodingContainer { var firstError: Error? do { - if let value = nonnull - ? (`throws` ? try decode(type, forKey: codingKey) : try? decode(type, forKey: codingKey)) - : (`throws` ? try decodeIfPresent(type, forKey: codingKey) : try? decodeIfPresent(type, forKey: codingKey)) { + if let value = (nonnull + ? (`throws` ? try decode(type, forKey: codingKey) : try? decode(type, forKey: codingKey)) + : (`throws` ? try decodeIfPresent(type, forKey: codingKey) : try? decodeIfPresent(type, forKey: codingKey))) { return value } } diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 13f57d5..f4d9ae7 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -94,7 +94,9 @@ struct TestCustomEncodeDecode: Equatable { @ExCodable(Keys.int) var int: Int = 0 - var string: String? + @ExCodable(encode: { encoder, value in encoder["nested.nested.string"] = value }, + decode: { decoder in return decoder["nested.nested.string"] }) + var string: String? = nil @ExCodable(encode: { encoder, value in encoder[Keys.bool] = value }, decode: { decoder in return decoder[Keys.bool] }) @@ -104,7 +106,7 @@ struct TestCustomEncodeDecode: Equatable { extension TestCustomEncodeDecode: Codable { private enum Keys: CodingKey { - case int, string, bool + case int, bool } private static let dddd = "dddd" private func string(for int: Int) -> String { @@ -123,14 +125,14 @@ extension TestCustomEncodeDecode: Codable { init(from decoder: Decoder) throws { try decode(from: decoder, nonnull: false, throws: false) - string = decoder[Keys.string] + string = decoder["nested.nested.string"] if string == nil || string == Self.dddd { string = string(for: int) } } func encode(to encoder: Encoder) throws { try encode(to: encoder, nonnull: false, throws: false) - encoder[Keys.string] = Self.dddd + encoder["nested.nested.string"] = Self.dddd } } @@ -309,9 +311,9 @@ class TestSubclass: TestClass { } static func == (lhs: TestSubclass, rhs: TestSubclass) -> Bool { - return lhs.int == rhs.int - && lhs.string == rhs.string - && lhs.bool == rhs.bool + return (lhs.int == rhs.int + && lhs.string == rhs.string + && lhs.bool == rhs.bool) } } @@ -363,6 +365,11 @@ final class ExCodableTests: XCTestCase { let copy2 = try? TestStruct.decoded(from: data) { XCTAssertEqual(copy1, test) XCTAssertEqual(copy2, test) + + let string = "string: \(test)" + print(string) + print(test) + debugPrint(test) } else { XCTFail() @@ -417,7 +424,11 @@ final class ExCodableTests: XCTestCase { debugPrint(json) XCTAssertEqual(NSDictionary(dictionary: json), [ "int": 418, - "string": "dddd", + "nested": [ + "nested": [ + "string": "dddd" + ] + ], "bool": true ]) } From 0e35f279f7a8142a5e82bb9e0f3ef82023e44e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Mon, 23 Oct 2023 18:53:17 +0800 Subject: [PATCH 19/43] meta: readme --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b9fce2b..b8b4d0d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ En | [中文](https://iwill.im/ExCodable/) ## Features - Extends Swift `Codable` - `Encodable & Decodable`; -- Supports Key-Mapping via Property-Wrapper `ExCodable` + `String`: +- 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; @@ -45,7 +45,9 @@ En | [中文](https://iwill.im/ExCodable/) ## Usage -### 0. `Codable`: +### 0. ⭐️ Star this repo 🤭 + +### 1. `Codable`: With `Codable`, it just needs to adop the `Codable` protocol without implementing any method of it. @@ -118,7 +120,7 @@ extension TestExCodable: ExCodable { ``` -### 1. Key-Mapping for `struct`: +### 2. Key-Mapping for `struct`: With `ExCodable`, it needs to to declare properties with `var` and provide default values. @@ -150,7 +152,7 @@ extension TestStruct: ExCodable { ``` -### 2. Alternative-Keys: +### 3. Alternative-Keys: ```swift static let keyMapping: [KeyMap] = [ @@ -160,7 +162,7 @@ static let keyMapping: [KeyMap] = [ ``` -### 3. Nested-Keys: +### 4. Nested-Keys: ```swift static let keyMapping: [KeyMap] = [ @@ -170,7 +172,7 @@ static let keyMapping: [KeyMap] = [ ``` -### 4. Custom encode/decode: +### 5. Custom encode/decode: ```swift struct TestCustomEncodeDecode: Equatable { @@ -220,7 +222,7 @@ extension TestCustomEncodeDecode: ExCodable { ``` -### 5. Encode/decode constant properties with subscripts: +### 6. Encode/decode constant properties with subscripts: Using `let` to declare properties without default values. @@ -259,7 +261,7 @@ extension TestSubscript: Encodable, Decodable { ``` -### 6. Custom Type-Conversions: +### 7. Custom Type-Conversions: Declare struct `FloatToBoolDecodingTypeConverter` with protocol `ExCodableDecodingTypeConverter` and implement its method, decode values in alternative types and convert to target type: @@ -291,7 +293,7 @@ Register `FloatToBoolDecodingTypeConverter` with an instance: register(FloatToBoolDecodingTypeConverter()) ``` -### 7. Key-Mapping for `class`: +### 8. Key-Mapping for `class`: Cannot adopt `ExCodable` in extension of classes. @@ -320,7 +322,7 @@ class TestClass: ExCodable, Equatable { ``` -### 8. Key-Mapping for subclass: +### 9. Key-Mapping for subclass: Requires declaring another static Key-Mapping for subclass. @@ -354,7 +356,7 @@ class TestSubclass: TestClass { ``` -### 9. Encode/decode with Type-Inference: +### 10. Encode/decode with Type-Inference: ```swift let test = TestStruct(int: 304, string: "Not Modified") @@ -428,7 +430,7 @@ Hope you like this project, don't forget to give it a star [⭐](https://github. ## Connect with me -- Mr. Míng ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im +- Míng ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im ## License From 60754faa9d8454aaace4fe90b3cd88a5ef4afb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Mon, 23 Oct 2023 18:58:34 +0800 Subject: [PATCH 20/43] meta: copyright --- ExCodable.podspec | 2 +- LICENSE | 2 +- Sources/ExCodable/ExCodable+DEPRECATED.swift | 4 ++-- Sources/ExCodable/ExCodable.swift | 4 ++-- Tests/ExCodableTests/ExCodableTests.swift | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index 336bd9c..47caea5 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.license = "MIT" - s.author = { "Mr. Míng" => "i+ExCodable@iwill.im" } + s.author = { "Míng" => "i+ExCodable@iwill.im" } s.social_media_url = "https://iwill.im/about/" # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # diff --git a/LICENSE b/LICENSE index d4a2b48..0977b98 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Mr. Míng +Copyright (c) 2023 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/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index c23e974..c686bfe 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -2,8 +2,8 @@ // ExCodable.swift // ExCodable // -// Created by Mr. Míng on 2021-07-01. -// Copyright (c) 2022 Mr. Míng . Released under the MIT license. +// Created by Míng on 2021-07-01. +// Copyright (c) 2023 Míng . Released under the MIT license. // import Foundation diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 00a1da0..9c6402d 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -2,8 +2,8 @@ // ExCodable.swift // ExCodable // -// Created by Mr. Míng on 2021-02-10. -// Copyright (c) 2022 Mr. Míng . Released under the MIT license. +// Created by Míng on 2021-02-10. +// Copyright (c) 2023 Míng . Released under the MIT license. // import Foundation diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index f4d9ae7..ea9fac8 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -2,8 +2,8 @@ // ExCodableTests.swift // ExCodable // -// Created by Mr. Míng on 2021-02-10. -// Copyright (c) 2022 Mr. Míng . Released under the MIT license. +// Created by Míng on 2021-02-10. +// Copyright (c) 2023 Míng . Released under the MIT license. // import XCTest From 547e532846de5c149c9c0ba47b3f319a0124e02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Thu, 26 Oct 2023 00:50:19 +0800 Subject: [PATCH 21/43] meta: version 1.0.0 --- ExCodable.podspec | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index 47caea5..ec802c7 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -3,7 +3,7 @@ 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"] || "1.0.0-alpha" + s.version = ENV["LIB_VERSION"] || "1.0.0" s.summary = "Key-Mapping Extensions for Swift Codable" # s.description = "Key-Mapping Extensions for Swift Codable." s.homepage = "https://github.com/iwill/ExCodable" diff --git a/README.md b/README.md index b8b4d0d..a23e25b 100644 --- a/README.md +++ b/README.md @@ -379,14 +379,14 @@ XCTAssertEqual(copy2, test) - [Swift Package Manager](https://swift.org/package-manager/): ```swift -.package(url: "https://github.com/iwill/ExCodable", from: "0.6.0") +.package(url: "https://github.com/iwill/ExCodable", from: "1.0.0") ``` - [CocoaPods](http://cocoapods.org): ```ruby -pod 'ExCodable', '~> 0.6.0' +pod 'ExCodable', '~> 1.0.0' ``` From 9be03b0d3fb20a5a4887ffc76a6fc9c16455b216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Thu, 26 Oct 2023 10:57:48 +0800 Subject: [PATCH 22/43] meta: doc for 1.0 --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a23e25b..570e133 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,13 @@ En | [中文](https://iwill.im/ExCodable/) ## Usage -### 0. ⭐️ Star this repo 🤭 +### 0. Star this repo ⭐️ -### 1. `Codable`: +🤭 -With `Codable`, it just needs to adop the `Codable` protocol without implementing any method of it. +### 1. `Codable` vs `ExCodable`: + +With `Codable`, it just needs to adopt the `Codable` protocol without implementing any method of it. ```swift struct TestAutoCodable: Codable, Equatable { @@ -393,7 +395,7 @@ pod 'ExCodable', '~> 1.0.0' - Code Snippets: > Title: ExCodable -> Summary: Adopte to ExCodable protocol +> Summary: Adopt to ExCodable protocol > Language: Swift > Platform: All > Completion: ExCodable From 301e39a04ca817b6af7b9bb59f8b3ce1515b7dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Fri, 15 Dec 2023 15:32:22 +0800 Subject: [PATCH 23/43] fea: update swift and platform versions, fix warning, update tests --- ExCodable.podspec | 5 +++++ Package.swift | 16 ++++++++-------- Tests/ExCodableTests/ExCodableTests.swift | 16 ---------------- Tests/ExCodableTests/XCTestManifests.swift | 9 --------- Tests/LinuxMain.swift | 7 ------- 5 files changed, 13 insertions(+), 40 deletions(-) delete mode 100644 Tests/ExCodableTests/XCTestManifests.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/ExCodable.podspec b/ExCodable.podspec index ec802c7..43d4142 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -43,4 +43,9 @@ Pod::Spec.new do |s| # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } # s.dependency "pod", "~> 1.0.0" + # use <"> but not <'> for #{s.name} and #{s.version} + s.pod_target_xcconfig = { + "OTHER_SWIFT_FLAGS" => "$(inherited) -Xfrontend -module-interface-preserve-types-as-written", + } + end diff --git a/Package.swift b/Package.swift index 0f9b40c..27c2d88 100644 --- a/Package.swift +++ b/Package.swift @@ -1,19 +1,19 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 import PackageDescription let package = Package( name: "ExCodable", platforms: [ - .iOS(.v9), - .tvOS(.v9), - .macOS(.v10_10), - .watchOS(.v2) + .iOS(.v12), + .tvOS(.v12), + .macOS(.v10_13), + .watchOS(.v4) ], products: [ - .library( - name: "ExCodable", - targets: ["ExCodable"]), + .library(name: "ExCodable", targets: ["ExCodable"]), + // .library(name: "ExCodable-Static", type: .static, targets: ["ExCodable"]), + .library(name: "ExCodable-Dynamic", type: .dynamic, targets: ["ExCodable"]) ], dependencies: [ // .package(url: "https://github.com/user/repo", from: "1.0.0") diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index ea9fac8..ebfe97f 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -682,20 +682,4 @@ final class ExCodableTests: XCTestCase { let milliseconds = elapsed / 1_000_000 print("elapsed: \(milliseconds) ms") } - - static var allTests = [ - ("testAutoCodable", testAutoCodable), - ("testManualCodable", testManualCodable), - ("testStruct", testStruct), - ("testAlternativeKeys", testAlternativeKeys), - ("testNestedKeys", testNestedKeys), - ("testCustomEncodeDecode", testCustomEncodeDecode), - ("testSubscript", testSubscript), - ("testTypeConversions", testTypeConversions), - ("testCustomTypeConverter", testCustomTypeConverter), - ("testClass", testClass), - ("testSubclass", testSubclass), - ("testExCodable", testExCodable), - ("testElapsed", testElapsed) - ] } diff --git a/Tests/ExCodableTests/XCTestManifests.swift b/Tests/ExCodableTests/XCTestManifests.swift deleted file mode 100644 index d608652..0000000 --- a/Tests/ExCodableTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(ExCodableTests.allTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 863417c..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import ExCodableTests - -var tests = [XCTestCaseEntry]() -tests += ExCodableTests.allTests() -XCTMain(tests) From c309ce3497ce1c540770a7f9b43a22e3629a1368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Tue, 23 Jan 2024 04:24:58 +0800 Subject: [PATCH 24/43] fix/opt: not to encode nil if not nonnull --- ExCodable.podspec | 2 +- Sources/ExCodable/ExCodable.swift | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index 43d4142..1cb4459 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" s.osx.deployment_target = "10.10" s.watchos.deployment_target = "2.0" - s.swift_version = "5.0" + s.swift_version = "5.9" # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.source = { :git => "https://github.com/iwill/ExCodable.git", :tag => s.version.to_s } diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 9c6402d..6e10631 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -63,7 +63,17 @@ fileprivate protocol EncodablePropertyWrapper { 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`) } + else { + let value = if let optional = wrappedValue as? OptionalProtocol { + optional.wrapped + } + else { + wrappedValue + } + if value != nil || self.nonnull ?? nonnull { + try encoder.encode(wrappedValue, for: stringKeys?.first ?? String(label), nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`) + } + } } } @@ -530,3 +540,22 @@ extension JSONDecoder: DataDecoder {} extension PropertyListEncoder: DataEncoder {} @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension PropertyListDecoder: DataDecoder {} + +// - seealso: https://forums.swift.org/t/challenge-finding-base-type-of-nested-optionals/25096 + +fileprivate protocol OptionalProtocol { + var wrapped: Any? { get } +} + +extension Optional: OptionalProtocol { + public var wrapped: Any? { + return switch self { + case .some(let optional as OptionalProtocol): + optional.wrapped + case .some(let wrapped): + wrapped + case .none: + nil + } + } +} From 0f9a665cccc6043601d0e3b7074071902ccc115a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Wed, 31 Jul 2024 21:16:03 +0800 Subject: [PATCH 25/43] opt: deprecated --- Sources/ExCodable/ExCodable+DEPRECATED.swift | 16 +++++------ Sources/ExCodable/ExCodable.swift | 30 +++++++++++++++++--- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index c686bfe..840c9db 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -59,36 +59,36 @@ public final class KeyMap { 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) + 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) { + 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) + 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) { + 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) + 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) { + 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) + 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) { + if let value: Value = try decoder._decode(codingKeys, nonnull: nonnull ?? nonnullAll, throws: `throws` ?? throwsAll) { root[keyPath: keyPath] = value } }) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 6e10631..168204f 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -172,7 +172,8 @@ public extension Encoder { func encode(_ value: T?, for stringKey: String) { try? encode(value, for: stringKey, nonnull: false, throws: false) } - internal/* fileprivate */ func encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { + + 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 { @@ -203,7 +204,8 @@ public extension Encoder { func encode(_ value: T?, for codingKey: K) { try? encode(value, for: codingKey, nonnull: false, throws: false) } - internal/* fileprivate */ func encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws { + + 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) } @@ -233,7 +235,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) } - internal/* fileprivate */ func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? { + 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`) } @@ -255,7 +257,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) } - internal/* fileprivate */ func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? { + 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`) @@ -559,3 +561,23 @@ extension Optional: OptionalProtocol { } } } + +// MARK: TODO: DEPRECATED + +internal extension Encoder { + func _encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { + try encode(value, for: stringKey, nonnull: nonnull, throws: `throws`) + } + func _encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws { + try encode(value, for: codingKey, nonnull: nonnull, throws: `throws`) + } +} + +internal extension Decoder { + func _decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { + return try decode(stringKeys, as: type, nonnull: nonnull, throws: `throws`) + } + func _decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { + try decode(codingKeys, as: type, nonnull: nonnull, throws: `throws`) + } +} From e4b9a6fe38f4ffba6bfba8032bfde72109c12c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Fri, 26 Apr 2024 21:30:31 +0800 Subject: [PATCH 26/43] fea: better type converter --- README.md | 56 ++++++- Sources/ExCodable/ExCodable.swift | 177 ++++++++++++---------- Tests/ExCodableTests/ExCodableTests.swift | 141 ++++++++++++----- 3 files changed, 255 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 570e133..ce0792f 100644 --- a/README.md +++ b/README.md @@ -265,11 +265,11 @@ extension TestSubscript: Encodable, Decodable { ### 7. Custom Type-Conversions: -Declare struct `FloatToBoolDecodingTypeConverter` with protocol `ExCodableDecodingTypeConverter` and implement its method, decode values in alternative types and convert to target type: +A. For a specific type: ```swift -struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { - public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { +extension TestTypeConverter: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { // Bool -> Double if type is Double.Type || type is Double?.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { @@ -289,10 +289,56 @@ struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { ``` -Register `FloatToBoolDecodingTypeConverter` with an instance: +B. For multiple types: ```swift -register(FloatToBoolDecodingTypeConverter()) +extension ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + // Bool -> Double + if type is Double.Type || type is Double?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Bool -> Float + else if type is Float.Type || type is Float?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Double or Float NOT found + return nil + } +} + +extension TestTypeConverter001: ExCodableDecodingTypeConverter {} +extension TestTypeConverter002: ExCodableDecodingTypeConverter {} +extension TestTypeConverter003: ExCodableDecodingTypeConverter {} + +``` + +C. For all types - DONOT do this in public frameworks: + +```swift +extension KeyedDecodingContainer: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + // Bool -> Double + if type is Double.Type || type is Double?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Bool -> Float + else if type is Float.Type || type is Float?.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // Double or Float NOT found + return nil + } +} + ``` ### 8. Key-Mapping for `class`: diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 168204f..6a4378e 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -29,22 +29,23 @@ import Foundation */ @propertyWrapper -public final class ExCodable { +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) + private init(wrappedValue initialValue: 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) = (initialValue, stringKeys, nonnull, `throws`, encode, decode) } - 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) + public convenience init(wrappedValue initialValue: 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: initialValue, stringKeys: stringKey.map { [$0] }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) } - 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 initialValue: 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: initialValue, 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 convenience init(wrappedValue initialValue: 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: initialValue, stringKeys: codingKeys.map { $0.stringValue }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) } } extension ExCodable: Equatable where Value: Equatable { @@ -57,10 +58,11 @@ extension ExCodable: CustomStringConvertible { // CustomDebugStringConvertible // public var debugDescription: String { "\(type(of: self))(\(wrappedValue))" } } -fileprivate protocol EncodablePropertyWrapper { +fileprivate protocol ExCodablePropertyWrapper { func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws + func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws } -extension ExCodable: EncodablePropertyWrapper where Value: Encodable { +extension ExCodable: ExCodablePropertyWrapper { fileprivate func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws { if encode != nil { try encode!(encoder, wrappedValue) } else { @@ -75,16 +77,10 @@ extension ExCodable: EncodablePropertyWrapper where Value: Encodable { } } } -} - -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 { + fileprivate func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws { if let value = (decode != nil ? try decode!(decoder) - : try decoder.decode(stringKeys ?? [String(label)], nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`)) { + : try decoder.decode(stringKeys ?? [String(label)], as: Value.self, nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`, converter: converter)) { wrappedValue = value } } @@ -116,7 +112,7 @@ public extension Encodable { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { for child in mirror.children where child.label != nil { - try (child.value as? EncodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false) + try (child.value as? ExCodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false) } mirror = mirror.superclassMirror } @@ -127,8 +123,9 @@ public extension Decodable { func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { + let converter = mirror.subjectType as? ExCodableDecodingTypeConverter.Type for child in mirror.children where child.label != nil { - try (child.value as? DecodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false) + try (child.value as? ExCodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false, converter: converter) } mirror = mirror.superclassMirror } @@ -147,17 +144,17 @@ public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError } public extension Decoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false - subscript(stringKeys: [String]) -> T? { - return decode(stringKeys, as: T.self) + subscript(stringKeys: [String], converter converter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(stringKeys, as: T.self, converter: converter) } - subscript(stringKeys: String ...) -> T? { - return decode(stringKeys, as: T.self) + subscript(stringKeys: String ..., converter converter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(stringKeys, as: T.self, converter: converter) } - subscript(codingKeys: [K]) -> T? { - return decode(codingKeys, as: T.self) + subscript(codingKeys: [K], converter converter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(codingKeys, as: T.self, converter: converter) } - subscript(codingKeys: K ...) -> T? { - return decode(codingKeys, as: T.self) + subscript(codingKeys: K ..., converter converter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { + return decode(codingKeys, as: T.self, converter: converter) } } @@ -217,50 +214,50 @@ public extension Encoder { public extension Decoder { - func decodeNonnullThrows(_ stringKeys: String ..., as type: T.Type = T.self) throws -> T { - return try decodeNonnullThrows(stringKeys, as: type) + func decodeNonnullThrows(_ stringKeys: String ..., as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decodeNonnullThrows(stringKeys, as: type, converter: converter) } - func decodeNonnullThrows(_ stringKeys: [String], as type: T.Type = T.self) throws -> T { - return try decode(stringKeys, as: type, nonnull: true, throws: true)! + func decodeNonnullThrows(_ stringKeys: [String], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decode(stringKeys, as: type, nonnull: true, throws: true, converter: converter)! } - func decodeThrows(_ stringKeys: String ..., as type: T.Type = T.self) throws -> T? { - return try decodeThrows(stringKeys, as: type) + func decodeThrows(_ stringKeys: String ..., as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decodeThrows(stringKeys, as: type, converter: converter) } - func decodeThrows(_ stringKeys: [String], as type: T.Type = T.self) throws -> T? { - return try decode(stringKeys, as: type, nonnull: false, throws: true) + func decodeThrows(_ stringKeys: [String], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decode(stringKeys, as: type, nonnull: false, throws: true, converter: converter) } - func decode(_ stringKeys: String ..., as type: T.Type = T.self) -> T? { - return decode(stringKeys, as: type) + func decode(_ stringKeys: String ..., as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return decode(stringKeys, as: type, converter: converter) } - func decode(_ stringKeys: [String], as type: T.Type = T.self) -> T? { - return try? decode(stringKeys, as: type, nonnull: false, throws: false) + func decode(_ stringKeys: [String], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return try? decode(stringKeys, as: type, nonnull: false, throws: false, converter: converter) } - 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`) + fileprivate func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decode(stringKeys.map { ExCodingKey($0) }, as: type, nonnull: nonnull, throws: `throws`, converter: converter) } - func decodeNonnullThrows(_ codingKeys: K ..., as type: T.Type = T.self) throws -> T { - return try decodeNonnullThrows(codingKeys, as: type) + func decodeNonnullThrows(_ codingKeys: K ..., as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decodeNonnullThrows(codingKeys, as: type, converter: converter) } - func decodeNonnullThrows(_ codingKeys: [K], as type: T.Type = T.self) throws -> T { - return try decode(codingKeys, as: type, nonnull: true, throws: true)! + func decodeNonnullThrows(_ codingKeys: [K], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T { + return try decode(codingKeys, as: type, nonnull: true, throws: true, converter: converter)! } - func decodeThrows(_ codingKeys: K ..., as type: T.Type = T.self) throws -> T? { - return try decodeThrows(codingKeys, as: type) + func decodeThrows(_ codingKeys: K ..., as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decodeThrows(codingKeys, as: type, converter: converter) } - func decodeThrows(_ codingKeys: [K], as type: T.Type = T.self) throws -> T? { - return try decode(codingKeys, as: type, nonnull: false, throws: true) + func decodeThrows(_ codingKeys: [K], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + return try decode(codingKeys, as: type, nonnull: false, throws: true, converter: converter) } - func decode(_ codingKeys: K ..., as type: T.Type = T.self) -> T? { - return decode(codingKeys, as: type) + func decode(_ codingKeys: K ..., as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return decode(codingKeys, as: type, converter: converter) } - func decode(_ codingKeys: [K], as type: T.Type = T.self) -> T? { - return try? decode(codingKeys, as: type, nonnull: false, throws: false) + func decode(_ codingKeys: [K], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + return try? decode(codingKeys, as: type, nonnull: false, throws: false, converter: converter) } - fileprivate func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false) throws -> T? { + fileprivate func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { do { let container = try self.container(keyedBy: K.self) - return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`) + return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) } catch { if `throws` || nonnull { throw error } } return nil @@ -283,12 +280,12 @@ extension ExCodingKey: CodingKey { fileprivate extension KeyedDecodingContainer { - func decodeForAlternativeKeys(_ codingKeys: [Self.Key], as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? { + func decodeForAlternativeKeys(_ codingKeys: [Self.Key], as type: T.Type = T.self, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { var firstError: Error? do { let codingKey = codingKeys.first! - if let value = try decodeForNestedKeys(codingKey, as: type, nonnull: nonnull, throws: `throws`) { + if let value = try decodeForNestedKeys(codingKey, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { return value } } @@ -296,7 +293,7 @@ fileprivate extension KeyedDecodingContainer { let codingKeys = Array(codingKeys.dropFirst()) if !codingKeys.isEmpty, - let value = try? decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`) { + let value = try? decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { return value } @@ -304,11 +301,11 @@ fileprivate extension KeyedDecodingContainer { return nil } - func decodeForNestedKeys(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? { + func decodeForNestedKeys(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { var firstError: Error? do { - if let value = try decodeForValue(codingKey, as: type, nonnull: nonnull, throws: `throws`) { + if let value = try decodeForValue(codingKey, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { return value } } @@ -321,7 +318,7 @@ fileprivate extension KeyedDecodingContainer { if !keys.isEmpty, let container = nestedContainer(with: keys.dropLast()), let codingKey = keys.last, - let value = try? container.decodeForNestedKeys(codingKey as! Self.Key, as: type, nonnull: nonnull, throws: `throws`) { + let value = try? container.decodeForNestedKeys(codingKey as! Self.Key, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { return value } } @@ -339,7 +336,7 @@ fileprivate extension KeyedDecodingContainer { return container } - func decodeForValue(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool) throws -> T? { + func decodeForValue(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { var firstError: Error? do { @@ -352,7 +349,7 @@ fileprivate extension KeyedDecodingContainer { catch { firstError = error } if contains(codingKey), - let value = decodeForTypeConversion(codingKey, as: type) { + let value = decodeForTypeConversion(codingKey, as: type, converter: converter) { return value } @@ -360,7 +357,7 @@ fileprivate extension KeyedDecodingContainer { return nil } - func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self) -> T? { + func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self, converter selfConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { if type is Bool.Type { if let int = try? decodeIfPresent(Int.self, forKey: codingKey) { @@ -404,6 +401,7 @@ fileprivate extension KeyedDecodingContainer { else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return Int64(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Int64(string) { return value as? T } } + else if type is UInt.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt(bool ? 1 : 0) as? T } else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt(string) { return value as? T } @@ -440,13 +438,39 @@ fileprivate extension KeyedDecodingContainer { else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return String(describing: double) as? T } // include Float } - for converter in _decodingTypeConverters { - if let value = try? converter.decode(self, codingKey: codingKey, as: type) { - return value +#if os(iOS) || os(tvOS) + if #available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *) { + if type is Float16.Type { + if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float16(int64) as? T } // include all Int types + else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float16(string) { return value as? T } } + else if type is Float32.Type { + if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float32(int64) as? T } // include all Int types + else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float32(string) { return value as? T } + } + else if type is Float64.Type { + if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float64(int64) as? T } // include all Int types + else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float64(string) { return value as? T } + } + // else if type is Float80.Type { + // if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float80(int64) as? T } // include all Int types + // else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float80(string) { return value as? T } + // } + // else if type is Float96.Type { + // if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float96(int64) as? T } // include all Int types + // else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float96(string) { return value as? T } + // } + } +#endif + + // specific converter for type `T`, via `extension T: ExCodableDecodingTypeConverter` + if let selfConverter, + let value = try? selfConverter.decode(self, codingKey: codingKey, as: type) { + return value } - if let customConverter = self as? ExCodableDecodingTypeConverter, - let value = try? customConverter.decode(self, codingKey: codingKey, as: type) { + // global converter for all types, via `extension KeyedDecodingContainer: ExCodableDecodingTypeConverter` + if let globalConverter = Self.self as? ExCodableDecodingTypeConverter.Type, + let value = try? globalConverter.decode(self, codingKey: codingKey, as: type) { return value } @@ -455,12 +479,7 @@ fileprivate extension KeyedDecodingContainer { } public protocol ExCodableDecodingTypeConverter { - func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? -} - -fileprivate var _decodingTypeConverters: [ExCodableDecodingTypeConverter] = [] -public func register(_ decodingTypeConverter: ExCodableDecodingTypeConverter) { - _decodingTypeConverters.append(decodingTypeConverter) + static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? } // MARK: - Encodable & Decodable - external @@ -575,9 +594,9 @@ internal extension Encoder { internal extension Decoder { func _decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { - return try decode(stringKeys, as: type, nonnull: nonnull, throws: `throws`) + return try decode(stringKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) } func _decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { - try decode(codingKeys, as: type, nonnull: nonnull, throws: `throws`) + try decode(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) } } diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index ebfe97f..1f52d69 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -67,6 +67,18 @@ struct TestStruct: Equatable { } extension TestStruct: ExAutoCodable {} +// MARK: struct with enum + +enum TestEnum: Int, Codable { + case zero = 0, one = 1 +} + +struct TestStructWithEnum: Equatable { + @ExCodable("enum") + private(set) var `enum`: TestEnum = .zero +} +extension TestStructWithEnum: ExAutoCodable {} + // MARK: alternative-keys & alternative-keyMapping struct TestAlternativeKeys: Equatable { @@ -95,11 +107,11 @@ struct TestCustomEncodeDecode: Equatable { var int: Int = 0 @ExCodable(encode: { encoder, value in encoder["nested.nested.string"] = value }, - decode: { decoder in return decoder["nested.nested.string"] }) + decode: { decoder in return decoder["nested.nested.string"/*, converter: Self.self*/] }) var string: String? = nil @ExCodable(encode: { encoder, value in encoder[Keys.bool] = value }, - decode: { decoder in return decoder[Keys.bool] }) + decode: { decoder in return decoder[Keys.bool/*, converter: Self.self*/] }) var bool: Bool = false } @@ -125,7 +137,7 @@ extension TestCustomEncodeDecode: Codable { init(from decoder: Decoder) throws { try decode(from: decoder, nonnull: false, throws: false) - string = decoder["nested.nested.string"] + string = decoder["nested.nested.string"/*, converter: Self.self*/] if string == nil || string == Self.dddd { string = string(for: int) } @@ -151,17 +163,17 @@ extension TestSubscript: Encodable, Decodable { init(from decoder: Decoder) throws { // - seealso: - // string = decoder.decode(<#T##codingKeys: CodingKey...##CodingKey#>) - // string = try decoder.decodeThrows(<#T##codingKeys: CodingKey...##CodingKey#>) - // string = try decoder.decodeNonnullThrows(<#T##codingKeys: CodingKey...##CodingKey#>) - int = decoder[Keys.int] ?? 0 - string = decoder[Keys.string] ?? "" + // string = decoder.decode(Keys.string, as: String.self, converter: Self.self)! + // string = try decoder.decodeThrows(Keys.string, as: String.self, converter: Self.self)! + // string = try decoder.decodeNonnullThrows(Keys.string, as: String.self, converter: Self.self) + int = decoder[Keys.int/*, converter: Self.self*/] ?? 0 + string = decoder[Keys.string/*, converter: Self.self*/] ?? "" } func encode(to encoder: Encoder) throws { // - seealso: - // encoder.encode(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>) - // try encoder.encodeThrows(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>) - // try encoder.encodeNonnullThrows(<#T##value: Encodable##Encodable#>, for: <#T##CodingKey#>) + // encoder.encode(string, for: Keys.string) + // try encoder.encodeThrows(string, for: Keys.string) + // try encoder.encodeNonnullThrows(string, for: Keys.string) encoder[Keys.int] = int encoder[Keys.string] = string } @@ -190,20 +202,20 @@ extension TestTypeConversions: Encodable, Decodable { } init(from decoder: Decoder) throws { - boolFromInt = decoder[Keys.boolFromInt] - boolFromString = decoder[Keys.boolFromString] - intFromBool = decoder[Keys.intFromBool] - intFromDouble = decoder[Keys.intFromDouble] - intFromString = decoder[Keys.intFromString] - uIntFromBool = decoder[Keys.uIntFromBool] - uIntFromString = decoder[Keys.uIntFromString] - doubleFromInt64 = decoder[Keys.doubleFromInt64] - doubleFromString = decoder[Keys.doubleFromString] - floatFromInt64 = decoder[Keys.floatFromInt64] - floatFromString = decoder[Keys.floatFromString] - stringFromBool = decoder[Keys.stringFromBool] - stringFromInt64 = decoder[Keys.stringFromInt64] - stringFromDouble = decoder[Keys.stringFromDouble] + boolFromInt = decoder[Keys.boolFromInt/*, converter: Self.self*/] + boolFromString = decoder[Keys.boolFromString/*, converter: Self.self*/] + intFromBool = decoder[Keys.intFromBool/*, converter: Self.self*/] + intFromDouble = decoder[Keys.intFromDouble/*, converter: Self.self*/] + intFromString = decoder[Keys.intFromString/*, converter: Self.self*/] + uIntFromBool = decoder[Keys.uIntFromBool/*, converter: Self.self*/] + uIntFromString = decoder[Keys.uIntFromString/*, converter: Self.self*/] + doubleFromInt64 = decoder[Keys.doubleFromInt64/*, converter: Self.self*/] + doubleFromString = decoder[Keys.doubleFromString/*, converter: Self.self*/] + floatFromInt64 = decoder[Keys.floatFromInt64/*, converter: Self.self*/] + floatFromString = decoder[Keys.floatFromString/*, converter: Self.self*/] + stringFromBool = decoder[Keys.stringFromBool/*, converter: Self.self*/] + stringFromInt64 = decoder[Keys.stringFromInt64/*, converter: Self.self*/] + stringFromDouble = decoder[Keys.stringFromDouble/*, converter: Self.self*/] } func encode(to encoder: Encoder) throws { @@ -246,8 +258,23 @@ extension TestTypeConversions: Encodable, Decodable { // } // } -struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { - public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { +struct TestCustomTypeConverter: Equatable { + @ExCodable("enum") + private(set) var `enum`: TestEnum = .zero + @ExCodable("doubleFromBool") + var doubleFromBool: Double? = nil +} +extension TestCustomTypeConverter: ExAutoCodable {} + +extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { + static public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + // String -> TestEnum + if type is TestEnum.Type || type is TestEnum?.Type { + if let string = try? container.decodeIfPresent(String.self, forKey: codingKey), + let int = Int(string) { + return TestEnum(rawValue: int) as? T + } + } // Bool -> Double if type is Double.Type || type is Double?.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { @@ -265,12 +292,6 @@ struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter { } } -struct TestCustomTypeConverter: Equatable { - @ExCodable("doubleFromBool") - var doubleFromBool: Double? = nil -} -extension TestCustomTypeConverter: ExAutoCodable {} - // MARK: class class TestClass: Codable, Equatable { @@ -376,6 +397,56 @@ final class ExCodableTests: XCTestCase { } } + func testStructWithEnum() { + let test = TestStructWithEnum(enum: .one) + if let data = try? test.encoded() as Data, + let copy1 = try? data.decoded() as TestStructWithEnum, + let copy2 = try? TestStructWithEnum.decoded(from: data) { + XCTAssertEqual(copy1, test) + XCTAssertEqual(copy2, test) + + let string = String(data: data, encoding: .utf8) + print("TestStructWithEnum: \(string ?? "")") + } + else { + XCTFail() + } + } + + func testStructWithEnumFromJSON() { + let json = Data(#"{"enum":1}"#.utf8) + if let test = try? json.decoded() as TestStructWithEnum, + let data = try? test.encoded() as Data, + let copy = try? data.decoded() as TestStructWithEnum { + XCTAssertEqual(test, TestStructWithEnum(enum: .one)) + XCTAssertEqual(copy, test) + let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + XCTAssertEqual(NSDictionary(dictionary: localJSON), [ + "enum": 1 + ]) + } + else { + XCTFail() + } + } + + func testStructWithEnumFromJSONWithString() { + let json = Data(#"{"enum":"1"}"#.utf8) + if let test = try? json.decoded() as TestStructWithEnum, + let data = try? test.encoded() as Data, + let copy = try? data.decoded() as TestStructWithEnum { + // XCTAssertEqual(test, TestStructWithEnum(enum: .one)) + XCTAssertEqual(copy, test) + // let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + // XCTAssertEqual(NSDictionary(dictionary: localJSON), [ + // "enum": 1 + // ]) + } + else { + XCTFail() + } + } + func testAlternativeKeys() { let json = Data(#"{"i":403,"s":"Forbidden"}"#.utf8) if let test = try? json.decoded() as TestAlternativeKeys, @@ -528,14 +599,14 @@ final class ExCodableTests: XCTestCase { } func testCustomTypeConverter() { - register(FloatToBoolDecodingTypeConverter()) let data = Data(#""" { + "enum": "1", "doubleFromBool": true } """#.utf8) if let test = try? data.decoded() as TestCustomTypeConverter { - XCTAssertEqual(test, TestCustomTypeConverter(doubleFromBool: 1.0)) + XCTAssertEqual(test, TestCustomTypeConverter(enum: .one, doubleFromBool: 1.0)) } else { XCTFail() From 4d8cd611ad3d02abe91bdf980f26c13d87d952e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Fri, 26 Apr 2024 21:38:12 +0800 Subject: [PATCH 27/43] fea: builtin type-conversions support RawRepresentable --- ExCodable.podspec | 12 +++---- Package.swift | 12 +++---- Sources/ExCodable/ExCodable.swift | 43 ++++++++++++++++++----- Tests/ExCodableTests/ExCodableTests.swift | 10 +++--- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index 1cb4459..b6e3dd8 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -3,7 +3,7 @@ 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"] || "1.0.0" + s.version = ENV["LIB_VERSION"] || "1.0.0-rc02" s.summary = "Key-Mapping Extensions for Swift Codable" # s.description = "Key-Mapping Extensions for Swift Codable." s.homepage = "https://github.com/iwill/ExCodable" @@ -14,11 +14,11 @@ Pod::Spec.new do |s| s.social_media_url = "https://iwill.im/about/" # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - s.ios.deployment_target = "9.0" - s.tvos.deployment_target = "9.0" - s.osx.deployment_target = "10.10" - s.watchos.deployment_target = "2.0" - s.swift_version = "5.9" + s.ios.deployment_target = "12.0" + s.tvos.deployment_target = "12.0" + s.macos.deployment_target = "11.0" + s.watchos.deployment_target = "4.0" + s.swift_version = "5.10" # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.source = { :git => "https://github.com/iwill/ExCodable.git", :tag => s.version.to_s } diff --git a/Package.swift b/Package.swift index 27c2d88..72a6216 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 import PackageDescription @@ -7,7 +7,7 @@ let package = Package( platforms: [ .iOS(.v12), .tvOS(.v12), - .macOS(.v10_13), + .macOS(.v11), .watchOS(.v4) ], products: [ @@ -19,12 +19,8 @@ let package = Package( // .package(url: "https://github.com/user/repo", from: "1.0.0") ], targets: [ - .target( - name: "ExCodable", - dependencies: []), - .testTarget( - name: "ExCodableTests", - dependencies: ["ExCodable"]), + .target(name: "ExCodable", dependencies: []), + .testTarget(name: "ExCodableTests", dependencies: ["ExCodable"]) ], swiftLanguageVersions: [.v5] ) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 6a4378e..45b3390 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -31,12 +31,17 @@ import Foundation @propertyWrapper public final class ExCodable { + public var wrappedValue: Value + 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 initialValue: 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) = (initialValue, stringKeys, nonnull, `throws`, encode, decode) + fileprivate let encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, + decode: ((_ decoder: Decoder) throws -> Value?)? + fileprivate let decodeRawRepresentable: ((_ decoder: Decoder, _ stringKeys: [String], _ nonnull: Bool, _ throws: Bool, _ converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> Value?)? + + private init(wrappedValue initialValue: Value, stringKeys: [String]? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)?, decodeRawRepresentable: ((_ decoder: Decoder, _ stringKeys: [String], _ nonnull: Bool, _ throws: Bool, _ converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> Value?)? = nil) { + (self.wrappedValue, self.stringKeys, self.nonnull, self.throws, self.encode, self.decode, self.decodeRawRepresentable) + = (initialValue, stringKeys, nonnull, `throws`, encode, decode, decodeRawRepresentable) } public convenience init(wrappedValue initialValue: 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: initialValue, stringKeys: stringKey.map { [$0] }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) @@ -48,6 +53,26 @@ public final class ExCodable { self.init(wrappedValue: initialValue, stringKeys: codingKeys.map { $0.stringValue }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) } } + +extension ExCodable where Value: RawRepresentable, Value.RawValue: Decodable { + private convenience init(wrappedValue initialValue: Value, stringKeys: [String]? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)?) where Value: RawRepresentable, Value.RawValue: Decodable { + self.init(wrappedValue: initialValue, stringKeys: stringKeys, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode, decodeRawRepresentable: decode == nil ? { (_ decoder: Decoder, _ stringKeys: [String], _ nonnull: Bool, _ `throws`: Bool, _ converter: (any ExCodableDecodingTypeConverter.Type)?) throws in + guard let rawValue = try decoder.decode(stringKeys, as: Value.RawValue.self, nonnull: nonnull, throws: `throws`, converter: converter), + let value = Value(rawValue: rawValue) else { return nil } + return value + } : nil) + } + public convenience init(wrappedValue initialValue: Value, _ stringKey: String? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) where Value: RawRepresentable, Value.RawValue: Decodable { + self.init(wrappedValue: initialValue, stringKeys: stringKey.map { [$0] }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) + } + public convenience init(wrappedValue initialValue: Value, _ stringKeys: String..., nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) where Value: RawRepresentable, Value.RawValue: Decodable { + self.init(wrappedValue: initialValue, stringKeys: stringKeys, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) + } + public convenience init(wrappedValue initialValue: Value, _ codingKeys: CodingKey..., nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) where Value: RawRepresentable, Value.RawValue: Decodable { + self.init(wrappedValue: initialValue, stringKeys: codingKeys.map { $0.stringValue }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) + } +} + extension ExCodable: Equatable where Value: Equatable { public static func == (lhs: ExCodable, rhs: ExCodable) -> Bool { return lhs.wrappedValue == rhs.wrappedValue @@ -78,8 +103,8 @@ extension ExCodable: ExCodablePropertyWrapper { } } fileprivate func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws { - if let value = (decode != nil - ? try decode!(decoder) + if let value = (decode != nil ? try decode!(decoder) + : decodeRawRepresentable != nil ? try decodeRawRepresentable!(decoder, stringKeys ?? [String(label)], self.nonnull ?? nonnull, self.throws ?? `throws`, converter) : try decoder.decode(stringKeys ?? [String(label)], as: Value.self, nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`, converter: converter)) { wrappedValue = value } @@ -266,14 +291,14 @@ public extension Decoder { // MARK: - ExCodingKey -private struct ExCodingKey { +fileprivate 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) } + init?(stringValue: String) { self.init(stringValue) } + init?(intValue: Int) { self.init(intValue) } } // MARK: - KeyedDecodingContainer - alternative-keys + nested-keys + type-conversions diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 1f52d69..87fa3be 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -435,12 +435,12 @@ final class ExCodableTests: XCTestCase { if let test = try? json.decoded() as TestStructWithEnum, let data = try? test.encoded() as Data, let copy = try? data.decoded() as TestStructWithEnum { - // XCTAssertEqual(test, TestStructWithEnum(enum: .one)) + XCTAssertEqual(test, TestStructWithEnum(enum: .one)) XCTAssertEqual(copy, test) - // let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] - // XCTAssertEqual(NSDictionary(dictionary: localJSON), [ - // "enum": 1 - // ]) + let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + XCTAssertEqual(NSDictionary(dictionary: localJSON), [ + "enum": 1 + ]) } else { XCTFail() From 5eb6440e32369b221f57cb0d28d99c620c55c42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Wed, 31 Jul 2024 16:49:01 +0800 Subject: [PATCH 28/43] fix: ExAutoCodable supports type-conversions for Optional and nested Optional types --- Sources/ExCodable/ExCodable.swift | 75 +++++++++++++---------- Tests/ExCodableTests/ExCodableTests.swift | 50 +++++++++++++++ 2 files changed, 92 insertions(+), 33 deletions(-) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 45b3390..7f7f646 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -8,25 +8,23 @@ import Foundation -/** - * # ExCodable - * - * - `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#> <#property-wrapper#> <#coding-key#> <#subscript#> - * <#alternative-keys#> <#nested-keys#> <#type-conversions#> - * - * - 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. - */ +/// # ExCodable +/// +/// - `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#> <#property-wrapper#> <#coding-key#> <#subscript#> +/// <#alternative-keys#> <#nested-keys#> <#type-conversions#> +/// +/// - 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. @propertyWrapper public final class ExCodable { @@ -384,7 +382,9 @@ fileprivate extension KeyedDecodingContainer { func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self, converter selfConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { - if type is Bool.Type { + let wrappedType = T?.wrappedType + + if type is Bool.Type || wrappedType is Bool.Type { if let int = try? decodeIfPresent(Int.self, forKey: codingKey) { return (int != 0) as? T } @@ -401,63 +401,63 @@ fileprivate extension KeyedDecodingContainer { } } - else if type is Int.Type { + else if type is Int.Type || wrappedType is Int.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return Int(bool ? 1 : 0) as? T } else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return Int(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Int(string) { return value as? T } } - else if type is Int8.Type { + else if type is Int8.Type || wrappedType is Int8.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return Int8(bool ? 1 : 0) as? T } else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return Int8(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Int8(string) { return value as? T } } - else if type is Int16.Type { + else if type is Int16.Type || wrappedType is Int16.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return Int16(bool ? 1 : 0) as? T } else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return Int16(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Int16(string) { return value as? T } } - else if type is Int32.Type { + else if type is Int32.Type || wrappedType is Int32.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return Int32(bool ? 1 : 0) as? T } else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return Int32(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Int32(string) { return value as? T } } - else if type is Int64.Type { + else if type is Int64.Type || wrappedType is Int64.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return Int64(bool ? 1 : 0) as? T } else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return Int64(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Int64(string) { return value as? T } } - else if type is UInt.Type { + else if type is UInt.Type || wrappedType is UInt.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt(bool ? 1 : 0) as? T } else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt(string) { return value as? T } } - else if type is UInt8.Type { + else if type is UInt8.Type || wrappedType is UInt8.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt8(bool ? 1 : 0) as? T } else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt8(string) { return value as? T } } - else if type is UInt16.Type { + else if type is UInt16.Type || wrappedType is UInt16.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt16(bool ? 1 : 0) as? T } else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt16(string) { return value as? T } } - else if type is UInt32.Type { + else if type is UInt32.Type || wrappedType is UInt32.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt32(bool ? 1 : 0) as? T } else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt32(string) { return value as? T } } - else if type is UInt64.Type { + else if type is UInt64.Type || wrappedType is UInt64.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt64(bool ? 1 : 0) as? T } else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt64(string) { return value as? T } } - else if type is Double.Type { + else if type is Double.Type || wrappedType is Double.Type { if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Double(int64) as? T } // include all Int types else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Double(string) { return value as? T } } - else if type is Float.Type { + else if type is Float.Type || wrappedType is Float.Type { if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return Float(int64) as? T } // include all Int types else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = Float(string) { return value as? T } } - else if type is String.Type { + else if type is String.Type || wrappedType is String.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return String(describing: bool) as? T } else if let int64 = try? decodeIfPresent(Int64.self, forKey: codingKey) { return String(describing: int64) as? T } // include all Int types else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return String(describing: double) as? T } // include Float @@ -590,10 +590,19 @@ extension PropertyListDecoder: DataDecoder {} // - seealso: https://forums.swift.org/t/challenge-finding-base-type-of-nested-optionals/25096 fileprivate protocol OptionalProtocol { + static var wrappedType: Any.Type { get } var wrapped: Any? { get } } extension Optional: OptionalProtocol { + + fileprivate static var wrappedType: Any.Type { + if let optional = Wrapped.self as? OptionalProtocol.Type { + return optional.wrappedType + } + return Wrapped.self + } + public var wrapped: Any? { return switch self { case .some(let optional as OptionalProtocol): diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 87fa3be..2069d4d 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -181,6 +181,14 @@ extension TestSubscript: Encodable, Decodable { // MARK: type-conversions +struct TestTypeConversion: Equatable { + @ExCodable("intFromString") + var intFromString: Int? = nil + @ExCodable("stringFromInt") + var stringFromInt: String???? = nil +} +extension TestTypeConversion: ExAutoCodable {} + struct TestTypeConversions: Equatable { let boolFromInt, boolFromString: Bool? let intFromBool, intFromDouble, intFromString: Int? @@ -519,6 +527,48 @@ final class ExCodableTests: XCTestCase { } } + func testTypeConversion() { + + let data = Data(#""" + { + "intFromString": "123", + "stringFromInt": 456 + } + """#.utf8) + + if let test = try? data.decoded() as TestTypeConversion { + XCTAssertEqual(test, TestTypeConversion(intFromString: 123, stringFromInt: "456")) + } + else { + XCTFail() + } + + let data2 = Data(#""" + { + "stringFromInt64": 123 + } + """#.utf8) + if let test2 = try? data2.decoded() as TestTypeConversions { + XCTAssertEqual(test2, TestTypeConversions(boolFromInt: nil, + boolFromString: nil, + intFromBool: nil, + intFromDouble: nil, + intFromString: nil, + uIntFromBool: nil, + uIntFromString: nil, + doubleFromInt64: nil, + doubleFromString: nil, + floatFromInt64: nil, + floatFromString: nil, + stringFromBool: nil, + stringFromInt64: "123", + stringFromDouble: nil)) + } + else { + XCTFail() + } + } + func testTypeConversions() { let data = Data(#""" From 6345ebbad4e2a08a2ad376b4a0f9f260abdbdc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Wed, 24 Jan 2024 11:22:35 +0800 Subject: [PATCH 29/43] meta: version, target versions and fix: error from github actions --- .github/workflows/build-and-test.yml | 1 + .github/workflows/deploy_to_cocoapods.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c3ae7fb..aae5ab6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,6 +12,7 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 + - uses: swift-actions/setup-swift@v2 - name: Build run: swift build -v - name: Run tests diff --git a/.github/workflows/deploy_to_cocoapods.yml b/.github/workflows/deploy_to_cocoapods.yml index f241cb4..f17cbf7 100644 --- a/.github/workflows/deploy_to_cocoapods.yml +++ b/.github/workflows/deploy_to_cocoapods.yml @@ -10,7 +10,7 @@ jobs: build: runs-on: macOS-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Install Cocoapods run: gem install cocoapods - name: Deploy to Cocoapods From c38c8ef572b32550b8ea93c1eb080d362bfa1f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Thu, 1 Aug 2024 07:04:20 +0800 Subject: [PATCH 30/43] temp/meta: opt, org & readme --- ExCodable.podspec | 4 +- README.md | 511 ++++++++++--------- Sources/ExCodable/ExCodable+DEPRECATED.swift | 28 +- Sources/ExCodable/ExCodable.swift | 57 ++- Tests/ExCodableTests/ExCodableTests.swift | 244 ++++----- 5 files changed, 439 insertions(+), 405 deletions(-) diff --git a/ExCodable.podspec b/ExCodable.podspec index b6e3dd8..29fae1f 100644 --- a/ExCodable.podspec +++ b/ExCodable.podspec @@ -6,7 +6,7 @@ Pod::Spec.new do |s| s.version = ENV["LIB_VERSION"] || "1.0.0-rc02" s.summary = "Key-Mapping Extensions for Swift Codable" # s.description = "Key-Mapping Extensions for Swift Codable." - s.homepage = "https://github.com/iwill/ExCodable" + s.homepage = "https://github.com/ExCodable/ExCodable" # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.license = "MIT" @@ -21,7 +21,7 @@ Pod::Spec.new do |s| s.swift_version = "5.10" # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - s.source = { :git => "https://github.com/iwill/ExCodable.git", :tag => s.version.to_s } + s.source = { :git => "https://github.com/ExCodable/ExCodable.git", :tag => s.version.to_s } # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.source_files = "Sources", "Sources/**/*.{swift}" diff --git a/README.md b/README.md index ce0792f..8d5111e 100644 --- a/README.md +++ b/README.md @@ -4,70 +4,86 @@ [![Swift Package Manager](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) [![Platforms](https://img.shields.io/cocoapods/p/ExCodable.svg)](#readme)
-[![Build and Test](https://github.com/iwill/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/iwill/ExCodable/actions/workflows/build-and-test.yml) -[![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/iwill/ExCodable.svg?sort=semver)](https://github.com/iwill/ExCodable/releases) -[![Deploy to CocoaPods](https://github.com/iwill/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/iwill/ExCodable/actions/workflows/deploy_to_cocoapods.yml) +[![Build and Test](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml) +[![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/ExCodable/ExCodable.svg?sort=semver)](https://github.com/ExCodable/ExCodable/releases) +[![Deploy to CocoaPods](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml) [![Cocoapods](https://img.shields.io/cocoapods/v/ExCodable.svg)](https://cocoapods.org/pods/ExCodable)
-[![LICENSE](https://img.shields.io/github/license/iwill/ExCodable.svg)](https://github.com/iwill/ExCodable/blob/master/LICENSE) +[![LICENSE](https://img.shields.io/github/license/ExCodable/ExCodable.svg)](https://github.com/ExCodable/ExCodable/blob/master/LICENSE) [![@minglq](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fiwill%2FExCodable)](https://twitter.com/minglq) En | [中文](https://iwill.im/ExCodable/) -## Contents +## What's New in ExCodable 1.0 + +- Uses `@propertyWrapper` instead of key-mapping, the new syntax is more concise, elegant and easier to use. +- Optimized type conversions. +- Fix bugs. + +## Documentations - [Features](#features) - [Usage](#usage) - [Requirements](#requirements) -- [Migration Guides](#migration-guides) - [Installation](#installation) -- [Credits](#credits) -- [License](#license) +- [Migration](#migration) ## Features -- Extends Swift `Codable` - `Encodable & Decodable`; -- 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; - - In most cases, the `CodingKey` type is no longer necessary, because it will only be used once, `String` literals may be better. -- Supports multiple Key-Mappings for different data sources; -- Supports multiple Alternative-Keys via `Array` for decoding; -- Supports Nested-Keys via `String` with dot syntax; -- Supports customized encode/decode via subscripts; -- Supports builtin and custom Type-Conversions; -- Supports `struct`, `class` and subclass; -- Supports encode/decode with or without `IfPresent`; -- Supports abort (throws error) or continue (returns nil) encode/decode if error encountered; -- Uses JSON encoder/decoder by default, and supports PList; -- Uses Type-Inference, supports JSON `Data`, `String` and `Object`. +```swift +struct TestExCodable: ExAutoCodable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable("nested.nested.string", "string", "str", "s") private(set) + var string: String? = nil +} -## Usage +``` -### 0. Star this repo ⭐️ +- Converts between JSON and models. +- Based on and Extends Swift **`Codable`**. +- Associates JSON keys to properties via **annotations** - `@propertyWrapper`: + - `ExCodable` did not read/write memory via unsafe pointers. + - No need to encode and decode properties one by one. + - In most cases, the `CodingKey` types are no longer necessary, `String` literals are preferred. + - Currently, requires declaring properties with `var` and provide default values. +- Supports `struct`, `enum`, `class` and subclasses. +- Supports **alternative keys** for decoding. +- Supports **nested keys** via `String` with dot syntax. +- Supports builtin and custom **type conversions**, including **nested optionals** as well. +- Supports manual encoding/decoding using **subscripts**. +- Supports **continue or abort** if error encountered - returns nil or throws error. +- Supports **type inference**, including JSON `Data`, `String` and objects. +- Uses JSON encoder/decoder by default, supports PList and custom encoder/decoder. + +TODO: + +- [ ] Supports `let`. +- [ ] Supports `var` without default values. +- [ ] Replacing `ExAutoCodable` with `Codable`. -🤭 +## Usage -### 1. `Codable` vs `ExCodable`: +### 0. `Codable` vs `ExCodable`: -With `Codable`, it just needs to adopt the `Codable` protocol without implementing any method of it. +Swift builtin `Codable` is quite convenient, you just need: ```swift -struct TestAutoCodable: Codable, Equatable { +struct TestAutoCodable: Codable { private(set) var int: Int = 0 private(set) var string: String? + // the case `int` can be omitted, since the key is the same as its property name enum CodingKeys: String, CodingKey { - case int = "i", string = "s" + case int = "int", string = "s" } } ``` -But, if you have to encode/decode manually for some reason, e.g. Default-Value, Alternative-Keys, Nested-Keys or Type-Conversions ... +But, if you need some advanced features, e.g. alternative keys, nested keys or type conversions, you have to: ```swift -struct TestManualCodable: Equatable { +struct TestManualCodable { private(set) var int: Int = 0 private(set) var string: String? } @@ -75,18 +91,22 @@ struct TestManualCodable: Equatable { extension TestManualCodable: Codable { enum Keys: CodingKey { - case int, i - case nested, string + case int + case nested, string, s } init(from decoder: Decoder) throws { if let container = try? decoder.container(keyedBy: Keys.self) { - if let int = (try? container.decodeIfPresent(Int.self, forKey: Keys.int)) - ?? (try? container.decodeIfPresent(Int.self, forKey: Keys.i)) { + if let int = try? container.decodeIfPresent(Int.self, forKey: Keys.int) { self.int = int } if let nestedContainer = try? container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested), - let string = try? nestedContainer.decodeIfPresent(String.self, forKey: Keys.string) { + let nestedNestedContainer = try? nestedContainer.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested), + let string = try? nestedNestedContainer.decodeIfPresent(String.self, forKey: Keys.string) { + self.string = string + } + else if let string = (try? container.decodeIfPresent(String.self, forKey: Keys.string)) + ?? (try? container.decodeIfPresent(String.self, forKey: Keys.s)) { self.string = string } } @@ -96,323 +116,262 @@ extension TestManualCodable: Codable { var container = encoder.container(keyedBy: Keys.self) try? container.encodeIfPresent(int, forKey: Keys.int) var nestedContainer = container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested) - try? nestedContainer.encodeIfPresent(string, forKey: Keys.string) + var nestedNestedContainer = nestedContainer.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested) + try? nestedNestedContainer.encodeIfPresent(string, forKey: Keys.string) } } ``` -**With `ExCodable`**: +🤯 -```swift -struct TestExCodable: Equatable { - private(set) var int: Int = 0 - private(set) var string: String? -} +Fortunately, you can use **`ExCodable`**: -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) - } +```swift +struct TestExCodable: ExAutoCodable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable("nested.nested.string", "string", "s") private(set) + var string: String? = nil } ``` -### 2. Key-Mapping for `struct`: - -With `ExCodable`, it needs to to declare properties with `var` and provide default values. - -```swift -struct TestStruct: Equatable { - private(set) var int: Int = 0 - private(set) var string: String? - var bool: Bool! -} +### 1. `struct` -``` +`ExCodable` requires declaring properties with `var` and provide default values. ```swift -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) - // } +// Equatable for Assertions +struct TestStruct: ExAutoCodable, Equatable { + @ExCodable("int") private(set) + var int: Int = 0 + @ExCodable("string") private(set) + var string: String? = nil } ``` -### 3. Alternative-Keys: +### 2. Alternative Keys ```swift -static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: "int", "i"), - KeyMap(\.string, to: "string", "str", "s") -] +struct TestAlternativeKeys: ExAutoCodable { + @ExCodable("int", "i") private(set) + var int: Int = 0 + @ExCodable("string", "str", "s") private(set) + var string: String! = nil +} ``` -### 4. Nested-Keys: +### 3. Nested Keys ```swift -static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: "int"), - KeyMap(\.string, to: "nested.string") -] +struct TestNestedKeys: ExAutoCodable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable("nested.nested.string") private(set) + var string: String! = nil +} ``` -### 5. Custom encode/decode: +### 4. `enum` ```swift -struct TestCustomEncodeDecode: Equatable { - var int: Int = 0 - var string: String? +enum TestEnum: Int, Codable { + case zero = 0, one = 1 } -``` - -```swift -extension TestCustomEncodeDecode: ExCodable { - - private enum Keys: CodingKey { - case int, string - } - private static let dddd = "dddd" - private func string(for int: Int) -> String { - switch int { - case 100: return "Continue" - case 200: return "OK" - case 304: return "Not Modified" - case 403: return "Forbidden" - case 404: return "Not Found" - case 418: return "I'm a teapot" - case 500: return "Internal Server Error" - case 200..<400: return "success" - default: return "failure" - } - } - - static let keyMapping: [KeyMap] = [ - KeyMap(\.int, to: Keys.int), - ] - - init(from decoder: Decoder) throws { - try decode(from: decoder, with: Self.keyMapping) - 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) - encoder[Keys.string] = Self.dddd - } +struct TestStructWithEnum: ExAutoCodable { + @ExCodable("enum") private(set) + var `enum` = TestEnum.zero } ``` -### 6. Encode/decode constant properties with subscripts: +### 5. Type Conversions -Using `let` to declare properties without default values. +ExCodable builtin type conversions: -```swift -struct TestSubscript: Equatable { - let int: Int - let string: String -} +- Decoding `Bool` **from** `Int`, `String` +- Decoding `Int`, `Int8`, `Int16`, `Int32`, `Int64` **from** `Bool`, `Double`, `String` +- Decoding `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` **from** `Bool`, `String` +- Decoding `Double`, `Float` **from** `Int64`, `String` +- Decoding `String` **from** `Bool`, `Int64`, `Double` -``` +Custom type conversions for specific properties: ```swift -extension TestSubscript: Encodable, Decodable { +struct TestCustomEncodeDecode: ExAutoCodable { - enum Keys: CodingKey { - case int, string - } + @ExCodable private(set) + var int: Int = 0 - init(from decoder: Decoder) throws { - // - seealso: - // string = decoder.decode(<#T##codingKeys: CodingKey...##CodingKey#>) - // string = try decoder.decodeThrows(<#T##codingKeys: CodingKey...##CodingKey#>) - // string = try decoder.decodeNonnullThrows(<#T##codingKeys: CodingKey...##CodingKey#>) - int = decoder[Keys.int] ?? 0 - string = decoder[Keys.string] ?? "" - } - func encode(to encoder: Encoder) throws { - // - seealso: - // encoder.encode(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>) - // try encoder.encodeThrows(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>) - // try encoder.encodeNonnullThrows(<#T##value: Encodable##Encodable#>, for: <#T##CodingKey#>) - encoder[Keys.int] = int - encoder[Keys.string] = string - } + @ExCodable(encode: { encoder, value in + // skip encoding + }, decode: { decoder in + // custom decoding + if let int: Int = decoder["int"] { + return message(for: int) + } + return nil + }) private(set) + var string: String? = nil } ``` -### 7. Custom Type-Conversions: - -A. For a specific type: +Custom type conversions for specific type: ```swift -extension TestTypeConverter: ExCodableDecodingTypeConverter { +struct TestCustomTypeConverter: ExAutoCodable { + @ExCodable("doubleFromBool") private(set) + var doubleFromBool: Double? = nil +} + +extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { - // Bool -> Double - if type is Double.Type || type is Double?.Type { + + // for nested optionals, e.g. `var int: Int??? = nil` + let wrappedType = T?.wrappedType + + // decode Double from Bool + if type is Double.Type || wrappedType is Double.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { return (bool ? 1.0 : 0.0) as? T } } - // Bool -> Float - else if type is Float.Type || type is Float?.Type { + // decode Float from Bool + else if type is Float.Type || wrappedType is Float.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { return (bool ? 1.0 : 0.0) as? T } } - // Double or Float NOT found + return nil } } ``` -B. For multiple types: +Custom type conversions for global: ```swift -extension ExCodableDecodingTypeConverter { - public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { - // Bool -> Double - if type is Double.Type || type is Double?.Type { - if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { - return (bool ? 1.0 : 0.0) as? T - } - } - // Bool -> Float - else if type is Float.Type || type is Float?.Type { - if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { - return (bool ? 1.0 : 0.0) as? T +struct TestCustomGlobalTypeConverter: ExAutoCodable, Equatable { + @ExCodable("boolFromDouble") private(set) + var boolFromDouble: Bool? = nil +} + +extension ExCodableGlobalDecodingTypeConverter: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer<_K>, codingKey: _K, as type: T.Type) -> T? { + + // for nested optionals, e.g. `var int: Int??? = nil` + let wrappedType = T?.wrappedType + + // decode Bool from Double + if type is Bool.Type || wrappedType is Bool.Type { + if let double = try? container.decodeIfPresent(Double.self, forKey: codingKey) { + return (double != 0) as? T } } - // Double or Float NOT found + return nil } } -extension TestTypeConverter001: ExCodableDecodingTypeConverter {} -extension TestTypeConverter002: ExCodableDecodingTypeConverter {} -extension TestTypeConverter003: ExCodableDecodingTypeConverter {} - ``` -C. For all types - DONOT do this in public frameworks: +### 6. Constants, Manual Encoding/Decoding using Subscripts + +Declaring constants without default values: ```swift -extension KeyedDecodingContainer: ExCodableDecodingTypeConverter { - public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { - // Bool -> Double - if type is Double.Type || type is Double?.Type { - if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { - return (bool ? 1.0 : 0.0) as? T - } - } - // Bool -> Float - else if type is Float.Type || type is Float?.Type { - if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { - return (bool ? 1.0 : 0.0) as? T - } - } - // Double or Float NOT found - return nil - } +struct TestSubscript { + let int: Int + let string: String } ``` -### 8. Key-Mapping for `class`: - -Cannot adopt `ExCodable` in extension of classes. +Manual encoding/decoding using subscripts: ```swift -class TestClass: ExCodable, Equatable { +extension TestSubscript: Codable { - var int: Int = 0 - var string: String? = nil - init(int: Int, string: String?) { - (self.int, self.string) = (int, string) + enum Keys: CodingKey { + case 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) + init(from decoder: Decoder) throws { + int = decoder[Keys.int] ?? 0 + string = decoder[Keys.string] ?? "" } - - static func == (lhs: TestClass, rhs: TestClass) -> Bool { - return lhs.int == rhs.int && lhs.string == rhs.string + func encode(to encoder: Encoder) throws { + encoder[Keys.int] = int + encoder[Keys.string] = string } } ``` -### 9. Key-Mapping for subclass: +### 7. `class` and Subclasses + +```swift +class TestClass: ExAutoCodable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable private(set) + var string: String? = nil + + required init() {} + init(int: Int, string: String?) { + (self.int, self.string) = (int, string) + } +} -Requires declaring another static Key-Mapping for subclass. +``` ```swift class TestSubclass: TestClass { + + @ExCodable private(set) var bool: Bool = false + + required init() { super.init() } 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 { - return lhs.int == rhs.int - && lhs.string == rhs.string - && lhs.bool == rhs.bool - } } ``` -### 10. Encode/decode with Type-Inference: +### 8. Continue or Abort ```swift -let test = TestStruct(int: 304, string: "Not Modified") -let data = try? test.encoded() as Data? -let copy1 = try? data?.decoded() as TestStruct? -let copy2 = data.map { try? TestStruct.decoded(from: $0) } -XCTAssertEqual(copy1, test) -XCTAssertEqual(copy2, test) +encoding/decoding +nonnull || `throws` + +``` + +### 9. Type Inference + +```swift +let test = TestStruct(int: 304, string: "Not Modified"), + test2 = TestStruct(int: 304, string: "Not Modified") +// type of `data` inferenced from `Data` +// types of `copy` and `copy2` inferenced from `TestStruct` +if let data = try? test.encoded() as Data, + let copy = try? data.decoded() as TestStruct, + let copy2 = try? TestStruct.decoded(from: data) { + XCTAssertEqual(copy, test) + XCTAssertEqual(copy2, test2) +} +else { + XCTFail() +} ``` @@ -427,7 +386,7 @@ XCTAssertEqual(copy2, test) - [Swift Package Manager](https://swift.org/package-manager/): ```swift -.package(url: "https://github.com/iwill/ExCodable", from: "1.0.0") +.package(url: "https://github.com/ExCodable/ExCodable", from: "1.0.0") ``` @@ -463,20 +422,66 @@ pod 'ExCodable', '~> 1.0.0' ``` -## Like it? +## Migration + +### 0.x to 1.x -Hope you like this project, don't forget to give it a star [⭐](https://github.com/iwill/ExCodable#repository-container-header) +Quickly, but **DEPRECATED**: - +- Replace protocol `ExCodable` with `ExCodableDEPRECATED`. +- Add `static` to func `decodeForTypeConversion(_:codingKey:as:)` of protocol `KeyedDecodingContainerCustomTypeConversion`. + +```swift +struct TestExCodable { + private(set) var int: Int = 0 + private(set) var string: String? +} + +// replacing protocol `ExCodable`(0.x) with `ExCodableDEPRECATED`(1.x) +extension TestExCodable: ExCodableDEPRECATED { + static let keyMapping: [KeyMap] = [ + KeyMap(\.int, to: "int"), + KeyMap(\.string, to: "nested.nested.string", "string", "str", "s") + ] + init(from decoder: Decoder) throws { + try decode(from: decoder, with: Self.keyMapping) + } +} + +``` + +Upgrade, SUGGESTED: + +- Replace `ExCodable` with `ExAutoCodable`. +- Remove `static` properties `keyMapping`. +- Remove initializer `init(from decoder: Decoder) throws`. +- Use `@ExCodable("", "")`. +- See [Usage](#usage) for more details. + +```swift +struct TestExCodable: ExAutoCodable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable("nested.nested.string", "string", "str", "s") private(set) + var string: String? +} + +``` + +## Stars + + Star Chart +Hope ExCodable will help you! [Make a star](https://github.com/ExCodable/ExCodable/#repository-container-header) ⭐️ 🤩 + ## Thanks to - 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) -## Connect with me +## About Me - Míng ([@iwill](https://github.com/iwill)) | i+ExCodable@iwill.im diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index 840c9db..d17c7a0 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -8,17 +8,33 @@ import Foundation +// TODO: remove + +/// Migration ExCodable from 0.x to 1.x: +/// +/// DEPRECATED: +/// +/// - Replace `ExCodable` with `ExCodableDEPRECATED`. +/// +/// SUGGESTED: +/// +/// - Replace `ExCodable` with `ExAutoCodable`. +/// - Remove `static` properties `keyMapping`. +/// - Remove initializer `init(from decoder: Decoder) throws`. +/// - Use `@ExCodable("", "")`. +/// + // MARK: - keyMapping @available(*, deprecated, message: "Use `@ExCodable` property wrapper instead.") -public protocol ExCodableProtocol: Codable { - associatedtype Root = Self where Root: ExCodableProtocol +public protocol ExCodableDEPRECATED: Codable { + associatedtype Root = Self where Root: ExCodableDEPRECATED static var keyMapping: [KeyMap] { get } } @available(*, deprecated) -public extension ExCodableProtocol where Root == Self { - // default implementation of ExCodableProtocol +public extension ExCodableDEPRECATED where Root == Self { + // default implementation of ExCodableDEPRECATED static var keyMapping: [KeyMap] { [] } // default implementation of Encodable func encode(to encoder: Encoder) throws { @@ -31,7 +47,7 @@ public extension ExCodableProtocol where Root == Self { } @available(*, deprecated) -public extension ExCodableProtocol { +public extension ExCodableDEPRECATED { 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`) } } @@ -98,7 +114,7 @@ public extension KeyMap { // MARK: - @available(*, deprecated) -public extension ExCodableProtocol { +public extension ExCodableDEPRECATED { @available(*, deprecated, renamed: "encode(to:with:nonnull:throws:)") func encode(with keyMapping: [KeyMap], using encoder: Encoder) { try? encode(to: encoder, with: keyMapping) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 7f7f646..594e36e 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -21,7 +21,7 @@ import Foundation /// <#key-mapping#> <#property-wrapper#> <#coding-key#> <#subscript#> /// <#alternative-keys#> <#nested-keys#> <#type-conversions#> /// -/// - seealso: [Usage](https://github.com/iwill/ExCodable#usage) from the `README.md` +/// - seealso: [Usage](https://github.com/ExCodable/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. @@ -157,7 +157,7 @@ public extension Decodable { // MARK: - Encoder & Decoder -public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false +public extension Encoder { // , nonnull nonnull: Bool = false, throws throws: Bool = false subscript(stringKey: String) -> T? { get { return nil } nonmutating set { encode(newValue, for: stringKey) } } @@ -166,7 +166,7 @@ public extension Encoder { // , abortIfNull nonnull: Bool = false, abortOnError } } -public extension Decoder { // , abortIfNull nonnull: Bool = false, abortOnError throws: Bool = false +public extension Decoder { // , nonnull nonnull: Bool = false, throws throws: Bool = false subscript(stringKeys: [String], converter converter: (any ExCodableDecodingTypeConverter.Type)? = nil) -> T? { return decode(stringKeys, as: T.self, converter: converter) } @@ -212,7 +212,7 @@ public extension Encoder { if nonnull { try container.encode(value, forKey: codingKey) } else { try container.encodeIfPresent(value, forKey: codingKey) } } - catch { if `throws` || nonnull { throw error } } + catch { if nonnull || `throws` { throw error } } } func encodeNonnullThrows(_ value: T, for codingKey: K) throws { @@ -231,7 +231,7 @@ public extension Encoder { if nonnull { try container.encode(value, forKey: codingKey) } else { try container.encodeIfPresent(value, forKey: codingKey) } } - catch { if `throws` || nonnull { throw error } } + catch { if nonnull || `throws` { throw error } } } } @@ -282,7 +282,7 @@ public extension Decoder { let container = try self.container(keyedBy: K.self) return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) } - catch { if `throws` || nonnull { throw error } } + catch { if nonnull || `throws` { throw error } } return nil } } @@ -320,7 +320,7 @@ fileprivate extension KeyedDecodingContainer { return value } - if (`throws` || nonnull) && firstError != nil { throw firstError! } + if (nonnull || `throws`) && firstError != nil { throw firstError! } return nil } @@ -346,7 +346,7 @@ fileprivate extension KeyedDecodingContainer { } } - if firstError != nil && (`throws` || nonnull) { throw firstError! } + if firstError != nil && (nonnull || `throws`) { throw firstError! } return nil } @@ -376,12 +376,13 @@ fileprivate extension KeyedDecodingContainer { return value } - if firstError != nil && (`throws` || nonnull) { throw firstError! } + if firstError != nil && (nonnull || `throws`) { throw firstError! } return nil } func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self, converter selfConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + // for nested optionals, e.g. `var int: Int??? = nil` let wrappedType = T?.wrappedType if type is Bool.Type || wrappedType is Bool.Type { @@ -509,6 +510,22 @@ public protocol ExCodableDecodingTypeConverter { // MARK: - Encodable & Decodable - external +// Methods defined in JSON&PList Encoder&Decoder +public protocol DataEncoder { + func encode(_ value: T) throws -> Data +} +public protocol DataDecoder { + func decode(_ type: T.Type, from data: Data) throws -> T +} + +extension JSONEncoder: DataEncoder {} +extension JSONDecoder: DataDecoder {} + +@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 {} + // Encodable.encode() -> Data? public extension Encodable { func encoded(using encoder: DataEncoder = JSONEncoder()) throws -> Data { @@ -571,32 +588,18 @@ public extension String { } } -// Methods from JSON&PList Encoder&Decoder -public protocol DataEncoder { - func encode(_ value: T) throws -> Data -} -public protocol DataDecoder { - func decode(_ type: T.Type, from data: Data) throws -> T -} - -extension JSONEncoder: DataEncoder {} -extension JSONDecoder: DataDecoder {} - -@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 {} +// MARK: - OptionalProtocol for Nested Optional // - seealso: https://forums.swift.org/t/challenge-finding-base-type-of-nested-optionals/25096 -fileprivate protocol OptionalProtocol { +public protocol OptionalProtocol { static var wrappedType: Any.Type { get } var wrapped: Any? { get } } extension Optional: OptionalProtocol { - fileprivate static var wrappedType: Any.Type { + public static var wrappedType: Any.Type { if let optional = Wrapped.self as? OptionalProtocol.Type { return optional.wrappedType } @@ -615,7 +618,7 @@ extension Optional: OptionalProtocol { } } -// MARK: TODO: DEPRECATED +// MARK: DEPRECATED internal extension Encoder { func _encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 2069d4d..a45afee 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -31,18 +31,22 @@ struct TestManualCodable: Equatable { extension TestManualCodable: Codable { enum Keys: CodingKey { - case int, i - case nested, string + case int + case nested, string, s } init(from decoder: Decoder) throws { if let container = try? decoder.container(keyedBy: Keys.self) { - if let int = (try? container.decodeIfPresent(Int.self, forKey: Keys.int)) - ?? (try? container.decodeIfPresent(Int.self, forKey: Keys.i)) { + if let int = try? container.decodeIfPresent(Int.self, forKey: Keys.int) { self.int = int } if let nestedContainer = try? container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested), - let string = try? nestedContainer.decodeIfPresent(String.self, forKey: Keys.string) { + let nestedNestedContainer = try? nestedContainer.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested), + let string = try? nestedNestedContainer.decodeIfPresent(String.self, forKey: Keys.string) { + self.string = string + } + else if let string = (try? container.decodeIfPresent(String.self, forKey: Keys.string)) + ?? (try? container.decodeIfPresent(String.self, forKey: Keys.s)) { self.string = string } } @@ -52,20 +56,38 @@ extension TestManualCodable: Codable { var container = encoder.container(keyedBy: Keys.self) try? container.encodeIfPresent(int, forKey: Keys.int) var nestedContainer = container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested) - try? nestedContainer.encodeIfPresent(string, forKey: Keys.string) + var nestedNestedContainer = nestedContainer.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested) + try? nestedNestedContainer.encodeIfPresent(string, forKey: Keys.string) } } -// MARK: struct +// MARK: DEPRECATED -struct TestStruct: Equatable { - @ExCodable("int") +struct TestExCodable_0_x: Equatable { private(set) var int: Int = 0 - @ExCodable("string") - private(set) var string: String? = nil + private(set) var string: String? +} + +@available(*, deprecated) +extension TestExCodable_0_x: ExCodableDEPRECATED { + static let keyMapping: [KeyMap] = [ + KeyMap(\.int, to: "int"), + KeyMap(\.string, to: "string") + ] + init(from decoder: Decoder) throws { + try decode(from: decoder, with: Self.keyMapping) + } +} + +// MARK: struct + +struct TestStruct: ExAutoCodable, Equatable { + @ExCodable("int") private(set) + var int: Int = 0 + @ExCodable("string") private(set) + var string: String? = nil var bool: Bool! } -extension TestStruct: ExAutoCodable {} // MARK: struct with enum @@ -73,81 +95,69 @@ enum TestEnum: Int, Codable { case zero = 0, one = 1 } -struct TestStructWithEnum: Equatable { - @ExCodable("enum") - private(set) var `enum`: TestEnum = .zero +struct TestStructWithEnum: ExAutoCodable, Equatable { + @ExCodable("enum") private(set) + var `enum` = TestEnum.zero } -extension TestStructWithEnum: ExAutoCodable {} -// MARK: alternative-keys & alternative-keyMapping +// MARK: alternative-keys -struct TestAlternativeKeys: Equatable { - @ExCodable("int", "i") +struct TestAlternativeKeys: ExAutoCodable, Equatable { + @ExCodable("int", "i") private(set) var int: Int = 0 - @ExCodable("string", "str", "s") + @ExCodable("string", "str", "s") private(set) var string: String! = nil } -extension TestAlternativeKeys: ExAutoCodable {} // MARK: nested-keys -struct TestNestedKeys: Equatable { - @ExCodable +struct TestNestedKeys: ExAutoCodable, Equatable { + @ExCodable private(set) var int: Int = 0 - @ExCodable("nested.nested.string") + @ExCodable("nested.nested.string") private(set) var string: String! = nil } -extension TestNestedKeys: ExAutoCodable {} // MARK: custom encode/decode -struct TestCustomEncodeDecode: Equatable { +fileprivate func message(for int: Int) -> String { + switch int { + case 100: return "Continue" + case 200: return "OK" + case 304: return "Not Modified" + case 403: return "Forbidden" + case 404: return "Not Found" + case 418: return "I'm a teapot" + case 500: return "Internal Server Error" + case 200..<400: return "success" + default: return "failure" + } +} + +struct TestCustomEncodeDecode: ExAutoCodable, Equatable { - @ExCodable(Keys.int) + @ExCodable("int") private(set) var int: Int = 0 - @ExCodable(encode: { encoder, value in encoder["nested.nested.string"] = value }, - decode: { decoder in return decoder["nested.nested.string"/*, converter: Self.self*/] }) + @ExCodable(encode: { encoder, value in + encoder["nested.nested.string"] = "" + }, decode: { decoder in + if let string: String = decoder["nested.nested.string"], + string != "" { + return string + } + if let int: Int = decoder["int"] { + return message(for: int) + } + return nil + }) private(set) var string: String? = nil - @ExCodable(encode: { encoder, value in encoder[Keys.bool] = value }, - decode: { decoder in return decoder[Keys.bool/*, converter: Self.self*/] }) + @ExCodable(encode: { encoder, value in encoder["bool"] = value }, + decode: { decoder in return decoder["bool"/*, converter: Self.self*/] }) private(set) var bool: Bool = false } -extension TestCustomEncodeDecode: Codable { - - private enum Keys: CodingKey { - case int, bool - } - private static let dddd = "dddd" - private func string(for int: Int) -> String { - switch int { - case 100: return "Continue" - case 200: return "OK" - case 304: return "Not Modified" - case 403: return "Forbidden" - case 404: return "Not Found" - case 418: return "I'm a teapot" - case 500: return "Internal Server Error" - case 200..<400: return "success" - default: return "failure" - } - } - - init(from decoder: Decoder) throws { - try decode(from: decoder, nonnull: false, throws: false) - string = decoder["nested.nested.string"/*, converter: Self.self*/] - if string == nil || string == Self.dddd { - string = string(for: int) - } - } - func encode(to encoder: Encoder) throws { - try encode(to: encoder, nonnull: false, throws: false) - encoder["nested.nested.string"] = Self.dddd - } -} - // MARK: let + subscripts + CodingKey, without default values struct TestSubscript: Equatable { @@ -155,7 +165,7 @@ struct TestSubscript: Equatable { let string: String } -extension TestSubscript: Encodable, Decodable { +extension TestSubscript: Codable { enum Keys: CodingKey { case int, string @@ -181,13 +191,12 @@ extension TestSubscript: Encodable, Decodable { // MARK: type-conversions -struct TestTypeConversion: Equatable { - @ExCodable("intFromString") +struct TestTypeConversion: ExAutoCodable, Equatable { + @ExCodable("intFromString") private(set) var intFromString: Int? = nil - @ExCodable("stringFromInt") - var stringFromInt: String???? = nil + @ExCodable("stringFromInt") private(set) + var stringFromInt: String??? = nil } -extension TestTypeConversion: ExAutoCodable {} struct TestTypeConversions: Equatable { let boolFromInt, boolFromString: Bool? @@ -266,31 +275,25 @@ extension TestTypeConversions: Encodable, Decodable { // } // } -struct TestCustomTypeConverter: Equatable { - @ExCodable("enum") - private(set) var `enum`: TestEnum = .zero - @ExCodable("doubleFromBool") +struct TestCustomTypeConverter: ExAutoCodable, Equatable { + @ExCodable("boolFromDouble") private(set) + var boolFromDouble: Bool? = nil + @ExCodable("doubleFromBool") private(set) var doubleFromBool: Double? = nil } -extension TestCustomTypeConverter: ExAutoCodable {} extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { static public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { - // String -> TestEnum - if type is TestEnum.Type || type is TestEnum?.Type { - if let string = try? container.decodeIfPresent(String.self, forKey: codingKey), - let int = Int(string) { - return TestEnum(rawValue: int) as? T - } - } - // Bool -> Double - if type is Double.Type || type is Double?.Type { + let wrappedType = T?.wrappedType + // NOT implemented: decode Bool from Double + // decode Double from Bool + if type is Double.Type || wrappedType is Double.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { return (bool ? 1.0 : 0.0) as? T } } - // Bool -> Float - else if type is Float.Type || type is Float?.Type { + // decode Float from Bool + else if type is Float.Type || wrappedType is Float.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { return (bool ? 1.0 : 0.0) as? T } @@ -302,23 +305,17 @@ extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { // MARK: class -class TestClass: Codable, Equatable { - - @ExCodable("int") +class TestClass: ExAutoCodable, Equatable { + @ExCodable private(set) var int: Int = 0 - @ExCodable("string") + @ExCodable private(set) var string: String? = nil + + required init() {} init(int: Int, string: String?) { (self.int, self.string) = (int, string) } - required init(from decoder: Decoder) throws { - try decode(from: decoder, nonnull: false, throws: false) - } - func encode(to encoder: Encoder) throws { - try encode(to: encoder, nonnull: false, throws: false) - } - static func == (lhs: TestClass, rhs: TestClass) -> Bool { return lhs.int == rhs.int && lhs.string == rhs.string } @@ -328,17 +325,15 @@ class TestClass: Codable, Equatable { class TestSubclass: TestClass { - @ExCodable("bool") + @ExCodable private(set) var bool: Bool = false + + required init() { super.init() } required init(int: Int, string: String, bool: Bool) { self.bool = bool super.init(int: int, string: string) } - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - static func == (lhs: TestSubclass, rhs: TestSubclass) -> Bool { return (lhs.int == rhs.int && lhs.string == rhs.string @@ -348,13 +343,12 @@ class TestSubclass: TestClass { // MARK: ExCodable -struct TestExCodable: Equatable { - @ExCodable("int") - private(set) var int: Int = 0 - @ExCodable("string") - private(set) var string: String? = nil +struct TestExCodable: ExAutoCodable, Equatable { + @ExCodable("int") private(set) + var int: Int = 0 + @ExCodable("string") private(set) + var string: String? = nil } -extension TestExCodable: ExAutoCodable {} // MARK: - Tests @@ -374,13 +368,28 @@ final class ExCodableTests: XCTestCase { } func testManualCodable() { - let json = Data(#"{"int":200,"nested":{"string":"OK"}}"#.utf8) + let json = Data(#"{"int":200,"nested":{"nested":{"string":"OK"}}}"#.utf8) if let test = try? json.decoded() as TestManualCodable, let data = try? test.encoded() as Data, let copy = try? data.decoded() as TestManualCodable { XCTAssertEqual(copy, test) let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] - XCTAssertEqual(NSDictionary(dictionary: json), ["int":200,"nested":["string":"OK"]]) + XCTAssertEqual(NSDictionary(dictionary: json), ["int":200,"nested":["nested":["string":"OK"]]]) + } + else { + XCTFail() + } + } + + @available(*, deprecated) + func testExCodable_0_x() { + let json = Data(#"{"int":200,"string":"OK"}"#.utf8) + if let test = try? json.decoded() as TestExCodable_0_x, + let data = try? test.encoded() as Data, + let copy = try? data.decoded() as TestExCodable_0_x { + XCTAssertEqual(copy, test) + let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + XCTAssertEqual(NSDictionary(dictionary: json), ["int":200,"string":"OK"]) } else { XCTFail() @@ -388,12 +397,13 @@ final class ExCodableTests: XCTestCase { } func testStruct() { - let test = TestStruct(int: 304, string: "Not Modified") + let test = TestStruct(int: 304, string: "Not Modified"), + test2 = TestStruct(int: 304, string: "Not Modified", bool: nil) if let data = try? test.encoded() as Data, - let copy1 = try? data.decoded() as TestStruct, + let copy = try? data.decoded() as TestStruct, let copy2 = try? TestStruct.decoded(from: data) { - XCTAssertEqual(copy1, test) - XCTAssertEqual(copy2, test) + XCTAssertEqual(copy, test) + XCTAssertEqual(copy2, test2) let string = "string: \(test)" print(string) @@ -505,7 +515,7 @@ final class ExCodableTests: XCTestCase { "int": 418, "nested": [ "nested": [ - "string": "dddd" + "string": "" ] ], "bool": true @@ -651,12 +661,12 @@ final class ExCodableTests: XCTestCase { func testCustomTypeConverter() { let data = Data(#""" { - "enum": "1", + "boolFromDouble": 1.2, "doubleFromBool": true } """#.utf8) if let test = try? data.decoded() as TestCustomTypeConverter { - XCTAssertEqual(test, TestCustomTypeConverter(enum: .one, doubleFromBool: 1.0)) + XCTAssertEqual(test, TestCustomTypeConverter(boolFromDouble: nil, doubleFromBool: 1.0)) } else { XCTFail() From b6099c8f6eb890f55d73ae8f81040f8d6cf2ccfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Thu, 1 Aug 2024 22:10:48 +0800 Subject: [PATCH 31/43] opt&fix: nonnull and throws, and tests --- Sources/ExCodable/ExCodable+DEPRECATED.swift | 44 +++- Sources/ExCodable/ExCodable.swift | 204 +++++++++++-------- Tests/ExCodableTests/ExCodableTests.swift | 184 ++++++++++++++--- 3 files changed, 305 insertions(+), 127 deletions(-) diff --git a/Sources/ExCodable/ExCodable+DEPRECATED.swift b/Sources/ExCodable/ExCodable+DEPRECATED.swift index d17c7a0..daf5b30 100644 --- a/Sources/ExCodable/ExCodable+DEPRECATED.swift +++ b/Sources/ExCodable/ExCodable+DEPRECATED.swift @@ -1,14 +1,14 @@ // -// ExCodable.swift +// ExCodable+DEPRECATED.swift // ExCodable // // Created by Míng on 2021-07-01. -// Copyright (c) 2023 Míng . Released under the MIT license. +// Copyright (c) 2021 Míng . Released under the MIT license. // import Foundation -// TODO: remove +// TODO: REMOVE /// Migration ExCodable from 0.x to 1.x: /// @@ -24,9 +24,31 @@ import Foundation /// - Use `@ExCodable("", "")`. /// +// MARK: adaptor + +@available(*, deprecated) +fileprivate extension Encoder { + func _encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { + try encode(value, for: stringKey, nonnull: nonnull, throws: `throws`) + } + func _encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws { + try encode(value, for: codingKey, nonnull: nonnull, throws: `throws`) + } +} + +@available(*, deprecated) +fileprivate extension Decoder { + func _decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { + return try decode(stringKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) + } + func _decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { + try decode(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) + } +} + // MARK: - keyMapping -@available(*, deprecated, message: "Use `@ExCodable` property wrapper instead.") +@available(*, deprecated, message: "Use property wrapper `@ExCodable` instead.") public protocol ExCodableDEPRECATED: Codable { associatedtype Root = Self where Root: ExCodableDEPRECATED static var keyMapping: [KeyMap] { get } @@ -59,7 +81,7 @@ public extension ExCodableDEPRECATED { } } -@available(*, deprecated, message: "Use `@ExCodable` property wrapper instead.") +@available(*, deprecated, message: "Use property wrapper `@ExCodable` 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)? @@ -115,27 +137,27 @@ public extension KeyMap { @available(*, deprecated) public extension ExCodableDEPRECATED { - @available(*, deprecated, renamed: "encode(to:with:nonnull:throws:)") + @available(*, deprecated) func encode(with keyMapping: [KeyMap], using encoder: Encoder) { try? encode(to: encoder, with: keyMapping) } - @available(*, deprecated, renamed: "decode(from:with:nonnull:throws:)") + @available(*, deprecated) mutating func decode(with keyMapping: [KeyMap], using decoder: Decoder) { try? decode(from: decoder, with: keyMapping) } - @available(*, deprecated, renamed: "decodeReference(from:with:nonnull:throws:)") + @available(*, deprecated) func decodeReference(with keyMapping: [KeyMap], using decoder: Decoder) { try? decodeReference(from: decoder, with: keyMapping) } } -@available(*, deprecated, renamed: "append(decodingTypeConverter:)") +@available(*, deprecated) public protocol KeyedDecodingContainerCustomTypeConversion: ExCodableDecodingTypeConverter { - func decodeForTypeConversion(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? + static 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? { + static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { return decodeForTypeConversion(container, codingKey: codingKey, as: type) } } diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 594e36e..7dca2e6 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -3,24 +3,24 @@ // ExCodable // // Created by Míng on 2021-02-10. -// Copyright (c) 2023 Míng . Released under the MIT license. +// Copyright (c) 2021 Míng . Released under the MIT license. // import Foundation /// # ExCodable /// -/// - `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#> <#property-wrapper#> <#coding-key#> <#subscript#> /// <#alternative-keys#> <#nested-keys#> <#type-conversions#> /// +/// - `ExCodable`: A `@propertyWrapper` associates JSON keys to properties. +/// - `ExAutoEncodable` & `ExAutoDecodable`: Protocols with default implementation for Encodable & Decodable. +/// - `ExAutoCodable`: A typealias for `ExAutoEncodable & ExAutoDecodable`. +/// - `Encodable` & `Decodable` extensions for encoding/decoding from internal/external. +/// - `Encoder` & `Encoder` extensions for encoding/decoding properties one by one. +/// - Supports alternative keys, nested keys, type conversions, nested optionals and default values. +/// /// - seealso: [Usage](https://github.com/ExCodable/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) @@ -87,23 +87,33 @@ fileprivate protocol ExCodablePropertyWrapper { } extension ExCodable: ExCodablePropertyWrapper { fileprivate func encode(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws { - if encode != nil { try encode!(encoder, wrappedValue) } + let deepWrapped = if let optional = wrappedValue as? OptionalProtocol { + optional.wrapped + } else { - let value = if let optional = wrappedValue as? OptionalProtocol { - optional.wrapped + wrappedValue + } + // !!!: NOT `if let deepWrapped, self.nonnull ?? nonnull` + if deepWrapped != nil || self.nonnull ?? nonnull { + if let encode { + try encode(encoder, wrappedValue) } else { - wrappedValue - } - if value != nil || self.nonnull ?? nonnull { try encoder.encode(wrappedValue, for: stringKeys?.first ?? String(label), nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`) } } } fileprivate func decode(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws { - if let value = (decode != nil ? try decode!(decoder) - : decodeRawRepresentable != nil ? try decodeRawRepresentable!(decoder, stringKeys ?? [String(label)], self.nonnull ?? nonnull, self.throws ?? `throws`, converter) - : try decoder.decode(stringKeys ?? [String(label)], as: Value.self, nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`, converter: converter)) { + let value = if let decode { + try decode(decoder) + } + else if let decodeRawRepresentable { + try decodeRawRepresentable(decoder, stringKeys ?? [String(label)], self.nonnull ?? nonnull, self.throws ?? `throws`, converter) + } + else { + try decoder.decode(stringKeys ?? [String(label)], as: Value.self, nonnull: self.nonnull ?? nonnull, throws: self.throws ?? `throws`, converter: converter) + } + if let value { wrappedValue = value } } @@ -128,9 +138,10 @@ public extension ExAutoDecodable { public typealias ExAutoCodable = ExAutoEncodable & ExAutoDecodable -// MARK: - Encodable & Decodable - internal +// MARK: - Encodable & Decodable -public extension Encodable { +// TODO: internal -> fileprivate +internal extension Encodable { func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { @@ -142,7 +153,8 @@ public extension Encodable { } } -public extension Decodable { +// TODO: internal -> fileprivate +internal extension Decodable { func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws { var mirror: Mirror! = Mirror(reflecting: self) while mirror != nil { @@ -193,7 +205,8 @@ public extension Encoder { try? encode(value, for: stringKey, nonnull: false, throws: false) } - fileprivate func encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { + // TODO: internal -> fileprivate + internal 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 { @@ -208,11 +221,19 @@ public extension Encoder { } let codingKey = keys.last! - do { - if nonnull { try container.encode(value, forKey: codingKey) } - else { try container.encodeIfPresent(value, forKey: codingKey) } + if nonnull { + if value.wrapped == nil { + let desc = "Expected to encode nonnull for \(T?.wrappedType) but found null value instead." + throw EncodingError.invalidValue(value as Any, .init(codingPath: [ExCodingKey(stringKey)], debugDescription: desc)) + } + try container.encode(value, forKey: codingKey) + } + else if `throws` { + try container.encodeIfPresent(value, forKey: codingKey) + } + else { + try? container.encodeIfPresent(value, forKey: codingKey) } - catch { if nonnull || `throws` { throw error } } } func encodeNonnullThrows(_ value: T, for codingKey: K) throws { @@ -225,13 +246,22 @@ public extension Encoder { try? encode(value, for: codingKey, nonnull: false, throws: false) } - fileprivate func encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws { + // TODO: internal -> fileprivate + internal 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) } - else { try container.encodeIfPresent(value, forKey: codingKey) } + if nonnull { + if value.wrapped == nil { + let desc = "Expected to encode nonnull for \(T?.wrappedType) but found null value instead." + throw EncodingError.invalidValue(value as Any, .init(codingPath: [codingKey], debugDescription: desc)) + } + try container.encode(value, forKey: codingKey) + } + else if `throws` { + try container.encodeIfPresent(value, forKey: codingKey) + } + else { + try? container.encodeIfPresent(value, forKey: codingKey) } - catch { if nonnull || `throws` { throw error } } } } @@ -255,7 +285,9 @@ public extension Decoder { func decode(_ stringKeys: [String], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { return try? decode(stringKeys, as: type, nonnull: false, throws: false, converter: converter) } - fileprivate func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + + // TODO: internal -> fileprivate + internal func decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { return try decode(stringKeys.map { ExCodingKey($0) }, as: type, nonnull: nonnull, throws: `throws`, converter: converter) } @@ -277,12 +309,12 @@ public extension Decoder { func decode(_ codingKeys: [K], as type: T.Type = T.self, converter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { return try? decode(codingKeys, as: type, nonnull: false, throws: false, converter: converter) } - fileprivate func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { - do { - let container = try self.container(keyedBy: K.self) + + // TODO: internal -> fileprivate + internal func decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { + if let container = nonnull || `throws` ? try self.container(keyedBy: K.self) : try? self.container(keyedBy: K.self) { return try container.decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) } - catch { if nonnull || `throws` { throw error } } return nil } } @@ -305,48 +337,56 @@ fileprivate extension KeyedDecodingContainer { func decodeForAlternativeKeys(_ codingKeys: [Self.Key], as type: T.Type = T.self, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { - var firstError: Error? + var caughtError: Error? + do { let codingKey = codingKeys.first! if let value = try decodeForNestedKeys(codingKey, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { return value } } - catch { firstError = error } + catch { caughtError = error } - let codingKeys = Array(codingKeys.dropFirst()) - if !codingKeys.isEmpty, - let value = try? decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { - return value + do { + let codingKeys = Array(codingKeys.dropFirst()) + if !codingKeys.isEmpty, + let value = try decodeForAlternativeKeys(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { + return value + } } + catch { caughtError = caughtError ?? error } - if (nonnull || `throws`) && firstError != nil { throw firstError! } + if let caughtError { throw caughtError } return nil } func decodeForNestedKeys(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { - var firstError: Error? + var caughtError: Error? + do { if let value = try decodeForValue(codingKey, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { return value } } - catch { firstError = error } + catch { caughtError = error } - let dot: Character = "." - if let exCodingKey = codingKey as? ExCodingKey, // Self.Key is ExCodingKey.Type - exCodingKey.intValue == nil && exCodingKey.stringValue.contains(dot) { - let keys = exCodingKey.stringValue.split(separator: dot).map { ExCodingKey($0) } - if !keys.isEmpty, - let container = nestedContainer(with: keys.dropLast()), - let codingKey = keys.last, - let value = try? container.decodeForNestedKeys(codingKey as! Self.Key, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { - return value + do { + let dot: Character = "." + if let exCodingKey = codingKey as? ExCodingKey, // Self.Key is ExCodingKey.Type + exCodingKey.intValue == nil && exCodingKey.stringValue.contains(dot) { + let keys = exCodingKey.stringValue.split(separator: dot).map { ExCodingKey($0) } + if !keys.isEmpty, + let container = nestedContainer(with: keys.dropLast()), + let codingKey = keys.last, + let value = try container.decodeForNestedKeys(codingKey as! Self.Key, as: type, nonnull: nonnull, throws: `throws`, converter: converter) { + return value + } } } + catch { caughtError = caughtError ?? error } - if firstError != nil && (nonnull || `throws`) { throw firstError! } + if let caughtError { throw caughtError } return nil } @@ -361,26 +401,34 @@ fileprivate extension KeyedDecodingContainer { func decodeForValue(_ codingKey: Self.Key, as type: T.Type = T.self, nonnull: Bool, throws: Bool, converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> T? { - var firstError: Error? + var caughtError: Error? do { - if let value = (nonnull - ? (`throws` ? try decode(type, forKey: codingKey) : try? decode(type, forKey: codingKey)) - : (`throws` ? try decodeIfPresent(type, forKey: codingKey) : try? decodeIfPresent(type, forKey: codingKey))) { + if nonnull { + let value = try decode(type, forKey: codingKey) + if let optional = value as? OptionalProtocol, optional.wrapped == nil { + let desc = "Expected to decode nonnull for \(type) but found null value instead." + throw DecodingError.valueNotFound(type, .init(codingPath: [codingKey], debugDescription: desc)) + } + return value + } + if let value = `throws` + ? try decodeIfPresent(type, forKey: codingKey) + : try? decodeIfPresent(type, forKey: codingKey) { return value } } - catch { firstError = error } + catch { caughtError = error } if contains(codingKey), let value = decodeForTypeConversion(codingKey, as: type, converter: converter) { return value } - if firstError != nil && (nonnull || `throws`) { throw firstError! } + if let caughtError { throw caughtError } return nil } - func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self, converter selfConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { + func decodeForTypeConversion(_ codingKey: Self.Key, as type: T.Type = T.self, converter specificConverter: (any ExCodableDecodingTypeConverter.Type)?) -> T? { // for nested optionals, e.g. `var int: Int??? = nil` let wrappedType = T?.wrappedType @@ -489,14 +537,15 @@ fileprivate extension KeyedDecodingContainer { } #endif - // specific converter for type `T`, via `extension T: ExCodableDecodingTypeConverter` - if let selfConverter, - let value = try? selfConverter.decode(self, codingKey: codingKey, as: type) { + // converter for specific type, via `extension T: ExCodableDecodingTypeConverter` + if let specificConverter, + let value = specificConverter.decode(self, codingKey: codingKey, as: type) { return value } + // global converter for all types, via `extension KeyedDecodingContainer: ExCodableDecodingTypeConverter` - if let globalConverter = Self.self as? ExCodableDecodingTypeConverter.Type, - let value = try? globalConverter.decode(self, codingKey: codingKey, as: type) { + if let globalConverter = ExCodableGlobalDecodingTypeConverter.self as? ExCodableDecodingTypeConverter.Type, + let value = globalConverter.decode(self, codingKey: codingKey, as: type) { return value } @@ -505,8 +554,9 @@ fileprivate extension KeyedDecodingContainer { } public protocol ExCodableDecodingTypeConverter { - static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) throws -> T? + static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? } +public struct ExCodableGlobalDecodingTypeConverter {} // MARK: - Encodable & Decodable - external @@ -617,23 +667,3 @@ extension Optional: OptionalProtocol { } } } - -// MARK: DEPRECATED - -internal extension Encoder { - func _encode(_ value: T?, for stringKey: String, nonnull: Bool = false, throws: Bool = false) throws { - try encode(value, for: stringKey, nonnull: nonnull, throws: `throws`) - } - func _encode(_ value: T?, for codingKey: K, nonnull: Bool = false, throws: Bool = false) throws { - try encode(value, for: codingKey, nonnull: nonnull, throws: `throws`) - } -} - -internal extension Decoder { - func _decode(_ stringKeys: [String], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { - return try decode(stringKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) - } - func _decode(_ codingKeys: [K], as type: T.Type = T.self, nonnull: Bool = false, throws: Bool = false, converter: (any ExCodableDecodingTypeConverter.Type)? = nil) throws -> T? { - try decode(codingKeys, as: type, nonnull: nonnull, throws: `throws`, converter: converter) - } -} diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index a45afee..e35b79d 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -118,7 +118,7 @@ struct TestNestedKeys: ExAutoCodable, Equatable { var string: String! = nil } -// MARK: custom encode/decode +// MARK: custom encoding/decoding fileprivate func message(for int: Int) -> String { switch int { @@ -255,37 +255,19 @@ extension TestTypeConversions: Encodable, Decodable { // MARK: custom type-conversions -// @available(*, deprecated) -// extension KeyedDecodingContainer: KeyedDecodingContainerCustomTypeConversion { -// public func decodeForTypeConversion(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? where T: Decodable, K: CodingKey { -// -// if type is Double.Type || type is Double?.Type { -// if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey as! Self.Key) { -// return (bool ? 1.0 : 0.0) as? T -// } -// } -// -// else if type is Float.Type || type is Float?.Type { -// if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey as! Self.Key) { -// return (bool ? 1.0 : 0.0) as? T -// } -// } -// -// return nil -// } -// } - struct TestCustomTypeConverter: ExAutoCodable, Equatable { - @ExCodable("boolFromDouble") private(set) - var boolFromDouble: Bool? = nil @ExCodable("doubleFromBool") private(set) var doubleFromBool: Double? = nil + @ExCodable("boolFromDouble") private(set) + var boolFromDouble: Bool? = nil } extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { - static public func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + + // for nested optionals, e.g. `var int: Int??? = nil` let wrappedType = T?.wrappedType - // NOT implemented: decode Bool from Double + // decode Double from Bool if type is Double.Type || wrappedType is Double.Type { if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { @@ -298,7 +280,24 @@ extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { return (bool ? 1.0 : 0.0) as? T } } - // Double or Float NOT found + + return nil + } +} + +extension ExCodableGlobalDecodingTypeConverter: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer<_K>, codingKey: _K, as type: T.Type) -> T? { + + // for nested optionals, e.g. `var int: Int??? = nil` + let wrappedType = T?.wrappedType + + // decode Bool from Double + if type is Bool.Type || wrappedType is Bool.Type { + if let double = try? container.decodeIfPresent(Double.self, forKey: codingKey) { + return (double != 0) as? T + } + } + return nil } } @@ -341,6 +340,33 @@ class TestSubclass: TestClass { } } +// MARK: nonnull and `throws` + +struct TestNonnull: ExAutoCodable, Equatable { + @ExCodable("int", nonnull: true) private(set) + var nonnullInt: Int! = 0 +} + +struct TestNestedNonnull: ExAutoCodable, Equatable { + @ExCodable("nested.int", "int", nonnull: true) private(set) + var nonnullInt: Int! = 0 +} + +struct TestThrows: ExAutoCodable, Equatable { + @ExCodable("string", "nested.string", throws: true) private(set) + var throwsString: String! = "" +} + +struct TestNestedThrows: ExAutoCodable, Equatable { + @ExCodable("nested.string", "string", throws: true) private(set) + var throwsString: String! = "" +} + +struct TestNonnullWithThrows: ExAutoCodable, Equatable { + @ExCodable("throws", nonnull: true) private(set) + var testThrows: TestThrows! = nil +} + // MARK: ExCodable struct TestExCodable: ExAutoCodable, Equatable { @@ -661,12 +687,12 @@ final class ExCodableTests: XCTestCase { func testCustomTypeConverter() { let data = Data(#""" { - "boolFromDouble": 1.2, - "doubleFromBool": true + "doubleFromBool": true, + "boolFromDouble": 1.2 } """#.utf8) if let test = try? data.decoded() as TestCustomTypeConverter { - XCTAssertEqual(test, TestCustomTypeConverter(boolFromDouble: nil, doubleFromBool: 1.0)) + XCTAssertEqual(test, TestCustomTypeConverter(doubleFromBool: 1.0, boolFromDouble: true)) } else { XCTFail() @@ -695,6 +721,106 @@ final class ExCodableTests: XCTestCase { } } + func testNonnullAndThrows() { + + let noIntData = Data(#""" + { + "noInt": true + } + """#.utf8) + XCTAssertThrowsError(try noIntData.decoded() as TestNonnull) { error in + XCTAssertNotNil(error) + print("TestNonnull error: \(error)") + } + XCTAssertThrowsError(try noIntData.decoded() as TestNestedNonnull) { error in + XCTAssertNotNil(error) + print("TestNestedNonnull error: \(error)") + } + + let nullData = Data(#""" + { + "int": null, + "nested": { + "int": null + } + } + """#.utf8) + XCTAssertThrowsError(try nullData.decoded() as TestNonnull) { error in + XCTAssertNotNil(error) + print("TestNonnull error: \(error)") + } + XCTAssertThrowsError(try nullData.decoded() as TestNestedNonnull) { error in + XCTAssertNotNil(error) + print("TestNestedNonnull error: \(error)") + } + + let nonnullData = Data(#""" + { + "int": 1, + "nested": { + "int": 1 + } + } + """#.utf8) + XCTAssertEqual(try nonnullData.decoded() as TestNonnull, TestNonnull(nonnullInt: 1)) + XCTAssertEqual(try nonnullData.decoded() as TestNestedNonnull, TestNestedNonnull(nonnullInt: 1)) + + let throwsData = Data(#""" + { + "string": [], + "nested": { + "string": [] + } + } + """#.utf8) + XCTAssertThrowsError(try throwsData.decoded() as TestThrows) { error in + XCTAssertNotNil(error) + print("TestThrows error: \(error)") + } + XCTAssertThrowsError(try throwsData.decoded() as TestNestedThrows) { error in + XCTAssertNotNil(error) + print("TestNestedThrows error: \(error)") + } + + let noThrowsData = Data(#""" + { + "string": 123 + } + """#.utf8) + XCTAssertEqual(try noThrowsData.decoded() as TestThrows, TestThrows(throwsString: "123")) + XCTAssertEqual(try noThrowsData.decoded() as TestNestedThrows, TestNestedThrows(throwsString: "123")) + + let nonnullWithThrowsData = Data(#""" + { + "throws": { + "string": [] + } + } + """#.utf8) + XCTAssertThrowsError(try nonnullWithThrowsData.decoded() as TestNonnullWithThrows) { error in + XCTAssertNotNil(error) + print("TestNonnullWithThrows error: \(error)") + } + + XCTAssertThrowsError(try TestNonnull(nonnullInt: nil).encoded() as Data) { error in + XCTAssertNotNil(error) + print("TestNonnull error: \(error)") + } + XCTAssertThrowsError(try TestNestedNonnull(nonnullInt: nil).encoded() as Data) { error in + XCTAssertNotNil(error) + print("TestNestedNonnull error: \(error)") + } + + XCTAssertThrowsError(try TestThrows(throwsString: nil).encoded() as Data) { error in + XCTAssertNotNil(error) + print("TestThrows error: \(error)") + } + XCTAssertThrowsError(try TestNestedThrows(throwsString: nil).encoded() as Data) { error in + XCTAssertNotNil(error) + print("TestNestedThrows error: \(error)") + } + } + func testExCodable() { let array = [ TestExCodable(int: 100, string: "Continue"), From 1f1f43a795a3030f39522341760f6b05d7b9cb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Fri, 2 Aug 2024 17:50:40 +0800 Subject: [PATCH 32/43] meta: readme --- README.md | 63 ++++++++++++++++++++----------------------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 8d5111e..190f18c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![ExCodable](https://iwill.im/images/ExCodable-1920x500.png)](#readme) +[![ExCodable](https://iwill.im/images/ExCodable-1.x-1920x500.png)](#readme) -[![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg)](https://swift.org/) +[![Swift 5.10](https://img.shields.io/badge/Swift-5.10-orange.svg)](https://swift.org/) [![Swift Package Manager](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) [![Platforms](https://img.shields.io/cocoapods/p/ExCodable.svg)](#readme)
@@ -52,7 +52,7 @@ struct TestExCodable: ExAutoCodable { - Supports **nested keys** via `String` with dot syntax. - Supports builtin and custom **type conversions**, including **nested optionals** as well. - Supports manual encoding/decoding using **subscripts**. -- Supports **continue or abort** if error encountered - returns nil or throws error. +- Supports `return nil` or `throw error` when encoding/decoding failed. - Supports **type inference**, including JSON `Data`, `String` and objects. - Uses JSON encoder/decoder by default, supports PList and custom encoder/decoder. @@ -231,6 +231,7 @@ struct TestCustomTypeConverter: ExAutoCodable { } extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { // for nested optionals, e.g. `var int: Int??? = nil` @@ -264,6 +265,7 @@ struct TestCustomGlobalTypeConverter: ExAutoCodable, Equatable { } extension ExCodableGlobalDecodingTypeConverter: ExCodableDecodingTypeConverter { + public static func decode(_ container: KeyedDecodingContainer<_K>, codingKey: _K, as type: T.Type) -> T? { // for nested optionals, e.g. `var int: Int??? = nil` @@ -323,7 +325,6 @@ class TestClass: ExAutoCodable { var int: Int = 0 @ExCodable private(set) var string: String? = nil - required init() {} init(int: Int, string: String?) { (self.int, self.string) = (int, string) @@ -334,25 +335,31 @@ class TestClass: ExAutoCodable { ```swift class TestSubclass: TestClass { - @ExCodable private(set) var bool: Bool = false - required init() { super.init() } required init(int: Int, string: String, bool: Bool) { self.bool = bool super.init(int: int, string: string) } - } ``` -### 8. Continue or Abort +### 8. `return nil` or `throw error` - UNSTABLE + +While encoding/decoding, ExCodable ignores the `keyNotFound`, `valueNotFound` and `typeMismatch` errors and `return nil` by default. + +When encoding/decoding failed: + +- Use `nonnull: true` to throw `EncodingError.invalidValue`, `DecodingError.keyNotFound`, `DecodingError.valueNotFound`. +- Use `throws: true` to throw `DecodingError.typeMismatch`. ```swift -encoding/decoding -nonnull || `throws` +struct TestNonnullAndThrows: ExAutoCodable { + @ExCodable("int", nonnull: true, throws: true) private(set) + var nonnullInt: Int! = 0 +} ``` @@ -361,6 +368,7 @@ nonnull || `throws` ```swift let test = TestStruct(int: 304, string: "Not Modified"), test2 = TestStruct(int: 304, string: "Not Modified") + // type of `data` inferenced from `Data` // types of `copy` and `copy2` inferenced from `TestStruct` if let data = try? test.encoded() as Data, @@ -377,51 +385,26 @@ else { ## Requirements -- iOS 8.0+ | tvOS 9.0+ | macOS X 10.10+ | watchOS 2.0+ -- Xcode 12.0+ -- Swift 5.0+ +- iOS 12.0+ | tvOS 12.0+ | macOS 11.0+ | watchOS 4.0+ +- Xcode 15.4+ +- Swift 5.10+ ## Installation -- [Swift Package Manager](https://swift.org/package-manager/): +- Swift Package Manager: ```swift .package(url: "https://github.com/ExCodable/ExCodable", from: "1.0.0") ``` -- [CocoaPods](http://cocoapods.org): +- CocoaPods: ```ruby pod 'ExCodable', '~> 1.0.0' ``` -- Code Snippets: - -> Title: ExCodable -> Summary: Adopt to ExCodable protocol -> Language: Swift -> Platform: All -> Completion: ExCodable -> Availability: Top Level - -```swift -<#extension/struct/class#> <#Type#>: ExCodable { - static let <#keyMapping#>: [KeyMap<<#SelfType#>>] = [ - KeyMap(\.<#property#>, to: <#"key"#>), - <#...#> - ] - init(from decoder: Decoder) throws { - try decode<#Reference#>(from: decoder, with: Self.<#keyMapping#>) - } - func encode(to encoder: Encoder) throws { - try encode(to: encoder, with: Self.<#keyMapping#>) - } -} - -``` - ## Migration ### 0.x to 1.x From 6051c85529c3e47eea82e9320a14993a204726d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Sun, 4 Aug 2024 21:36:57 +0800 Subject: [PATCH 33/43] opt: test --- Tests/ExCodableTests/ExCodableTests.swift | 82 ++++++++++++----------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index e35b79d..4ee54ef 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -383,7 +383,7 @@ final class ExCodableTests: XCTestCase { func testAutoCodable() { let test = TestAutoCodable(int: 100, string: "Continue") if let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestAutoCodable, + let copy = try? TestAutoCodable.decoded(from: data), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { XCTAssertEqual(copy, test) XCTAssertEqual(NSDictionary(dictionary: json), ["i": 100, "s": "Continue"]) @@ -395,6 +395,7 @@ final class ExCodableTests: XCTestCase { func testManualCodable() { let json = Data(#"{"int":200,"nested":{"nested":{"string":"OK"}}}"#.utf8) + // decoded with type-inference if let test = try? json.decoded() as TestManualCodable, let data = try? test.encoded() as Data, let copy = try? data.decoded() as TestManualCodable { @@ -410,9 +411,9 @@ final class ExCodableTests: XCTestCase { @available(*, deprecated) func testExCodable_0_x() { let json = Data(#"{"int":200,"string":"OK"}"#.utf8) - if let test = try? json.decoded() as TestExCodable_0_x, + if let test = try? TestExCodable_0_x.decoded(from: json), let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestExCodable_0_x { + let copy = try? TestExCodable_0_x.decoded(from: data) { XCTAssertEqual(copy, test) let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] XCTAssertEqual(NSDictionary(dictionary: json), ["int":200,"string":"OK"]) @@ -426,7 +427,7 @@ final class ExCodableTests: XCTestCase { let test = TestStruct(int: 304, string: "Not Modified"), test2 = TestStruct(int: 304, string: "Not Modified", bool: nil) if let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestStruct, + let copy = try? TestStruct.decoded(from: data), let copy2 = try? TestStruct.decoded(from: data) { XCTAssertEqual(copy, test) XCTAssertEqual(copy2, test2) @@ -444,7 +445,7 @@ final class ExCodableTests: XCTestCase { func testStructWithEnum() { let test = TestStructWithEnum(enum: .one) if let data = try? test.encoded() as Data, - let copy1 = try? data.decoded() as TestStructWithEnum, + let copy1 = try? TestStructWithEnum.decoded(from: data), let copy2 = try? TestStructWithEnum.decoded(from: data) { XCTAssertEqual(copy1, test) XCTAssertEqual(copy2, test) @@ -459,9 +460,9 @@ final class ExCodableTests: XCTestCase { func testStructWithEnumFromJSON() { let json = Data(#"{"enum":1}"#.utf8) - if let test = try? json.decoded() as TestStructWithEnum, + if let test = try? TestStructWithEnum.decoded(from: json), let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestStructWithEnum { + let copy = try? TestStructWithEnum.decoded(from: data) { XCTAssertEqual(test, TestStructWithEnum(enum: .one)) XCTAssertEqual(copy, test) let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] @@ -476,9 +477,9 @@ final class ExCodableTests: XCTestCase { func testStructWithEnumFromJSONWithString() { let json = Data(#"{"enum":"1"}"#.utf8) - if let test = try? json.decoded() as TestStructWithEnum, + if let test = try? TestStructWithEnum.decoded(from: json), let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestStructWithEnum { + let copy = try? TestStructWithEnum.decoded(from: data) { XCTAssertEqual(test, TestStructWithEnum(enum: .one)) XCTAssertEqual(copy, test) let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] @@ -493,9 +494,9 @@ final class ExCodableTests: XCTestCase { func testAlternativeKeys() { let json = Data(#"{"i":403,"s":"Forbidden"}"#.utf8) - if let test = try? json.decoded() as TestAlternativeKeys, + if let test = try? TestAlternativeKeys.decoded(from: json), let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestAlternativeKeys { + let copy = try? TestAlternativeKeys.decoded(from: data) { XCTAssertEqual(test, TestAlternativeKeys(int: 403, string: "Forbidden")) XCTAssertEqual(copy, test) let localJSON = try! JSONSerialization.jsonObject(with: data) as! [String: Any] @@ -512,7 +513,7 @@ final class ExCodableTests: XCTestCase { func testNestedKeys() { let test = TestNestedKeys(int: 404, string: "Not Found") if let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestNestedKeys { + let copy = try? TestNestedKeys.decoded(from: data) { XCTAssertEqual(copy, test) let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] debugPrint(json) @@ -533,7 +534,7 @@ final class ExCodableTests: XCTestCase { func testCustomEncodeDecode() { 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 { + let copy = try? TestCustomEncodeDecode.decoded(from: data) { XCTAssertEqual(copy, test) let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] debugPrint(json) @@ -555,7 +556,7 @@ final class ExCodableTests: XCTestCase { func testSubscript() { let test = TestSubscript(int: 500, string: "Internal Server Error") if let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestSubscript { + let copy = try? TestSubscript.decoded(from: data) { XCTAssertEqual(copy, test) } else { @@ -572,7 +573,7 @@ final class ExCodableTests: XCTestCase { } """#.utf8) - if let test = try? data.decoded() as TestTypeConversion { + if let test = try? TestTypeConversion.decoded(from: data) { XCTAssertEqual(test, TestTypeConversion(intFromString: 123, stringFromInt: "456")) } else { @@ -584,7 +585,7 @@ final class ExCodableTests: XCTestCase { "stringFromInt64": 123 } """#.utf8) - if let test2 = try? data2.decoded() as TestTypeConversions { + if let test2 = try? TestTypeConversions.decoded(from: data2) { XCTAssertEqual(test2, TestTypeConversions(boolFromInt: nil, boolFromString: nil, intFromBool: nil, @@ -625,7 +626,7 @@ final class ExCodableTests: XCTestCase { "stringFromDouble": 12.3 } """#.utf8) - if let test = try? data.decoded() as TestTypeConversions { + if let test = try? TestTypeConversions.decoded(from: data) { XCTAssertEqual(test, TestTypeConversions(boolFromInt: true, boolFromString: true, intFromBool: 1, @@ -663,7 +664,7 @@ final class ExCodableTests: XCTestCase { "stringFromDouble": 12.3 } """#.utf8) - if let test2 = try? data2.decoded() as TestTypeConversions { + if let test2 = try? TestTypeConversions.decoded(from: data2) { XCTAssertEqual(test2, TestTypeConversions(boolFromInt: false, boolFromString: nil, intFromBool: 0, @@ -691,7 +692,7 @@ final class ExCodableTests: XCTestCase { "boolFromDouble": 1.2 } """#.utf8) - if let test = try? data.decoded() as TestCustomTypeConverter { + if let test = try? TestCustomTypeConverter.decoded(from: data) { XCTAssertEqual(test, TestCustomTypeConverter(doubleFromBool: 1.0, boolFromDouble: true)) } else { @@ -702,7 +703,7 @@ final class ExCodableTests: XCTestCase { func testClass() { let test = TestClass(int: 502, string: "Bad Gateway") if let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestClass { + let copy = try? TestClass.decoded(from: data) { XCTAssertEqual(copy, test) } else { @@ -713,7 +714,7 @@ final class ExCodableTests: XCTestCase { func testSubclass() { let test = TestSubclass(int: 504, string: "Gateway Timeout", bool: true) if let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestSubclass { + let copy = try? TestSubclass.decoded(from: data) { XCTAssertEqual(copy, test) } else { @@ -728,11 +729,11 @@ final class ExCodableTests: XCTestCase { "noInt": true } """#.utf8) - XCTAssertThrowsError(try noIntData.decoded() as TestNonnull) { error in + XCTAssertThrowsError(try TestNonnull.decoded(from: noIntData)) { error in XCTAssertNotNil(error) print("TestNonnull error: \(error)") } - XCTAssertThrowsError(try noIntData.decoded() as TestNestedNonnull) { error in + XCTAssertThrowsError(try TestNestedNonnull.decoded(from: noIntData)) { error in XCTAssertNotNil(error) print("TestNestedNonnull error: \(error)") } @@ -745,11 +746,11 @@ final class ExCodableTests: XCTestCase { } } """#.utf8) - XCTAssertThrowsError(try nullData.decoded() as TestNonnull) { error in + XCTAssertThrowsError(try TestNonnull.decoded(from: nullData)) { error in XCTAssertNotNil(error) print("TestNonnull error: \(error)") } - XCTAssertThrowsError(try nullData.decoded() as TestNestedNonnull) { error in + XCTAssertThrowsError(try TestNestedNonnull.decoded(from: nullData)) { error in XCTAssertNotNil(error) print("TestNestedNonnull error: \(error)") } @@ -762,8 +763,8 @@ final class ExCodableTests: XCTestCase { } } """#.utf8) - XCTAssertEqual(try nonnullData.decoded() as TestNonnull, TestNonnull(nonnullInt: 1)) - XCTAssertEqual(try nonnullData.decoded() as TestNestedNonnull, TestNestedNonnull(nonnullInt: 1)) + XCTAssertEqual(try TestNonnull.decoded(from: nonnullData), TestNonnull(nonnullInt: 1)) + XCTAssertEqual(try TestNestedNonnull.decoded(from: nonnullData), TestNestedNonnull(nonnullInt: 1)) let throwsData = Data(#""" { @@ -773,11 +774,11 @@ final class ExCodableTests: XCTestCase { } } """#.utf8) - XCTAssertThrowsError(try throwsData.decoded() as TestThrows) { error in + XCTAssertThrowsError(try TestThrows.decoded(from: throwsData)) { error in XCTAssertNotNil(error) print("TestThrows error: \(error)") } - XCTAssertThrowsError(try throwsData.decoded() as TestNestedThrows) { error in + XCTAssertThrowsError(try TestNestedThrows.decoded(from: throwsData)) { error in XCTAssertNotNil(error) print("TestNestedThrows error: \(error)") } @@ -787,8 +788,8 @@ final class ExCodableTests: XCTestCase { "string": 123 } """#.utf8) - XCTAssertEqual(try noThrowsData.decoded() as TestThrows, TestThrows(throwsString: "123")) - XCTAssertEqual(try noThrowsData.decoded() as TestNestedThrows, TestNestedThrows(throwsString: "123")) + XCTAssertEqual(try TestThrows.decoded(from: noThrowsData), TestThrows(throwsString: "123")) + XCTAssertEqual(try TestNestedThrows.decoded(from: noThrowsData), TestNestedThrows(throwsString: "123")) let nonnullWithThrowsData = Data(#""" { @@ -797,7 +798,7 @@ final class ExCodableTests: XCTestCase { } } """#.utf8) - XCTAssertThrowsError(try nonnullWithThrowsData.decoded() as TestNonnullWithThrows) { error in + XCTAssertThrowsError(try TestNonnullWithThrows.decoded(from: nonnullWithThrowsData)) { error in XCTAssertNotNil(error) print("TestNonnullWithThrows error: \(error)") } @@ -846,7 +847,7 @@ final class ExCodableTests: XCTestCase { ] if let json = try? array.encoded() as Data, - let copies = try? json.decoded() as [TestExCodable], + let copies = try? [TestExCodable].decoded(from: json), let copies2 = try? [TestExCodable].decoded(from: json) { XCTAssertEqual(copies, array) XCTAssertEqual(copies2, array) @@ -855,7 +856,7 @@ final class ExCodableTests: XCTestCase { XCTFail() } if let json = try? array.encoded() as [Any], - let copies = try? json.decoded() as [TestExCodable], + let copies = try? [TestExCodable].decoded(from: json), let copies2 = try? [TestExCodable].decoded(from: json) { XCTAssertEqual(copies, array) XCTAssertEqual(copies2, array) @@ -864,7 +865,7 @@ final class ExCodableTests: XCTestCase { XCTFail() } if let json = try? array.encoded() as String, - let copies = try? json.decoded() as [TestExCodable], + let copies = try? [TestExCodable].decoded(from: json), let copies2 = try? [TestExCodable].decoded(from: json) { XCTAssertEqual(copies, array) XCTAssertEqual(copies2, array) @@ -874,7 +875,7 @@ final class ExCodableTests: XCTestCase { } if let json = try? dictionary.encoded() as Data, - let copies = try? json.decoded() as [String: TestExCodable], + let copies = try? [String: TestExCodable].decoded(from: json), let copies2 = try? [String: TestExCodable].decoded(from: json) { XCTAssertEqual(copies, dictionary) XCTAssertEqual(copies2, dictionary) @@ -883,7 +884,7 @@ final class ExCodableTests: XCTestCase { XCTFail() } if let json = try? dictionary.encoded() as [String: Any], - let copies = try? json.decoded() as [String: TestExCodable], + let copies = try? [String: TestExCodable].decoded(from: json), let copies2 = try? [String: TestExCodable].decoded(from: json) { XCTAssertEqual(copies, dictionary) XCTAssertEqual(copies2, dictionary) @@ -892,7 +893,7 @@ final class ExCodableTests: XCTestCase { XCTFail() } if let json = try? dictionary.encoded() as String, - let copies = try? json.decoded() as [String: TestExCodable], + let copies = try? [String: TestExCodable].decoded(from: json), let copies2 = try? [String: TestExCodable].decoded(from: json) { XCTAssertEqual(copies, dictionary) XCTAssertEqual(copies2, dictionary) @@ -917,7 +918,7 @@ final class ExCodableTests: XCTestCase { let test2 = TestClass(int: 502, string: "Bad Gateway") if let data = try? test2.encoded() as Data, - let copy = try? data.decoded() as TestClass { + let copy = try? TestClass.decoded(from: data) { XCTAssertEqual(copy, test2) } else { @@ -926,7 +927,7 @@ final class ExCodableTests: XCTestCase { let test3 = TestSubclass(int: 504, string: "Gateway Timeout", bool: true) if let data = try? test3.encoded() as Data, - let copy = try? data.decoded() as TestSubclass { + let copy = try? TestSubclass.decoded(from: data) { XCTAssertEqual(copy, test3) } else { @@ -938,5 +939,6 @@ final class ExCodableTests: XCTestCase { // let seconds = elapsed / 1_000_000_000 let milliseconds = elapsed / 1_000_000 print("elapsed: \(milliseconds) ms") + // TODO: compare to builtin and 0.x } } From c2b11431ac917d9943d3c1f909ec0a7e731bb7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Mon, 5 Aug 2024 11:17:18 +0800 Subject: [PATCH 34/43] opt: private decodeRawRepresentable --- Sources/ExCodable/ExCodable.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 7dca2e6..5d2322e 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -35,7 +35,7 @@ public final class ExCodable { fileprivate let nonnull, `throws`: Bool? fileprivate let encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)? - fileprivate let decodeRawRepresentable: ((_ decoder: Decoder, _ stringKeys: [String], _ nonnull: Bool, _ throws: Bool, _ converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> Value?)? + private let decodeRawRepresentable: ((_ decoder: Decoder, _ stringKeys: [String], _ nonnull: Bool, _ throws: Bool, _ converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> Value?)? private init(wrappedValue initialValue: Value, stringKeys: [String]? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)?, decodeRawRepresentable: ((_ decoder: Decoder, _ stringKeys: [String], _ nonnull: Bool, _ throws: Bool, _ converter: (any ExCodableDecodingTypeConverter.Type)?) throws -> Value?)? = nil) { (self.wrappedValue, self.stringKeys, self.nonnull, self.throws, self.encode, self.decode, self.decodeRawRepresentable) @@ -54,11 +54,16 @@ public final class ExCodable { extension ExCodable where Value: RawRepresentable, Value.RawValue: Decodable { private convenience init(wrappedValue initialValue: Value, stringKeys: [String]? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)?, decode: ((_ decoder: Decoder) throws -> Value?)?) where Value: RawRepresentable, Value.RawValue: Decodable { - self.init(wrappedValue: initialValue, stringKeys: stringKeys, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode, decodeRawRepresentable: decode == nil ? { (_ decoder: Decoder, _ stringKeys: [String], _ nonnull: Bool, _ `throws`: Bool, _ converter: (any ExCodableDecodingTypeConverter.Type)?) throws in - guard let rawValue = try decoder.decode(stringKeys, as: Value.RawValue.self, nonnull: nonnull, throws: `throws`, converter: converter), - let value = Value(rawValue: rawValue) else { return nil } - return value - } : nil) + if let decode { + self.init(wrappedValue: initialValue, stringKeys: stringKeys, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode, decodeRawRepresentable: nil) + } + else { + self.init(wrappedValue: initialValue, stringKeys: stringKeys, nonnull: nonnull, throws: `throws`, encode: encode, decode: nil, decodeRawRepresentable: { decoder, stringKeys, nonnull, `throws`, converter throws in + guard let rawValue = try decoder.decode(stringKeys, as: Value.RawValue.self, nonnull: nonnull, throws: `throws`, converter: converter), + let value = Value(rawValue: rawValue) else { return nil } + return value + }) + } } public convenience init(wrappedValue initialValue: Value, _ stringKey: String? = nil, nonnull: Bool? = nil, throws: Bool? = nil, encode: ((_ encoder: Encoder, _ value: Value) throws -> Void)? = nil, decode: ((_ decoder: Decoder) throws -> Value?)? = nil) where Value: RawRepresentable, Value.RawValue: Decodable { self.init(wrappedValue: initialValue, stringKeys: stringKey.map { [$0] }, nonnull: nonnull, throws: `throws`, encode: encode, decode: decode) From 329fc1a1b621d86ed608d7f1fcb0b1017bcafd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Mon, 5 Aug 2024 11:27:22 +0800 Subject: [PATCH 35/43] meta: readme and test --- README.md | 147 +++++++++++----------- Tests/ExCodableTests/ExCodableTests.swift | 15 ++- 2 files changed, 88 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 190f18c..01bafca 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,22 @@ struct TestExCodable: ExAutoCodable { - No need to encode and decode properties one by one. - In most cases, the `CodingKey` types are no longer necessary, `String` literals are preferred. - Currently, requires declaring properties with `var` and provide default values. -- Supports `struct`, `enum`, `class` and subclasses. - Supports **alternative keys** for decoding. - Supports **nested keys** via `String` with dot syntax. - Supports builtin and custom **type conversions**, including **nested optionals** as well. - Supports manual encoding/decoding using **subscripts**. - Supports `return nil` or `throw error` when encoding/decoding failed. -- Supports **type inference**, including JSON `Data`, `String` and objects. +- Supports `struct`, `enum`, `class` and subclasses. +- Supports JSON `Data`, `String`, `Array` and `Dictionary`. +- Supports **type inference**. - Uses JSON encoder/decoder by default, supports PList and custom encoder/decoder. +- Super lightweight. TODO: - [ ] Supports `let`. - [ ] Supports `var` without default values. -- [ ] Replacing `ExAutoCodable` with `Codable`. +- [ ] Use Macros instead of protocol `ExAutoCodable`. ## Usage @@ -137,14 +139,13 @@ struct TestExCodable: ExAutoCodable { ``` -### 1. `struct` +### 1. ExCodable `ExCodable` requires declaring properties with `var` and provide default values. ```swift -// Equatable for Assertions -struct TestStruct: ExAutoCodable, Equatable { - @ExCodable("int") private(set) +struct TestStruct: ExAutoCodable { + @ExCodable private(set) var int: Int = 0 @ExCodable("string") private(set) var string: String? = nil @@ -156,8 +157,6 @@ struct TestStruct: ExAutoCodable, Equatable { ```swift struct TestAlternativeKeys: ExAutoCodable { - @ExCodable("int", "i") private(set) - var int: Int = 0 @ExCodable("string", "str", "s") private(set) var string: String! = nil } @@ -168,8 +167,6 @@ struct TestAlternativeKeys: ExAutoCodable { ```swift struct TestNestedKeys: ExAutoCodable { - @ExCodable private(set) - var int: Int = 0 @ExCodable("nested.nested.string") private(set) var string: String! = nil } @@ -190,44 +187,57 @@ struct TestStructWithEnum: ExAutoCodable { ``` -### 5. Type Conversions +### 5. Custom Encoding/Decoding + +```swift +struct TestManualEncodeDecode: ExAutoCodable { + @ExCodable("int", encode: { encoder, value in + encoder["int"] = value <= 0 ? 0 : value + }, decode: { decoder in + if let int: Int = decoder["int"], int > 0 { + return int + } + return 0 + }) private(set) + var int: Int = 0 +} + +``` + +### 6. Type Conversions ExCodable builtin type conversions: -- Decoding `Bool` **from** `Int`, `String` -- Decoding `Int`, `Int8`, `Int16`, `Int32`, `Int64` **from** `Bool`, `Double`, `String` -- Decoding `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` **from** `Bool`, `String` -- Decoding `Double`, `Float` **from** `Int64`, `String` -- Decoding `String` **from** `Bool`, `Int64`, `Double` +- `Bool` **from** `Int`, `String` +- `Int`, `Int8`, `Int16`, `Int32`, `Int64` **from** `Bool`, `Double`, `String` +- `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` **from** `Bool`, `String` +- `Double`, `Float` **from** `Int64`, `String` +- `String` **from** `Bool`, `Int64`, `Double` Custom type conversions for specific properties: ```swift struct TestCustomEncodeDecode: ExAutoCodable { - - @ExCodable private(set) - var int: Int = 0 - - @ExCodable(encode: { encoder, value in - // skip encoding - }, decode: { decoder in - // custom decoding - if let int: Int = decoder["int"] { - return message(for: int) + @ExCodable("int", decode: { decoder in + if let string: String = decoder["string"], + let int = Int(string) { + return int } - return nil + return 0 }) private(set) - var string: String? = nil + var int: Int = 0 } ``` -Custom type conversions for specific type: +Custom type conversions for specific model: ```swift struct TestCustomTypeConverter: ExAutoCodable { @ExCodable("doubleFromBool") private(set) var doubleFromBool: Double? = nil + @ExCodable("floatFromBool") private(set) + var floatFromBool: Double? = nil } extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { @@ -284,12 +294,12 @@ extension ExCodableGlobalDecodingTypeConverter: ExCodableDecodingTypeConverter { ``` -### 6. Constants, Manual Encoding/Decoding using Subscripts +### 7. Manual Encoding/Decoding using Subscripts -Declaring constants without default values: +A type without `ExCodable`: ```swift -struct TestSubscript { +struct TestManualEncodingDecoding { let int: Int let string: String } @@ -299,7 +309,7 @@ struct TestSubscript { Manual encoding/decoding using subscripts: ```swift -extension TestSubscript: Codable { +extension TestManualEncodingDecoding: Codable { enum Keys: CodingKey { case int, string @@ -317,7 +327,24 @@ extension TestSubscript: Codable { ``` -### 7. `class` and Subclasses +### 8. `return nil` or `throw error` - UNSTABLE + +While encoding/decoding, ExCodable ignores the `keyNotFound`, `valueNotFound`, `invalidValue` and `typeMismatch` errors and `return nil` by default, only throws JSON errors. + +ExCodable also supports throw errors: + +- Use `nonnull: true` to throw `EncodingError.invalidValue`, `DecodingError.keyNotFound`, `DecodingError.valueNotFound`. +- Use `throws: true` to throw `DecodingError.typeMismatch`. + +```swift +struct TestNonnullAndThrows: ExAutoCodable { + @ExCodable("int", nonnull: true, throws: true) private(set) + var nonnullInt: Int! = 0 +} + +``` + +### 9. `class` and Subclasses ```swift class TestClass: ExAutoCodable { @@ -346,40 +373,21 @@ class TestSubclass: TestClass { ``` -### 8. `return nil` or `throw error` - UNSTABLE - -While encoding/decoding, ExCodable ignores the `keyNotFound`, `valueNotFound` and `typeMismatch` errors and `return nil` by default. - -When encoding/decoding failed: - -- Use `nonnull: true` to throw `EncodingError.invalidValue`, `DecodingError.keyNotFound`, `DecodingError.valueNotFound`. -- Use `throws: true` to throw `DecodingError.typeMismatch`. +### 10. Type Inference ```swift -struct TestNonnullAndThrows: ExAutoCodable { - @ExCodable("int", nonnull: true, throws: true) private(set) - var nonnullInt: Int! = 0 +struct TestStruct: ExAutoCodable, Equatable { + @ExCodable("int") private(set) + var int: Int = 0 + @ExCodable("string") private(set) + var string: String? = nil } -``` +let json = Data(#"{"int":200,"string":"OK"}"#.utf8) +let model = try? TestStruct.decoded(from: json) -### 9. Type Inference - -```swift -let test = TestStruct(int: 304, string: "Not Modified"), - test2 = TestStruct(int: 304, string: "Not Modified") - -// type of `data` inferenced from `Data` -// types of `copy` and `copy2` inferenced from `TestStruct` -if let data = try? test.encoded() as Data, - let copy = try? data.decoded() as TestStruct, - let copy2 = try? TestStruct.decoded(from: data) { - XCTAssertEqual(copy, test) - XCTAssertEqual(copy2, test2) -} -else { - XCTFail() -} +let dict = try? model.encoded() as [String: Any] +let copy = try? dict.decoded() as TestStruct ``` @@ -391,14 +399,14 @@ else { ## Installation -- Swift Package Manager: +Swift Package Manager: ```swift .package(url: "https://github.com/ExCodable/ExCodable", from: "1.0.0") ``` -- CocoaPods: +CocoaPods: ```ruby pod 'ExCodable', '~> 1.0.0' @@ -420,7 +428,6 @@ struct TestExCodable { private(set) var string: String? } -// replacing protocol `ExCodable`(0.x) with `ExCodableDEPRECATED`(1.x) extension TestExCodable: ExCodableDEPRECATED { static let keyMapping: [KeyMap] = [ KeyMap(\.int, to: "int"), @@ -435,10 +442,10 @@ extension TestExCodable: ExCodableDEPRECATED { Upgrade, SUGGESTED: -- Replace `ExCodable` with `ExAutoCodable`. -- Remove `static` properties `keyMapping`. +- Replace `protocol` `ExCodable` with `ExAutoCodable`. - Remove initializer `init(from decoder: Decoder) throws`. -- Use `@ExCodable("", "")`. +- Remove `static` properties `keyMapping`. +- Use `@ExCodable("", "", ...)`. - See [Usage](#usage) for more details. ```swift diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index 4ee54ef..bee26a4 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -136,7 +136,14 @@ fileprivate func message(for int: Int) -> String { struct TestCustomEncodeDecode: ExAutoCodable, Equatable { - @ExCodable("int") private(set) + @ExCodable("int", encode: { encoder, value in + encoder["int"] = value <= 0 ? 0 : value + }, decode: { decoder in + if let int: Int = decoder["int"], int > 0 { + return int + } + return 0 + }) private(set) var int: Int = 0 @ExCodable(encode: { encoder, value in @@ -426,9 +433,9 @@ final class ExCodableTests: XCTestCase { func testStruct() { let test = TestStruct(int: 304, string: "Not Modified"), test2 = TestStruct(int: 304, string: "Not Modified", bool: nil) - if let data = try? test.encoded() as Data, - let copy = try? TestStruct.decoded(from: data), - let copy2 = try? TestStruct.decoded(from: data) { + if let dict = try? test.encoded() as [String: Any], + let copy = try? TestStruct.decoded(from: dict), + let copy2 = try? TestStruct.decoded(from: dict) { XCTAssertEqual(copy, test) XCTAssertEqual(copy2, test2) From 8639bd942bfd3c2a6e7bb099c69d7a47865be1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Mon, 5 Aug 2024 22:00:39 +0800 Subject: [PATCH 36/43] opt: type conversions --- README.md | 29 +++++++++++++++++++++++------ Sources/ExCodable/ExCodable.swift | 5 +++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 01bafca..5618f9e 100644 --- a/README.md +++ b/README.md @@ -208,11 +208,28 @@ struct TestManualEncodeDecode: ExAutoCodable { ExCodable builtin type conversions: -- `Bool` **from** `Int`, `String` -- `Int`, `Int8`, `Int16`, `Int32`, `Int64` **from** `Bool`, `Double`, `String` -- `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` **from** `Bool`, `String` -- `Double`, `Float` **from** `Int64`, `String` -- `String` **from** `Bool`, `Int64`, `Double` +- boolean: + - `Bool` + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `String` +- integer: + - `Bool` + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `Double`, `Float` + - `String` +- float: + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `Double`, `Float` + - `String` +- string: + - `Bool` + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `Double`, `Float` + - `String` Custom type conversions for specific properties: @@ -339,7 +356,7 @@ ExCodable also supports throw errors: ```swift struct TestNonnullAndThrows: ExAutoCodable { @ExCodable("int", nonnull: true, throws: true) private(set) - var nonnullInt: Int! = 0 + var int: Int! = 0 } ``` diff --git a/Sources/ExCodable/ExCodable.swift b/Sources/ExCodable/ExCodable.swift index 5d2322e..ad51145 100644 --- a/Sources/ExCodable/ExCodable.swift +++ b/Sources/ExCodable/ExCodable.swift @@ -483,22 +483,27 @@ fileprivate extension KeyedDecodingContainer { else if type is UInt.Type || wrappedType is UInt.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt(bool ? 1 : 0) as? T } + else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return UInt(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt(string) { return value as? T } } else if type is UInt8.Type || wrappedType is UInt8.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt8(bool ? 1 : 0) as? T } + else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return UInt8(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt8(string) { return value as? T } } else if type is UInt16.Type || wrappedType is UInt16.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt16(bool ? 1 : 0) as? T } + else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return UInt16(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt16(string) { return value as? T } } else if type is UInt32.Type || wrappedType is UInt32.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt32(bool ? 1 : 0) as? T } + else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return UInt32(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt32(string) { return value as? T } } else if type is UInt64.Type || wrappedType is UInt64.Type { if let bool = try? decodeIfPresent(Bool.self, forKey: codingKey) { return UInt64(bool ? 1 : 0) as? T } + else if let double = try? decodeIfPresent(Double.self, forKey: codingKey) { return UInt64(double) as? T } // include Float else if let string = try? decodeIfPresent(String.self, forKey: codingKey), let value = UInt64(string) { return value as? T } } From b69194cea1bad2895dbed5a967fab29519b229ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Tue, 6 Aug 2024 12:03:31 +0800 Subject: [PATCH 37/43] opt: test and docs --- README.md | 19 +++++++------- Tests/ExCodableTests/ExCodableTests.swift | 30 +++++++++++------------ 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 5618f9e..b87a716 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ struct TestStructWithEnum: ExAutoCodable { ```swift struct TestManualEncodeDecode: ExAutoCodable { - @ExCodable("int", encode: { encoder, value in + @ExCodable(encode: { encoder, value in encoder["int"] = value <= 0 ? 0 : value }, decode: { decoder in if let int: Int = decoder["int"], int > 0 { @@ -235,10 +235,9 @@ Custom type conversions for specific properties: ```swift struct TestCustomEncodeDecode: ExAutoCodable { - @ExCodable("int", decode: { decoder in - if let string: String = decoder["string"], - let int = Int(string) { - return int + @ExCodable(decode: { decoder in + if let string: String = decoder["string"] { + return string.count } return 0 }) private(set) @@ -251,9 +250,9 @@ Custom type conversions for specific model: ```swift struct TestCustomTypeConverter: ExAutoCodable { - @ExCodable("doubleFromBool") private(set) + @ExCodable private(set) var doubleFromBool: Double? = nil - @ExCodable("floatFromBool") private(set) + @ExCodable private(set) var floatFromBool: Double? = nil } @@ -355,7 +354,7 @@ ExCodable also supports throw errors: ```swift struct TestNonnullAndThrows: ExAutoCodable { - @ExCodable("int", nonnull: true, throws: true) private(set) + @ExCodable(nonnull: true, throws: true) private(set) var int: Int! = 0 } @@ -394,9 +393,9 @@ class TestSubclass: TestClass { ```swift struct TestStruct: ExAutoCodable, Equatable { - @ExCodable("int") private(set) + @ExCodable private(set) var int: Int = 0 - @ExCodable("string") private(set) + @ExCodable private(set) var string: String? = nil } diff --git a/Tests/ExCodableTests/ExCodableTests.swift b/Tests/ExCodableTests/ExCodableTests.swift index bee26a4..0626569 100644 --- a/Tests/ExCodableTests/ExCodableTests.swift +++ b/Tests/ExCodableTests/ExCodableTests.swift @@ -11,6 +11,15 @@ import XCTest // @testable import ExCodable +// MARK: ExCodable + +struct TestExCodable: ExAutoCodable, Equatable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable private(set) + var string: String? = nil +} + // MARK: auto codable struct TestAutoCodable: Codable, Equatable { @@ -96,7 +105,7 @@ enum TestEnum: Int, Codable { } struct TestStructWithEnum: ExAutoCodable, Equatable { - @ExCodable("enum") private(set) + @ExCodable private(set) var `enum` = TestEnum.zero } @@ -136,7 +145,7 @@ fileprivate func message(for int: Int) -> String { struct TestCustomEncodeDecode: ExAutoCodable, Equatable { - @ExCodable("int", encode: { encoder, value in + @ExCodable(encode: { encoder, value in encoder["int"] = value <= 0 ? 0 : value }, decode: { decoder in if let int: Int = decoder["int"], int > 0 { @@ -199,9 +208,9 @@ extension TestSubscript: Codable { // MARK: type-conversions struct TestTypeConversion: ExAutoCodable, Equatable { - @ExCodable("intFromString") private(set) + @ExCodable private(set) var intFromString: Int? = nil - @ExCodable("stringFromInt") private(set) + @ExCodable private(set) var stringFromInt: String??? = nil } @@ -263,9 +272,9 @@ extension TestTypeConversions: Encodable, Decodable { // MARK: custom type-conversions struct TestCustomTypeConverter: ExAutoCodable, Equatable { - @ExCodable("doubleFromBool") private(set) + @ExCodable private(set) var doubleFromBool: Double? = nil - @ExCodable("boolFromDouble") private(set) + @ExCodable private(set) var boolFromDouble: Bool? = nil } @@ -374,15 +383,6 @@ struct TestNonnullWithThrows: ExAutoCodable, Equatable { var testThrows: TestThrows! = nil } -// MARK: ExCodable - -struct TestExCodable: ExAutoCodable, Equatable { - @ExCodable("int") private(set) - var int: Int = 0 - @ExCodable("string") private(set) - var string: String? = nil -} - // MARK: - Tests final class ExCodableTests: XCTestCase { From ea4ef9fd16e7447b8be8d17e5ee1454ce75dc37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=CC=81ng?= Date: Tue, 6 Aug 2024 13:13:15 +0800 Subject: [PATCH 38/43] opt: docs --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b87a716..8fe742f 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,8 @@ let copy = try? dict.decoded() as TestStruct ``` +> See the tests for more examples. + ## Requirements - iOS 12.0+ | tvOS 12.0+ | macOS 11.0+ | watchOS 4.0+ @@ -433,7 +435,9 @@ pod 'ExCodable', '~> 1.0.0' ### 0.x to 1.x -Quickly, but **DEPRECATED**: +When you update to ExCodable 1.0. + +Step 1: Update your code to use the old API - **DEPRECATED** but quick. - Replace protocol `ExCodable` with `ExCodableDEPRECATED`. - Add `static` to func `decodeForTypeConversion(_:codingKey:as:)` of protocol `KeyedDecodingContainerCustomTypeConversion`. @@ -456,7 +460,7 @@ extension TestExCodable: ExCodableDEPRECATED { ``` -Upgrade, SUGGESTED: +Step 2: Upgrade your models to the new API one by one - SUGGESTED: - Replace `protocol` `ExCodable` with `ExAutoCodable`. - Remove initializer `init(from decoder: Decoder) throws`. From 0e298f108cfc5e51efe6884d0828ad4877e75a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Wed, 7 Aug 2024 23:44:49 +0800 Subject: [PATCH 39/43] opt: docs --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8fe742f..89cea93 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ struct TestExCodable: ExAutoCodable { ## Usage + + ### 1. ExCodable `ExCodable` requires declaring properties with `var` and provide default values. From 542456bb324247cff953ed34ad50e1a8f1ace266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Thu, 5 Sep 2024 16:22:44 +0800 Subject: [PATCH 40/43] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 89cea93..a525867 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@
[![Build and Test](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml) [![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/ExCodable/ExCodable.svg?sort=semver)](https://github.com/ExCodable/ExCodable/releases) +[![GitHub stars](https://badgen.net/github/stars/ExCodable/ExCodable)](https://github.com/ExCodable/ExCodable/stargazers/) +
[![Deploy to CocoaPods](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml) [![Cocoapods](https://img.shields.io/cocoapods/v/ExCodable.svg)](https://cocoapods.org/pods/ExCodable)
From 601cdcbb94709fb59f9705bdb0d261a1715a9d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Fri, 6 Sep 2024 21:13:44 +0800 Subject: [PATCH 41/43] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a525867..79e9037 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,11 @@
[![Build and Test](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml) [![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/ExCodable/ExCodable.svg?sort=semver)](https://github.com/ExCodable/ExCodable/releases) -[![GitHub stars](https://badgen.net/github/stars/ExCodable/ExCodable)](https://github.com/ExCodable/ExCodable/stargazers/) -
[![Deploy to CocoaPods](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml) [![Cocoapods](https://img.shields.io/cocoapods/v/ExCodable.svg)](https://cocoapods.org/pods/ExCodable)
[![LICENSE](https://img.shields.io/github/license/ExCodable/ExCodable.svg)](https://github.com/ExCodable/ExCodable/blob/master/LICENSE) +[![GitHub stars](https://badgen.net/github/stars/ExCodable/ExCodable)](https://github.com/ExCodable/ExCodable/stargazers/) [![@minglq](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fiwill%2FExCodable)](https://twitter.com/minglq) En | [中文](https://iwill.im/ExCodable/) From 7f31d9a4a80036343a694fb68db866ef32c91ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Wed, 9 Oct 2024 19:35:06 +0800 Subject: [PATCH 42/43] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79e9037..4cd0655 100644 --- a/README.md +++ b/README.md @@ -486,7 +486,7 @@ struct TestExCodable: ExAutoCodable { ## Stars - Star Chart + Star Chart Hope ExCodable will help you! [Make a star](https://github.com/ExCodable/ExCodable/#repository-container-header) ⭐️ 🤩 From f4686d1845ab6c2a8da1763e7c6fb0fa35e216a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=ADng?= Date: Thu, 26 Dec 2024 21:26:31 +0800 Subject: [PATCH 43/43] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cd0655..2da0241 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![GitHub stars](https://badgen.net/github/stars/ExCodable/ExCodable)](https://github.com/ExCodable/ExCodable/stargazers/) [![@minglq](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fiwill%2FExCodable)](https://twitter.com/minglq) -En | [中文](https://iwill.im/ExCodable/) +En | [中文](https://excodable.iwill.im/) ## What's New in ExCodable 1.0