├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CodableWrapper.podspec ├── Development Docs ├── 一、设计目标和手动实现这些目标特性.md └── 二、Codable宏的开发和实现.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── CodableWrapper │ ├── AnyCodingKey.swift │ ├── AnyDecodable.swift │ ├── BuiltInBridgeType.swift │ ├── CodableWrapperMacros.swift │ ├── Decoder.swift │ ├── Encoder.swift │ ├── Error.swift │ ├── SnakeCamelConvert.swift │ ├── TransformOf.swift │ └── TransformType.swift └── CodableWrapperMacros │ ├── ASTError.swift │ ├── Codable.swift │ ├── CodableKey.swift │ ├── CodableSubclass.swift │ ├── CodingNestedKey.swift │ ├── CodingTransformer.swift │ ├── ModelMemberPropertyContainer.swift │ ├── Plugin.swift │ └── VariableDeclSyntaxExtension.swift └── Tests └── CodableWrapperTests ├── CodableWrapperTests.swift ├── DeclareTests.swift ├── ExampleTest.swift ├── ExtensionTest.swift ├── NestedKeyTest.swift ├── TransformTest.swift └── Transforms.swift /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | *.xcuserdata 3 | .DS_Store 4 | .build/ -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CodableWrapper.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint CodableWrapper.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'CodableWrapper' 11 | s.version = '1.1.1' 12 | s.summary = 'A short description of CodableWrapper.' 13 | 14 | s.description = <<-DESC 15 | CodableWrapper Pod 16 | DESC 17 | 18 | s.homepage = 'https://github.com/winddpan/CodableWrapper' 19 | s.author = { 'winddpan' => 'https://github.com/winddpan' } 20 | s.source = { :git => 'git@github.com:winddpan/CodableWrapper.git', :tag => s.version.to_s } 21 | 22 | s.ios.deployment_target = '13.0' 23 | 24 | s.source_files = 'Sources/CodableWrapper/*{.swift}' 25 | s.preserve_paths = ["Package.swift", "Sources/CodableWrapperMacros", "Tests", "Bin"] 26 | 27 | s.pod_target_xcconfig = { 28 | "OTHER_SWIFT_FLAGS" => "-Xfrontend -load-plugin-executable -Xfrontend $(PODS_BUILD_DIR)/CodableWrapper/release/CodableWrapperMacros-tool#CodableWrapperMacros" 29 | } 30 | 31 | s.user_target_xcconfig = { 32 | "OTHER_SWIFT_FLAGS" => "-Xfrontend -load-plugin-executable -Xfrontend $(PODS_BUILD_DIR)/CodableWrapper/release/CodableWrapperMacros-tool#CodableWrapperMacros" 33 | } 34 | 35 | script = <<-SCRIPT 36 | env -i PATH="$PATH" "$SHELL" -l -c "swift build -c release --package-path \\"$PODS_TARGET_SRCROOT\\" --build-path \\"${PODS_BUILD_DIR}/CodableWrapper\\"" 37 | SCRIPT 38 | 39 | s.script_phase = { 40 | :name => 'Build CodableWrapper macro plugin', 41 | :script => script, 42 | :execution_position => :before_compile 43 | } 44 | end 45 | -------------------------------------------------------------------------------- /Development Docs/一、设计目标和手动实现这些目标特性.md: -------------------------------------------------------------------------------- 1 | # 关于CodableWrapper 2 | 3 | * Codable很好,但是有一些缺陷:比如严格要求数据源,定义为String给了Int就抛异常、支持自定义CodingKey但是写法十分麻烦、缺字段的情况下不适用Optional会抛异常而不是使用缺省值等等。 4 | * 之前发布过[PropertyWrapper版](https://github.com/winddpan/CodableWrapper/tree/0.3.3),主要使用PropertyWrapper标记属性来提高了Codable的使用体验,使用了几个比较tricky的黑科技,所以API也比市面上的同类库要简单。 5 | * 现在Swift5.9支持宏了,决定写一个没有任何tricky的宏版本的CodableWrapper。目前已开发完毕[CodableWrapper/swift5.9-macro](https://github.com/winddpan/CodableWrapper/tree/swift5.9-macro) 6 | 7 | ## 首先确定目标 8 | 1. 支持缺省值,JSON缺少字段容错 9 | 2. 支持 `String` `Bool` `Number` 等基本类型互转 10 | 3. 驼峰大小写自动互转 11 | 4. 自定义解析key 12 | 5. 自定义解析规则 (Transformer) 13 | 6. 方便的 `Codable Class` 子类 14 | 15 | ## 设计API 16 | * 定义宏: 17 | * `@Codable` 18 | * `@CodableSubclass` 19 | * `@CodingKey(..)` 20 | * `@CodingNestedKey(..)` 21 | * `@CodingTransformer(..)` 22 | 23 | * 例子: 24 | ``` 25 | @Codable 26 | struct BasicModel { 27 | var defaultVal: String = "hello world" 28 | var defaultVal2: String = Bool.random() ? "hello world" : "" 29 | let strict: String 30 | let noStrict: String? 31 | let autoConvert: Int? 32 | 33 | @CodingKey("hello") 34 | var hi: String = "there" 35 | 36 | @CodingNestedKey("nested.hi") 37 | @CodingTransformer(StringPrefixTransform("HELLO -> ")) 38 | var codingKeySupport: String 39 | 40 | @CodingNestedKey("nested.b") 41 | var nestedB: String 42 | 43 | var testGetter: String { 44 | nestedB 45 | } 46 | } 47 | ``` 48 | 49 | ## 手动实现这些目标特性 50 | 想要宏自动生成代码,需要先弄清楚怎么手动实现这些目标特性。先想象一下编译器怎么生成Codable的实现的: 51 | 52 | ``` 53 | struct BasicModel: Codable { 54 | var defaultVal: String = "hello world" 55 | } 56 | ``` 57 | 58 | ``` 59 | // 编译器生成的 60 | struct BasicModel: Codable { 61 | var defaultVal: String = "hello world" 62 | 63 | enum CodingKeys: String, CodingKey { 64 | case defaultVal 65 | } 66 | 67 | init(from decoder: Decoder) throws { 68 | let container = try decoder.container(keyedBy: CodingKeys.self) 69 | self.defaultVal = try container.decode(String.self, forKey: .defaultVal) 70 | } 71 | 72 | func encode(to encoder: Encoder) throws { 73 | var container = encoder.container(keyedBy: CodingKeys.self) 74 | try container.encode(self.defaultVal, forKey: .defaultVal) 75 | } 76 | } 77 | ``` 78 | 79 | ### 手动实现目标1:支持缺省值 80 | 81 | ``` 82 | init(from decoder: Decoder) throws { 83 | let container = try decoder.container(keyedBy: CodingKeys.self) 84 | self.defaultVal = (try? container.decode(String.self, forKey: .defaultVal)) ?? "hello world" 85 | } 86 | ``` 87 | 88 | ### 手动实现目标2:支持 `String` `Bool` `Number` 等基本类型互转 89 | 90 | ``` 91 | init(from decoder: Decoder) throws { 92 | let container = try decoder.container(keyedBy: CodingKeys.self) 93 | 94 | if let value = try? container.decode(Int.self, forKey: .defaultVal) { 95 | self.defaultVal = String(value) 96 | } else if let value = try? container.decode(String.self, forKey: .defaultVal) { 97 | self.defaultVal = String(value) 98 | } else if let value = try? container.decode(Float.self, forKey: .defaultVal) { 99 | self.defaultVal = String(value) 100 | } 101 | // 各种基本类型尝试转换 else if ... 102 | else { 103 |         self.defaultVal = "hello world" 104 |     } 105 | } 106 | ``` 107 | 108 | 这么解析肯定不是个办法,这里结合了两个开源库的实现,第一步使用[AnyDecodable](https://github.com/Flight-School/AnyCodable/blob/master/Sources/AnyCodable/AnyDecodable.swift)解析出JSON内的数据,解析出来可能是任意基本类型。第二步使用[BuiltInBridgeType](https://github.com/alibaba/HandyJSON/blob/master/Source/BuiltInBridgeType.swift)和[BuiltInBridgeType](https://github.com/alibaba/HandyJSON/blob/master/Source/BuiltInBridgeType.swift)将解析出来的基本类型尝试转换成目标类型。完整实现如下: 109 | 110 | ```swift 111 | extension Decodable { 112 | static func decode(from container: KeyedDecodingContainer, forKey key: KeyedDecodingContainer.Key) throws -> Self { 113 | return try container.decode(Self.self, forKey: key) 114 | } 115 | } 116 | 117 | struct BasicModel: Codable { 118 | var defaultVal: String = "hello world" 119 | 120 | enum CodingKeys: CodingKey { 121 | case defaultVal 122 | } 123 | 124 | init(from decoder: Decoder) throws { 125 | let container = try decoder.container(keyedBy: CodingKeys.self) 126 | defaultVal = container.tryNormalKeyDecode(type: String.self, key: .defaultVal) ?? "hello world" 127 | } 128 | 129 | func encode(to encoder: Encoder) throws { 130 | var container = encoder.container(keyedBy: CodingKeys.self) 131 | try container.encode(defaultVal, forKey: .defaultVal) 132 | } 133 | } 134 | 135 | extension KeyedDecodingContainer { 136 | func tryNormalKeyDecode(type _: Value.Type, key: K) -> Value? { 137 | let value = try? decodeIfPresent(AnyDecodable.self, forKey: key)?.value 138 | if let value = value { 139 | if let converted = value as? Value { 140 | return converted 141 | } 142 | if let _bridged = (Value.self as? _BuiltInBridgeType.Type)?._transform(from: value), let __bridged = _bridged as? Value { 143 | return __bridged 144 | } 145 | // 如果是非基本类型,那继续尝试Decode 146 | if let valueType = Value.self as? Decodable.Type { 147 | if let value = try? valueType.decode(from: self, forKey: key) as? Value { 148 | return value 149 | } 150 | } 151 | } 152 | return nil 153 | } 154 | } 155 | ``` 156 | 157 | ### 手动实现目标3:支持驼峰大小写自动互转 158 | ```swift 159 | // 为了简化CodingKey,使用AnyCodingKey实现CodingKey 160 | public struct AnyCodingKey: CodingKey { 161 | public var stringValue: String 162 | public var intValue: Int? 163 | 164 | public init?(stringValue: String) { 165 | self.stringValue = stringValue 166 | intValue = nil 167 | } 168 | 169 | public init?(intValue: Int) { 170 | stringValue = "\(intValue)" 171 | self.intValue = intValue 172 | } 173 | 174 | public init(index: Int) { 175 | stringValue = "\(index)" 176 | intValue = index 177 | } 178 | } 179 | 180 | struct BasicModel: Codable { 181 | var defaultVal: String = "hello world" 182 | 183 | init(from decoder: Decoder) throws { 184 | // CodingKeys 改为 AnyCodingKey 185 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 186 | defaultVal = container.tryNormalKeyDecode(type: String.self, key: "defaultVal") ?? "hello world" 187 | } 188 | } 189 | 190 | extension KeyedDecodingContainer where K == AnyCodingKey { 191 | func tryNormalKeyDecode(type: Value.Type, key: String) -> Value? { 192 | func _decode(key: String) -> Value? { 193 | let value = try? decodeIfPresent(AnyDecodable.self, forKey: key)?.value 194 | if let value = value { 195 | if let converted = value as? Value { 196 | return converted 197 | } 198 | if let _bridged = (Value.self as? _BuiltInBridgeType.Type)?._transform(from: value), let __bridged = _bridged as? Value { 199 | return __bridged 200 | } 201 | if let valueType = Value.self as? Decodable.Type { 202 | if let value = try? valueType.decode(from: self, forKey: key) as? Value { 203 | return value 204 | } 205 | } 206 | } 207 | return nil 208 | } 209 | 210 | for newKey in [key, key.snakeCamelConvert()].compactMap({ $0 }) { 211 | if let value = _decode(key: newKey) { 212 | return value 213 | } 214 | } 215 | return nil 216 | } 217 | } 218 | 219 | extension String { 220 | func snakeCamelConvert() -> String? { 221 | // 驼峰大小写互转 222 | ... 223 | } 224 | } 225 | ``` 226 | 227 | ### 手动实现目标4:自定义解析Key和NestedKey 228 | [container.decode](https://github.com/winddpan/CodableWrapper/blob/swift5.9-macro/Sources/CodableWrapper/Decoder.swift)封装 [container.encode](https://github.com/winddpan/CodableWrapper/blob/swift5.9-macro/Sources/CodableWrapper/Encoder.swift)封装 229 | 230 | ```swift 231 | struct TestModel: Codable { 232 | // @CodingKey("u1", "u2", "u9") 233 | let userName: String = "" 234 | 235 | // @CodingNestedKey("data.u1", "data.u2", "data.u9") 236 | let userName2: String = "" 237 | 238 | init(from decoder: Decoder) throws { 239 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 240 | userName = container.decode(type: String.self, keys: ["u1", "u2", "u9"], nestedKeys: []) ?? "" 241 | userName2 = container.decode(type: String.self, keys: [], nestedKeys: ["data.u1", "data.u2", "data.u9"]) ?? "" 242 | } 243 | 244 | func encode(to encoder: Encoder) throws { 245 | var container = encoder.container(keyedBy: CodingKeys.self) 246 | try container.encode(value: self.userName, keys: ["u1", "u2", "u9"], nestedKeys: []) 247 | try container.encode(value: self.userName2, keys: [], nestedKeys: ["data.u1", "data.u2", "data.u9"]) 248 | } 249 | } 250 | ``` 251 | 252 | ### 手动实现目标5:自定义解析规则Transformer 253 | ```swift 254 | // 定义TransformType协议 255 | public protocol TransformType { 256 | associatedtype Object 257 | associatedtype JSON: Codable 258 | 259 | func transformFromJSON(_ json: JSON?) -> Object 260 | func transformToJSON(_ object: Object) -> JSON? 261 | } 262 | ``` 263 | 264 | ```swift 265 | // 定义一个不遵循Codable的结构体 266 | struct DateWrapper { 267 | let timestamp: TimeInterval 268 | 269 | var date: Date { 270 | Date(timeIntervalSince1970: timestamp) 271 | } 272 | 273 | init(timestamp: TimeInterval) { 274 | self.timestamp = timestamp 275 | } 276 | 277 | static var transformer = TransformOf(fromJSON: { DateWrapper(timestamp: $0 ?? 0) }, toJSON: { $0.timestamp }) 278 | } 279 | 280 | struct DateModel: Codable { 281 | // @CodingTransformer(DateWrapper.transformer) 282 | var time: DateWrapper? = DateWrapper(timestamp: 0) 283 | 284 | // @CodingTransformer(DateWrapper.transformer) 285 | var time1: DateWrapper = .init(timestamp: 0) 286 | 287 | // @CodingTransformer(DateWrapper.transformer) 288 | var time2: DateWrapper? 289 | 290 | // @CodingTransformer(DateWrapper.transformer) 291 | var time3: DateWrapper 292 | 293 | init(from decoder: Decoder) throws { 294 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 295 | let time = try? container.decode(type: TimeInterval.self, keys: ["time"], nestedKeys: []) 296 | self.time = DateWrapper.transformer.transformFromJSON(time) ?? DateWrapper(timestamp: 0) 297 | 298 | let time1 = try? container.decode(type: TimeInterval.self, keys: ["time1"], nestedKeys: []) 299 | self.time1 = DateWrapper.transformer.transformFromJSON(time1) ?? .init(timestamp: 0) 300 | 301 | let time2 = try? container.decode(type: TimeInterval.self, keys: ["time2"], nestedKeys: []) 302 | self.time2 = DateWrapper.transformer.transformFromJSON(time2) 303 | 304 | let time3 = try? container.decode(type: TimeInterval.self, keys: ["time3"], nestedKeys: []) 305 | self.time3 = DateWrapper.transformer.transformFromJSON(time3) 306 | } 307 | 308 | func encode(to encoder: Encoder) throws { 309 | let container = encoder.container(keyedBy: AnyCodingKey.self) 310 | if let time = self.time, let value = DateWrapper.transformer.transformToJSON(self.time) { 311 | try container.encode(value: value, keys: ["time"], nestedKeys: []) 312 | } 313 | if let value = DateWrapper.transformer.transformToJSON(self.time1) { 314 | try container.encode(value: value, keys: ["time1"], nestedKeys: []) 315 | } 316 | if let time2 = self.time2, let value = DateWrapper.transformer.transformToJSON(time2) { 317 | try container.encode(value: value, keys: ["time2"], nestedKeys: []) 318 | } 319 | if let value = DateWrapper.transformer.transformToJSON(time3) { 320 | try container.encode(value: value, keys: ["time3"], nestedKeys: []) 321 | } 322 | } 323 | } 324 | ``` 325 | 326 | ### 手动实现目标6:“不方便”的 `Codable Class` 子类 327 | ``` 328 | class ClassModel1: Codable { 329 | var val: String? 330 | } 331 | 332 | class ClassSubmodel1: ClassModel1 { 333 | var subVal: String = "1_1" 334 | 335 | required init(from decoder: Decoder) throws { 336 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 337 | self.subVal = (try? container.decode(type: type(of: self.subVal), keys: ["subVal"], nestedKeys: [])) ?? ("1_1") 338 | try super.init(from: decoder) 339 | } 340 | 341 | override func encode(to encoder: Encoder) throws { 342 | try super.encode(to: encoder) 343 | let container = encoder.container(keyedBy: AnyCodingKey.self) 344 | try container.encode(value: self.subVal, keys: ["subVal"], nestedKeys: []) 345 | } 346 | } 347 | ``` 348 | 349 | 下一篇文章将介绍Codable宏的开发和实现。 350 | 351 | ----- 352 | **文章目录** 353 | * [一、设计目标和手动实现这些目标特性](https://juejin.cn/post/7251501945272270908) 354 | * [二、Codable宏的开发和实现](https://juejin.cn/post/7252170693676499004) 355 | -------------------------------------------------------------------------------- /Development Docs/二、Codable宏的开发和实现.md: -------------------------------------------------------------------------------- 1 | *接上一章 CodableWrapper Macro 版的 [设计目标和手动实现这些目标特性](https://juejin.cn/post/7251501945272270908),本章节主要讲Codable宏的开发和实现。* 2 | 3 | ## 搭建环境 4 | 1. 目前Swift5.9还在Beta阶段 5 | 2. 下载 Xcode15 Beta 或者更之后的版本 6 | 3. 从swift.org下载安装Swift 5.9 Development for Xcode [Snapshot](https://www.swift.org/download/#snapsh) 7 | 4. 打开Xcode15,File -> New -> Package -> Swift Macro,项目名为CodableWrapper 8 | 5. Xcode会自动拉取swift-syntax依赖,整个项目自动生成3个target和一个Tests,4个目录,分别为: 9 | 1. Sources/CodableWrapper Package库目录,用于存放宏定义,以及库提供的一些API和实现。 10 | 2. Sources/CodableWrapperClient 本地测试运行使用,本文使用TDD方式,所以不需要它 11 | 3. Sources/CodableWrapperMacros 宏实现的地方 12 | 4. Tests/CodableWrapperTests 宏的测试用例 13 | 14 | ## 改造Package.Swift 15 | 因为使用TDD方式开发,开发和测试用例都基于Tests。删除CodableWrapperClient,CodableWrapperTests依赖改为CodableWrapper这个framework而不是CodableWrapperMacros。 16 | 17 | ``` 18 | let package = Package( 19 | name: "CodableWrapper", 20 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], 21 | products: [ 22 | // Products define the executables and libraries a package produces, making them visible to other packages. 23 | .library( 24 | name: "CodableWrapper", 25 | targets: ["CodableWrapper"] 26 | ), 27 | ], 28 | dependencies: [ 29 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax 30 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"), 31 | ], 32 | targets: [ 33 | // Targets are the basic building blocks of a package, defining a module or a test suite. 34 | // Targets can depend on other targets in this package and products from dependencies. 35 | // Macro implementation that performs the source transformation of a macro. 36 | .macro( 37 | name: "CodableWrapperMacros", 38 | dependencies: [ 39 | .product(name: "SwiftSyntax", package: "swift-syntax"), 40 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 41 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 42 | ] 43 | ), 44 | 45 | // Library that exposes a macro as part of its API, which is used in client programs. 46 | .target(name: "CodableWrapper", dependencies: ["CodableWrapperMacros"]), 47 | 48 | // A test target used to develop the macro implementation. 49 | .testTarget( 50 | name: "CodableWrapperTests", 51 | dependencies: [ 52 | "CodableWrapper", 53 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 54 | ] 55 | ), 56 | ] 57 | ) 58 | ``` 59 | 60 | ## 先写一个基本测试用例: 61 | 62 | ``` 63 | // CodableWrapperTests.swift 64 | 65 | @Codable 66 | struct BasicModel { 67 | var defaultVal: String = "hello world" 68 | var strict: String 69 | var noStrict: String? 70 | var autoConvert: Int? 71 | 72 | @CodingKey("customKey") 73 | var codingKeySupport: String 74 | } 75 | 76 | final class CodableWrapperTests: XCTestCase { 77 | func testBasicExample() throws { 78 | let jsonStr = """ 79 | {"strict": "value of strict", "autoConvert": "998", "customKey": "value of customKey"} 80 | """ 81 | 82 | let model = try JSONDecoder().decode(BasicModel.self, from: jsonStr.data(using: .utf8)!) 83 | XCTAssertEqual(model.defaultVal, "hello world") 84 | XCTAssertEqual(model.strict, "value of strictValue") 85 | XCTAssertEqual(model.noStrict, nil) 86 | XCTAssertEqual(model.autoConvert, 998) 87 | XCTAssertEqual(model.codingKeySupport, "value of customKey") 88 | } 89 | } 90 | ``` 91 | 92 | ## Swift Macro 的一些基本概念 93 | 这里推荐一篇掘金的文章、Swift Macro提议发起者的demo、一个Swift AST解析工具(下面会经常用到) 94 | 1. [【WWDC23】一文看懂 Swift Macro](https://juejin.cn/post/7249888320166903867) 95 | 2. [swift-macro-examples](https://github.com/DougGregor/swift-macro-examples) 96 | 3. [Swift AST Explorer](https://swift-ast-explorer.com/) 97 | 98 | 本项目使用了`@attached(member)`和`@attached(conformance)`两种类型的宏 99 | 100 | ## 简单定义宏和过编译 101 | 102 | 测试用例很明显编译会报错,先定义Codable和CodingKey宏。 103 | 104 | ``` 105 | // CodableWrapperMacros/CodableWrapper.swift 106 | 107 | @attached(member, names: named(init(from:)), named(encode(to:))) 108 | @attached(conformance) 109 | public macro Codable() = #externalMacro(module: "CodableWrapperMacros", type: "Codable") 110 | 111 | @attached(member) 112 | public macro CodingKey(_ key: String ...) = #externalMacro(module: "CodableWrapperMacros", type: "CodingKey") 113 | ``` 114 | 115 | 实现@Codable和@CodingKey宏。 116 | 117 | ``` 118 | // CodableWrapperMacros/Codable.swift 119 | import SwiftSyntax 120 | import SwiftSyntaxMacros 121 | 122 | public struct Codable: MemberMacro { 123 | public static func expansion(of _: AttributeSyntax, 124 | providingConformancesOf declaration: some DeclGroupSyntax, 125 | in _: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] 126 | { 127 | return [] 128 | } 129 | 130 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, 131 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 132 | in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] 133 | { 134 |         return [] 135 |     } 136 | } 137 | ``` 138 | 139 | ``` 140 | // CodableWrapperMacros/CodingKey.swift 141 | import SwiftSyntax 142 | import SwiftSyntaxMacros 143 | 144 | public struct CodingKey: ConformanceMacro, MemberMacro { 145 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, 146 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 147 | in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] 148 | { 149 |         return [] 150 |     } 151 | } 152 | ``` 153 | 154 | ``` 155 | // CodableWrapperMacros/Plugin.swift 156 | import SwiftCompilerPlugin 157 | import SwiftSyntaxMacros 158 | 159 | @main 160 | struct CodableWrapperPlugin: CompilerPlugin { 161 | let providingMacros: [Macro.Type] = [ 162 | Codable.self, 163 | CodingKey.self, 164 | ] 165 | } 166 | ``` 167 | 168 | 如果你还不了解宏的各种类型,我建议先阅读这篇文章:[【WWDC23】一文看懂 Swift Macro](https://juejin.cn/post/7249888320166903867)。 169 | 170 | 在这里,`@Codable`实现了两种宏,一种是一致性宏(Conformance Macro),另一种是成员宏(Member Macro)。 171 | 172 | 一些关于这些宏的说明: 173 | - `@Codable`和`Codable`协议的宏名不会冲突,这样的命名一致性可以降低认知负担。 174 | - Conformance Macro用于自动让数据模型遵循Codable协议(如果尚未遵循)。 175 | - Member Macro用于添加`init(from decoder: Decoder)`和`func encode(to encoder: Encoder)`这两个方法。在`@attached(member, named(init(from:)), named(encode(to:)))`中,必须声明新增方法的名称才是合法的。 176 | 177 | 运行测试用例,按下Command+U,编译通过了,但是测试用例很明显会失败。因为Codable不支持使用默认值的方式,所以无法找到`defaultValue`这个key。 178 | 179 | ### 实现自动遵循Codable协议 180 | ``` 181 | // CodableWrapperMacros/Codable.swift 182 | 183 | public struct Codable: ConformanceMacro, MemberMacro { 184 | public static func expansion(of node: AttributeSyntax, 185 | providingConformancesOf declaration: some DeclGroupSyntax, 186 | in context: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { 187 | return [("Codable", nil)] 188 | } 189 | 190 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, 191 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 192 | in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] 193 | { 194 | return [] 195 | } 196 | } 197 | ``` 198 | 199 | 编译一下。右键`@Codable` -> `Expand Macro`查看扩写的代码,看起来还不错。 200 | ![20230704155615](http://images.testfast.cn/20230704155615.png) 201 | ![20230704155520](http://images.testfast.cn/20230704155520.png) 202 | 203 | 但如果`BasicModel`本身就遵循了`Codable`,编译就报错了。所以希望先检查数据模型是否遵循`Codable`协议,如果没有的话再遵循它,怎么办呢? 204 | 打开[Swift AST Explorer ](https://swift-ast-explorer.com/)编写一个简单`Struct`和`Class`,可以看到整个AST,`declaration: some DeclGroupSyntax`对象根据模型是`struct`还是`class`分别对应了`StructDecl`和`ClassDecl`。 205 | 206 | ![20230704160841](http://images.testfast.cn/20230704160841.png) 207 | 208 | 一番探究,补上检查代码如下。 209 | 210 | ``` 211 | public static func expansion(of node: AttributeSyntax, 212 | providingConformancesOf declaration: some DeclGroupSyntax, 213 | in context: some MacroExpansionContext) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { 214 | var inheritedTypes: InheritedTypeListSyntax? 215 | if let declaration = declaration.as(StructDeclSyntax.self) { 216 | inheritedTypes = declaration.inheritanceClause?.inheritedTypeCollection 217 | } else if let declaration = declaration.as(ClassDeclSyntax.self) { 218 | inheritedTypes = declaration.inheritanceClause?.inheritedTypeCollection 219 | } else { 220 | throw ASTError("use @Codable in `struct` or `class`") 221 | } 222 | if let inheritedTypes = inheritedTypes, 223 | inheritedTypes.contains(where: { inherited in inherited.typeName.trimmedDescription == "Codable" }) 224 | { 225 | return [] 226 | } 227 | return [("Codable" as TypeSyntax, nil)] 228 | } 229 | ``` 230 | 这里顺便检查了一下是否是 `class` 或 `struct`,如果不是则会提示。 231 | 232 | ![20230704175012](http://images.testfast.cn/20230704175012.png) 233 | 234 | 至此,第一个 Macro 编写流程已经跑通。 235 | 236 | ### 新增 Macro `@CodingNestedKey` `@CodingTransformer` 和丰富测试用例 237 | 根据上一章的[设计目标和手动实现这些目标特性](https://juejin.cn/post/7251501945272270908)确定了目标和手动实现。 238 | - 目标如下: 239 | 1. 支持缺省值,JSON 缺少字段容错 240 | 2. 支持 String Bool Number 等基本类型互转 241 | 3. 驼峰大小写自动互转 242 | 4. 自定义解析 key 243 | 5. 自定义解析规则 (Transformer) 244 | 6. 方便的 Codable Class 子类 245 | 246 | 为了达成目标,新增 Macro `@CodingNestedKey` `@CodingTransformer` 和完善测试用例。这两个 Macro 的声明和实现同上面的 `@CodingKey` 一致。 247 | ``` 248 | @Codable 249 | struct BasicModel { 250 | var defaultVal: String = "hello world" 251 | var defaultVal2: String = Bool.random() ? "hello world" : "" 252 | let strict: String 253 | let noStrict: String? 254 | let autoConvert: Int? 255 | 256 | @CodingKey("hello") 257 | var hi: String = "there" 258 | 259 | @CodingNestedKey("nested.hi") 260 | @CodingTransformer(StringPrefixTransform("HELLO -> ")) 261 | var codingKeySupport: String 262 | 263 | @CodingNestedKey("nested.b") 264 | var nestedB: String 265 | ``` 266 | 267 | 268 | ### 实现 `@Codable` 功能 269 | 根据上一章的[设计目标和手动实现这些目标特性](https://juejin.cn/post/7251501945272270908)确定了目标和手动实现。 270 | 271 | 先定义个 `ModelMemberPropertyContainer`,`init(from decoder: Decoder)` 和 `func encode(to encoder: Encoder)` 的扩展都在里面实现。 272 | 273 | ``` 274 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, 275 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 276 | in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] 277 | { 278 | let propertyContainer = try ModelMemberPropertyContainer(decl: declaration, context: context) 279 | let decoder = try propertyContainer.genDecoderInitializer(config: .init(isOverride: false)) 280 | let encoder = try propertyContainer.genEncodeFunction(config: .init(isOverride: false)) 281 | return [decoder, encoder] 282 | } 283 | ``` 284 | 285 | ``` 286 | // CodableWrapperMacros/ModelMemberPropertyContainer.swift 287 | 288 | import SwiftSyntax 289 | import SwiftSyntaxMacros 290 | 291 | struct GenConfig { 292 | let isOverride: Bool 293 | } 294 | 295 | struct ModelMemberPropertyContainer { 296 | let context: MacroExpansionContext 297 | fileprivate let decl: DeclGroupSyntax 298 | 299 | init(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws { 300 | self.decl = decl 301 | self.context = context 302 | } 303 | 304 | func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax { 305 | return """ 306 | init(from decoder: Decoder) throws { 307 | fatalError() 308 | } 309 | """ as DeclSyntax 310 | } 311 | 312 | func genEncodeFunction(config: GenConfig) throws -> DeclSyntax { 313 | return """ 314 | func encode(to encoder: Encoder) throws { 315 | fatalError() 316 | } 317 | """ as DeclSyntax 318 | } 319 | } 320 | 321 | ``` 322 | [查看ModelMemberPropertyContainer完整源码](https://github.com/winddpan/CodableWrapper/blob/swift5.9-macro/Sources/CodableWrapperMacros/ModelMemberPropertyContainer.swift) 323 | 324 | 简单实现了框架,编译并查看一下扩写的代码。 325 | 326 | ![20230704170239](http://images.testfast.cn/20230704170239.png) 327 | 328 | #### 填充`init(from decoder: Decoder)` 329 | 根据上一章的[设计目标和手动实现这些目标特性](https://juejin.cn/post/7251501945272270908),我们已经封装好了`container.decode(type:keys:nestedKeys:)`和`container.encode(type:keys:nestedKeys:)`。希望将`BasicModel`扩展为以下形式: 330 | ``` 331 | @Codable 332 | struct BasicModel { 333 | var defaultVal: String = "hello world" 334 | var defaultVal2: String = Bool.random() ? "hello world" : "" 335 | let strict: String 336 | let noStrict: String? 337 | let autoConvert: Int? 338 | 339 | @CodingKey("hello") 340 | var hi: String = "there" 341 | 342 | @CodingNestedKey("nested.hi") 343 | @CodingTransformer(StringPrefixTransform("HELLO -> ")) 344 | var codingKeySupport: String 345 | 346 | @CodingNestedKey("nested.b") 347 | var nestedB: String 348 | 349 | init(from decoder: Decoder) throws { 350 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 351 | 352 | self.defaultVal = (try? container.decode(type: type(of: self.defaultVal), keys: ["defaultVal"], nestedKeys: [])) ?? ("hello world") 353 | self.defaultVal2 = (try? container.decode(type: type(of: self.defaultVal2), keys: ["defaultVal2"], nestedKeys: [])) ?? (Bool.random() ? "hello world" : "") 354 | self.strict = try container.decode(type: type(of: self.strict), keys: ["strict"], nestedKeys: []) 355 | self.noStrict = try container.decode(type: type(of: self.noStrict), keys: ["noStrict"], nestedKeys: []) 356 | self.autoConvert = try container.decode(type: type(of: self.autoConvert), keys: ["autoConvert"], nestedKeys: []) 357 | self.hi = (try? container.decode(type: type(of: self.hi), keys: ["hello", "hi"], nestedKeys: [])) ?? ("there") 358 | 359 | let transformer = StringPrefixTransform("HELLO -> ") 360 | let codingKeySupport = try? container.decode(type: type(of: transformer).JSON.self, keys: ["codingKeySupport"], nestedKeys: ["nested.hi"]) 361 | self.codingKeySupport = transformer.transformFromJSON(codingKeySupport) 362 | 363 | self.nestedB = try container.decode(type: type(of: self.nestedB), keys: ["nestedB"], nestedKeys: ["nested.b"]) 364 | } 365 | } 366 | ``` 367 | 368 | 这里使用`type(of: self.defaultVal)`而不是`String`是因为如果这样定义`var defaultVal = "hello world"`,就无法在AST阶段获取类型,需要到语义分析阶段才行。感谢编译器优化,`type(of: self.defaultVal)`会自动在之后的阶段被正确转换为`String.self`(测试一下`type(of: self.strict)`在`self.strict`未初始化的时候也能被编译过)。为了获取`Transformer`的源类型,也同样使用`type(of: \(transformerVar)).JSON.self`。 369 | 370 | 分析一下希望生成的代码:需要得知属性名、`@CodingKey`的参数、`@CodingNestedKey`的参数、`@CodingTransformer`的参数、初始化表达式。设计一个结构体: 371 | ``` 372 | private struct ModelMemberProperty { 373 | var name: String 374 | var type: String 375 | var isOptional: Bool = false 376 | var normalKeys: [String] = [] 377 | var nestedKeys: [String] = [] 378 | var transformerExpr: String? 379 | var initializerExpr: String? 380 | } 381 | ``` 382 | `transformerExpr`和`initializerExpr`都是表达式,因为参数可能是一个实例对象,也可能是整个构造方法。我们要做的只是把它原封不动地塞过去。 383 | 384 | ``` 385 | func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax { 386 | // memberProperties: [ModelMemberProperty] 387 | let body = memberProperties.enumerated().map { idx, member in 388 | 389 | if let transformerExpr = member.transformerExpr { 390 | let transformerVar = context.makeUniqueName(String(idx)) 391 | let tempJsonVar = member.name 392 | 393 | var text = """ 394 | let \(transformerVar) = \(transformerExpr) 395 | let \(tempJsonVar) = try? container.decode(type: type(of: \(transformerVar)).JSON.self, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) 396 | """ 397 | 398 | if let initializerExpr = member.initializerExpr { 399 | text.append(""" 400 | self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar), fallback: \(initializerExpr)) 401 | """) 402 | } else { 403 | text.append(""" 404 | self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar)) 405 | """) 406 | } 407 | 408 | return text 409 | } else { 410 | let body = "container.decode(type: type(of: self.\(member.name)), keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])" 411 | 412 | if let initializerExpr = member.initializerExpr { 413 | return "self.\(member.name) = (try? \(body)) ?? (\(initializerExpr))" 414 | } else { 415 | return "self.\(member.name) = try \(body)" 416 | } 417 | } 418 | } 419 | .joined(separator: "\n") 420 | 421 | let decoder: DeclSyntax = """ 422 | \(raw: attributesPrefix(option: [.public, .required]))init(from decoder: Decoder) throws { 423 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 424 | \(raw: body) 425 | } 426 | """ 427 | 428 | return decoder 429 | } 430 | ``` 431 | 432 | * ```let transformerVar = context.makeUniqueName(String(idx))``` 433 | 需要生成一个局部transformer变量,为了防止变量名冲突使用了`makeUniqueName`生成唯一变量名 434 | 435 | * `attributesPrefix(option: [.public, .required])` 436 | 根据 struct/class 是 open/public 生成正确的修饰。所有情况展开如下: 437 | 438 | ``` 439 | open class Model: Codable { 440 | public required init(from decoder: Decoder) throws {} 441 | } 442 | 443 | public class Model: Codable { 444 | public required init(from decoder: Decoder) throws {} 445 | } 446 | 447 | class Model: Codable { 448 | required init(from decoder: Decoder) throws {} 449 | } 450 | 451 | public struct Model: Codable { 452 | public init(from decoder: Decoder) throws {} 453 | } 454 | 455 | struct Model: Codable { 456 | init(from decoder: Decoder) throws {} 457 | } 458 | ``` 459 | 460 | #### 填充`func encode(to encoder: Encoder)` 461 | 462 | ``` 463 | @Codable 464 | struct BasicModel { 465 | var defaultVal: String = "hello world" 466 | var defaultVal2: String = Bool.random() ? "hello world" : "" 467 | let strict: String 468 | let noStrict: String? 469 | let autoConvert: Int? 470 | 471 | @CodingKey("hello") 472 | var hi: String = "there" 473 | 474 | @CodingNestedKey("nested.hi") 475 | @CodingTransformer(StringPrefixTransform("HELLO -> ")) 476 | var codingKeySupport: String 477 | 478 | @CodingNestedKey("nested.b") 479 | var nestedB: String 480 | 481 | var testGetter: String { 482 | nestedB 483 | } 484 | 485 | func encode(to encoder: Encoder) throws { 486 | let container = encoder.container(keyedBy: AnyCodingKey.self) 487 | try container.encode(value: self.defaultVal, keys: ["defaultVal"], nestedKeys: []) 488 | try container.encode(value: self.defaultVal2, keys: ["defaultVal2"], nestedKeys: []) 489 | try container.encode(value: self.strict, keys: ["strict"], nestedKeys: []) 490 | try container.encode(value: self.noStrict, keys: ["noStrict"], nestedKeys: []) 491 | try container.encode(value: self.autoConvert, keys: ["autoConvert"], nestedKeys: []) 492 | try container.encode(value: self.hi, keys: ["hello", "hi"], nestedKeys: []) 493 | let $s19CodableWrapperTests10BasicModel0A0fMm_16fMu0_ = StringPrefixTransform("HELLO -> ") 494 | if let value = $s19CodableWrapperTests10BasicModel0A0fMm_16fMu0_.transformToJSON(self.codingKeySupport) { 495 | try container.encode(value: value, keys: ["codingKeySupport"], nestedKeys: ["nested.hi"]) 496 | } 497 | try container.encode(value: self.nestedB, keys: ["nestedB"], nestedKeys: ["nested.b"]) 498 | } 499 | } 500 | ``` 501 | 基本流程与`init(from decoder: Decoder)`一致,原则上是有值才encode而不是encode进去一个`nil`,扩写代码如下: 502 | ``` 503 | func genEncodeFunction(config: GenConfig) throws -> DeclSyntax { 504 | let body = memberProperties.enumerated().map { idx, member in 505 | if let transformerExpr = member.transformerExpr { 506 | let transformerVar = context.makeUniqueName(String(idx)) 507 | 508 | if member.isOptional { 509 | return """ 510 | let \(transformerVar) = \(transformerExpr) 511 | if let \(member.name) = self.\(member.name), let value = \(transformerVar).transformToJSON(\(member.name)) { 512 | try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) 513 | } 514 | """ 515 | } else { 516 | return """ 517 | let \(transformerVar) = \(transformerExpr) 518 | if let value = \(transformerVar).transformToJSON(self.\(member.name)) { 519 | try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) 520 | } 521 | """ 522 | } 523 | 524 | } else { 525 | return "try container.encode(value: self.\(member.name), keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])" 526 | } 527 | } 528 | .joined(separator: "\n") 529 | 530 | let encoder: DeclSyntax = """ 531 | \(raw: attributesPrefix(option: [.open, .public]))func encode(to encoder: Encoder) throws { 532 | let container = encoder.container(keyedBy: AnyCodingKey.self) 533 | \(raw: body) 534 | } 535 | """ 536 | 537 | return encoder 538 | } 539 | ``` 540 | 541 | ## `@CodingKey` `@CodingNestedKey` `@CodingTransformer`增加Diagnostics 542 | 这些宏是用作占位标记的,不需要实际扩展。但为了增加一些严谨性,比如在以下情况下希望增加错误提示: 543 | ``` 544 | @CodingKey("a") 545 | struct StructWraning1 {} 546 | ``` 547 | 实现也很简单,抛异常即可。 548 | ``` 549 | public struct CodingKey: MemberMacro { 550 | public static func expansion(of node: AttributeSyntax, providingMembersOf _: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { 551 | throw ASTError("`\(self.self)` only use for `Property`") 552 | } 553 | } 554 | ``` 555 | ![20230705111402](http://images.testfast.cn/20230705111402.png) 556 | 557 | 这里也就印证了 `@CodingKey` 为什么不用 `@attached(memberAttribute)`(Member Attribute Macro) 而使用 `@attached(member)`(Member Macro) 的原因。如果不声明使用`@attached(member)`,就不会执行`MemberMacro`协议的实现,在`MemberMacro`位置写上`@CodingKey("a")`也就不会报错。 558 | 559 | ## 实现`@CodableSubclass`,方便的`Codable Class子类` 560 | 561 | 先举例展示`Codable Class子类`的缺陷。编写一个简单的测试用例: 562 | ![20230705113137](http://images.testfast.cn/20230705113137.png) 563 | 564 | 是不是出乎意料,原因是编译器只给`ClassModel`添加了`init(from decoder: Decoder)`,`ClassSubmodel`则没有。要解决问题还需要手动实现子类的`Codable`协议,十分不便: 565 | ![20230705113820](http://images.testfast.cn/20230705113820.png) 566 | 567 | `@CodableSubclass`就是解决这个问题,实现也很简单,在适时的位置super call,方法标记成`override`就可以了。 568 | 569 | ``` 570 | func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax { 571 | ... 572 | let decoder: DeclSyntax = """ 573 | \(raw: attributesPrefix(option: [.public, .required]))init(from decoder: Decoder) throws { 574 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 575 | \(raw: body)\(raw: config.isOverride ? "\ntry super.init(from: decoder)" : "") 576 | } 577 | """ 578 | } 579 | 580 | func genEncodeFunction(config: GenConfig) throws -> DeclSyntax { 581 | ... 582 | let encoder: DeclSyntax = """ 583 | \(raw: attributesPrefix(option: [.open, .public]))\(raw: config.isOverride ? "override " : "")func encode(to encoder: Encoder) throws { 584 | \(raw: config.isOverride ? "try super.encode(to: encoder)\n" : "")let container = encoder.container(keyedBy: AnyCodingKey.self) 585 | \(raw: body) 586 | } 587 | """ 588 | } 589 | ``` 590 | 591 | ## 总结 592 | 至此,我们已经完成了 `@Codable` `@CodingKey` `@CodingNestedKey` `@CodingTransformer` `@CodableSubclass` 宏的全部实现。目前Swift Macro还处于Beta阶段, [CodableWrapper Macro 版](https://github.com/winddpan/CodableWrapper/tree/swift5.9-macro) 也还处于初期版本,未来还会迭代。 593 | 如果你觉得还不错,请给我的项目点个star吧 [https://github.com/winddpan/CodableWrapper/tree/swift5.9-macro](https://github.com/winddpan/CodableWrapper/tree/swift5.9-macro) 594 | 595 | ----- 596 | **文章目录** 597 | * [一、设计目标和手动实现这些目标特性](https://juejin.cn/post/7251501945272270908) 598 | * [二、Codable宏的开发和实现](https://juejin.cn/post/7252170693676499004) 599 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 winddpan@126.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swiftlang/swift-syntax", 7 | "state" : { 8 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", 9 | "version" : "509.1.1" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import CompilerPluginSupport 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "CodableWrapper", 9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13), .visionOS(.v1)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, making them visible to other packages. 12 | .library( 13 | name: "CodableWrapper", 14 | targets: ["CodableWrapper"] 15 | ) 16 | ], 17 | dependencies: [ 18 | // Depend on the latest Swift 5.9 SwiftSyntax 19 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease") 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package, defining a module or a test suite. 23 | // Targets can depend on other targets in this package and products from dependencies. 24 | // Macro implementation that performs the source transformation of a macro. 25 | .macro( 26 | name: "CodableWrapperMacros", 27 | dependencies: [ 28 | .product(name: "SwiftSyntax", package: "swift-syntax"), 29 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 30 | .product(name: "SwiftOperators", package: "swift-syntax"), 31 | .product(name: "SwiftParser", package: "swift-syntax"), 32 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"), 33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 34 | ] 35 | ), 36 | 37 | // Library that exposes a macro as part of its API, which is used in client programs. 38 | .target( 39 | name: "CodableWrapper", 40 | dependencies: ["CodableWrapperMacros"]), 41 | 42 | // A test target used to develop the macro implementation. 43 | .testTarget( 44 | name: "CodableWrapperTests", 45 | dependencies: [ 46 | "CodableWrapper", 47 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 48 | ] 49 | ), 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | | Xcode | Minimun Deployments | Version | 4 | | --------- | ------------------- | -------------------------------------------------------------- | 5 | | Xcode15+ | iOS13+ / macOS11+ | 1.0+ | 6 | | < Xcode15 | < iOS13 / macOS11 | [0.3.3](https://github.com/winddpan/CodableWrapper/tree/0.3.3) | 7 | 8 | # About 9 | 10 | The project objective is to enhance the usage experience of the Codable protocol using the macro provided by Swift 5.9 and to address the shortcomings of various official versions. 11 | 12 | # Feature 13 | 14 | * Default value 15 | * Basic type automatic convertible, between `String` `Bool` `Number` etc. 16 | * Custom multiple `CodingKey` 17 | * Nested Dictionary `CodingKey` 18 | * Automatic compatibility between camel case and snake case 19 | * Convenience `Codable` subclass 20 | * Transformer 21 | 22 | ## Installation 23 | 24 | #### CocoaPods 25 | ``` pod 'CodableWrapper', :git => 'https://github.com/winddpan/CodableWrapper.git' ``` 26 | 27 | #### Swift Package Manager 28 | ``` https://github.com/winddpan/CodableWrapper ``` 29 | 30 | # Example 31 | 32 | ```swift 33 | @Codable 34 | struct BasicModel { 35 | var defaultVal: String = "hello world" 36 | var defaultVal2: String = Bool.random() ? "hello world" : "" 37 | let strict: String 38 | let noStrict: String? 39 | let autoConvert: Int? 40 | 41 | @CodingKey("hello") 42 | var hi: String = "there" 43 | 44 | @CodingNestedKey("nested.hi") 45 | @CodingTransformer(StringPrefixTransform("HELLO -> ")) 46 | var codingKeySupport: String 47 | 48 | @CodingNestedKey("nested.b") 49 | var nestedB: String 50 | 51 | var testGetter: String { 52 | nestedB 53 | } 54 | } 55 | 56 | final class CodableWrapperTests: XCTestCase { 57 | func testBasicUsage() throws { 58 | let jsonStr = """ 59 | { 60 | "strict": "value of strict", 61 | "autoConvert": "998", 62 | "nested": { 63 | "hi": "nested there", 64 | "b": "b value" 65 | } 66 | } 67 | """ 68 | 69 | let model = try JSONDecoder().decode(BasicModel.self, from: jsonStr.data(using: .utf8)!) 70 | XCTAssertEqual(model.defaultVal, "hello world") 71 | XCTAssertEqual(model.strict, "value of strict") 72 | XCTAssertEqual(model.noStrict, nil) 73 | XCTAssertEqual(model.autoConvert, 998) 74 | XCTAssertEqual(model.hi, "there") 75 | XCTAssertEqual(model.codingKeySupport, "HELLO -> nested there") 76 | XCTAssertEqual(model.nestedB, "b value") 77 | 78 | let encoded = try JSONEncoder().encode(model) 79 | let dict = try JSONSerialization.jsonObject(with: encoded) as! [String: Any] 80 | XCTAssertEqual(model.defaultVal, dict["defaultVal"] as! String) 81 | XCTAssertEqual(model.strict, dict["strict"] as! String) 82 | XCTAssertNil(dict["noStrict"]) 83 | XCTAssertEqual(model.autoConvert, dict["autoConvert"] as? Int) 84 | XCTAssertEqual(model.hi, dict["hello"] as! String) 85 | XCTAssertEqual("nested there", (dict["nested"] as! [String: Any])["hi"] as! String) 86 | XCTAssertEqual(model.nestedB, (dict["nested"] as! [String: Any])["b"] as! String) 87 | } 88 | } 89 | ``` 90 | 91 | # Macro usage 92 | 93 | ## @Codable 94 | 95 | * Auto conformance `Codable` protocol if not explicitly declared 96 | 97 | ```swift 98 | // both below works well 99 | 100 | @Codable 101 | struct BasicModel {} 102 | 103 | @Codable 104 | struct BasicModel: Codable {} 105 | ``` 106 | 107 | * Default value 108 | 109 | ```swift 110 | @Codable 111 | struct TestModel { 112 | let name: String 113 | var balance: Double = 0 114 | } 115 | 116 | // { "name": "jhon" } 117 | ``` 118 | 119 | * Basic type automatic convertible, between `String` `Bool` `Number` etc. 120 | 121 | ```swift 122 | @Codable 123 | struct TestModel { 124 | let autoConvert: Int? 125 | } 126 | 127 | // { "autoConvert": "998" } 128 | ``` 129 | 130 | * Automatic compatibility between camel case and snake case 131 | 132 | ```swift 133 | @Codable 134 | struct TestModel { 135 | var userName: String = "" 136 | } 137 | 138 | // { "user_name": "jhon" } 139 | ``` 140 | 141 | * Member Wise Init 142 | 143 | ```swift 144 | @Codable 145 | public struct TestModel { 146 | public var userName: String = "" 147 | 148 | // Automatic generated 149 | public init(userName: String = "") { 150 | self.userName = userName 151 | } 152 | } 153 | ``` 154 | 155 | ## @CodingKey 156 | 157 | * Custom `CodingKey`s 158 | 159 | ```swift 160 | @Codable 161 | struct TestModel { 162 | @CodingKey("u1", "u2", "u9") 163 | var userName: String = "" 164 | } 165 | 166 | // { "u9": "jhon" } 167 | ``` 168 | 169 | ## @CodingNestedKey 170 | 171 | * Custom `CodingKey`s in `nested dictionary` 172 | 173 | ```swift 174 | @Codable 175 | struct TestModel { 176 | @CodingNestedKey("data.u1", "data.u2", "data.u9") 177 | var userName: String = "" 178 | } 179 | 180 | // { "data": {"u9": "jhon"} } 181 | ``` 182 | 183 | ## @CodableSubclass 184 | 185 | * Automatic generate `Codable` class's subclass `init(from:)` and `encode(to:)` super calls 186 | 187 | ```swift 188 | @Codable 189 | class BaseModel { 190 | let userName: String 191 | } 192 | 193 | @CodableSubclass 194 | class SubModel: BaseModel { 195 | let age: Int 196 | } 197 | 198 | // {"user_name": "jhon", "age": 22} 199 | ``` 200 | 201 | ## @CodingTransformer 202 | 203 | * Transformer between in `Codable` / `NonCodable` model 204 | 205 | ```swift 206 | struct DateWrapper { 207 | let timestamp: TimeInterval 208 | 209 | var date: Date { 210 | Date(timeIntervalSince1970: timestamp) 211 | } 212 | 213 | init(timestamp: TimeInterval) { 214 | self.timestamp = timestamp 215 | } 216 | 217 | static var transformer = TransformOf(fromJSON: { DateWrapper(timestamp: $0 ?? 0) }, toJSON: { $0.timestamp }) 218 | } 219 | 220 | @Codable 221 | struct DateModel { 222 | @CodingTransformer(DateWrapper.transformer) 223 | var time: DateWrapper? = DateWrapper(timestamp: 0) 224 | 225 | @CodingTransformer(DateWrapper.transformer) 226 | var time1: DateWrapper = DateWrapper(timestamp: 0) 227 | 228 | @CodingTransformer(DateWrapper.transformer) 229 | var time2: DateWrapper? 230 | } 231 | 232 | class TransformTest: XCTestCase { 233 | func testDateModel() throws { 234 | let json = """ 235 | {"time": 12345} 236 | """ 237 | 238 | let model = try JSONDecoder().decode(DateModel.self, from: json.data(using: .utf8)!) 239 | XCTAssertEqual(model.time?.timestamp, 12345) 240 | XCTAssertEqual(model.time?.date.description, "1970-01-01 03:25:45 +0000") 241 | 242 | let encode = try JSONEncoder().encode(model) 243 | let jsonObject = try JSONSerialization.jsonObject(with: encode, options: []) as! [String: Any] 244 | XCTAssertEqual(jsonObject["time"] as! TimeInterval, 12345) 245 | } 246 | } 247 | ``` 248 | 249 | 250 | # Star History 251 | 252 | [![Star History Chart](https://api.star-history.com/svg?repos=winddpan/CodableWrapper&type=Date)](https://star-history.com/#winddpan/CodableWrapper&Date) 253 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/AnyCodingKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCodingKey.swift 3 | // CodableWrapper 4 | // 5 | // Created by winddpan on 2020/8/15. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AnyCodingKey: CodingKey { 11 | public var stringValue: String 12 | public var intValue: Int? 13 | 14 | public init?(stringValue: String) { 15 | self.stringValue = stringValue 16 | intValue = nil 17 | } 18 | 19 | public init?(intValue: Int) { 20 | stringValue = "\(intValue)" 21 | self.intValue = intValue 22 | } 23 | 24 | public init(index: Int) { 25 | stringValue = "\(index)" 26 | intValue = index 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/AnyDecodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type-erased `Decodable` value. 5 | 6 | The `AnyDecodable` type forwards decoding responsibilities 7 | to an underlying value, hiding its specific underlying type. 8 | 9 | You can decode mixed-type values in dictionaries 10 | and other collections that require `Decodable` conformance 11 | by declaring their contained type to be `AnyDecodable`: 12 | 13 | let json = """ 14 | { 15 | "boolean": true, 16 | "integer": 42, 17 | "double": 3.141592653589793, 18 | "string": "string", 19 | "array": [1, 2, 3], 20 | "nested": { 21 | "a": "alpha", 22 | "b": "bravo", 23 | "c": "charlie" 24 | }, 25 | "null": null 26 | } 27 | """.data(using: .utf8)! 28 | 29 | let decoder = JSONDecoder() 30 | let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json) 31 | */ 32 | struct AnyDecodable: Decodable { 33 | public let value: Any 34 | 35 | public init(_ value: T?) { 36 | self.value = value ?? () 37 | } 38 | } 39 | 40 | @usableFromInline 41 | protocol _AnyDecodable { 42 | var value: Any { get } 43 | init(_ value: T?) 44 | } 45 | 46 | extension AnyDecodable: _AnyDecodable {} 47 | 48 | extension _AnyDecodable { 49 | public init(from decoder: Decoder) throws { 50 | let container = try decoder.singleValueContainer() 51 | 52 | if container.decodeNil() { 53 | #if canImport(Foundation) 54 | self.init(NSNull()) 55 | #else 56 | self.init(Self?.none) 57 | #endif 58 | } else if let bool = try? container.decode(Bool.self) { 59 | self.init(bool) 60 | } else if let int = try? container.decode(Int.self) { 61 | self.init(int) 62 | } else if let uint = try? container.decode(UInt.self) { 63 | self.init(uint) 64 | } else if let double = try? container.decode(Double.self) { 65 | self.init(double) 66 | } else if let string = try? container.decode(String.self) { 67 | self.init(string) 68 | } else if let array = try? container.decode([AnyDecodable].self) { 69 | self.init(array.map { $0.value }) 70 | } else if let dictionary = try? container.decode([String: AnyDecodable].self) { 71 | self.init(dictionary.mapValues { $0.value }) 72 | } else { 73 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyDecodable value cannot be decoded") 74 | } 75 | } 76 | } 77 | 78 | extension AnyDecodable: Equatable { 79 | public static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool { 80 | switch (lhs.value, rhs.value) { 81 | #if canImport(Foundation) 82 | case is (NSNull, NSNull), is (Void, Void): 83 | return true 84 | #endif 85 | case let (lhs as Bool, rhs as Bool): 86 | return lhs == rhs 87 | case let (lhs as Int, rhs as Int): 88 | return lhs == rhs 89 | case let (lhs as Int8, rhs as Int8): 90 | return lhs == rhs 91 | case let (lhs as Int16, rhs as Int16): 92 | return lhs == rhs 93 | case let (lhs as Int32, rhs as Int32): 94 | return lhs == rhs 95 | case let (lhs as Int64, rhs as Int64): 96 | return lhs == rhs 97 | case let (lhs as UInt, rhs as UInt): 98 | return lhs == rhs 99 | case let (lhs as UInt8, rhs as UInt8): 100 | return lhs == rhs 101 | case let (lhs as UInt16, rhs as UInt16): 102 | return lhs == rhs 103 | case let (lhs as UInt32, rhs as UInt32): 104 | return lhs == rhs 105 | case let (lhs as UInt64, rhs as UInt64): 106 | return lhs == rhs 107 | case let (lhs as Float, rhs as Float): 108 | return lhs == rhs 109 | case let (lhs as Double, rhs as Double): 110 | return lhs == rhs 111 | case let (lhs as String, rhs as String): 112 | return lhs == rhs 113 | case let (lhs as [String: AnyDecodable], rhs as [String: AnyDecodable]): 114 | return lhs == rhs 115 | case let (lhs as [AnyDecodable], rhs as [AnyDecodable]): 116 | return lhs == rhs 117 | default: 118 | return false 119 | } 120 | } 121 | } 122 | 123 | extension AnyDecodable: CustomStringConvertible { 124 | public var description: String { 125 | switch value { 126 | case is Void: 127 | return String(describing: nil as Any?) 128 | case let value as CustomStringConvertible: 129 | return value.description 130 | default: 131 | return String(describing: value) 132 | } 133 | } 134 | } 135 | 136 | extension AnyDecodable: CustomDebugStringConvertible { 137 | public var debugDescription: String { 138 | switch value { 139 | case let value as CustomDebugStringConvertible: 140 | return "AnyDecodable(\(value.debugDescription))" 141 | default: 142 | return "AnyDecodable(\(description))" 143 | } 144 | } 145 | } 146 | 147 | extension AnyDecodable: Hashable { 148 | public func hash(into hasher: inout Hasher) { 149 | switch value { 150 | case let value as Bool: 151 | hasher.combine(value) 152 | case let value as Int: 153 | hasher.combine(value) 154 | case let value as Int8: 155 | hasher.combine(value) 156 | case let value as Int16: 157 | hasher.combine(value) 158 | case let value as Int32: 159 | hasher.combine(value) 160 | case let value as Int64: 161 | hasher.combine(value) 162 | case let value as UInt: 163 | hasher.combine(value) 164 | case let value as UInt8: 165 | hasher.combine(value) 166 | case let value as UInt16: 167 | hasher.combine(value) 168 | case let value as UInt32: 169 | hasher.combine(value) 170 | case let value as UInt64: 171 | hasher.combine(value) 172 | case let value as Float: 173 | hasher.combine(value) 174 | case let value as Double: 175 | hasher.combine(value) 176 | case let value as String: 177 | hasher.combine(value) 178 | case let value as [String: AnyDecodable]: 179 | hasher.combine(value) 180 | case let value as [AnyDecodable]: 181 | hasher.combine(value) 182 | default: 183 | break 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/BuiltInBridgeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuiltInBridgeType.swift 3 | // HandyJSON 4 | // 5 | // Created by zhouzhuo on 15/07/2017. 6 | // Copyright © 2017 aliyun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol _BuiltInBridgeType { 12 | static func _transform(from object: Any) -> Self? 13 | } 14 | 15 | // Suppport integer type 16 | 17 | protocol IntegerPropertyProtocol: FixedWidthInteger, _BuiltInBridgeType { 18 | init?(_ text: String, radix: Int) 19 | init(_ number: NSNumber) 20 | } 21 | 22 | extension IntegerPropertyProtocol { 23 | static func _transform(from object: Any) -> Self? { 24 | switch object { 25 | case let str as String: 26 | return Self(str, radix: 10) 27 | case let num as NSNumber: 28 | return Self(num) 29 | default: 30 | return nil 31 | } 32 | } 33 | } 34 | 35 | extension Int: IntegerPropertyProtocol {} 36 | extension UInt: IntegerPropertyProtocol {} 37 | extension Int8: IntegerPropertyProtocol {} 38 | extension Int16: IntegerPropertyProtocol {} 39 | extension Int32: IntegerPropertyProtocol {} 40 | extension Int64: IntegerPropertyProtocol {} 41 | extension UInt8: IntegerPropertyProtocol {} 42 | extension UInt16: IntegerPropertyProtocol {} 43 | extension UInt32: IntegerPropertyProtocol {} 44 | extension UInt64: IntegerPropertyProtocol {} 45 | 46 | extension Bool: _BuiltInBridgeType { 47 | static func _transform(from object: Any) -> Bool? { 48 | switch object { 49 | case let str as NSString: 50 | let lowerCase = str.lowercased 51 | if ["0", "false"].contains(lowerCase) { 52 | return false 53 | } 54 | if ["1", "true"].contains(lowerCase) { 55 | return true 56 | } 57 | return nil 58 | case let num as NSNumber: 59 | return num.boolValue 60 | default: 61 | return nil 62 | } 63 | } 64 | } 65 | 66 | // Support float type 67 | 68 | protocol FloatPropertyProtocol: _BuiltInBridgeType, LosslessStringConvertible { 69 | init(_ number: NSNumber) 70 | } 71 | 72 | extension FloatPropertyProtocol { 73 | static func _transform(from object: Any) -> Self? { 74 | switch object { 75 | case let str as String: 76 | return Self(str) 77 | case let num as NSNumber: 78 | return Self(num) 79 | default: 80 | return nil 81 | } 82 | } 83 | } 84 | 85 | extension Float: FloatPropertyProtocol {} 86 | extension Double: FloatPropertyProtocol {} 87 | 88 | private let formatter: NumberFormatter = { 89 | let formatter = NumberFormatter() 90 | formatter.usesGroupingSeparator = false 91 | formatter.numberStyle = .decimal 92 | formatter.maximumFractionDigits = 16 93 | return formatter 94 | }() 95 | 96 | extension String: _BuiltInBridgeType { 97 | static func _transform(from object: Any) -> String? { 98 | switch object { 99 | case let str as String: 100 | return str 101 | case let num as NSNumber: 102 | // Boolean Type Inside 103 | if NSStringFromClass(type(of: num)) == "__NSCFBoolean" { 104 | if num.boolValue { 105 | return "true" 106 | } else { 107 | return "false" 108 | } 109 | } 110 | return formatter.string(from: num) 111 | case _ as NSNull: 112 | return nil 113 | default: 114 | return "\(object)" 115 | } 116 | } 117 | } 118 | 119 | extension NSString: _BuiltInBridgeType { 120 | static func _transform(from object: Any) -> Self? { 121 | if let str = String._transform(from: object) { 122 | return Self(string: str) 123 | } 124 | return nil 125 | } 126 | } 127 | 128 | extension NSNumber: _BuiltInBridgeType { 129 | static func _transform(from object: Any) -> Self? { 130 | switch object { 131 | case let num as Self: 132 | return num 133 | case let str as NSString: 134 | let lowercase = str.lowercased 135 | if lowercase == "true" { 136 | return Self(booleanLiteral: true) 137 | } else if lowercase == "false" { 138 | return Self(booleanLiteral: false) 139 | } else { 140 | return Self(value: str.doubleValue) 141 | } 142 | default: 143 | return nil 144 | } 145 | } 146 | } 147 | 148 | extension NSArray: _BuiltInBridgeType { 149 | static func _transform(from object: Any) -> Self? { 150 | return object as? Self 151 | } 152 | } 153 | 154 | extension NSDictionary: _BuiltInBridgeType { 155 | static func _transform(from object: Any) -> Self? { 156 | return object as? Self 157 | } 158 | } 159 | 160 | extension Optional: _BuiltInBridgeType { 161 | static func _transform(from object: Any) -> Optional? { 162 | if let value = (Wrapped.self as? _BuiltInBridgeType.Type)?._transform(from: object) as? Wrapped { 163 | return Optional(value) 164 | } else if let value = object as? Wrapped { 165 | return Optional(value) 166 | } 167 | return nil 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/CodableWrapperMacros.swift: -------------------------------------------------------------------------------- 1 | @attached(member, names: named(init(from:)), named(encode(to:)), arbitrary) 2 | @attached(extension, conformances: Codable) 3 | public macro Codable(wiseInit: Bool = true) = #externalMacro(module: "CodableWrapperMacros", type: "Codable") 4 | 5 | @attached(member, names: named(init(from:)), named(encode(to:)), arbitrary) 6 | public macro CodableSubclass() = #externalMacro(module: "CodableWrapperMacros", type: "CodableSubclass") 7 | 8 | @attached(peer) 9 | public macro CodingKey(_ key: String ...) = #externalMacro(module: "CodableWrapperMacros", type: "CodingKey") 10 | 11 | @attached(peer) 12 | public macro CodingNestedKey(_ key: String ...) = #externalMacro(module: "CodableWrapperMacros", type: "CodingNestedKey") 13 | 14 | @attached(peer) 15 | public macro CodingTransformer(_ transformer: any TransformType) = #externalMacro(module: "CodableWrapperMacros", type: "CodingTransformer") 16 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/Decoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension KeyedDecodingContainer where K == AnyCodingKey { 4 | func decode(type: Value.Type, 5 | keys: [String], 6 | nestedKeys: [String]) throws -> Value { 7 | for key in nestedKeys { 8 | if let value = tryNestedKeyDecode(type: type, key: key) { 9 | return value 10 | } 11 | } 12 | for key in keys { 13 | if let value = tryNormalKeyDecode(type: type, key: key) { 14 | return value 15 | } 16 | } 17 | // if Value is Optional,return nil 18 | if let valueType = Value.self as? ExpressibleByNilLiteral.Type { 19 | return valueType.init(nilLiteral: ()) as! Value 20 | } 21 | 22 | throw CodableWrapperError("decode failure: keys: \(keys), nestedKeys: \(nestedKeys)") 23 | } 24 | } 25 | 26 | private extension KeyedDecodingContainer where K == AnyCodingKey { 27 | func tryNormalKeyDecode(type: Value.Type, key: String) -> Value? { 28 | func _decode(key: String) -> Value? { 29 | guard let key = Key(stringValue: key) else { 30 | return nil 31 | } 32 | if let value = try? decodeIfPresent(type, forKey: key) { 33 | return value 34 | } 35 | let value = try? decodeIfPresent(AnyDecodable.self, forKey: key)?.value 36 | if let value = value { 37 | if let converted = value as? Value { 38 | return converted 39 | } 40 | if let _bridged = (Value.self as? _BuiltInBridgeType.Type)?._transform(from: value), let __bridged = _bridged as? Value { 41 | return __bridged 42 | } 43 | if let value = try? Value.decode(from: self, forKey: key) { 44 | return value 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | for newKey in [key, key.snakeCamelConvert()].compactMap({ $0 }) { 51 | if let value = _decode(key: newKey) { 52 | return value 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | private func tryNestedKeyDecode(type: Value.Type, key: String) -> Value? { 59 | var keyComps = key.components(separatedBy: ".") 60 | guard let rootKey = AnyCodingKey(stringValue: keyComps.removeFirst()) else { 61 | return nil 62 | } 63 | var container: KeyedDecodingContainer? 64 | container = try? nestedContainer(keyedBy: AnyCodingKey.self, forKey: rootKey) 65 | let lastKey = keyComps.removeLast() 66 | for keyComp in keyComps { 67 | container = try? container?.nestedContainer(keyedBy: AnyCodingKey.self, forKey: .init(stringValue: keyComp)!) 68 | } 69 | if let container = container { 70 | if let value = container.tryNormalKeyDecode(type: type, key: lastKey) { 71 | return value 72 | } 73 | } 74 | return nil 75 | } 76 | } 77 | 78 | private extension Decodable { 79 | static func decode(from container: KeyedDecodingContainer, forKey key: KeyedDecodingContainer.Key) throws -> Self { 80 | return try container.decode(Self.self, forKey: key) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/Encoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension KeyedEncodingContainer where K == AnyCodingKey { 4 | func encode(value: Value, 5 | keys: [String], 6 | nestedKeys: [String]) throws { 7 | // check if value is nil 8 | if case Optional.none = (value as Any) { 9 | return 10 | } 11 | if !nestedKeys.isEmpty { 12 | return try encodeNestedKey(value: value, key: nestedKeys[0]) 13 | } else { 14 | var mutatingSelf = self 15 | return try encodeNormakKey(value: value, key: keys[0], container: &mutatingSelf) 16 | } 17 | } 18 | } 19 | 20 | private extension KeyedEncodingContainer where K == AnyCodingKey { 21 | func encodeNestedKey(value: Encodable, key: String) throws { 22 | var keyComps = key.components(separatedBy: ".") 23 | let lastKey = keyComps.removeLast() 24 | var nestedContainer: KeyedEncodingContainer? = self 25 | for keyComp in keyComps { 26 | nestedContainer = nestedContainer?.nestedContainer(keyedBy: AnyCodingKey.self, forKey: .init(stringValue: keyComp)!) 27 | } 28 | if var nestedContainer = nestedContainer { 29 | try encodeNormakKey(value: value, key: lastKey, container: &nestedContainer) 30 | } 31 | } 32 | 33 | func encodeNormakKey(value: Encodable, key: String, container: inout KeyedEncodingContainer) throws { 34 | let codingKey = AnyCodingKey(stringValue: key)! 35 | try container.encode(value, forKey: codingKey) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CodableWrapperError: CustomStringConvertible, Error { 4 | let text: String 5 | 6 | init(_ text: String) { 7 | self.text = text 8 | } 9 | 10 | var description: String { 11 | text 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/SnakeCamelConvert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnakeCamelConvert.swift 3 | // CodableWrapper 4 | // 5 | // Created by PAN on 2021/10/12. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | private var isSnake: Bool { 12 | return contains("_") 13 | } 14 | 15 | // https://github.com/apple/swift-corelibs-foundation/blob/73d1943d0184040688a0f3ba5d66b61ea9e10a09/Sources/Foundation/JSONDecoder.swift#L103 16 | private func snakeToCamel() -> String { 17 | let stringKey = self 18 | guard !stringKey.isEmpty else { return stringKey } 19 | 20 | // Find the first non-underscore character 21 | guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else { 22 | // Reached the end without finding an _ 23 | return stringKey 24 | } 25 | 26 | // Find the last non-underscore character 27 | var lastNonUnderscore = stringKey.index(before: stringKey.endIndex) 28 | while lastNonUnderscore > firstNonUnderscore, stringKey[lastNonUnderscore] == "_" { 29 | stringKey.formIndex(before: &lastNonUnderscore) 30 | } 31 | 32 | let keyRange = firstNonUnderscore ... lastNonUnderscore 33 | let leadingUnderscoreRange = stringKey.startIndex ..< firstNonUnderscore 34 | let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore) ..< stringKey.endIndex 35 | 36 | let components = stringKey[keyRange].split(separator: "_") 37 | let joinedString: String 38 | if components.count == 1 { 39 | // No underscores in key, leave the word as is - maybe already camel cased 40 | joinedString = String(stringKey[keyRange]) 41 | } else { 42 | joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined() 43 | } 44 | 45 | // Do a cheap isEmpty check before creating and appending potentially empty strings 46 | let result: String 47 | if leadingUnderscoreRange.isEmpty, trailingUnderscoreRange.isEmpty { 48 | result = joinedString 49 | } else if !leadingUnderscoreRange.isEmpty, !trailingUnderscoreRange.isEmpty { 50 | // Both leading and trailing underscores 51 | result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange]) 52 | } else if !leadingUnderscoreRange.isEmpty { 53 | // Just leading 54 | result = String(stringKey[leadingUnderscoreRange]) + joinedString 55 | } else { 56 | // Just trailing 57 | result = joinedString + String(stringKey[trailingUnderscoreRange]) 58 | } 59 | return result 60 | } 61 | 62 | private func camelToSnake() -> String { 63 | var chars = Array(self) 64 | for (i, char) in chars.enumerated().reversed() { 65 | if char.isUppercase { 66 | chars[i] = String.Element(char.lowercased()) 67 | if i > 0 { 68 | chars.insert("_", at: i) 69 | } 70 | } 71 | } 72 | return String(chars) 73 | } 74 | 75 | func snakeCamelConvert() -> String? { 76 | let result: String 77 | if isSnake { 78 | result = snakeToCamel() 79 | } else { 80 | result = camelToSnake() 81 | } 82 | if self == result { 83 | return nil 84 | } 85 | return result 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/TransformOf.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransformOf.swift 3 | // CodableWrapperDev 4 | // 5 | // Created by winddpan on 2020/8/15. 6 | // Copyright © 2020 YR. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class TransformOf: TransformType { 12 | open var fromJSON: (JSON?) -> Object 13 | open var toJSON: (Object) -> JSON? 14 | 15 | public init(fromJSON: @escaping ((JSON?) -> Object), 16 | toJSON: @escaping ((Object) -> JSON?)) 17 | { 18 | self.fromJSON = fromJSON 19 | self.toJSON = toJSON 20 | } 21 | 22 | open func transformFromJSON(_ json: JSON?) -> Object { 23 | fromJSON(json) 24 | } 25 | 26 | open func transformToJSON(_ object: Object) -> JSON? { 27 | toJSON(object) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CodableWrapper/TransformType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transform.swift 3 | // CodableWrapperDev 4 | // 5 | // Created by winddpan on 2020/8/15. 6 | // Copyright © 2020 YR. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol TransformType { 12 | associatedtype Object 13 | associatedtype JSON: Codable 14 | 15 | func transformFromJSON(_ json: JSON?) -> Object 16 | func transformToJSON(_ object: Object) -> JSON? 17 | } 18 | 19 | public extension TransformType { 20 | func transformFromJSON(_ json: JSON?, 21 | fallback _: @autoclosure () -> Object) -> Object { 22 | return transformFromJSON(json) 23 | } 24 | 25 | func transformFromJSON(_ json: JSON?, 26 | fallback: @autoclosure () -> Wrapped) -> Wrapped where Object == Wrapped? 27 | { 28 | if let value = transformFromJSON(json) { 29 | return value 30 | } 31 | return fallback() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/ASTError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ASTError: CustomStringConvertible, Error { 4 | let text: String 5 | 6 | init(_ text: String) { 7 | self.text = text 8 | } 9 | 10 | var description: String { 11 | text 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/Codable.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct Codable: ExtensionMacro, MemberMacro { 5 | public static func expansion(of node: AttributeSyntax, 6 | attachedTo declaration: some DeclGroupSyntax, 7 | providingExtensionsOf type: some TypeSyntaxProtocol, 8 | conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { 9 | var inheritedTypes: InheritedTypeListSyntax? 10 | if let declaration = declaration.as(StructDeclSyntax.self) { 11 | inheritedTypes = declaration.inheritanceClause?.inheritedTypes 12 | } else if let declaration = declaration.as(ClassDeclSyntax.self) { 13 | inheritedTypes = declaration.inheritanceClause?.inheritedTypes 14 | } else { 15 | throw ASTError("use @Codable in `struct` or `class`") 16 | } 17 | if let inheritedTypes = inheritedTypes, 18 | inheritedTypes.contains(where: { inherited in inherited.type.trimmedDescription == "Codable" }) { 19 | return [] 20 | } 21 | 22 | let ext: DeclSyntax = 23 | """ 24 | extension \(type.trimmed): Codable {} 25 | """ 26 | 27 | return [ext.cast(ExtensionDeclSyntax.self)] 28 | } 29 | 30 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, 31 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 32 | in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { 33 | // TODO: diagnostic do not implement `init(from:)` or `encode(to:))` 34 | 35 | let propertyContainer = try ModelMemberPropertyContainer(decl: declaration, context: context) 36 | let decoder = try propertyContainer.genDecoderInitializer(config: .init(isOverride: false)) 37 | let encoder = try propertyContainer.genEncodeFunction(config: .init(isOverride: false)) 38 | 39 | var hasWiseInit = true 40 | if case let .argumentList(list) = node.arguments, list.first?.expression.description == "false" { 41 | hasWiseInit = false 42 | } 43 | 44 | if !hasWiseInit { 45 | return [decoder, encoder] 46 | } else { 47 | let memberwiseInit = try propertyContainer.genMemberwiseInit(config: .init(isOverride: false)) 48 | return [decoder, encoder, memberwiseInit] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/CodableKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct CodingKey: PeerMacro { 5 | public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { 6 | return [] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/CodableSubclass.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct CodableSubclass: MemberMacro { 5 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, 6 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 7 | in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] 8 | { 9 | guard declaration.is(ClassDeclSyntax.self) else { 10 | throw ASTError("not a `subclass`") 11 | } 12 | 13 | let propertyContainer = try ModelMemberPropertyContainer(decl: declaration, context: context) 14 | let decoder = try propertyContainer.genDecoderInitializer(config: .init(isOverride: true)) 15 | let encoder = try propertyContainer.genEncodeFunction(config: .init(isOverride: true)) 16 | return [decoder, encoder] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/CodingNestedKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct CodingNestedKey: PeerMacro { 5 | public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { 6 | return [] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/CodingTransformer.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct CodingTransformer: PeerMacro { 5 | public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { 6 | return [] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/ModelMemberPropertyContainer.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | private struct ModelMemberProperty { 5 | var name: String 6 | var type: String 7 | var modifiers: DeclModifierListSyntax = [] 8 | var isOptional: Bool = false 9 | var normalKeys: [String] = [] 10 | var nestedKeys: [String] = [] 11 | var transformerExpr: String? 12 | var initializerExpr: String? 13 | 14 | var codingKeys: [String] { 15 | let raw = ["\"\(name)\""] 16 | if normalKeys.isEmpty { 17 | return raw 18 | } 19 | return normalKeys + raw 20 | } 21 | } 22 | 23 | struct ModelMemberPropertyContainer { 24 | struct AttributeOption: OptionSet { 25 | let rawValue: UInt 26 | 27 | static let open = AttributeOption(rawValue: 1 << 0) 28 | static let `public` = AttributeOption(rawValue: 1 << 1) 29 | static let required = AttributeOption(rawValue: 1 << 2) 30 | } 31 | 32 | struct GenConfig { 33 | let isOverride: Bool 34 | } 35 | 36 | let context: MacroExpansionContext 37 | fileprivate let decl: DeclGroupSyntax 38 | fileprivate var memberProperties: [ModelMemberProperty] = [] 39 | 40 | init(decl: DeclGroupSyntax, context: some MacroExpansionContext) throws { 41 | self.decl = decl 42 | self.context = context 43 | memberProperties = try fetchModelMemberProperties() 44 | } 45 | 46 | private func attributesPrefix(option: AttributeOption) -> String { 47 | let hasPublicProperites = memberProperties.contains(where: { 48 | $0.modifiers.contains(where: { 49 | $0.name.text == "public" || $0.name.text == "open" 50 | }) 51 | }) 52 | 53 | let modifiers = decl.modifiers.compactMap { $0.name.text } 54 | var attributes: [String] = [] 55 | if option.contains(.open), modifiers.contains("open") { 56 | attributes.append("open") 57 | } else if option.contains(.public), hasPublicProperites || modifiers.contains("open") || modifiers.contains("public") { 58 | attributes.append("public") 59 | } 60 | if option.contains(.required), decl.is(ClassDeclSyntax.self) { 61 | attributes.append("required") 62 | } 63 | if !attributes.isEmpty { 64 | attributes.append("") 65 | } 66 | 67 | return attributes.joined(separator: " ") 68 | } 69 | 70 | func genDecoderInitializer(config: GenConfig) throws -> DeclSyntax { 71 | let body = memberProperties.enumerated().map { idx, member in 72 | if let transformerExpr = member.transformerExpr { 73 | let transformerVar = context.makeUniqueName(String(idx)) 74 | let tempJsonVar = member.name 75 | 76 | var text = """ 77 | let \(transformerVar) = \(transformerExpr) 78 | let \(tempJsonVar) = try? container.decode(type: Swift.type(of: \(transformerVar)).JSON.self, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) 79 | """ 80 | 81 | if let initializerExpr = member.initializerExpr { 82 | text.append(""" 83 | self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar), fallback: \(initializerExpr)) 84 | """) 85 | } else { 86 | text.append(""" 87 | self.\(member.name) = \(transformerVar).transformFromJSON(\(tempJsonVar)) 88 | """) 89 | } 90 | 91 | return text 92 | } else { 93 | let body = "container.decode(type: \(member.type).self, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])" 94 | 95 | if let initializerExpr = member.initializerExpr { 96 | return "self.\(member.name) = (try? \(body)) ?? (\(initializerExpr))" 97 | } else { 98 | return "self.\(member.name) = try \(body)" 99 | } 100 | } 101 | } 102 | .joined(separator: "\n") 103 | 104 | let decoder: DeclSyntax = """ 105 | \(raw: attributesPrefix(option: [.public, .required]))init(from decoder: Decoder) throws { 106 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 107 | \(raw: body)\(raw: config.isOverride ? "\ntry super.init(from: decoder)" : "") 108 | } 109 | """ 110 | 111 | return decoder 112 | } 113 | 114 | func genEncodeFunction(config: GenConfig) throws -> DeclSyntax { 115 | let body = memberProperties.enumerated().map { idx, member in 116 | if let transformerExpr = member.transformerExpr { 117 | let transformerVar = context.makeUniqueName(String(idx)) 118 | 119 | if member.isOptional { 120 | return """ 121 | let \(transformerVar) = \(transformerExpr) 122 | if let \(member.name) = self.\(member.name), let value = \(transformerVar).transformToJSON(\(member.name)) { 123 | try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) 124 | } 125 | """ 126 | } else { 127 | return """ 128 | let \(transformerVar) = \(transformerExpr) 129 | if let value = \(transformerVar).transformToJSON(self.\(member.name)) { 130 | try container.encode(value: value, keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))]) 131 | } 132 | """ 133 | } 134 | 135 | } else { 136 | return "try container.encode(value: self.\(member.name), keys: [\(member.codingKeys.joined(separator: ", "))], nestedKeys: [\(member.nestedKeys.joined(separator: ", "))])" 137 | } 138 | } 139 | .joined(separator: "\n") 140 | 141 | let encoder: DeclSyntax = """ 142 | \(raw: attributesPrefix(option: [.open, .public]))\(raw: config.isOverride ? "override " : "")func encode(to encoder: Encoder) throws { 143 | \(raw: config.isOverride ? "try super.encode(to: encoder)\n" : "")let container = encoder.container(keyedBy: AnyCodingKey.self) 144 | \(raw: body) 145 | } 146 | """ 147 | 148 | return encoder 149 | } 150 | 151 | func genMemberwiseInit(config: GenConfig) throws -> DeclSyntax { 152 | let parameters = memberProperties.map { property in 153 | var text = property.name 154 | text += ": " + property.type 155 | if let initializerExpr = property.initializerExpr { 156 | text += "= \(initializerExpr)" 157 | } else if property.isOptional { 158 | text += "= nil" 159 | } 160 | return text 161 | } 162 | 163 | let overrideInit = config.isOverride ? "super.init()\n" : "" 164 | 165 | return 166 | """ 167 | \(raw: attributesPrefix(option: [.public]))init(\(raw: parameters.joined(separator: ", "))) { 168 | \(raw: overrideInit)\(raw: memberProperties.map { "self.\($0.name) = \($0.name)" }.joined(separator: "\n")) 169 | } 170 | """ as DeclSyntax 171 | } 172 | } 173 | 174 | private extension ModelMemberPropertyContainer { 175 | func fetchModelMemberProperties() throws -> [ModelMemberProperty] { 176 | let memberList = decl.memberBlock.members 177 | let memberProperties = try memberList.flatMap { member -> [ModelMemberProperty] in 178 | guard let variable = member.decl.as(VariableDeclSyntax.self), variable.isStoredProperty else { 179 | return [] 180 | } 181 | let patterns = variable.bindings.map(\.pattern) 182 | let names = patterns.compactMap { $0.as(IdentifierPatternSyntax.self)?.identifier.text } 183 | 184 | return try names.compactMap { name -> ModelMemberProperty? in 185 | guard !variable.isLazyVar else { 186 | return nil 187 | } 188 | guard let type = variable.inferType else { 189 | throw ASTError("please declare property type: \(name)") 190 | } 191 | 192 | var mp = ModelMemberProperty(name: name, type: type) 193 | mp.modifiers = variable.modifiers 194 | let attributes = variable.attributes 195 | 196 | // isOptional 197 | mp.isOptional = variable.isOptionalType 198 | 199 | // CodingKey 200 | if let customKeyMacro = attributes.first(where: { element in 201 | element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description == "CodingKey" 202 | }) { 203 | mp.normalKeys = customKeyMacro.as(AttributeSyntax.self)?.arguments?.as(LabeledExprListSyntax.self)?.compactMap { $0.expression.description } ?? [] 204 | } 205 | 206 | // CodingNestedKey 207 | if let customKeyMacro = attributes.first(where: { element in 208 | element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description == "CodingNestedKey" 209 | }) { 210 | mp.nestedKeys = customKeyMacro.as(AttributeSyntax.self)?.arguments?.as(LabeledExprListSyntax.self)?.compactMap { $0.expression.description } ?? [] 211 | } 212 | 213 | // CodableTransform 214 | if let customKeyMacro = attributes.first(where: { element in 215 | element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description == "CodingTransformer" 216 | }) { 217 | mp.transformerExpr = customKeyMacro.as(AttributeSyntax.self)?.arguments?.as(LabeledExprListSyntax.self)?.first?.expression.description 218 | } 219 | 220 | // initializerExpr 221 | if let initializer = variable.bindings.compactMap(\.initializer).first { 222 | mp.initializerExpr = initializer.value.description 223 | } 224 | return mp 225 | } 226 | } 227 | return memberProperties 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/Plugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct CodableWrapperPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | Codable.self, 8 | CodableSubclass.self, 9 | CodingKey.self, 10 | CodingNestedKey.self, 11 | CodingTransformer.self, 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CodableWrapperMacros/VariableDeclSyntaxExtension.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension VariableDeclSyntax { 4 | /// Determine whether this variable has the syntax of a stored property. 5 | /// 6 | /// This syntactic check cannot account for semantic adjustments due to, 7 | /// e.g., accessor macros or property wrappers. 8 | var isStoredProperty: Bool { 9 | if modifiers.compactMap({ $0.as(DeclModifierSyntax.self) }).contains(where: { $0.name.text == "static" }) { 10 | return false 11 | } 12 | if bindings.count < 1 { 13 | return false 14 | } 15 | let binding = bindings.last! 16 | switch binding.accessorBlock?.accessors { 17 | case .none: 18 | return true 19 | case let .accessors(o): 20 | for accessor in o { 21 | switch accessor.accessorSpecifier.tokenKind { 22 | case .keyword(.willSet), .keyword(.didSet): 23 | // Observers can occur on a stored property. 24 | break 25 | default: 26 | // Other accessors make it a computed property. 27 | return false 28 | } 29 | } 30 | return true 31 | case .getter: 32 | return false 33 | } 34 | } 35 | 36 | var inferType: String? { 37 | var type: String? = bindings.compactMap(\.typeAnnotation).first?.type.trimmedDescription 38 | // try infer type 39 | if type == nil, let initExpr = bindings.compactMap(\.initializer).first?.value { 40 | if initExpr.is(StringLiteralExprSyntax.self) { 41 | type = "String" 42 | } else if initExpr.is(IntegerLiteralExprSyntax.self) { 43 | type = "Int" 44 | } else if initExpr.is(FloatLiteralExprSyntax.self) { 45 | type = "Double" 46 | } else if initExpr.is(BooleanLiteralExprSyntax.self) { 47 | type = "Bool" 48 | } else if let funcDecl = initExpr.as(FunctionCallExprSyntax.self), 49 | let declRef = funcDecl.calledExpression.as(DeclReferenceExprSyntax.self) { 50 | type = declRef.trimmedDescription 51 | } 52 | } 53 | return type 54 | } 55 | 56 | var isOptionalType: Bool { 57 | if bindings.compactMap(\.typeAnnotation).first?.type.is(OptionalTypeSyntax.self) == true { 58 | return true 59 | } 60 | if bindings.compactMap(\.initializer).first?.value.as(DeclReferenceExprSyntax.self)?.description.hasPrefix("Optional<") == true { 61 | return true 62 | } 63 | if bindings.compactMap(\.initializer).first?.value.as(DeclReferenceExprSyntax.self)?.description.hasPrefix("Optional(") == true { 64 | return true 65 | } 66 | return false 67 | } 68 | 69 | var isLazyVar: Bool { 70 | if modifiers.contains(where: { $0.name.trimmedDescription == "lazy" }) { 71 | return true 72 | } 73 | return false 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/CodableWrapperTests/CodableWrapperTests.swift: -------------------------------------------------------------------------------- 1 | import CodableWrapper 2 | import SwiftSyntaxMacros 3 | import SwiftSyntaxMacrosTestSupport 4 | import XCTest 5 | 6 | @Codable 7 | struct Basic0Model { 8 | var defaultVal: String = "hello world" 9 | var strict: String 10 | var noStrict: String? 11 | var autoConvert: Int? 12 | 13 | @CodingKey("customKey") 14 | var codingKeySupport: String 15 | } 16 | 17 | @Codable 18 | struct BasicModel { 19 | var defaultVal: String = "hello world" 20 | var defaultVal2: String = Bool.random() ? "hello world" : "" 21 | let strict: String 22 | let noStrict: String? 23 | let autoConvert: Int? 24 | 25 | @CodingKey("hello") 26 | var hi: String = "there" 27 | 28 | @CodingNestedKey("nested.hi") 29 | @CodingTransformer(StringPrefixTransform("HELLO -> ")) 30 | var codingKeySupport: String 31 | 32 | @CodingNestedKey("nested.b") 33 | var nestedB: String 34 | 35 | var testGetter: String { 36 | nestedB 37 | } 38 | } 39 | 40 | final class CodableWrapperTests: XCTestCase { 41 | func testBasicUsage() throws { 42 | let jsonStr = """ 43 | { 44 | "strict": "value of strict", 45 | "autoConvert": "998", 46 | "nested": { 47 | "hi": "nested there", 48 | "b": "b value" 49 | } 50 | } 51 | """ 52 | 53 | let model = try JSONDecoder().decode(BasicModel.self, from: jsonStr.data(using: .utf8)!) 54 | XCTAssertEqual(model.defaultVal, "hello world") 55 | XCTAssertEqual(model.strict, "value of strict") 56 | XCTAssertEqual(model.noStrict, nil) 57 | XCTAssertEqual(model.autoConvert, 998) 58 | XCTAssertEqual(model.hi, "there") 59 | XCTAssertEqual(model.codingKeySupport, "HELLO -> nested there") 60 | XCTAssertEqual(model.nestedB, "b value") 61 | 62 | let encoded = try JSONEncoder().encode(model) 63 | let dict = try JSONSerialization.jsonObject(with: encoded) as! [String: Any] 64 | XCTAssertEqual(model.defaultVal, dict["defaultVal"] as! String) 65 | XCTAssertEqual(model.strict, dict["strict"] as! String) 66 | XCTAssertNil(dict["noStrict"]) 67 | XCTAssertEqual(model.autoConvert, dict["autoConvert"] as? Int) 68 | XCTAssertEqual(model.hi, dict["hello"] as! String) 69 | XCTAssertEqual("nested there", (dict["nested"] as! [String: Any])["hi"] as! String) 70 | XCTAssertEqual(model.nestedB, (dict["nested"] as! [String: Any])["b"] as! String) 71 | 72 | print(String(data: encoded, encoding: .utf8)!) 73 | } 74 | 75 | func testSystemCodableSubclass_v2() throws { 76 | class ClassModel: Codable { 77 | var val: String? 78 | } 79 | 80 | class ClassSubmodel: ClassModel { 81 | var subVal: String? 82 | 83 | enum CodingKeys: CodingKey { 84 | case subVal 85 | } 86 | 87 | required init(from decoder: Decoder) throws { 88 | let container = try decoder.container(keyedBy: CodingKeys.self) 89 | subVal = try container.decode(String.self, forKey: .subVal) 90 | try super.init(from: decoder) 91 | } 92 | 93 | override func encode(to encoder: Encoder) throws { 94 | try super.encode(to: encoder) 95 | var container = encoder.container(keyedBy: CodingKeys.self) 96 | try container.encode(subVal, forKey: .subVal) 97 | } 98 | } 99 | 100 | let jsonStr = """ 101 | { 102 | "val": "a", 103 | "subVal": "b", 104 | } 105 | """ 106 | 107 | let model = try JSONDecoder().decode(ClassSubmodel.self, from: jsonStr.data(using: .utf8)!) 108 | XCTAssertEqual(model.val, "a") 109 | XCTAssertEqual(model.subVal, "b") 110 | } 111 | 112 | func testSystemCodableSubclass() throws { 113 | class ClassModel: Codable { 114 | var val: String? 115 | } 116 | 117 | class ClassSubmodel: ClassModel { 118 | var subVal: String? 119 | } 120 | 121 | let jsonStr = """ 122 | { 123 | "val": "a", 124 | "subVal": "b", 125 | } 126 | """ 127 | 128 | let model = try JSONDecoder().decode(ClassSubmodel.self, from: jsonStr.data(using: .utf8)!) 129 | XCTAssertEqual(model.val, "a") 130 | XCTAssertNotEqual(model.subVal, "b") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/CodableWrapperTests/DeclareTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by wp on 2023/6/29. 6 | // 7 | 8 | import CodableWrapper 9 | import Foundation 10 | 11 | @Codable 12 | class ClassModel0: Codable { 13 | var val: String = "0" 14 | var val1 = "abc" 15 | var val11 = 123 16 | var val111 = 123.4 17 | var val1111 = true 18 | var val2: Int? 19 | 20 | lazy var lazyVal: Double = val111 * 2 21 | 22 | // var val3 = [String: String].init() 23 | // var val4 = [123] + [4] 24 | } 25 | 26 | @Codable 27 | class ClassModel1 { 28 | var val: String = "1" 29 | } 30 | 31 | @Codable 32 | public class ClassModel11 { 33 | var val: String = "1" 34 | } 35 | 36 | @Codable 37 | open class ClassModel111 { 38 | open var val: String = "1" 39 | } 40 | 41 | @CodableSubclass 42 | class ClassSubmodel0: ClassModel0 { 43 | @CodingKey("abc") 44 | var x: String = "0_0" 45 | var y: String 46 | 47 | init(x: String, y: String) { 48 | self.x = x 49 | self.y = y 50 | super.init() 51 | } 52 | } 53 | 54 | @CodableSubclass 55 | class ClassSubmodel1: ClassModel1 { 56 | var subVal: String = "1_1" 57 | } 58 | 59 | struct StructWraningX { 60 | @CodingKey("abc") var subVal: String = "1_1" 61 | } 62 | 63 | public extension ClassModel11 { 64 | @Codable 65 | class A { 66 | public var val: String = "1" 67 | } 68 | } 69 | 70 | // @CodableSubclass 71 | // struct StructWraning0 {} 72 | // 73 | // @CodingKey("a") 74 | // struct StructWraning1 {} 75 | // 76 | // @CodingNestedKey("a") 77 | // struct StructWraning2 {} 78 | // 79 | // @CodingTransformer(StringPrefixTransform("HELLO -> ")) 80 | // struct StructWraning3 {} 81 | -------------------------------------------------------------------------------- /Tests/CodableWrapperTests/ExampleTest.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ExampleTest.swift 3 | //// CodableWrapperTest 4 | //// 5 | //// Created by PAN on 2020/7/16. 6 | //// 7 | // 8 | import CodableWrapper 9 | import XCTest 10 | 11 | enum Animal: String, Codable { 12 | case dog 13 | case cat 14 | case fish 15 | } 16 | 17 | @Codable 18 | struct ExampleModel: Codable { 19 | @CodingKey("aString") 20 | var stringVal: String = "scyano" 21 | 22 | @CodingKey("aInt") 23 | var intVal: Int? = 123_456 24 | 25 | var array: [Double] = [1.998, 2.998, 3.998] 26 | 27 | var bool: Bool = false 28 | 29 | var bool2: Bool = true 30 | 31 | var unImpl: String? 32 | 33 | var animal: Animal = .dog 34 | 35 | var testInt: Int? 36 | 37 | var testFloat: Float? 38 | 39 | @CodingKey("1") var testBool: Bool? = nil 40 | 41 | var testFloats: [Float]? 42 | 43 | static var empty: ExampleModel = .init() 44 | } 45 | 46 | @Codable 47 | struct SimpleModel { 48 | var a, b: Int 49 | var val: Int = 2 50 | } 51 | 52 | @Codable 53 | struct OptionalModel { 54 | var val: String? = "default" 55 | } 56 | 57 | @Codable 58 | struct OptionalNullModel { 59 | var val: String? 60 | } 61 | 62 | @Codable 63 | struct RootModel: Codable { 64 | @CodingKey("rt") var root: SubRootModelCodec = .init(value: nil, value2: ExampleModel()) 65 | var root2: SubRootModel? = SubRootModel(value: nil, value2: nil, value3: nil) 66 | } 67 | 68 | @Codable 69 | struct SubRootModelCodec { 70 | var value: ExampleModel? 71 | var value2: ExampleModel = .init() 72 | } 73 | 74 | @Codable 75 | struct SubRootModel { 76 | var value: ExampleModel? 77 | var value2: ExampleModel? 78 | var value3: ExampleModel? = .init() 79 | } 80 | 81 | @Codable 82 | struct SnakeCamelModel { 83 | var snake_string: String = "" 84 | var camelString: String = "" 85 | } 86 | 87 | class ExampleTest: XCTestCase { 88 | private var didSetCount = 0 89 | var setTestModel: ExampleModel? { 90 | didSet { 91 | didSetCount += 1 92 | } 93 | } 94 | 95 | override class func setUp() {} 96 | 97 | func testStructCopyOnWrite() { 98 | let a = ExampleModel() 99 | let valueInA = a.stringVal 100 | var b = a 101 | b.stringVal = "changed!" 102 | XCTAssertEqual(a.stringVal, valueInA) 103 | } 104 | 105 | func testBasicUsage() throws { 106 | let json = #"{"stringVal": "pan", "intVal": "233", "bool": "1", "animal": "cat"}"# 107 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 108 | XCTAssertEqual(model.intVal, 233) 109 | XCTAssertEqual(model.stringVal, "pan") 110 | XCTAssertEqual(model.unImpl, nil) 111 | XCTAssertEqual(model.array, [1.998, 2.998, 3.998]) 112 | XCTAssertEqual(model.bool, true) 113 | XCTAssertEqual(model.animal, .cat) 114 | } 115 | 116 | func testCodingKeyEncode() throws { 117 | let json = """ 118 | {"intVal": 233, "stringVal": "pan"} 119 | """ 120 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 121 | 122 | let data = try JSONEncoder().encode(model) 123 | let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] 124 | XCTAssertEqual(jsonObject["aInt"] as? Int, 233) 125 | XCTAssertEqual(jsonObject["aString"] as? String, "pan") 126 | } 127 | 128 | func testSnakeCamel() throws { 129 | let json = #"{"snakeString":"snake", "camel_string": "camel"}"# 130 | 131 | let model = try JSONDecoder().decode(SnakeCamelModel.self, from: json.data(using: .utf8)!) 132 | XCTAssertEqual(model.snake_string, "snake") 133 | XCTAssertEqual(model.camelString, "camel") 134 | } 135 | 136 | func testCodingKeyDecode() throws { 137 | let json = """ 138 | {"aString": "pan", "aInt": "233"} 139 | """ 140 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 141 | XCTAssertEqual(model.intVal, 233) 142 | XCTAssertEqual(model.stringVal, "pan") 143 | } 144 | 145 | func testDefaultVale() throws { 146 | let json = """ 147 | {"intVal": "wrong value"} 148 | """ 149 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 150 | XCTAssertEqual(model.intVal, 123_456) 151 | XCTAssertEqual(model.stringVal, "scyano") 152 | XCTAssertEqual(model.animal, .dog) 153 | } 154 | 155 | func testNested() throws { 156 | let json = """ 157 | {"rt": {"value": {"stringVal":"x"}}, "root2": {"value": {"stringVal":"y"}}} 158 | """ 159 | let model = try JSONDecoder().decode(RootModel.self, from: json.data(using: .utf8)!) 160 | XCTAssertEqual(model.root.value?.stringVal, "x") 161 | XCTAssertEqual(model.root.value2.stringVal, "scyano") 162 | XCTAssertEqual(model.root2?.value?.stringVal, "y") 163 | XCTAssertEqual(model.root2?.value2?.stringVal, nil) 164 | XCTAssertEqual(model.root2?.value3?.stringVal, "scyano") 165 | } 166 | 167 | func testDidSet() throws { 168 | didSetCount = 0 169 | 170 | let json = """ 171 | {"intVal": 233, "stringVal": "pan"} 172 | """ 173 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 174 | setTestModel = model 175 | setTestModel!.intVal = 222 176 | setTestModel!.stringVal = "ok" 177 | 178 | XCTAssertEqual(didSetCount, 3) 179 | } 180 | 181 | func testLiteral() throws { 182 | let model = ExampleModel(stringVal: "1", intVal: 1, array: [], bool: true, bool2: true, unImpl: "123", animal: .cat, testInt: 111, testFloat: 1.2, testBool: true, testFloats: [1, 2]) 183 | XCTAssertEqual(model.unImpl, "123") 184 | XCTAssertEqual(model.testFloats, [1, 2]) 185 | } 186 | 187 | func testOptionalWithValue() throws { 188 | let json = """ 189 | {"val": "default2"} 190 | """ 191 | let model = try JSONDecoder().decode(OptionalModel.self, from: json.data(using: .utf8)!) 192 | XCTAssertEqual(model.val, "default2") 193 | } 194 | 195 | func testOptionalWithNull() throws { 196 | let json = """ 197 | {"val": null} 198 | """ 199 | let model = try JSONDecoder().decode(OptionalNullModel.self, from: json.data(using: .utf8)!) 200 | XCTAssertEqual(model.val, nil) 201 | 202 | let json2 = """ 203 | {} 204 | """ 205 | let model2 = try JSONDecoder().decode(OptionalNullModel.self, from: json2.data(using: .utf8)!) 206 | XCTAssertEqual(model2.val, nil) 207 | } 208 | 209 | func testBasicTypeBridge() throws { 210 | let json = """ 211 | {"intVal": "1", "stringVal": 2, "bool": "true"} 212 | """ 213 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 214 | XCTAssertEqual(model.intVal, 1) 215 | XCTAssertEqual(model.stringVal, "2") 216 | XCTAssertEqual(model.bool, true) 217 | 218 | let jsonData = try JSONEncoder().encode(model) 219 | let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any] 220 | XCTAssertEqual(jsonObject["aString"] as? String, "2") 221 | } 222 | 223 | func testMutiThread() throws { 224 | let expectation = XCTestExpectation(description: "") 225 | let expectation2 = XCTestExpectation(description: "") 226 | 227 | var array: [ExampleModel] = [] 228 | var array2: [ExampleModel] = [] 229 | 230 | DispatchQueue.global().async { 231 | do { 232 | for i in 5000 ..< 6000 { 233 | let json = """ 234 | {"intVal": \(i)} 235 | """ 236 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 237 | XCTAssertEqual(model.intVal, i) 238 | XCTAssertEqual(model.stringVal, "scyano") 239 | XCTAssertEqual(model.unImpl, nil) 240 | XCTAssertEqual(model.array, [1.998, 2.998, 3.998]) 241 | // print(model.intVal) 242 | 243 | array.append(model) 244 | } 245 | expectation.fulfill() 246 | } catch let e { 247 | print(e) 248 | } 249 | } 250 | 251 | DispatchQueue.global().async { 252 | do { 253 | for i in 1 ..< 1000 { 254 | let json = """ 255 | {"intVal": \(i), "stringVal": "string_\(i)", "array": [123456789]} 256 | """ 257 | let model = try JSONDecoder().decode(ExampleModel.self, from: json.data(using: .utf8)!) 258 | XCTAssertEqual(model.intVal, i) 259 | XCTAssertEqual(model.stringVal, "string_\(i)") 260 | XCTAssertEqual(model.unImpl, nil) 261 | XCTAssertEqual(model.array, [123_456_789]) 262 | 263 | array2.append(model) 264 | } 265 | expectation2.fulfill() 266 | } catch let e { 267 | print(e) 268 | } 269 | } 270 | 271 | wait(for: [expectation, expectation2], timeout: 10.0) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /Tests/CodableWrapperTests/ExtensionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionTest.swift 3 | // 4 | // 5 | // Created by winddpan on 2023/1/5. 6 | // 7 | 8 | import CodableWrapper 9 | import XCTest 10 | 11 | @Codable 12 | struct HashableModel: Hashable { 13 | var value1: String? 14 | } 15 | 16 | struct NavtiveHashableModel: Hashable, Codable { 17 | var value1: String? 18 | } 19 | 20 | struct NavtiveHashableModel2: Hashable, Codable { 21 | var value1: String? 22 | } 23 | 24 | final class ExtensionTest: XCTestCase { 25 | override func setUpWithError() throws { 26 | // Put setup code here. This method is called before the invocation of each test method in the class. 27 | } 28 | 29 | override func tearDownWithError() throws { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | } 32 | 33 | func testHashable() throws { 34 | let a = HashableModel(value1: "abc") 35 | let b = HashableModel(value1: "abc") 36 | 37 | let c = NavtiveHashableModel(value1: "abc") 38 | let d = NavtiveHashableModel2(value1: "abc") 39 | 40 | XCTAssertEqual(a.hashValue, b.hashValue) 41 | XCTAssertEqual(a.hashValue, c.hashValue) 42 | XCTAssertEqual(c.hashValue, d.hashValue) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/CodableWrapperTests/NestedKeyTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NestedKeyTest.swift 3 | // CodableWrapperTest 4 | // 5 | // Created by PAN on 2021/10/19. 6 | // 7 | 8 | import CodableWrapper 9 | import XCTest 10 | 11 | @Codable 12 | struct Episode { 13 | @CodingKey("title") 14 | var show: String? 15 | 16 | @CodingNestedKey("actor.actorName") 17 | var actorName1: String? = nil 18 | 19 | @CodingNestedKey("actor.actor_name") 20 | var actorName2: String? = nil 21 | 22 | @CodingKey("actor.actor_name") 23 | var noNestedActorName: String? = nil 24 | 25 | @CodingNestedKey("actor.iq") 26 | var iq: Int = 0 27 | } 28 | 29 | class NestedKeyTest: XCTestCase { 30 | let JSON = """ 31 | { 32 | "title": "The Big Bang Theory", 33 | "actor.actor_name": "just a test", 34 | "actor": { 35 | "actor_name": "Sheldon Cooper", 36 | "iq": 140 37 | } 38 | } 39 | """ 40 | 41 | func testNestedKey() throws { 42 | let model = try JSONDecoder().decode(Episode.self, from: JSON.data(using: .utf8)!) 43 | 44 | XCTAssertEqual(model.show, "The Big Bang Theory") 45 | XCTAssertEqual(model.noNestedActorName, "just a test") 46 | XCTAssertEqual(model.actorName1, "Sheldon Cooper") 47 | XCTAssertEqual(model.actorName2, "Sheldon Cooper") 48 | XCTAssertEqual(model.iq, 140) 49 | 50 | let jsonData = try JSONEncoder().encode(model) 51 | let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any] 52 | let actor = (jsonObject["actor"] as? [String: Any]) 53 | 54 | XCTAssertEqual(jsonObject["title"] as? String, "The Big Bang Theory") 55 | XCTAssertEqual(jsonObject["actor.actor_name"] as? String, "just a test") 56 | XCTAssertEqual(actor?["actorName"] as? String, "Sheldon Cooper") 57 | XCTAssertEqual(actor?["actor_name"] as? String, "Sheldon Cooper") 58 | XCTAssertEqual(actor?["iq"] as? Int, 140) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/CodableWrapperTests/TransformTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransformTest.swift 3 | // CodableWrapperTest 4 | // 5 | // Created by winddpan on 2020/8/21. 6 | // 7 | 8 | import CodableWrapper 9 | import XCTest 10 | 11 | struct DateWrapper { 12 | let timestamp: TimeInterval 13 | 14 | var date: Date { 15 | Date(timeIntervalSince1970: timestamp) 16 | } 17 | 18 | init(timestamp: TimeInterval) { 19 | self.timestamp = timestamp 20 | } 21 | 22 | static var transformer = TransformOf(fromJSON: { DateWrapper(timestamp: $0 ?? 0) }, toJSON: { $0.timestamp }) 23 | } 24 | 25 | enum EnumInt: Int, Codable { 26 | case zero, one, two, three 27 | 28 | static var transformer: TransformOf { 29 | return TransformOf(fromJSON: { json in 30 | if let json = json, let result = EnumInt(rawValue: json) { 31 | return result 32 | } 33 | return .zero 34 | }, 35 | toJSON: { $0.rawValue }) 36 | } 37 | 38 | static var optionalTransformer: TransformOf { 39 | return TransformOf(fromJSON: { json in 40 | if let json = json, let result = EnumInt(rawValue: json) { 41 | return result 42 | } 43 | return nil 44 | }, 45 | toJSON: { $0?.rawValue }) 46 | } 47 | } 48 | 49 | @Codable 50 | struct StringModel { 51 | @CodingTransformer(StringPrefixTransform("hello:")) 52 | var title: String = "world" 53 | } 54 | 55 | @Codable 56 | struct DateModel { 57 | @CodingTransformer(DateWrapper.transformer) 58 | var time: DateWrapper? = .init(timestamp: 0) 59 | 60 | @CodingTransformer(DateWrapper.transformer) 61 | var time1: DateWrapper = .init(timestamp: 0) 62 | 63 | @CodingTransformer(DateWrapper.transformer) 64 | var time2: DateWrapper? 65 | 66 | @CodingTransformer(DateWrapper.transformer) 67 | var time3: DateWrapper 68 | } 69 | 70 | struct DateModel_produce: Codable { 71 | var time: DateWrapper? 72 | 73 | init(from decoder: Decoder) throws { 74 | let container = try decoder.container(keyedBy: AnyCodingKey.self) 75 | let time = try container.decode(type: Swift.type(of: DateWrapper.transformer).JSON.self, keys: ["time"], nestedKeys: []) 76 | self.time = DateWrapper.transformer.transformFromJSON(time) 77 | } 78 | 79 | func encode(to encoder: Encoder) throws { 80 | var container = encoder.container(keyedBy: AnyCodingKey.self) 81 | if let value = DateWrapper.transformer.transformToJSON(time ?? .init(timestamp: 0)) { 82 | try container.encode(value, forKey: .init(stringValue: "time")!) 83 | } 84 | } 85 | 86 | init(time: DateWrapper) { 87 | self.time = time 88 | } 89 | } 90 | 91 | struct ValueWrapper: Equatable, Codable { 92 | var value: String? 93 | } 94 | 95 | @Codable 96 | struct TransformExampleModel { 97 | @CodingTransformer(TransformOf(fromJSON: { ValueWrapper(value: $0) }, toJSON: { $0.value })) 98 | var valueA: ValueWrapper = .init(value: "A") 99 | 100 | @CodingTransformer(TransformOf(fromJSON: { ValueWrapper(value: $0) }, toJSON: { $0?.value })) 101 | var valueB: ValueWrapper? = .init(value: "B") 102 | 103 | @CodingTransformer(TransformOf(fromJSON: { $0 != nil ? ValueWrapper(value: $0) : nil }, toJSON: { $0?.value })) 104 | var valueC: ValueWrapper? = .init(value: "C") 105 | 106 | @CodingTransformer(TransformOf(fromJSON: { $0 != nil ? ValueWrapper(value: $0) : nil }, toJSON: { $0?.value })) 107 | var valueD: ValueWrapper? 108 | } 109 | 110 | @Codable 111 | struct CustomUnOptional: Codable { 112 | @CodingTransformer(EnumInt.transformer) 113 | var one: EnumInt = .three 114 | 115 | @CodingTransformer(EnumInt.transformer) 116 | var two: EnumInt? 117 | } 118 | 119 | @Codable 120 | struct CustomOptional: Codable { 121 | @CodingTransformer(EnumInt.optionalTransformer) 122 | var one: EnumInt = .three 123 | 124 | @CodingTransformer(EnumInt.optionalTransformer) 125 | var two: EnumInt? 126 | } 127 | 128 | @Codable 129 | struct CustomTransformCodec { 130 | var id: Int = 0 131 | 132 | @CodingKey("tuple", "tp") 133 | @CodingTransformer(tupleTransform) 134 | var tuple: (String, String)? 135 | 136 | @CodingTransformer(tupleTransform) 137 | var tupleOptional: (String, String)? 138 | } 139 | 140 | class TransformTest: XCTestCase { 141 | func testBuild() {} 142 | 143 | func testStringTransform() throws { 144 | let json = """ 145 | {"title": "json"} 146 | """ 147 | 148 | let model = try JSONDecoder().decode(StringModel.self, from: json.data(using: .utf8)!) 149 | XCTAssertEqual(model.title, "hello:json") 150 | } 151 | 152 | func testDateModel() throws { 153 | let json = """ 154 | {"time": 12345} 155 | """ 156 | 157 | let model = try JSONDecoder().decode(DateModel.self, from: json.data(using: .utf8)!) 158 | XCTAssertEqual(model.time?.timestamp, 12345) 159 | XCTAssertEqual(model.time?.date.description, "1970-01-01 03:25:45 +0000") 160 | 161 | let encode = try JSONEncoder().encode(model) 162 | let jsonObject = try JSONSerialization.jsonObject(with: encode, options: []) as! [String: Any] 163 | XCTAssertEqual(jsonObject["time"] as! TimeInterval, 12345) 164 | } 165 | 166 | func testDateModel_produce() throws { 167 | let json = """ 168 | {"time": 12345} 169 | """ 170 | 171 | let model = try JSONDecoder().decode(DateModel_produce.self, from: json.data(using: .utf8)!) 172 | XCTAssertEqual(model.time?.timestamp, 12345) 173 | XCTAssertEqual(model.time?.date.description, "1970-01-01 03:25:45 +0000") 174 | 175 | let encode = try JSONEncoder().encode(model) 176 | let jsonObject = try JSONSerialization.jsonObject(with: encode, options: []) as! [String: Any] 177 | XCTAssertEqual(jsonObject["time"] as! TimeInterval, 12345) 178 | } 179 | 180 | func testTransformOf() throws { 181 | let fullModel = try JSONDecoder().decode(TransformExampleModel.self, from: #"{"valueA": "something_a", "valueB": "something_b", "valueC": "something_c", "valueD": "something_d"}"#.data(using: .utf8)!) 182 | let emptyModel = try JSONDecoder().decode(TransformExampleModel.self, from: #"{}"#.data(using: .utf8)!) 183 | 184 | XCTAssertEqual(fullModel.valueA, ValueWrapper(value: "something_a")) 185 | XCTAssertEqual(fullModel.valueB, ValueWrapper(value: "something_b")) 186 | XCTAssertEqual(fullModel.valueC, ValueWrapper(value: "something_c")) 187 | XCTAssertEqual(fullModel.valueD, ValueWrapper(value: "something_d")) 188 | 189 | XCTAssertEqual(emptyModel.valueA, ValueWrapper(value: nil)) 190 | XCTAssertEqual(emptyModel.valueB, ValueWrapper(value: nil)) 191 | XCTAssertEqual(emptyModel.valueC, ValueWrapper(value: "C")) 192 | XCTAssertEqual(emptyModel.valueD, nil) 193 | } 194 | 195 | func testCustomUnOptionalTransform() throws { 196 | let json = "{}" 197 | let model = try JSONDecoder().decode(CustomUnOptional.self, from: json.data(using: .utf8)!) 198 | XCTAssertEqual(model.one, .zero) 199 | XCTAssertEqual(model.two, .zero) 200 | } 201 | 202 | func testCustomOptionalTransform() throws { 203 | let json = "{}" 204 | let model = try JSONDecoder().decode(CustomOptional.self, from: json.data(using: .utf8)!) 205 | XCTAssertEqual(model.one, EnumInt.three) 206 | XCTAssertEqual(model.two, nil) 207 | } 208 | 209 | func testCustomTransformCodec() throws { 210 | let json = """ 211 | {"id": 1, "tp": "left|right"} 212 | """ 213 | let model = try JSONDecoder().decode(CustomTransformCodec.self, from: json.data(using: .utf8)!) 214 | XCTAssertEqual(model.id, 1) 215 | XCTAssertEqual(model.tuple?.0, "left") 216 | XCTAssertEqual(model.tuple?.1, "right") 217 | XCTAssertEqual(model.tupleOptional?.0, nil) 218 | 219 | let jsonData = try JSONEncoder().encode(model) 220 | let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any] 221 | XCTAssertEqual(jsonObject["id"] as? Int, 1) 222 | XCTAssertEqual(jsonObject["tuple"] as? String, "left|right") 223 | // XCTAssertEqual(String(data: jsonData, encoding: .utf8), "{\"id\":1,\"tuple\":\"left|right\"}") 224 | } 225 | 226 | // func testDateTransfrom() throws { 227 | // struct TransformExampleModel: Codable { 228 | // @CodingTransformer(SecondDateTransform()) 229 | // var sencondsDate: Date? 230 | // 231 | // @CodingTransformer(MillisecondDateTransform()) 232 | // var millSecondsDate: Date? 233 | // } 234 | // 235 | // let date = Date() 236 | // let json = """ 237 | // {"sencondsDate": \(date.timeIntervalSince1970), "millSecondsDate": \(date.timeIntervalSince1970 * 1000)} 238 | // """ 239 | // 240 | // let model = try JSONDecoder().decode(TransformExampleModel.self, from: json.data(using: .utf8)!) 241 | // XCTAssertEqual(model.sencondsDate?.timeIntervalSince1970, date.timeIntervalSince1970) 242 | // XCTAssertEqual(model.millSecondsDate?.timeIntervalSince1970, date.timeIntervalSince1970) 243 | // } 244 | } 245 | -------------------------------------------------------------------------------- /Tests/CodableWrapperTests/Transforms.swift: -------------------------------------------------------------------------------- 1 | import CodableWrapper 2 | import Foundation 3 | 4 | class StringPrefixTransform: TransformType { 5 | typealias Object = String 6 | typealias JSON = String 7 | 8 | let prefix: String 9 | 10 | init(_ prefix: String) { 11 | self.prefix = prefix 12 | } 13 | 14 | func transformFromJSON(_ json: String?) -> String { 15 | return prefix + (json ?? "") 16 | } 17 | 18 | func transformToJSON(_ object: String) -> String? { 19 | object.replacing(prefix, with: "") 20 | } 21 | } 22 | 23 | class TimestampTransform: TransformType { 24 | typealias Object = Date 25 | typealias JSON = TimeInterval 26 | 27 | func transformToJSON(_ date: Object) -> JSON? { 28 | date.timeIntervalSince1970 29 | } 30 | 31 | func transformFromJSON(_ timestamp: JSON?) -> Object { 32 | return Date(timeIntervalSince1970: timestamp ?? 0) 33 | } 34 | } 35 | 36 | let tupleTransform = TransformOf<(String, String)?, String>(fromJSON: { json in 37 | if let json = json { 38 | let comps = json.components(separatedBy: "|") 39 | return (comps.first ?? "", comps.last ?? "") 40 | } 41 | return nil 42 | }, toJSON: { tuple in 43 | if let tuple = tuple { 44 | return "\(tuple.0)|\(tuple.1)" 45 | } 46 | return nil 47 | }) 48 | --------------------------------------------------------------------------------