├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── SuperCodable.xcscheme
├── Codable_vs_SuperCoable.swift
├── Package.swift
├── README.md
├── Sources
└── SuperCodable
│ ├── @Keyed.swift
│ ├── @KeyedOptional.swift
│ ├── @KeyedTransform.swift
│ ├── CodingKey+DynamicKey.swift
│ ├── Lazy.swift
│ ├── SuperCodable.swift
│ ├── SuperDecodable.swift
│ └── SuperEncodable.swift
└── Tests
├── LinuxMain.swift
└── SuperCodableTests
├── AnyValueDecode.swift
├── DecodeTests.swift
├── EncodeTests.swift
├── Polymorphic.swift
├── SuperCodableTests.swift
└── XCTestManifests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/SuperCodable.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
46 |
47 |
53 |
54 |
60 |
61 |
62 |
63 |
66 |
72 |
73 |
74 |
75 |
76 |
86 |
87 |
93 |
94 |
100 |
101 |
102 |
103 |
105 |
106 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/Codable_vs_SuperCoable.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/10
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 |
12 | // MARK: - StudentWithCodable
13 |
14 | struct StudentWithCodable: Codable {
15 | // MARK: Lifecycle
16 |
17 | init(from decoder: Decoder) throws {
18 | let container = try decoder.container(keyedBy: CodingKeys.self)
19 | self.id = try container.decode(String.self, forKey: .id)
20 | self.aName = try container.decode(String.self, forKey: .aName)
21 | let gradeDecoded = try container.decode(Double.self, forKey: .grade)
22 | self.grade = Int(gradeDecoded)
23 | }
24 |
25 | // MARK: Internal
26 |
27 | enum CodingKeys: String, CodingKey {
28 | case id = "id"
29 | case aName = "name"
30 | case grade = "grade"
31 |
32 | }
33 |
34 | var id: String
35 | var aName: String
36 | var grade: Int
37 |
38 | func encode(to encoder: Encoder) throws {
39 | var container = encoder.container(keyedBy: CodingKeys.self)
40 | try container.encode(id, forKey: .id)
41 | try container.encode(aName, forKey: .aName)
42 | try container.encode(Double(grade), forKey: .grade)
43 |
44 | }
45 | }
46 |
47 | // MARK: - StudentWithSuperCodable
48 |
49 | struct StudentWithSuperCodable: SuperCodable {
50 | var aID: String
51 |
52 | @Keyed("name")
53 | var aName: String
54 |
55 | @KeyedTransform(ToolBox.doubleTransform)
56 | var AGrade: Int
57 | }
58 |
59 | // MARK: - ToolBox
60 |
61 | enum ToolBox {
62 | static let doubleTransform = SCTransformOf {
63 | (double) -> Int in
64 | Int(double)
65 | } toEncoder: { (int) -> Double in
66 | Double(int) * 10
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SuperCodable",
8 | products: [
9 | // Products define the executables and libraries a package produces, and make them visible to other packages.
10 | .library(
11 | name: "SuperCodable",
12 | targets: ["SuperCodable"]),
13 | ],
14 | dependencies: [
15 | // Dependencies declare other packages that this package depends on.
16 | // .package(url: /* package url */, from: "1.0.0"),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
20 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
21 | .target(
22 | name: "SuperCodable",
23 | dependencies: []),
24 | .testTarget(
25 | name: "SuperCodableTests",
26 | dependencies: ["SuperCodable"]),
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SuperCodable
2 |
3 |
4 | ## From Foundation
5 |
6 | ```swift
7 | struct AStudent: Codable {
8 | init(from decoder: Decoder) throws {
9 | let container = try decoder.container(keyedBy: CodingKeys.self)
10 | self.aID = try container.decode(String.self, forKey: .aID)
11 | self.aName = try container.decode(String.self, forKey: .aName)
12 | let gradeDecoded = try container.decode(Double.self, forKey: .aGrade)
13 | self.AGrede = Int(gradeDecoded)
14 | }
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case aName = "name"
18 | case aGrade = "grade"
19 | case aID = "id"
20 | }
21 |
22 | var aID: String
23 | var aName: String
24 | var AGrede: Int
25 |
26 | func encode(to encoder: Encoder) throws {
27 | var container = encoder.container(keyedBy: CodingKeys.self)
28 | try container.encode(aID, forKey: .aID)
29 | try container.encode(Double(AGrede), forKey: .aGrade)
30 | try container.encode(aName, forKey: .aName)
31 | }
32 | }
33 | ```
34 |
35 |
36 | ## To SuperCodable
37 |
38 | ```swift
39 | struct Student: SuperCodable {
40 | @Keyed
41 | var id: String
42 |
43 | @Keyed("name")
44 | var aName: String
45 |
46 | @KeyedTransform("grade", doubleTransform)
47 | var AGrade: Int
48 | }
49 |
50 | let doubleTransform = SCTransformOf {
51 | (double) -> Int in
52 | Int(double)
53 | } toEncoder: { (int) -> Double in
54 | Double(int)
55 | }
56 | ```
57 |
58 | ## Even random backend type
59 |
60 | ```swift
61 | struct AnyValueJSON: SuperCodable {
62 | @KeyedTransform(IDTransform)
63 | var id:Int
64 | }
65 |
66 | let data =
67 | #"""
68 | [
69 | {
70 | "id": "0",
71 | },
72 | {
73 | "id": 1,
74 | },
75 | {
76 | "id": "abc",
77 | },
78 | {
79 | "id": true,
80 | },
81 | ]
82 | """#.data(using: .utf8)!
83 | let sut = try! JSONDecoder().decode([AnyValueJSON].self, from: data)
84 | XCTAssertEqual(sut.count, 4)
85 | XCTAssertEqual(sut.map(\.id), [0, 1, 0, 1])
86 | ```
87 |
88 | Can be found in [Tests/SuperCodableTests/AnyValueDecode.swift](Tests/SuperCodableTests/AnyValueDecode.swift)
89 |
90 | ## Feature
91 |
92 | - Working with Nested `Foundation.Codable` property
93 |
94 | ## Known side effect
95 |
96 | - SuperDecoable must construct from nothing `init()`
97 | - `@Keyed var id:Int` will do **O(n) calculation** on underlaying wrapper `_VARIABLE_NAME` into key `VARIABLE_NAME`. **Be ware of variable name takes too long**
98 |
99 |
100 | ## Known Disability
101 |
102 | - Every property in a SuperCodable should a `DecodableKey` / `EncodableKey`, otherwise the property(which should be `Codable`) will **simply ignored** during the Codable process.
103 | > Why:
104 | >> Basically Mirror can't mutating the object value during the `init(from decoder:) throws`, since we create the object from `self.init()`
105 |
106 |
107 | ## Other notes
108 |
109 | - Inspired by: https://medium.com/trueid-developers/combined-propertywrapper-with-codable-swift-368dc4aa2703
110 |
111 | - Try to merge `@KeyedTransform` into `@Keyed`, but it required `@Keyed var id: String` to be `@Keyed() var id: String`, with extra `()` 🧐
112 |
113 | - Swift should auto generate `STRUCT.init(....)` for you, **but** if you using `@Keyed var id: String` without default value, it will generate `init(id: Keyed)`, by giving default value `@Keyed var id: String = ""` should solve this problem.
114 |
115 | ## Know Issues
116 |
117 | - `@Keyed var id:String?` will cause fatalError on force unwrapping `Keyed.value?`, you can using `@OptionalKeyed` to make it works.
118 | - `OptionalKeyed` may / may not a good name, I am thinking of make the easy to change, maybe `KeyedOptional` is EASY change? 🤔
119 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/@Keyed.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/11
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 |
12 | // MARK: - Keyed
13 |
14 | @propertyWrapper
15 | public struct Keyed {
16 | public init(_ key: String) {
17 | self.inner = Inner(key)
18 | }
19 |
20 | public init(wrappedValue: Value, _ key: String) {
21 | self.inner = Inner(key)
22 | inner.value = .already(wrappedValue)
23 | }
24 |
25 | public init() {
26 | self.inner = Inner("")
27 | }
28 |
29 | public init(wrappedValue: Value) {
30 | self.inner = Inner("")
31 | inner.value = .already(wrappedValue)
32 | }
33 |
34 | // MARK: Public
35 |
36 | public var wrappedValue: Value {
37 | get {
38 | switch inner.value {
39 | case .yet:
40 | fatalError("did not decode")
41 | case let .already(v):
42 | return v
43 | case .none:
44 | fatalError("😅 @Keyed current not support Optional property, please consider using @OptionalKeyed")
45 | }
46 | }
47 | nonmutating set {
48 | inner.value = .already(newValue)
49 | }
50 | }
51 |
52 | // MARK: Private
53 |
54 | private final class Inner {
55 | // MARK: Lifecycle
56 |
57 | public init(_ key: String) {
58 | self.key = key
59 | }
60 |
61 | // MARK: Fileprivate
62 |
63 | fileprivate let key: String
64 | fileprivate var value: Lazy = .none
65 | }
66 |
67 | private let inner: Inner
68 | }
69 |
70 | // MARK: EncodableKey
71 |
72 | extension Keyed: EncodableKey where Value: Encodable {
73 | public func encodeValue(from container: inout EncodeContainer) throws {
74 | try encoding(for: inner.key, from: &container)
75 | }
76 |
77 | func encoding(for key: String, from container: inout EncodeContainer) throws {
78 | let codingKey = DynamicKey(key: key)
79 | try container.encodeIfPresent(wrappedValue, forKey: codingKey)
80 | }
81 | }
82 |
83 | // MARK: RunTimeEncodableKey
84 |
85 | extension Keyed: RunTimeEncodableKey where Value: Encodable {
86 | public func shouldApplyRunTimeEncoding() -> Bool {
87 | inner.key.isEmpty
88 | }
89 |
90 | public func encodeValue(with key: String, from container: inout EncodeContainer) throws {
91 | try encoding(for: key, from: &container)
92 | }
93 | }
94 |
95 | // MARK: DecodableKey
96 |
97 | extension Keyed: DecodableKey where Value: Decodable {
98 | public func decodeValue(from container: DecodeContainer) throws {
99 | if inner.key.isEmpty {
100 | throw DecodableKeyError("key is missing")
101 | }
102 | try decoding(container, for: inner.key)
103 | }
104 |
105 | private func decoding(_ container: Keyed.DecodeContainer, for key: String) throws {
106 | let codingKey = DynamicKey(key: key)
107 |
108 | if let value = try container.decodeIfPresent(Value.self, forKey: codingKey) {
109 | inner.value = .already(value)
110 | } else {
111 | inner.value = .none
112 | }
113 | }
114 | }
115 |
116 | // MARK: RunTimeDecodableKey
117 |
118 | extension Keyed: RunTimeDecodableKey where Value: Decodable {
119 | func shouldApplyRunTimeDecoding() -> Bool {
120 | inner.key.isEmpty
121 | }
122 |
123 | func decodeValue(with key: String, from container: DecodeContainer) throws {
124 | try decoding(container, for: key)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/@KeyedOptional.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/12
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 |
11 | import Foundation
12 |
13 | @propertyWrapper
14 | public struct OptionalKeyed {
15 | public init(_ key: String) {
16 | self.inner = Inner(key)
17 | }
18 | public init(wrappedValue: Value?, _ key: String) {
19 | self.inner = Inner(key)
20 | inner.value = .already(wrappedValue)
21 | }
22 |
23 | public init() {
24 | self.inner = Inner("")
25 | }
26 | public init(wrappedValue: Value?) {
27 | self.inner = Inner("")
28 | inner.value = .already(wrappedValue)
29 | }
30 |
31 | public var wrappedValue: Value? {
32 | get {
33 | switch inner.value {
34 | case .yet:
35 | fatalError("did not decode")
36 | case let .already(v):
37 | return v
38 | case .none:
39 | return nil
40 | }
41 | }
42 | nonmutating set {
43 | inner.value = .already(newValue)
44 | }
45 | }
46 |
47 | private final class Inner {
48 | public init(_ key: String) {
49 | self.key = key
50 | }
51 |
52 | fileprivate let key: String
53 | fileprivate var value: Lazy = .yet
54 | }
55 |
56 | private let inner: Inner
57 | }
58 |
59 | // MARK: EncodableKey
60 |
61 | extension OptionalKeyed: EncodableKey where Value: Encodable {
62 | public func encodeValue(from container: inout EncodeContainer) throws {
63 | try encoding(for: inner.key, from: &container)
64 | }
65 |
66 | func encoding(for key: String, from container: inout EncodeContainer) throws {
67 | let codingKey = DynamicKey(key: key)
68 | try container.encodeIfPresent(wrappedValue, forKey: codingKey)
69 | }
70 | }
71 |
72 | // MARK: RunTimeEncodableKey
73 |
74 | extension OptionalKeyed: RunTimeEncodableKey where Value: Encodable {
75 | public func shouldApplyRunTimeEncoding() -> Bool {
76 | inner.key.isEmpty
77 | }
78 |
79 | public func encodeValue(with key: String, from container: inout EncodeContainer) throws {
80 | try encoding(for: key, from: &container)
81 | }
82 | }
83 |
84 | // MARK: DecodableKey
85 |
86 | extension OptionalKeyed: DecodableKey where Value: Decodable {
87 | public func decodeValue(from container: DecodeContainer) throws {
88 | if inner.key.isEmpty {
89 | throw DecodableKeyError("key is missing")
90 | }
91 | try decoding(container, for: inner.key)
92 | }
93 |
94 | private func decoding(_ container: Keyed.DecodeContainer, for key: String) throws {
95 | let codingKey = DynamicKey(key: key)
96 |
97 | if let value = try container.decodeIfPresent(Value.self, forKey: codingKey) {
98 | inner.value = .already(value)
99 | }
100 | else {
101 | inner.value = .none
102 | }
103 | }
104 | }
105 |
106 | // MARK: RunTimeDecodableKey
107 |
108 | extension OptionalKeyed: RunTimeDecodableKey where Value: Decodable {
109 | func shouldApplyRunTimeDecoding() -> Bool {
110 | inner.key.isEmpty
111 | }
112 |
113 | func decodeValue(with key: String, from container: DecodeContainer) throws {
114 | try decoding(container, for: key)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/@KeyedTransform.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/10
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 |
12 | // MARK: - SCTransformFromDecoder
13 |
14 | protocol SCTransformFromDecoder {
15 | associatedtype Output
16 | associatedtype FromDecoderType
17 | func transformFromDecoder(
18 | _ container: DecodableKey.DecodeContainer,
19 | key: String
20 | ) throws -> Output
21 | }
22 |
23 | // MARK: - SCTransformIntoEncoder
24 |
25 | protocol SCTransformIntoEncoder {
26 | associatedtype EncoderOut
27 | func transformToEncoder(
28 | _ container: inout EncodableKey.EncodeContainer,
29 | _ value: EncoderOut,
30 | key: String
31 | ) throws
32 | }
33 |
34 | // MARK: - SCTransformOfError
35 |
36 | struct SCTransformOfError: LocalizedError {
37 | // MARK: Lifecycle
38 |
39 | internal init(_ errorDescription: String? = nil) {
40 | self.errorDescription = errorDescription
41 | }
42 |
43 | // MARK: Internal
44 |
45 | var errorDescription: String?
46 | }
47 |
48 | // MARK: - SCTransformOf
49 |
50 | public class SCTransformOf {
51 | // MARK: Lifecycle
52 |
53 | public init(fromDecoder: @escaping (RelateType) throws -> Value,
54 | toEncoder: @escaping (Value) throws -> RelateType)
55 | {
56 | self.fromDecoder = fromDecoder
57 | self.toEncoder = toEncoder
58 | }
59 |
60 | // MARK: Private
61 |
62 | private let fromDecoder: (RelateType) throws -> Value
63 | private let toEncoder: (Value) throws -> RelateType
64 | }
65 |
66 | // MARK: SCTransformIntoEncoder
67 |
68 | extension SCTransformOf: SCTransformIntoEncoder where RelateType: Encodable {
69 | func transformToEncoder(_ container: inout KeyedEncodingContainer,
70 | _ value: Value,
71 | key: String) throws
72 | {
73 | let toEncoderValue = try toEncoder(value)
74 | let codingKey = DynamicKey(key: key)
75 | try container.encodeIfPresent(toEncoderValue, forKey: codingKey)
76 | }
77 | }
78 |
79 | // MARK: SCTransformFromDecoder
80 |
81 | extension SCTransformOf: SCTransformFromDecoder where RelateType: Decodable {
82 | typealias Output = Value
83 |
84 | typealias FromDecoderType = RelateType
85 |
86 | func transformFromDecoder(
87 | _ container: DecodableKey.DecodeContainer,
88 | key: String
89 | ) throws -> Value {
90 | let codingKey = DynamicKey(key: key)
91 | if let value = try container.decodeIfPresent(RelateType.self, forKey: codingKey) {
92 | return try fromDecoder(value)
93 | }
94 | else {
95 | throw DecodableKeyError(#"key `\#(key)` not found"#)
96 | }
97 | }
98 | }
99 |
100 | // MARK: - KeyedTransform
101 |
102 | @propertyWrapper
103 | public struct KeyedTransform {
104 | // MARK: Lifecycle
105 |
106 | public init(_ key: String,
107 | _ transform: SCTransformOf)
108 | {
109 | self.inner = Inner(key, transform)
110 | }
111 |
112 | public init(
113 | _ transform: SCTransformOf)
114 | {
115 | self.inner = Inner("", transform)
116 | }
117 |
118 | // MARK: Public
119 |
120 | public var wrappedValue: RelateType {
121 | get {
122 | inner.value!
123 | }
124 | nonmutating set {
125 | inner.value = newValue
126 | }
127 | }
128 |
129 | // MARK: Fileprivate
130 |
131 | fileprivate final class Inner {
132 | // MARK: Lifecycle
133 |
134 | public init(_ key: String,
135 | _ transform: SCTransformOf)
136 | {
137 | self.key = key
138 | self.transform = transform
139 | }
140 |
141 | // MARK: Internal
142 |
143 | let key: String
144 | var transform: SCTransformOf
145 |
146 | var value: RelateType?
147 | }
148 |
149 | // MARK: Private
150 |
151 | private let inner: Inner
152 | }
153 |
154 | // MARK: EncodableKey
155 |
156 | extension KeyedTransform: EncodableKey where Value: Encodable {
157 | public func encodeValue(from container: inout EncodeContainer) throws {
158 | try encoding(key: inner.key, from: &container)
159 | }
160 |
161 | private func encoding(key: String, from container: inout EncodeContainer) throws {
162 | try inner.transform.transformToEncoder(&container, wrappedValue, key: key)
163 | }
164 | }
165 |
166 | // MARK: RunTimeEncodableKey
167 |
168 | extension KeyedTransform: RunTimeEncodableKey where Value: Encodable {
169 | public func shouldApplyRunTimeEncoding() -> Bool {
170 | inner.key.isEmpty
171 | }
172 |
173 | public func encodeValue(with key: String, from container: inout EncodeContainer) throws {
174 | try encoding(key: key, from: &container)
175 | }
176 | }
177 |
178 | // MARK: DecodableKey
179 |
180 | extension KeyedTransform: DecodableKey where Value: Decodable {
181 | public func decodeValue(from container: DecodeContainer) throws {
182 | try decoding(with: inner.key, from: container)
183 | }
184 |
185 | private func decoding(with key: String, from container: DecodeContainer) throws {
186 | wrappedValue = try inner.transform.transformFromDecoder(container, key: key)
187 | }
188 | }
189 |
190 | // MARK: RunTimeDecodableKey
191 |
192 | extension KeyedTransform: RunTimeDecodableKey where Value: Decodable {
193 | func shouldApplyRunTimeDecoding() -> Bool {
194 | inner.key.isEmpty
195 | }
196 |
197 | func decodeValue(with key: String, from container: DecodeContainer) throws {
198 | try decoding(with: key, from: container)
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/CodingKey+DynamicKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/11
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 |
12 | // MARK: - DynamicCodingKeys
13 |
14 | public struct DynamicKey: CodingKey {
15 | // MARK: Lifecycle
16 |
17 | init(key: String) {
18 | self.stringValue = key
19 | }
20 |
21 | public init?(stringValue: String) {
22 | self.stringValue = stringValue
23 | }
24 |
25 | public init?(intValue: Int) {
26 | self.intValue = intValue
27 | self.stringValue = String(intValue)
28 | }
29 |
30 | // MARK: Public
31 |
32 | public var stringValue: String
33 | public var intValue: Int?
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/Lazy.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/12
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 |
11 | import Foundation
12 | enum Lazy {
13 | case yet
14 | case already(T)
15 | case none
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/SuperCodable.swift:
--------------------------------------------------------------------------------
1 | public typealias SuperCodable = SuperEncodable & SuperDecodable
2 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/SuperDecodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/11
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 |
12 | // MARK: - DecodableKey
13 |
14 | public protocol DecodableKey {
15 | typealias DecodeContainer = KeyedDecodingContainer
16 | func decodeValue(from container: DecodeContainer) throws
17 | }
18 |
19 | // MARK: - RunTimeDecodableKey
20 |
21 | protocol RunTimeDecodableKey: DecodableKey {
22 | func shouldApplyRunTimeDecoding() -> Bool
23 | func decodeValue(with key: String, from container: DecodeContainer) throws
24 | }
25 |
26 | // MARK: - DecodableKeyError
27 |
28 | struct DecodableKeyError: LocalizedError {
29 | // MARK: Lifecycle
30 |
31 | init(_ errorDescription: String) {
32 | self.errorDescription = errorDescription
33 | }
34 |
35 | // MARK: Internal
36 |
37 | var errorDescription: String?
38 | }
39 |
40 | // MARK: - SuperDecodable
41 |
42 | public protocol SuperDecodable: Decodable {
43 | init()
44 | }
45 |
46 | public extension SuperDecodable {
47 | init(from decoder: Decoder) throws {
48 | self.init()
49 | let container = try decoder.container(keyedBy: DynamicKey.self)
50 | for child in Mirror(reflecting: self).children {
51 | guard let decodableKey = child.value as? DecodableKey else { continue }
52 |
53 | if let runTimeDecodable = decodableKey as? RunTimeDecodableKey,
54 | runTimeDecodable.shouldApplyRunTimeDecoding()
55 | {
56 | let key = child.label?.dropFirst() ?? ""
57 | try runTimeDecodable.decodeValue(with: String(key), from: container)
58 | } else {
59 | try decodableKey.decodeValue(from: container)
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/SuperCodable/SuperEncodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/11
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 |
12 | // MARK: - EncodableKey
13 |
14 | public protocol EncodableKey {
15 | typealias EncodeContainer = KeyedEncodingContainer
16 | func encodeValue(from container: inout EncodeContainer) throws
17 | }
18 |
19 | // MARK: - RunTimeEncodableKey
20 |
21 | public protocol RunTimeEncodableKey: EncodableKey {
22 | func shouldApplyRunTimeEncoding() -> Bool
23 | func encodeValue(with key: String, from container: inout EncodeContainer) throws
24 | }
25 |
26 | // MARK: - SuperEncodable
27 |
28 | public protocol SuperEncodable: Encodable {}
29 | public extension SuperEncodable {
30 | func encode(to encoder: Encoder) throws {
31 | var container = encoder.container(keyedBy: DynamicKey.self)
32 | for child in Mirror(reflecting: self).children {
33 | guard let encodableKey = child.value as? EncodableKey else { continue }
34 | if let runtimeEncodableKey = encodableKey as? RunTimeEncodableKey,
35 | runtimeEncodableKey.shouldApplyRunTimeEncoding()
36 | {
37 | let label = child.label?.dropFirst() ?? ""
38 | try runtimeEncodableKey.encodeValue(with: String(label), from: &container)
39 | } else {
40 | try encodableKey.encodeValue(from: &container)
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SuperCodableTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += SuperCodableTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/SuperCodableTests/AnyValueDecode.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/9/14
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 12.0
8 | */
9 |
10 | import XCTest
11 | import SuperCodable
12 |
13 | final class AnyValueTests: XCTestCase {
14 | func test() throws {
15 | let data =
16 | #"""
17 | [
18 | {
19 | "id": "0",
20 | },
21 | {
22 | "id": 1,
23 | },
24 | {
25 | "id": "abc",
26 | },
27 | {
28 | "id": true,
29 | },
30 | ]
31 | """#.data(using: .utf8)!
32 | let sut = try! JSONDecoder().decode([AnyValueJSON].self, from: data)
33 | XCTAssertEqual(sut.count,
34 | 4)
35 | XCTAssertEqual(
36 | sut.map(\.id),
37 | [0, 1, 0, 1])
38 |
39 | }
40 | }
41 |
42 | private struct AnyValueJSON: SuperCodable {
43 | @KeyedTransform(IDTransform)
44 | var id:Int
45 | }
46 |
47 | let IDTransform = SCTransformOf {
48 | (anyValue) -> Int in
49 | switch anyValue {
50 | case let .string(obj): return Int(atoi(obj))
51 | case let .bool(obj): return obj ? 1 :0
52 | case let .int(obj): return obj
53 | case let .double(obj): return Int(obj)
54 | case let .dictionary(obj): return 0
55 | case let .array(obj): return 0
56 | case .null: return 0
57 | }
58 | } toEncoder: { (int) -> AnyValue in
59 | return .int(int)
60 | }
61 |
62 | public enum AnyValue: Equatable {
63 | case string(String)
64 | case bool(Bool)
65 | case int(Int)
66 | case double(Double)
67 | case dictionary([String: AnyValue])
68 | case array([AnyValue])
69 | case null
70 | }
71 |
72 | extension AnyValue: Codable {
73 | public init(from decoder: Decoder) throws {
74 | let container = try decoder.singleValueContainer()
75 |
76 | if let value = try? container.decode(String.self) {
77 | self = .string(value)
78 | } else if let value = try? container.decode(Bool.self) {
79 | self = .bool(value)
80 | } else if let value = try? container.decode(Int.self) {
81 | self = .int(value)
82 | } else if let value = try? container.decode(Double.self) {
83 | self = .double(value)
84 | } else if let value = try? container.decode([String: AnyValue].self) {
85 | self = .dictionary(value)
86 | } else if let value = try? container.decode([AnyValue].self) {
87 | self = .array(value)
88 | } else if container.decodeNil() {
89 | self = .null
90 | } else {
91 | let context = DecodingError.Context(codingPath: container.codingPath,
92 | debugDescription: "Cannot decode AnyValue")
93 | throw DecodingError.typeMismatch(AnyValue.self, context)
94 | }
95 | }
96 |
97 | public func encode(to encoder: Encoder) throws {
98 | var container = encoder.singleValueContainer()
99 |
100 | switch self {
101 | case let .string(value):
102 | try container.encode(value)
103 | case let .int(value):
104 | try container.encode(value)
105 | case let .double(value):
106 | try container.encode(value)
107 | case let .bool(value):
108 | try container.encode(value)
109 | case let .dictionary(value):
110 | try container.encode(value)
111 | case let .array(value):
112 | try container.encode(value)
113 | case .null:
114 | try container.encodeNil()
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Tests/SuperCodableTests/DecodeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/11
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 | import SuperCodable
12 | import XCTest
13 |
14 | // MARK: - DecodeTests
15 |
16 | final class DecodeTests: XCTestCase {
17 | // MARK: Internal
18 |
19 | func testKeyedWithKey() throws {
20 | let sut = try makeSUT(for: KeyedWithKey.self)
21 | XCTAssertEqual(sut.aID, "1")
22 | }
23 |
24 | func testOptionalKeyedWithKey() throws {
25 | let sut = try makeSUT(for: OptionalKeyedWithKey.self)
26 | XCTAssertEqual(sut.aID, "1")
27 | }
28 |
29 | func testOptionalKeyedWithKeyButMissingValue() throws {
30 | let sut = try makeSUT(for: OptionalKeyedWithKeyButWithMissValue.self)
31 | XCTAssertEqual(sut.aID, "1")
32 | XCTAssertNil(sut.notKey)
33 | }
34 |
35 | func testOptionalKeyedWitouthKey() throws {
36 | let sut = try makeSUT(for: OptionalKeyedWithoutKey.self)
37 | XCTAssertEqual(sut.id, "1")
38 | }
39 |
40 | func testOptionalKeyedWithoutKeyButMissingValue() throws {
41 | let sut = try makeSUT(for: OptionalKeyedWithoutKeyButWithMissValue.self)
42 | XCTAssertEqual(sut.id, "1")
43 | XCTAssertNil(sut.notKey)
44 | }
45 |
46 | func testKeyedWithoutKey() throws {
47 | let sut = try makeSUT(for: KeyedWithoutKey.self)
48 | XCTAssertEqual(sut.id, "1")
49 | }
50 |
51 | func testKeyedWithNestedKeyed() throws {
52 | let data =
53 | #"""
54 | {
55 | "object": {
56 | "id": "1"
57 | }
58 | }
59 | """#.data(using: .utf8)!
60 | let sut = try JSONDecoder().decode(KeyedWitNestedKeyed.self, from: data)
61 | XCTAssertEqual(sut.aObject.aID, "1")
62 | }
63 |
64 | func testKeyedWithNestedCodable() throws {
65 | let data =
66 | #"""
67 | {
68 | "object": {
69 | "bObject": {
70 | "id": "1"
71 | }
72 | }
73 | }
74 | """#.data(using: .utf8)!
75 | let sut = try JSONDecoder().decode(KeyWithNestedDeodable.self, from: data)
76 | XCTAssertEqual(sut.aObject.bObject.id, "1")
77 | }
78 |
79 | func testSuperDecodableWithNoneKeyedPropertyCannotSuccessfulDecode() throws {
80 | let sut = try makeSUT(for: SuperDecodableWithNoneKeyedProperty.self)
81 | XCTAssertNotEqual(sut.id, "1")
82 | XCTAssertTrue(sut.captured.isEmpty)
83 | }
84 |
85 | func testTransformWithKey() throws {
86 | let sut = try makeSUT(for: TransformWithKey.self)
87 | XCTAssertEqual(sut.aID, 1)
88 | }
89 |
90 | func testTransformWithKeyButTransformFailureShouldMakeDecodeFailure() throws {
91 | XCTAssertThrowsError(
92 | try makeSUT(for: TransformWithKey.self, customID: "abc")
93 | )
94 | }
95 | func testTransformWithoutKey() throws {
96 | let sut = try makeSUT(for: TransformWithoutKey.self)
97 | XCTAssertEqual(sut.id, 1)
98 | }
99 |
100 | func testTransformWithoutKeyButTransformFailureShouldMakeDecodeFailure() throws {
101 | XCTAssertThrowsError(
102 | try makeSUT(for: TransformWithoutKey.self, customID: "abc")
103 | )
104 | }
105 |
106 | // MARK: Private
107 |
108 | private func makeSUT(
109 | for type: T.Type,
110 | customID: String = "1",
111 | file: StaticString = #filePath, line: UInt = #line
112 | ) throws -> T {
113 | let data =
114 | #"""
115 | {
116 | "id": "\#(customID)"
117 | }
118 | """#.data(using: .utf8)!
119 | let sut = try JSONDecoder().decode(type.self, from: data)
120 | return sut
121 | }
122 | }
123 |
124 | // MARK: - KeyedWithKey
125 |
126 | private struct KeyedWithKey: SuperDecodable {
127 | @Keyed("id")
128 | var aID: String
129 | }
130 | private struct OptionalKeyedWithKey: SuperDecodable {
131 | @OptionalKeyed("id")
132 | var aID: String?
133 | }
134 | private struct OptionalKeyedWithKeyButWithMissValue: SuperDecodable {
135 | @OptionalKeyed("id")
136 | var aID: String?
137 | @OptionalKeyed("notKey")
138 | var notKey: String?
139 | }
140 |
141 | private struct OptionalKeyedWithoutKey: SuperDecodable {
142 | @OptionalKeyed
143 | var id: String?
144 | }
145 | private struct OptionalKeyedWithoutKeyButWithMissValue: SuperDecodable {
146 | @OptionalKeyed
147 | var id: String?
148 | @OptionalKeyed
149 | var notKey: String?
150 | }
151 |
152 | // MARK: - KeyedWithoutKey
153 |
154 | private struct KeyedWithoutKey: SuperDecodable {
155 | @Keyed
156 | var id: String
157 | }
158 |
159 | // MARK: - KeyedWitNestedKeyed
160 |
161 | private struct KeyedWitNestedKeyed: SuperDecodable {
162 | @Keyed("object")
163 | var aObject: KeyedWithKey
164 | }
165 |
166 | // MARK: - KeyWithNestedDeodable
167 |
168 | private struct KeyWithNestedDeodable: SuperDecodable {
169 | struct AObject: Decodable {
170 | struct Bobject: Decodable {
171 | var id: String
172 | }
173 |
174 | var bObject: Bobject
175 | }
176 |
177 | @Keyed("object")
178 | var aObject: AObject
179 | }
180 |
181 | // MARK: - SuperDecodableWithNoneKeyedProperty
182 |
183 | private struct SuperDecodableWithNoneKeyedProperty: SuperDecodable {
184 | var captured: [String] = []
185 |
186 | var id: String = "" {
187 | didSet {
188 | captured.append(id)
189 | }
190 | }
191 | }
192 |
193 | // MARK: - TransformWithKey
194 |
195 | private struct TransformWithKey: SuperDecodable {
196 | @KeyedTransform("id", SCTransformOf(fromDecoder: {
197 | str in
198 | guard let transformed = Int(str) else {
199 | throw NSError(domain: "transform Error, str:\(str) is not a Int", code: 0)
200 | }
201 | return transformed
202 | }, toEncoder: {
203 | _ in
204 | fatalError("Not a test subject, should never happen")
205 | }))
206 | var aID: Int
207 | }
208 |
209 |
210 | private struct TransformWithoutKey: SuperDecodable {
211 | @KeyedTransform(SCTransformOf(fromDecoder: {
212 | str in
213 | guard let transformed = Int(str) else {
214 | throw NSError(domain: "transform Error, str:\(str) is not a Int", code: 0)
215 | }
216 | return transformed
217 | }, toEncoder: {
218 | _ in
219 | fatalError("Not a test subject, should never happen")
220 | }))
221 | var id: Int
222 | }
223 |
--------------------------------------------------------------------------------
/Tests/SuperCodableTests/EncodeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/11
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import SuperCodable
11 | import XCTest
12 |
13 | // MARK: - EncodableTests
14 |
15 | final class EncodableTests: XCTestCase {
16 | func testKeyedWithKey() throws {
17 | let sut = KeyedWithKey(id: "1")
18 | XCTAssertEqual(sut.aID, "1")
19 |
20 | let data = try JSONEncoder().encode(sut)
21 | XCTAssertEqual(
22 | String(data: data, encoding: .utf8),
23 | #"""
24 | {"id":"1"}
25 | """#)
26 | }
27 |
28 | func testKeyedWithoutKey() throws {
29 | let sut = KeyedWithoutKey(id: "1")
30 | XCTAssertEqual(sut.id, "1")
31 |
32 | let data = try JSONEncoder().encode(sut)
33 | XCTAssertEqual(
34 | String(data: data, encoding: .utf8),
35 | #"""
36 | {"id":"1"}
37 | """#)
38 | }
39 |
40 | func testOptionalKeyedWithKey() throws {
41 | let sut = OptionalKeyedWithKey(id: "1")
42 | XCTAssertEqual(sut.aID, "1")
43 |
44 | let data = try JSONEncoder().encode(sut)
45 | XCTAssertEqual(
46 | String(data: data, encoding: .utf8),
47 | #"""
48 | {"id":"1"}
49 | """#)
50 | }
51 |
52 | func testOptionalKeyedWithoutKey() throws {
53 | let sut = OptionalKeyedWithoutKey(id: nil)
54 | XCTAssertNil(sut.id, "1")
55 |
56 | let data = try JSONEncoder().encode(sut)
57 | XCTAssertEqual(
58 | String(data: data, encoding: .utf8),
59 | #"""
60 | {}
61 | """#)
62 | }
63 |
64 | func testKeyedWithNestedEncodable() throws {
65 | let sut = KeyWithNestedEncodable()
66 | sut.object = KeyWithNestedEncodable.AObject(bObject: KeyWithNestedEncodable.AObject.Bobject(id: "1"))
67 | XCTAssertEqual(sut.object.bObject.id, "1")
68 | let data = try JSONEncoder().encode(sut)
69 | XCTAssertEqual(
70 | String(data: data, encoding: .utf8),
71 | #"""
72 | {"Aobject":{"bObject":{"id":"1"}}}
73 | """#)
74 | }
75 |
76 | func testTransformWithKey() throws {
77 | let sut = TransformWithKey()
78 | sut.aID = "1"
79 | XCTAssertEqual(sut.aID, "1")
80 | let data = try JSONEncoder().encode(sut)
81 | XCTAssertEqual(
82 | String(data: data, encoding: .utf8),
83 | #"""
84 | {"id":1}
85 | """#)
86 | }
87 |
88 | func testTransformWithKeyButFailureShouldHappenEncodeFailure() throws {
89 | let sut = TransformWithKey()
90 | sut.aID = "nan"
91 | XCTAssertEqual(sut.aID, "nan")
92 | XCTAssertThrowsError(
93 | try JSONEncoder().encode(sut)
94 | )
95 | }
96 |
97 | func testTransformWithoutKey() throws {
98 | let sut = TransformWithoutKey()
99 | sut.id = "1"
100 | XCTAssertEqual(sut.id, "1")
101 | let data = try JSONEncoder().encode(sut)
102 | XCTAssertEqual(
103 | String(data: data, encoding: .utf8),
104 | #"""
105 | {"id":1}
106 | """#)
107 | }
108 |
109 | func testTransformWithoutKeyButFailureShouldHappenEncodeFailure() throws {
110 | let sut = TransformWithoutKey()
111 | sut.id = "nan"
112 | XCTAssertEqual(sut.id, "nan")
113 | XCTAssertThrowsError(
114 | try JSONEncoder().encode(sut)
115 | )
116 | }
117 | }
118 |
119 | // MARK: - KeyedWithKey
120 |
121 | private struct KeyedWithKey: SuperEncodable {
122 | // MARK: Lifecycle
123 |
124 | init(id: String) {
125 | self.aID = id
126 | }
127 |
128 | // MARK: Internal
129 |
130 | @Keyed("id")
131 | var aID: String
132 | }
133 |
134 | // MARK: - OptionalKeyedWithKey
135 |
136 | private struct OptionalKeyedWithKey: SuperEncodable {
137 | // MARK: Lifecycle
138 |
139 | init(id: String) {
140 | self._aID.wrappedValue = id
141 | }
142 |
143 | // MARK: Internal
144 |
145 | @OptionalKeyed("id")
146 | var aID: String? = "1"
147 | }
148 |
149 | // MARK: - KeyedWithoutKey
150 |
151 | private struct KeyedWithoutKey: SuperEncodable {
152 | // MARK: Lifecycle
153 |
154 | init(id: String) {
155 | self._id = .init("")
156 | self.id = id
157 | }
158 |
159 | // MARK: Internal
160 |
161 | @Keyed
162 | var id: String
163 | }
164 |
165 | // MARK: - OptionalKeyedWithoutKey
166 |
167 | private struct OptionalKeyedWithoutKey: SuperEncodable {
168 | // MARK: Internal
169 |
170 | @OptionalKeyed
171 | var id: String? = nil
172 | }
173 |
174 | // MARK: - KeyWithNestedEncodable
175 |
176 | private struct KeyWithNestedEncodable: SuperEncodable {
177 | struct AObject: Encodable {
178 | struct Bobject: Encodable {
179 | var id: String
180 | }
181 |
182 | var bObject: Bobject
183 | }
184 |
185 | @Keyed("Aobject")
186 | var object: AObject
187 | }
188 |
189 | // MARK: - TransformWithKey
190 |
191 | private struct TransformWithKey: SuperEncodable {
192 | @KeyedTransform(
193 | "id",
194 | SCTransformOf(
195 | fromDecoder: { _ in
196 | fatalError("not a test subject, should never happen")
197 | },
198 | toEncoder: {
199 | str in
200 | guard let transformed = Int(str) else {
201 | throw NSError(domain: "transform Error, str:\(str) is not a Int", code: 0)
202 | }
203 | return transformed
204 | }))
205 | var aID: String
206 | }
207 |
208 | // MARK: - TransformWithoutKey
209 |
210 | private struct TransformWithoutKey: SuperEncodable {
211 | @KeyedTransform(
212 | SCTransformOf(
213 | fromDecoder: { _ in
214 | fatalError("not a test subject, should never happen")
215 | },
216 | toEncoder: {
217 | str in
218 | guard let transformed = Int(str) else {
219 | throw NSError(domain: "transform Error, str:\(str) is not a Int", code: 0)
220 | }
221 | return transformed
222 | }))
223 | var id: String
224 | }
225 |
--------------------------------------------------------------------------------
/Tests/SuperCodableTests/Polymorphic.swift:
--------------------------------------------------------------------------------
1 | //
2 | /*
3 | * Created by 游宗諭 in 2021/4/16
4 | *
5 | * Using Swift 5.0
6 | *
7 | * Running on macOS 11.2
8 | */
9 |
10 | import Foundation
11 |
12 | // MARK: - Polymorphic
13 |
14 | protocol Polymorphic: Codable {
15 | static var id: String { get }
16 | }
17 |
18 | extension Polymorphic {
19 | static var id: String {
20 | String(describing: Self.self)
21 | }
22 | }
23 |
24 | extension Encoder {
25 | func encode(_ value: ValueType) throws {
26 | guard let value = value as? Polymorphic else {
27 | throw PolymorphicCodableError.unableToRepresentAsPolymorphicForEncoding
28 | }
29 | var container = self.container(
30 | keyedBy: PolymorphicMetaContainerKeys.self
31 | )
32 | try container.encode(type(of: value).id, forKey: ._type)
33 | try value.encode(to: self)
34 | }
35 | }
36 |
37 | // MARK: - PolymorphicCodableError
38 |
39 | enum PolymorphicCodableError: Error {
40 | case missingPolymorphicTypes
41 | case unableToFindPolymorphicType(String)
42 | case unableToCast(decoded: Polymorphic, into: String)
43 | case unableToRepresentAsPolymorphicForEncoding
44 | }
45 |
46 | // MARK: - PolymorphicMetaContainerKeys
47 |
48 | enum PolymorphicMetaContainerKeys: CodingKey {
49 | case _type
50 | }
51 |
52 | extension Decoder {
53 | func decode(_ expectedType: ExpectedType.Type) throws -> ExpectedType {
54 | let container = try self.container(keyedBy: PolymorphicMetaContainerKeys.self)
55 | let typeID = try container.decode(String.self, forKey: ._type)
56 |
57 | guard let types = self.userInfo[.polymorphicTypes] as? [Polymorphic.Type] else {
58 | throw PolymorphicCodableError.missingPolymorphicTypes
59 | }
60 |
61 | let _matchingType = types.first { type in
62 | type.id == typeID
63 | }
64 |
65 | guard let matchingType = _matchingType else {
66 | throw PolymorphicCodableError.unableToFindPolymorphicType(typeID)
67 | }
68 |
69 | let _decoded = try matchingType.init(from: self)
70 |
71 | guard let decoded = _decoded as? ExpectedType else {
72 | throw PolymorphicCodableError.unableToCast(
73 | decoded: _decoded,
74 | into: String(describing: ExpectedType.self)
75 | )
76 | }
77 | return decoded
78 | }
79 | }
80 |
81 | extension CodingUserInfoKey {
82 | static var polymorphicTypes: CodingUserInfoKey {
83 | CodingUserInfoKey(rawValue: "com.codable.polymophicTypes")!
84 | }
85 | }
86 |
87 | // MARK: - PolymorphicValue
88 |
89 | @propertyWrapper
90 | struct PolymorphicValue {
91 | var wrappedValue: Value
92 | }
93 |
94 | // MARK: Codable
95 |
96 | extension PolymorphicValue: Codable {
97 | init(from decoder: Decoder) throws {
98 | self.wrappedValue = try decoder.decode(Value.self)
99 | }
100 |
101 | func encode(to encoder: Encoder) throws {
102 | try encoder.encode(self.wrappedValue)
103 | }
104 | }
105 |
106 | // MARK: - UserRecord
107 |
108 | struct UserRecord: Codable {
109 | let name: String
110 |
111 | @PolymorphicValue
112 | var pet: Animal
113 | }
114 |
115 | import XCTest
116 |
117 | // MARK: - ATests
118 |
119 | final class ATests: XCTestCase {
120 | func test() throws {
121 | let model = UserRecord(name: "A name", pet: Snake(name: "A Snake"))
122 | let data = try JSONEncoder().encode(model)
123 | XCTAssertEqual(
124 | String(data: data, encoding: .utf8),
125 | #"""
126 | {"name":"A name","pet":{"_type":"Snake","name":"A Snake"}}
127 | """#
128 | )
129 | }
130 |
131 | func testDecodeSnake() throws {
132 | let data = #"""
133 | {
134 | "name": "A name",
135 | "pet": {
136 | "_type": "Snake",
137 | "name": "A Snake"
138 | }
139 | }
140 | """#.data(using: .utf8)!
141 | let model = try makeDecoder().decode(UserRecord.self, from: data)
142 | XCTAssertEqual(model.name, "A name")
143 | let pet = try XCTUnwrap(model.pet as? Snake)
144 | XCTAssertEqual(pet.name, "A Snake")
145 | }
146 |
147 | func testDecodeDog() throws {
148 | let data = #"""
149 | {
150 | "name": "A name",
151 | "pet": {
152 | "_type": "Dog",
153 | "petName": "A dog"
154 | }
155 | }
156 | """#.data(using: .utf8)!
157 | let model = try makeDecoder().decode(UserRecord.self, from: data)
158 | XCTAssertEqual(model.name, "A name")
159 | let pet = try XCTUnwrap(model.pet as? Dog)
160 | XCTAssertEqual(pet.petName, "A dog")
161 | }
162 |
163 | func makeDecoder() -> JSONDecoder {
164 | let decoder = JSONDecoder()
165 | decoder.userInfo[.polymorphicTypes] = [
166 | Snake.self,
167 | Dog.self
168 | ]
169 |
170 | return decoder
171 | }
172 | }
173 |
174 | // MARK: - Animal
175 |
176 | protocol Animal: Polymorphic {}
177 |
178 | // MARK: - Snake
179 |
180 | struct Snake: Animal {
181 | var name: String
182 | }
183 |
184 | // MARK: - Dog
185 |
186 | struct Dog: Animal {
187 | var petName: String
188 | }
189 |
190 |
--------------------------------------------------------------------------------
/Tests/SuperCodableTests/SuperCodableTests.swift:
--------------------------------------------------------------------------------
1 | import SuperCodable
2 | import XCTest
3 |
4 | let doubleTransform = SCTransformOf {
5 | (double) -> Int in
6 | Int(double)
7 | } toEncoder: { (int) -> Double in
8 | Double(int) * 10
9 | }
10 |
11 | // MARK: - Student
12 |
13 | struct Student: SuperCodable {
14 | @Keyed
15 | var id: String
16 | @Keyed("name")
17 | var aName: String
18 | @KeyedTransform("grade", doubleTransform)
19 | var AGrade: Int
20 | }
21 |
22 | // MARK: - SuperCodableTests
23 |
24 | final class SuperCodableTests: XCTestCase {
25 | func test() throws {
26 | let data =
27 | #"""
28 | [
29 | {
30 | "id": "1",
31 | "name": "Josh",
32 | "grade": 3.18
33 | },
34 | {
35 | "id": "2",
36 | "name": "Marc",
37 | "grade": 2.25
38 | },
39 | {
40 | "id": "3",
41 | "name": "Judy",
42 | "grade": 4.00
43 | }
44 | ]
45 | """#.data(using: .utf8)!
46 | let sut = try! JSONDecoder().decode([Student].self, from: data)
47 | XCTAssertEqual(sut.count,
48 | 3)
49 | XCTAssertEqual(
50 | sut.map(\.AGrade),
51 | [3, 2, 4])
52 | let encoded = try JSONEncoder().encode(sut)
53 | let encodedString = try XCTUnwrap(String(data: encoded, encoding: .utf8))
54 | XCTAssertEqual("""
55 | [{"id":"1","name":"Josh","grade":30},{"id":"2","name":"Marc","grade":20},{"id":"3","name":"Judy","grade":40}]
56 | """, encodedString)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/SuperCodableTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(SuperCodableTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------