├── .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 | 
201 | 
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 | 
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 | 
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 | 
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 | 
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 | 
563 |
564 | 是不是出乎意料,原因是编译器只给`ClassModel`添加了`init(from decoder: Decoder)`,`ClassSubmodel`则没有。要解决问题还需要手动实现子类的`Codable`协议,十分不便:
565 | 
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 | [](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