├── .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 | --------------------------------------------------------------------------------