├── .gitignore ├── LICENSE.md ├── Package.swift ├── ReadMe.md ├── Sources └── DeepCodable │ ├── Codable.swift │ ├── Decodable.swift │ ├── Encodable.swift │ ├── Node.swift │ ├── Tree.swift │ └── Value.swift └── Tests └── DeepCodableTests ├── BareEncodingTests.swift ├── DecodingTests.swift ├── RealWorldDecodingTests.swift └── WrappedEncodingTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Swift build products. 2 | /.build 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "deep-codable", 7 | products: [ 8 | .library( 9 | name: "DeepCodable", 10 | targets: ["DeepCodable"] 11 | ), 12 | ], 13 | dependencies: [], 14 | targets: [ 15 | .target( 16 | name: "DeepCodable", 17 | dependencies: [] 18 | ), 19 | .testTarget( 20 | name: "DeepCodableTests", 21 | dependencies: ["DeepCodable"] 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # `DeepCodable`: Encode and decode deeply-nested data into flat Swift objects # 2 | 3 | Have you ever gotten a response from an API that looked like this and wanted to pull out and flatten the values you care about? 4 | (This is a real response from the GitHub GraphQL API, with only the actual values changed) 5 | 6 | ```json 7 | { 8 | "data": { 9 | "node": { 10 | "content": { 11 | "__typename": "Example type", 12 | "title": "Example title" 13 | }, 14 | "fieldValues": { 15 | "nodes": [ 16 | {}, 17 | {}, 18 | { 19 | "name": "Example node name", 20 | "field": { 21 | "name": "Example field name" 22 | } 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | `DeepCodable` lets you easily do so in Swift while maintaining type-safety, with the magic of result builders, key paths, and property wrappers: 32 | 33 | ```swift 34 | import DeepCodable 35 | 36 | struct GithubGraphqlResponse: DeepDecodable { 37 | static let codingTree = CodingTree { 38 | Key("data") { 39 | Key("node") { 40 | Key("content") { 41 | Key("__typename", containing: \._type) 42 | Key("title", containing: \._title) 43 | } 44 | 45 | Key("fieldValues") { 46 | Key("nodes", containing: \._nodes) 47 | } 48 | } 49 | } 50 | } 51 | 52 | 53 | struct Node: DeepDecodable { 54 | static let codingTree = CodingTree { 55 | Key("name", containing: \._name) 56 | 57 | Key("field", "name", containing: \._fieldName) 58 | /* 59 | The above is a "flattened" shortcut for: 60 | Key("field") { 61 | Key("name", containing: \._fieldName) 62 | } 63 | */ 64 | } 65 | 66 | 67 | @Value var name: String? 68 | @Value var fieldName: String? 69 | } 70 | 71 | enum TypeName: String, Decodable { 72 | case example = "Example type" 73 | } 74 | 75 | @Value var title: String 76 | @Value var nodes: [Node] 77 | @Value var type: TypeName 78 | } 79 | 80 | dump(try JSONDecoder().decode(GithubGraphqlResponse.self, from: jsonData)) 81 | ``` 82 | 83 | 84 | ## Quick start ## 85 | 86 | Add to your `Package.Swift`: 87 | ```swift 88 | ... 89 | dependencies: [ 90 | ... 91 | .package(url: "https://github.com/MPLew-is/deep-codable", branch: "main"), 92 | ], 93 | targets: [ 94 | ... 95 | .target( 96 | ... 97 | dependencies: [ 98 | ... 99 | .product(name: "DeepCodable", package: "deep-codable"), 100 | ] 101 | ), 102 | ... 103 | ] 104 | ] 105 | ``` 106 | 107 | Conform a type you want to decode to `DeepDecodable` by defining a coding tree representing which nodes are bound to which values: 108 | ```swift 109 | struct DeeplyNestedResponse: DeepDecodable { 110 | static let codingTree = CodingTree { 111 | Key("topLevel") { 112 | Key("secondLevel") { 113 | Key("thirdLevel", containing: \._property) 114 | } 115 | } 116 | } 117 | /* 118 | Also valid is the flattened form: 119 | static let codingTree = CodingTree { 120 | Key("topLevel", "secondLevel", "thirdLevel", containing: \._property) 121 | } 122 | */ 123 | 124 | @Value var property: String 125 | } 126 | /* 127 | Corresponding JSON would look like: 128 | { 129 | "topLevel": { 130 | "secondLevel": { 131 | "thirdLevel: "{some value}" 132 | } 133 | } 134 | } 135 | */ 136 | ``` 137 | 138 | Nodes in your `codingTree` are made of `Key`s initialized one of the following ways: 139 | 140 | - `Key("name") { /* More Keys */ }`: node that don't capture values directly, but contain other nodes 141 | - This maps to a serialized representation like `{"name": { ... } }` 142 | 143 | - `Key("name", containing: \._value)`: node that should be decoded into the `value` property 144 | 145 | All values to decode must be wrapped with the `@Value` property wrapper, and the `\._{name}` syntax refers directly to the wrapping instance (`\.{name}` without the underscore refers to the actual underlying value). 146 | 147 | 148 | Decode a value into an instance of your type: 149 | ```swift 150 | let instance = try JSONDecoder().decode(Response.self, from: jsonData) 151 | ``` 152 | 153 | `DeepCodable` is built on top of normal `Codable`, so any decoder (like [the property list decoder in `Foundation`](https://developer.apple.com/documentation/foundation/propertylistdecoder) or [the excellent third-party YAML decoder, Yams](https://github.com/jpsim/Yams)) can be used to decode values. 154 | 155 | 156 | ## Encoding ## 157 | 158 | While decoding is probably the most common use-case for this type of nested decoding, this package also supports encoding a flat Swift struct into a deeply nested one with the same pattern: 159 | ```swift 160 | struct DeeplyNestedRequest: DeepEncodable { 161 | static let codingTree = CodingTree { 162 | Key("topLevel") { 163 | Key("secondLevel") { 164 | Key("thirdLevel", containing: \.bareProperty) 165 | } 166 | 167 | Key("otherSecondLevel", containing: \._wrappedProperty) 168 | } 169 | } 170 | /* 171 | Also valid is the flattened form: 172 | static let codingTree = CodingTree { 173 | Key("topLevel") { 174 | Key("secondLevel", "thirdLevel", containing: \.bareProperty) 175 | 176 | Key("otherSecondLevel", containing: \._wrappedProperty) 177 | } 178 | } 179 | */ 180 | 181 | let bareProperty: String 182 | @Value var wrappedProperty: String 183 | } 184 | /* 185 | Corresponding JSON would look like: 186 | { 187 | "topLevel": { 188 | "secondLevel": { 189 | "thirdLevel: "{bareProperty}" 190 | }, 191 | "otherSecondLevel": "{wrappedProperty}" 192 | } 193 | } 194 | */ 195 | 196 | let instance: DeeplyNestedRequest = ... 197 | let jsonData = try JSONEncoder().encode(instance) 198 | ``` 199 | 200 | With encoding, you don't have to use the `@Value` wrappers, though you can if you'd like to support decoding and encoding on the same type (in which case you can conform to `DeepCodable` as an alias for the two). 201 | 202 | 203 | ## Key features ## 204 | 205 | - Encoding and decoding a Swift object to/from an arbitrarily complex deeply nested serialized representation without manually writing `Codable` implementations 206 | 207 | - Preservation of existing `Codable` behavior on the values being encoded/decoded, including custom types 208 | - Since `DeepCodable` is just a custom implementation of the `Codable` requirements, this also means you can nest `DeepCodable` objects like in the `GithubGraphqlResponse` example 209 | 210 | - When conforming to `DeepEncodable` or `DeepDecodable`, don't interfere with the opposite normal `Codable` implementation (`Decodable`/`Encodable`, respectively) 211 | - You can declare something like `struct Response: DeepDecodable, Encodable { ... }` and decode from a deeply nested tree, and then re-encode back to a flat structure like normal `Encodable` objects 212 | 213 | - No requirement for `@Value` property wrapper for types only conforming to `DeepEncodable` 214 | 215 | - Omission of the corresponding tree sections when all values at the leaves are `nil` 216 | - This makes it so trying to encode an object with a `nil` value doesn't result in something like `{"top": {"second": {"third": null} } }` 217 | 218 | - Flattened shortcuts using variadic parameters for long paths with no branching: 219 | - `Key("topLevel", "secondLevel", containing: \._property)` instead of `Key("topLevel") { Key("secondLevel", containing: \._property) }` 220 | -------------------------------------------------------------------------------- /Sources/DeepCodable/Codable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | A type that can convert itself into and out of an arbitrarily deep tree structure of an external representation 3 | 4 | This is simply an alias for `DeepDecodable` and `DeepEncodable`, like `Codable`. 5 | */ 6 | public typealias DeepCodable = DeepDecodable & DeepEncodable 7 | 8 | 9 | // Add override implementations for encoding and decoding when the root type opts into it, and this node contains children. 10 | public extension DeepCodingNode where Root: DeepCodable { 11 | /** 12 | Initialize an instance containing child nodes from a result builder, providing multiple intermediate keys at once in a "flattened" initializer format. 13 | 14 | - Parameters: 15 | - key: root coding key used to index this node 16 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 17 | - builder: closure representing the output of a result builder block containing child nodes 18 | */ 19 | init(_ key: String, _ intermediateKeys: String..., @TreeBuilder builder: () -> [Self]) { 20 | self.init(key: key, intermediateKeys: intermediateKeys, children: builder()) 21 | } 22 | 23 | /** 24 | Initialize an instance containing child nodes, attaching the children at an arbitrarily deep level of intermediate nodes. 25 | 26 | This initializer is exposed primarily to allow other libraries to be built on top of this one and provide child nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 27 | 28 | - Parameters: 29 | - key: root coding key used to index this node 30 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 31 | - children: array of direct child nodes 32 | */ 33 | init(key: String, intermediateKeys: [String], children: [Self]) { 34 | guard !intermediateKeys.isEmpty else { 35 | self.init(key: key, children: children) 36 | return 37 | } 38 | 39 | // When no intermediate keys are provided, this simply falls back to adding the children from the builder directly to this node. 40 | // Note that the builder children need to be added to the furthest node down, so we reverse the ordering and build nested children from bottom up. 41 | var children_current = children 42 | for intermediateKey in intermediateKeys.reversed() { 43 | children_current = [.init(key: intermediateKey, children: children_current)] 44 | } 45 | 46 | self.init(key: key, children: children_current) 47 | } 48 | 49 | /** 50 | Initialize an instance containing child nodes directly, without any intermediate nodes. 51 | 52 | This initializer is exposed primarily to allow other libraries to be built on top of this one and provide child nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 53 | 54 | - Parameters: 55 | - key: coding key used to index this node 56 | - children: array of direct child nodes 57 | */ 58 | init(key: String, children: [Self]) { 59 | let optionalToDecode = children.allSatisfy(\.optionalToDecode) 60 | 61 | self.children = children 62 | 63 | self.decodeImplementation = Self.getRecursingDecodeImplementation(key: key, children: children, optionalToDecode: optionalToDecode) 64 | self.optionalToDecode = optionalToDecode 65 | 66 | self.encodeImplementation = Self.getRecursingEncodeImplementation(key: key, children: children) 67 | self.shouldEncodeImplementation = Self.getRecursingShouldEncodeImplementation(children: children) 68 | } 69 | } 70 | 71 | // Add override implementations for encoding and decoding when the root type opts into it, and this node represents a value. 72 | public extension DeepCodingNode where Root: DeepCodable { 73 | /** 74 | Initialize an instance representing a non-optional value, providing multiple intermediate keys at once in a "flattened" initializer format before the actual value. 75 | 76 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 77 | 78 | Note that the `init` and `init` methods defined previously still exist, but will only be invoked when the root type conforms to `DeepCodable` but one of the node capture types only conforms to either `Decodable` or `Encodable`. 79 | This should pretty much never happen (why would a user want both encoding and decoding, and then have one of the properties only provide one side of that?), but if it does the `encode`/`decode` implementation will fall back to `nil` and the node will be silently ignored during tree traversal for whichever coding type is missing. 80 | 81 | This means that someone could purposely have a value they want to read from deep in a tree, but not be present when re-encoded (if that's what they really want). 82 | 83 | Alternatives to this design that were considered were: 84 | 85 | - Providing `init?` and `init?` methods on this extension that just return `nil`, which will be preferred over the others 86 | - Users would then be alerted with a compile-time error that they need to handle the optional, but no clearer error message could be given which might lead to some painful debugging and/or runtime crashes 87 | 88 | - Providing `init` and `init` methods on this extension that are marked as unavailable/deprecated 89 | - This would be the best since a custom message could be returned telling the user how to fix it, but it just flat-out didn't work and silently fell back to the other declarations 90 | 91 | - Parameters: 92 | - key: root coding key used to index this node 93 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 94 | - targetPath: key path into the root type where the values should be read from/written to during decoding/encoding 95 | */ 96 | init(_ key: String, _ intermediateKeys: String..., containing targetPath: WritableKeyPath>) { 97 | /* 98 | This implementation is simply copy-pasted in multiple places out of necessity. 99 | If we try to abstract this out into a single place, we lose some generic type context and end up calling the wrong underlying initializer for the actual value type. 100 | Luckily, this is a pretty small/simple implementation, so it's not too painful. 101 | */ 102 | 103 | guard let lastIntermediateKey = intermediateKeys.last else { 104 | self.init(key: key, containing: targetPath) 105 | return 106 | } 107 | 108 | let lastChild = Self(key: lastIntermediateKey, containing: targetPath) 109 | self.init(key: key, intermediateKeys: intermediateKeys.dropLast(), children: [lastChild]) 110 | } 111 | 112 | /** 113 | Initialize an instance representing a non-optional value directly, without any intermediate keys. 114 | 115 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 116 | 117 | This initializer is exposed primarily to allow other libraries to be built on top of this one and provide child nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 118 | 119 | - Parameters: 120 | - key: coding key used to index this node 121 | - targetPath: key path into the root type where the values should be read from/written to during decoding/encoding 122 | */ 123 | init(key: String, containing targetPath: WritableKeyPath>) { 124 | self.children = nil 125 | 126 | self.decodeImplementation = Self.getDirectDecodeImplementation(key: key, targetPath: targetPath) 127 | self.optionalToDecode = false 128 | 129 | self.encodeImplementation = Self.getDirectEncodeImplementation(key: key, targetPath: targetPath) 130 | self.shouldEncodeImplementation = Self.getDirectShouldEncodeImplementation(targetPath: targetPath) 131 | } 132 | 133 | 134 | /** 135 | Initialize an instance representing an optional value, providing multiple intermediate keys at once in a "flattened" initializer format before the actual value. 136 | 137 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 138 | 139 | Note that the `init` and `init` methods defined previously still exist, but will only be invoked when the root type conforms to `DeepCodable` but one of the node capture types only conforms to either `Decodable` or `Encodable`. 140 | This should pretty much never happen (why would a user want both encoding and decoding, and then have one of the properties only provide one side of that?), but if it does the `encode`/`decode` implementation will fall back to `nil` and the node will be silently ignored during tree traversal for whichever coding type is missing. 141 | 142 | This means that someone could purposely have a value they want to read from deep in a tree, but not be present when re-encoded (if that's what they really want). 143 | 144 | Alternatives to this design that were considered were: 145 | 146 | - Providing `init?` and `init?` methods on this extension that just return `nil`, which will be preferred over the others 147 | - Users would then be alerted with a compile-time error that they need to handle the optional, but no clearer error message could be given which might lead to some painful debugging and/or runtime crashes 148 | 149 | - Providing `init` and `init` methods on this extension that are marked as unavailable/deprecated 150 | - This would be the best since a custom message could be returned telling the user how to fix it, but it just flat-out didn't work and silently fell back to the other declarations 151 | 152 | - Parameters: 153 | - key: root coding key used to index this node 154 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 155 | - targetPath: key path into the root type where the values should be read from/written to during decoding/encoding 156 | */ 157 | init(_ key: String, _ intermediateKeys: String..., containing targetPath: WritableKeyPath>) { 158 | /* 159 | This implementation is simply copy-pasted in multiple places out of necessity. 160 | If we try to abstract this out into a single place, we lose some generic type context and end up calling the wrong underlying initializer for the actual value type. 161 | Luckily, this is a pretty small/simple implementation, so it's not too painful. 162 | */ 163 | 164 | guard let lastIntermediateKey = intermediateKeys.last else { 165 | self.init(key: key, containing: targetPath) 166 | return 167 | } 168 | 169 | let lastChild = Self(key: lastIntermediateKey, containing: targetPath) 170 | self.init(key: key, intermediateKeys: intermediateKeys.dropLast(), children: [lastChild]) 171 | } 172 | 173 | /** 174 | Initialize an instance representing an optional value directly, without any intermediate keys. 175 | 176 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 177 | 178 | This initializer is exposed primarily to allow other libraries to be built on top of this one and provide child nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 179 | 180 | - Parameters: 181 | - key: coding key used to index this node 182 | - targetPath: key path into the root type where the values should be read from/written to during decoding/encoding 183 | */ 184 | init(key: String, containing targetPath: WritableKeyPath>) { 185 | self.children = nil 186 | 187 | self.decodeImplementation = Self.getDirectDecodeImplementation(key: key, targetPath: targetPath) 188 | self.optionalToDecode = true 189 | 190 | self.encodeImplementation = Self.getDirectEncodeImplementation(key: key, targetPath: targetPath) 191 | self.shouldEncodeImplementation = Self.getDirectShouldEncodeImplementation(targetPath: targetPath) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Sources/DeepCodable/Decodable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | A type that can decode itself from an arbitrarily deep tree structure 3 | 4 | All properties on this type that are to be decoded from a serialized representation must be `var` properties and have a default value, so the decoder can fill them in as it walks the serialized tree. 5 | In general, the `CodingValue` property wrapper should be used to allow for clear type definitions (otherwise, values would likely have to be optional), which will handle this conformance and associated corner-cases. 6 | */ 7 | public protocol DeepDecodable: _DeepCodingTreeDefiner, Decodable { 8 | /** 9 | Initialize an instance with only default values. 10 | 11 | This empty initializer is required for decoding, as the implementation uses `KeyPath`s to fill in the instance's properties as it decodes the encoded tree. 12 | This also precludes the use of `let` properties as the values must be modifiable by the decoding implementation during tree traversal. 13 | */ 14 | init() 15 | } 16 | 17 | // Provide a default decoding implementation that just delegates to the defined coding tree. 18 | public extension DeepDecodable { 19 | init(from decoder: Decoder) throws { 20 | self.init() 21 | 22 | try Self.codingTree.decode(from: decoder, into: &self) 23 | } 24 | } 25 | public extension DeepCodingTree where Root: DeepDecodable { 26 | /** 27 | Decode values from the input decoder, setting the corresponding properties on the input instance to be decoded. 28 | 29 | Does not actually do any decoding itself, just starts the recursive tree walking and lets the nodes handle actual decoding. 30 | 31 | - Parameters: 32 | - decoder: `Codable` decoder instance representing the serialized representation 33 | - target: instance of the type being decoded, into which decoded values should be written 34 | 35 | - Throws: Only rethrows errors produced in normal `Codable` decoding 36 | */ 37 | func decode(from decoder: Decoder, into target: inout Root) throws { 38 | let container = try decoder.container(keyedBy: DynamicStringCodingKey.self) 39 | try self.decode(from: container, into: &target) 40 | } 41 | 42 | /** 43 | Decode values from the input container, setting the corresponding properties on the input instance to be decoded. 44 | 45 | Does not actually do any decoding itself, just starts the recursive tree walking and lets the nodes handle actual decoding. 46 | 47 | - Parameters: 48 | - container: `Codable` container instance representing the serialized representation 49 | - target: instance of the type being decoded, into which decoded values should be written 50 | 51 | - Throws: Only rethrows errors produced in normal `Codable` decoding 52 | */ 53 | func decode(from container: DecodingContainer, into target: inout Root) throws { 54 | for node in self.nodes { 55 | try node.decode(from: container, into: &target) 56 | } 57 | } 58 | } 59 | internal extension DeepCodingNode where Root: DeepDecodable { 60 | /** 61 | Decode values from the input decoder, setting the corresponding properties on the input instance to be decoded. 62 | 63 | Invokes the stored implementation closure to allow for type-erasure of any `KeyPath` value types. 64 | 65 | - Parameters: 66 | - container: `Codable` decoding container instance representing the serialized representation 67 | - target: instance of the type being decoded, into which decoded values should be written 68 | 69 | - Throws: Only rethrows errors produced in normal `Codable` decoding 70 | */ 71 | func decode(from container: DecodingContainer, into target: inout Root) throws { 72 | if let decodeImplementation = self.decodeImplementation { 73 | try decodeImplementation(container, &target) 74 | } 75 | } 76 | } 77 | 78 | 79 | // Add implementations for decoding when the node contains other nodes. 80 | public extension DeepCodingNode where Root: DeepDecodable { 81 | /** 82 | Get the implementation for the `decode` method when this node has children. 83 | 84 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 85 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 86 | 87 | - Parameters: 88 | - key: coding key used to index this node 89 | - children: immediate child nodes of this one 90 | 91 | - Returns: A closure conforming to the type needed for calling in `decode` (or `nil` if no children can decode anything) 92 | */ 93 | internal static func getRecursingDecodeImplementation(key: String, children: [Self], optionalToDecode: Bool) -> DecodeSignature { 94 | return { (container, target: inout Root) in 95 | let nestedContainer: DecodingContainer 96 | 97 | // If this node is optional, silently ignore if its corresponding key is missing. 98 | if optionalToDecode { 99 | guard let nestedContainer_optional = try? container.nestedContainer(keyedBy: DynamicStringCodingKey.self, forKey: .init(stringValue: key)) else { 100 | return 101 | } 102 | 103 | nestedContainer = nestedContainer_optional 104 | } 105 | // Otherwise, propagate decoding errors back to the caller. 106 | else { 107 | nestedContainer = try container.nestedContainer(keyedBy: DynamicStringCodingKey.self, forKey: .init(stringValue: key)) 108 | } 109 | 110 | 111 | for child in children { 112 | try child.decode(from: nestedContainer, into: &target) 113 | } 114 | } 115 | } 116 | 117 | 118 | /** 119 | Initialize an instance containing child nodes from a result builder, providing multiple intermediate keys at once in a "flattened" initializer format. 120 | 121 | - Parameters: 122 | - key: root coding key used to index this node 123 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 124 | - builder: closure representing the output of a result builder block containing child nodes 125 | */ 126 | init(_ key: String, _ intermediateKeys: String..., @TreeBuilder builder: () -> [Self]) { 127 | self.init(key: key, intermediateKeys: intermediateKeys, children: builder()) 128 | } 129 | 130 | /** 131 | Initialize an instance containing child nodes, attaching the children at an arbitrarily deep level of intermediate nodes. 132 | 133 | This initializer is exposed primarily to allow other libraries to be built on top of this one and provide child nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 134 | 135 | - Parameters: 136 | - key: root coding key used to index this node 137 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 138 | - children: array of direct child nodes 139 | */ 140 | init(key: String, intermediateKeys: [String], children: [Self]) { 141 | guard !intermediateKeys.isEmpty else { 142 | self.init(key: key, children: children) 143 | return 144 | } 145 | 146 | // When no intermediate keys are provided, this simply falls back to adding the input children directly to this node. 147 | // Note that the children need to be added to the furthest node down, so we reverse the ordering and build nested children from bottom up. 148 | var children_current = children 149 | for intermediateKey in intermediateKeys.reversed() { 150 | children_current = [.init(key: intermediateKey, children: children_current)] 151 | } 152 | 153 | self.init(key: key, children: children_current) 154 | } 155 | 156 | /** 157 | Initialize an instance containing child nodes directly, without any intermediate nodes. 158 | 159 | This initializer is exposed primarily to allow other libraries to be built on top of this one and provide child nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 160 | 161 | - Parameters: 162 | - key: coding key used to index this node 163 | - children: array of direct child nodes 164 | */ 165 | init(key: String, children: [Self]) { 166 | // Nodes are considered optional if all of their children are also optional, or they directly decode an optional value. 167 | let optionalToDecode = children.allSatisfy(\.optionalToDecode) 168 | 169 | self.children = children 170 | 171 | self.decodeImplementation = Self.getRecursingDecodeImplementation(key: key, children: children, optionalToDecode: optionalToDecode) 172 | self.optionalToDecode = optionalToDecode 173 | 174 | // If the root type is also `DeepEncodable`, this initializer is overridden - this is only called when the type is actually only `DeepDecodable`. 175 | self.encodeImplementation = nil 176 | self.shouldEncodeImplementation = nil 177 | } 178 | } 179 | 180 | 181 | // Add implementations for decoding when the node represents a non-optional value. 182 | public extension DeepCodingNode where Root: DeepDecodable { 183 | /** 184 | Get the implementation for the `decode` method when this node captures a non-optional value directly. 185 | 186 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 187 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 188 | 189 | - Parameters: 190 | - key: coding key used to index this node 191 | - targetPath: key path into the root type where the decoded value should be written 192 | 193 | - Returns: A closure conforming to the type needed for calling in `decode` 194 | */ 195 | internal static func getDirectDecodeImplementation(key: String, targetPath: KeyPath>) -> DecodeSignature { 196 | return { (container, target: inout Root) in 197 | target[keyPath: targetPath].wrappedValue = try container.decode(Value.self, forKey: .init(stringValue: key)) 198 | } 199 | } 200 | 201 | 202 | /** 203 | Initialize an instance capturing a non-optional value to decode, providing multiple intermediate keys at once in a "flattened" initializer format before the actual value. 204 | 205 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 206 | 207 | - Parameters: 208 | - key: root coding key used to index this node 209 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the value-containing node (can be empty) 210 | - targetPath: key path into the root type where the decoded value should be written 211 | */ 212 | init(_ key: String, _ intermediateKeys: String..., containing targetPath: KeyPath>) { 213 | /* 214 | This implementation is simply copy-pasted in multiple places out of necessity. 215 | If we try to abstract this out into a single place, we lose some generic type context and end up calling the wrong underlying initializer for the actual value type. 216 | Luckily, this is a pretty small/simple implementation, so it's not too painful. 217 | */ 218 | 219 | guard let lastIntermediateKey = intermediateKeys.last else { 220 | self.init(key: key, containing: targetPath) 221 | return 222 | } 223 | 224 | let lastChild = Self(key: lastIntermediateKey, containing: targetPath) 225 | self.init(key: key, intermediateKeys: intermediateKeys.dropLast(), children: [lastChild]) 226 | } 227 | 228 | /** 229 | Initialize an instance capturing a non-optional value to decode directly, without any intermediate keys. 230 | 231 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 232 | 233 | This initializer is exposed primarily to allow other libraries to be built on top of this one and create nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 234 | 235 | - Parameters: 236 | - key: coding key used to index this node 237 | - targetPath: key path into the root type where the decoded value should be written 238 | */ 239 | init(key: String, containing targetPath: KeyPath>) { 240 | self.children = nil 241 | // This node captures a non-optional value, cannot be optional to decode. 242 | self.optionalToDecode = false 243 | 244 | self.decodeImplementation = Self.getDirectDecodeImplementation(key: key, targetPath: targetPath) 245 | 246 | // If the root type is also `DeepEncodable`, this initializer is overridden - this is only called when the type is actually only `DeepDecodable`. 247 | self.encodeImplementation = nil 248 | self.shouldEncodeImplementation = nil 249 | } 250 | } 251 | 252 | 253 | // Add implementations for decoding when the node represents an optional value. 254 | public extension DeepCodingNode where Root: DeepDecodable { 255 | /** 256 | Get the implementation for the `decode` method when this node captures an optional value directly. 257 | 258 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 259 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 260 | 261 | - Parameters: 262 | - key: coding key used to index this node 263 | - targetPath: key path into the root type where the decoded value should be written 264 | 265 | - Returns: A closure conforming to the type needed for calling in `decode` 266 | */ 267 | internal static func getDirectDecodeImplementation(key: String, targetPath: KeyPath>) -> DecodeSignature { 268 | return { (container, target: inout Root) in 269 | target[keyPath: targetPath].wrappedValue = try container.decodeIfPresent(Value.self, forKey: .init(stringValue: key)) 270 | } 271 | } 272 | 273 | 274 | /** 275 | Initialize an instance capturing an optional value to decode, providing multiple intermediate keys at once in a "flattened" initializer format before the actual value. 276 | 277 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 278 | 279 | - Parameters: 280 | - key: root coding key used to index this node 281 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the value-containing node (can be empty) 282 | - targetPath: key path into the root type where the decoded value should be written 283 | */ 284 | init(_ key: String, _ intermediateKeys: String..., containing targetPath: KeyPath>) { 285 | /* 286 | This implementation is simply copy-pasted in multiple places out of necessity. 287 | If we try to abstract this out into a single place, we lose some generic type context and end up calling the wrong underlying initializer for the actual value type. 288 | Luckily, this is a pretty small/simple implementation, so it's not too painful. 289 | */ 290 | 291 | guard let lastIntermediateKey = intermediateKeys.last else { 292 | self.init(key: key, containing: targetPath) 293 | return 294 | } 295 | 296 | let lastChild = Self(key: lastIntermediateKey, containing: targetPath) 297 | self.init(key: key, intermediateKeys: intermediateKeys.dropLast(), children: [lastChild]) 298 | } 299 | 300 | /** 301 | Initialize an instance capturing an optional value to decode. 302 | 303 | This method type-erases the input key path's value type using a stored closure (which does not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 304 | 305 | This initializer is exposed primarily to allow other libraries to be built on top of this one and create nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 306 | 307 | - Parameters: 308 | - key: coding key used to index this node 309 | - targetPath: key path into the root type where the decoded value should be written 310 | */ 311 | init(key: String, containing targetPath: KeyPath>) { 312 | self.children = nil 313 | // This node captures an optional value and is always optional to decode. 314 | self.optionalToDecode = true 315 | 316 | self.decodeImplementation = Self.getDirectDecodeImplementation(key: key, targetPath: targetPath) 317 | 318 | // If the root type is also `DeepEncodable`, this initializer is overridden - this is only called when the type is actually only `DeepDecodable`. 319 | self.encodeImplementation = nil 320 | self.shouldEncodeImplementation = nil 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /Sources/DeepCodable/Encodable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | A type that can encode itself to an arbitrarily deep tree structure 3 | 4 | Unlike `DeepDecodable`, no limitations are placed on `let` vs `var` for properties of conforming types since we're encoding an already constructed instance. 5 | */ 6 | public protocol DeepEncodable: _DeepCodingTreeDefiner, Encodable {} 7 | 8 | 9 | // Provide a default encoding implementation that just delegates to the defined coding tree. 10 | public extension DeepEncodable { 11 | func encode(to encoder: Encoder) throws { 12 | try Self.codingTree.encode(to: encoder, from: self) 13 | } 14 | } 15 | public extension DeepCodingTree where Root: DeepEncodable { 16 | /** 17 | Encode values into the input encoder, reading the corresponding properties on the input instance to be encoded. 18 | 19 | Does not actually do any encoding itself, just starts the recursive tree walking and lets the nodes handle actual encoding. 20 | 21 | - Parameters: 22 | - encoder: `Codable` encoder instance encoding to the serialized representation 23 | - target: instance of the type being encoded, from which values to encode should be read 24 | 25 | - Throws: Only rethrows errors produced in normal `Codable` encoding 26 | */ 27 | func encode(to encoder: Encoder, from target: Root) throws { 28 | var container = encoder.container(keyedBy: DynamicStringCodingKey.self) 29 | 30 | for node in self.nodes { 31 | try node.encode(to: &container, from: target) 32 | } 33 | } 34 | } 35 | internal extension DeepCodingNode where Root: DeepEncodable { 36 | /** 37 | Encode values from the input instance into the input encoder for serialization. 38 | 39 | Invokes the stored implementation closure to allow for type-erasure of any `KeyPath` value types. 40 | 41 | - Parameters: 42 | - container: `Codable` encoding container instance representing the serialized representation 43 | - target: instance of the type being encoded, from which values to be encoded should be read 44 | 45 | - Throws: Only rethrows errors produced in normal `Codable` encoding 46 | */ 47 | func encode(to container: inout EncodingContainer, from target: Root) throws { 48 | if let encodeImplementation = self.encodeImplementation { 49 | try encodeImplementation(&container, target) 50 | } 51 | } 52 | } 53 | 54 | 55 | internal extension DeepCodingNode where Root: DeepEncodable { 56 | /** 57 | Check if the node should be encoded given a specific target instance, to prevent encoding a deeply nested tree that just ends in `null`. 58 | 59 | Invokes the stored implementation closure to allow for type-erasure of any `KeyPath` value types. 60 | 61 | - Parameter target: instance of the type being encoded, from which values to be encoded should be read 62 | - Returns: whether the node should have a key created for it in the serialized representation 63 | */ 64 | func shouldEncode(target: Root) -> Bool { 65 | guard let shouldEncodeImplementation = self.shouldEncodeImplementation else { 66 | return false 67 | } 68 | 69 | return shouldEncodeImplementation(target) 70 | } 71 | } 72 | 73 | 74 | // Add implementations for encoding when the node contains other nodes. 75 | public extension DeepCodingNode where Root: DeepEncodable { 76 | /** 77 | Get the implementation for the `encode` method when this node has children. 78 | 79 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 80 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 81 | 82 | - Parameters: 83 | - key: coding key used to index this node 84 | - children: immediate child nodes of this one 85 | 86 | - Returns: A closure conforming to the type needed for calling in `encode` 87 | */ 88 | internal static func getRecursingEncodeImplementation(key: String, children: [Self]) -> EncodeSignature { 89 | return { (container: inout EncodingContainer, target) in 90 | /* 91 | Skip creating an encoding container and exit early if no child should be encoded. 92 | 93 | This implementation is tragic, as it will repeatedly call all leaf nodes (and then any intermediate nodes between here and them) at every single level of the tree, even though the value on the target hasn't changed. 94 | 95 | Unfortunately we don't have any great options to solve this by memoizing the tree as we walk it - these nodes are defined statically on the root types, so any storage we build into the nodes will need to be cleared after every encoding so that we don't end up accidentally encoding a value that should be omitted or ignoring a real value. 96 | 97 | We can't just use the target itself as a cache key since it isn't guaranteed to be `Equatable` or a class (for identity comparison/`===`). 98 | We also can't really derive anything that could be used as a key from the target (like a pointer value), since it's perfectly valid for the same target to be mutated and re-encoded. 99 | 100 | We could have the tree itself provide an ephemeral storage container when it calls into the top-level nodes, but then we have to effectively construct a shadow tree inside that container so the nodes can look up their cached value later. 101 | This would probably be the least gross solution, but it's not worth the complexity at this point - we'll just take the performance hit on encoding (which is probably rarely used) for now and circle back later. 102 | */ 103 | guard children.contains(where: { $0.shouldEncode(target: target) }) else { 104 | return 105 | } 106 | 107 | var nestedContainer = container.nestedContainer(keyedBy: DynamicStringCodingKey.self, forKey: .init(stringValue: key)) 108 | 109 | for child in children { 110 | try child.encode(to: &nestedContainer, from: target) 111 | } 112 | } 113 | } 114 | 115 | /** 116 | Get the implementation for the `shouldEncode` method when this node has children. 117 | 118 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 119 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 120 | 121 | - Parameter children: immediate child nodes of this one 122 | - Returns: A closure conforming to the type needed for calling in `shouldEncode` 123 | */ 124 | internal static func getRecursingShouldEncodeImplementation(children: [Self]) -> ShouldEncodeSignature { 125 | return { target in 126 | // If any of this node's children should be encoded, the node itself must also be. 127 | return children.contains { 128 | $0.shouldEncode(target: target) 129 | } 130 | } 131 | } 132 | 133 | 134 | /** 135 | Initialize an instance containing child nodes from a result builder, providing multiple intermediate keys at once in a "flattened" initializer format. 136 | 137 | - Parameters: 138 | - key: root coding key used to index this node 139 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 140 | - builder: closure representing the output of a result builder block containing this node's children 141 | */ 142 | init(_ key: String, _ intermediateKeys: String..., @TreeBuilder builder: () -> [Self]) { 143 | self.init(key: key, intermediateKeys: intermediateKeys, children: builder()) 144 | } 145 | 146 | /** 147 | Initialize an instance containing child nodes, attaching the children at an arbitrarily deep level of intermediate nodes. 148 | 149 | This initializer is exposed primarily to allow other libraries to be built on top of this one and provide child nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 150 | 151 | - Parameters: 152 | - key: root coding key used to index this node 153 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the children (can be empty) 154 | - children: array of direct child nodes 155 | */ 156 | init(key: String, intermediateKeys: [String], children: [Self]) { 157 | guard !intermediateKeys.isEmpty else { 158 | self.init(key: key, children: children) 159 | return 160 | } 161 | 162 | // When no intermediate keys are provided, this simply falls back to adding the input children directly to this node. 163 | // Note that the children need to be added to the furthest node down, so we reverse the ordering and build nested children from bottom up. 164 | var children_current = children 165 | for intermediatedKey in intermediateKeys.reversed() { 166 | children_current = [.init(key: intermediatedKey, children: children_current)] 167 | } 168 | 169 | self.init(key: key, children: children_current) 170 | } 171 | 172 | /** 173 | Initialize an instance containing child nodes directly, without any intermediate nodes. 174 | 175 | - Parameters: 176 | - key: coding key used to index this node 177 | - children: array of direct child nodes 178 | */ 179 | init(key: String, children: [Self]) { 180 | self.children = children 181 | 182 | // If the root type is also `DeepDecodable`, this initializer is overridden - this is only called when the type is actually only `DeepEncodable`. 183 | self.decodeImplementation = nil 184 | self.optionalToDecode = true 185 | 186 | self.encodeImplementation = Self.getRecursingEncodeImplementation(key: key, children: children) 187 | self.shouldEncodeImplementation = Self.getRecursingShouldEncodeImplementation(children: children) 188 | } 189 | } 190 | 191 | 192 | // Add implementations for encoding when the node represents a non-optional value directly. 193 | public extension DeepCodingNode where Root: DeepEncodable { 194 | /** 195 | Get the implementation for the `encode` method when this node represents a non-optional value directly. 196 | 197 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 198 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 199 | 200 | - Parameters: 201 | - key: coding key used to index this node 202 | - targetPath: key path into the root type where the value to be encoded should be read from 203 | 204 | - Returns: A closure conforming to the type needed for calling in `encode` 205 | */ 206 | internal static func getDirectEncodeImplementation(key: String, targetPath: KeyPath) -> EncodeSignature { 207 | return { (container: inout EncodingContainer, target) in 208 | try container.encode(target[keyPath: targetPath], forKey: .init(stringValue: key)) 209 | } 210 | } 211 | 212 | /** 213 | Get the implementation for the `shouldEncode` method when this node represents a non-optional value directly. 214 | 215 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 216 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 217 | 218 | - Parameter targetPath: key path into the root type where the value to be encoded should be read from (though this isn't actually used in this implementation) 219 | - Returns: A closure conforming to the type needed for calling in `shouldEncode` 220 | */ 221 | internal static func getDirectShouldEncodeImplementation(targetPath _: KeyPath) -> ShouldEncodeSignature { 222 | return { _ in true } 223 | } 224 | 225 | 226 | /** 227 | Initialize an instance directly representing a non-optional value, providing multiple intermediate keys at once in a "flattened" initializer format before the actual value. 228 | 229 | This method type-erases the input key path's value type using stored closures (which do not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 230 | 231 | Note that this will also match nodes containing a non-optional `@CodingValue`-wrapped value, which is fine since we want identical behaviors. 232 | 233 | - Parameters: 234 | - key: root coding key used to index this node 235 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the value-containing node (can be empty) 236 | - targetPath: key path into the root type where the value to be encoded should be read from 237 | */ 238 | init(_ key: String, _ intermediateKeys: String..., containing targetPath: KeyPath) { 239 | /* 240 | This implementation is simply copy-pasted in multiple places out of necessity. 241 | If we try to abstract this out into a single place, we lose some generic type context and end up calling the wrong underlying initializer for the actual value type. 242 | Luckily, this is a pretty small/simple implementation, so it's not too painful. 243 | */ 244 | 245 | guard let lastIntermediateKey = intermediateKeys.last else { 246 | self.init(key: key, containing: targetPath) 247 | return 248 | } 249 | 250 | let lastChild = Self(key: lastIntermediateKey, containing: targetPath) 251 | self.init(key: key, intermediateKeys: intermediateKeys.dropLast(), children: [lastChild]) 252 | } 253 | 254 | /** 255 | Initialize an instance directly representing a non-optional value, without any intermediate keys. 256 | 257 | This method type-erases the input key path's value type using stored closures (which do not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 258 | 259 | Note that this will also match nodes containing a non-optional `@CodingValue`-wrapped value, which is fine since we want identical behaviors. 260 | 261 | This initializer is exposed primarily to allow other libraries to be built on top of this one and create nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 262 | 263 | - Parameters: 264 | - key: coding key used to index this node 265 | - targetPath: key path into the root type where the value to be encoded should be read from 266 | */ 267 | init(key: String, containing targetPath: KeyPath) { 268 | self.children = nil 269 | 270 | // If the root type is also `DeepDecodable`, this initializer is overridden - this is only called when the type is actually only `DeepEncodable`. 271 | self.decodeImplementation = nil 272 | self.optionalToDecode = true 273 | 274 | self.encodeImplementation = Self.getDirectEncodeImplementation(key: key, targetPath: targetPath) 275 | self.shouldEncodeImplementation = Self.getDirectShouldEncodeImplementation(targetPath: targetPath) 276 | } 277 | } 278 | 279 | 280 | // Add implementations for encoding when the node represents an optional value directly. 281 | public extension DeepCodingNode where Root: DeepEncodable { 282 | /** 283 | Get the implementation for the `encode` method when this node represents an optional value directly. 284 | 285 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 286 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 287 | 288 | - Parameters: 289 | - key: coding key used to index this node 290 | - targetPath: key path into the root type where the value to be encoded should be read from 291 | 292 | - Returns: A closure conforming to the type needed for calling in `encode` 293 | */ 294 | internal static func getDirectEncodeImplementation(key: String, targetPath: KeyPath) -> EncodeSignature { 295 | return { (container: inout EncodingContainer, target) in 296 | try container.encodeIfPresent(target[keyPath: targetPath], forKey: .init(stringValue: key)) 297 | } 298 | } 299 | 300 | /** 301 | Get the implementation for the `shouldEncode` method when this node represents an optional value directly. 302 | 303 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 304 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 305 | 306 | - Parameter targetPath: key path into the root type where the value to be encoded should be read from 307 | - Returns: A closure conforming to the type needed for calling in `shouldEncode` 308 | */ 309 | internal static func getDirectShouldEncodeImplementation(targetPath: KeyPath) -> ShouldEncodeSignature { 310 | return { target in 311 | return (target[keyPath: targetPath] != nil) 312 | } 313 | } 314 | 315 | 316 | /** 317 | Initialize an instance directly representing an optional value, providing multiple intermediate keys at once in a "flattened" initializer format before the actual value. 318 | 319 | This method type-erases the input key path's value type using stored closures (which do not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 320 | 321 | - Parameters: 322 | - key: root coding key used to index this node 323 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the value-containing node (can be empty) 324 | - targetPath: key path into the root type where the value to be encoded should be read from 325 | */ 326 | init(_ key: String, _ intermediateKeys: String..., containing targetPath: KeyPath) { 327 | /* 328 | This implementation is simply copy-pasted in multiple places out of necessity. 329 | If we try to abstract this out into a single place, we lose some generic type context and end up calling the wrong underlying initializer for the actual value type. 330 | Luckily, this is a pretty small/simple implementation, so it's not too painful. 331 | */ 332 | 333 | guard let lastIntermediateKey = intermediateKeys.last else { 334 | self.init(key: key, containing: targetPath) 335 | return 336 | } 337 | 338 | let lastChild = Self(key: lastIntermediateKey, containing: targetPath) 339 | self.init(key: key, intermediateKeys: intermediateKeys.dropLast(), children: [lastChild]) 340 | } 341 | 342 | /** 343 | Initialize an instance directly representing an optional value, without any intermediate keys. 344 | 345 | This method type-erases the input key path's value type using stored closures (which do not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 346 | 347 | This initializer is exposed primarily to allow other libraries to be built on top of this one and create nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 348 | 349 | - Parameters: 350 | - key: coding key used to index this node 351 | - targetPath: key path into the root type where the value to be encoded should be read from 352 | */ 353 | init(key: String, containing targetPath: KeyPath) { 354 | self.children = nil 355 | 356 | // If the root type is also `DeepDecodable`, this initializer is overridden - this is only called when the type is actually only `DeepEncodable`. 357 | self.decodeImplementation = nil 358 | self.optionalToDecode = true 359 | 360 | self.encodeImplementation = Self.getDirectEncodeImplementation(key: key, targetPath: targetPath) 361 | self.shouldEncodeImplementation = Self.getDirectShouldEncodeImplementation(targetPath: targetPath) 362 | } 363 | } 364 | 365 | 366 | // Add implementations for encoding when the node represents an optional value wrapped in a `@CodingValue` property wrapper. 367 | public extension DeepCodingNode where Root: DeepEncodable { 368 | /** 369 | Get the implementation for the `encode` method when this node represents an optional value directly and the property is wrapped in a `DeepCodingValue` property wrapper. 370 | 371 | We need this specialized implementation for the wrapped-optional case to prevent empty keys from making it into the output serialization. 372 | If not defined, since the property wrapper is a non-optional type, we'd revert to the non-optional implementation and end up with an empty key. 373 | 374 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 375 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 376 | 377 | - Parameters: 378 | - key: coding key used to index this node 379 | - targetPath: key path into the root type where the value to be encoded should be read from 380 | 381 | - Returns: A closure conforming to the type needed for calling in `encode` 382 | */ 383 | internal static func getDirectEncodeImplementation(key: String, targetPath: KeyPath>) -> EncodeSignature { 384 | return { (container: inout EncodingContainer, target) in 385 | try container.encodeIfPresent(target[keyPath: targetPath], forKey: .init(stringValue: key)) 386 | } 387 | } 388 | 389 | /** 390 | Get the implementation for the `shouldEncode` method when this node represents an optional value wrapped in a `@CodingValue` property wrapper. 391 | 392 | We need this specialized implementation for the wrapped-optional case to prevent empty keys from making it into the output serialization. 393 | If not defined, since the property wrapper is a non-optional type, we'd revert to the non-optional implementation and end up with an empty key. 394 | 395 | This is just a helper method to centralize the definition of the closure, so it can be referenced from multiple places. 396 | This can't be a stored/computed property since we need to capture the input parameters into the closure. 397 | 398 | - Parameter targetPath: key path into the root type where the value to be encoded should be read from 399 | - Returns: A closure conforming to the type needed for calling in `shouldEncode` 400 | */ 401 | internal static func getDirectShouldEncodeImplementation(targetPath: KeyPath>) -> ShouldEncodeSignature { 402 | return { target in 403 | return (target[keyPath: targetPath].wrappedValue != nil) 404 | } 405 | } 406 | 407 | 408 | /** 409 | Initialize an instance representing an optional value wrapped in a `@CodingValue` property wrapper, providing multiple intermediate keys at once in a "flattened" initializer format before the actual value. 410 | 411 | We need this specialized implementation for the wrapped-optional case to prevent empty keys from making it into the output serialization. 412 | If not defined, since the property wrapper is a non-optional type, we'd revert to the non-optional implementation and end up with an empty key. 413 | 414 | This method type-erases the input key path's value type using stored closures (which do not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 415 | 416 | - Parameters: 417 | - key: root coding key used to index this node 418 | - intermediateKeys: any intermediate keys that should also be added as recursive descendants to this node, before adding the value-containing node (can be empty) 419 | - targetPath: key path into the root type where the value to be encoded should be read from 420 | */ 421 | init(_ key: String, _ intermediateKeys: String..., containing targetPath: KeyPath>) { 422 | /* 423 | This implementation is simply copy-pasted in multiple places out of necessity. 424 | If we try to abstract this out into a single place, we lose some generic type context and end up calling the wrong underlying initializer for the actual value type. 425 | Luckily, this is a pretty small/simple implementation, so it's not too painful. 426 | */ 427 | 428 | guard let lastIntermediateKey = intermediateKeys.last else { 429 | self.init(key: key, containing: targetPath) 430 | return 431 | } 432 | 433 | let lastChild = Self(key: lastIntermediateKey, containing: targetPath) 434 | self.init(key: key, intermediateKeys: intermediateKeys.dropLast(), children: [lastChild]) 435 | } 436 | 437 | /** 438 | Initialize an instance representing an optional value wrapped in a `@CodingValue` property wrapper, without any intermediate keys. 439 | 440 | We need this specialized implementation for the wrapped-optional case to prevent empty keys from making it into the output serialization. 441 | If not defined, since the property wrapper is a non-optional type, we'd revert to the non-optional implementation and end up with an empty key. 442 | 443 | This method type-erases the input key path's value type using stored closures (which do not expose the value's type), so parents of this node can simply use arrays of this node without any other hassles from generics. 444 | 445 | This initializer is exposed primarily to allow other libraries to be built on top of this one and create nodes directly - primary direct usage should be using other initializers with unlabeled parameters. 446 | 447 | - Parameters: 448 | - key: coding key used to index this node 449 | - targetPath: key path into the root type where the value to be encoded should be read from 450 | */ 451 | init(key: String, containing targetPath: KeyPath>) { 452 | self.children = nil 453 | 454 | // If the root type is also `DeepDecodable`, this initializer is overridden - this is only called when the type is actually only `DeepEncodable`. 455 | self.decodeImplementation = nil 456 | self.optionalToDecode = true 457 | 458 | self.encodeImplementation = Self.getDirectEncodeImplementation(key: key, targetPath: targetPath) 459 | self.shouldEncodeImplementation = Self.getDirectShouldEncodeImplementation(targetPath: targetPath) 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /Sources/DeepCodable/Node.swift: -------------------------------------------------------------------------------- 1 | /// Object representing a node on the coding tree 2 | public struct DeepCodingNode { 3 | /// Shortcut alias for the type of the coding tree 4 | public typealias Tree = DeepCodingTree 5 | /// Shortcut alias for the type of the result builder 6 | public typealias TreeBuilder = Tree.Builder 7 | 8 | /// Immediate children of this node 9 | internal let children: [Self]? 10 | 11 | 12 | /** 13 | Whether it's acceptable for an error to be thrown trying to create a decoding container for this node's key 14 | 15 | This should be `false` when this node or any of its children have a non-optional decoding value declared, `true` otherwise 16 | */ 17 | internal let optionalToDecode: Bool 18 | 19 | 20 | /// Shortcut alias for the `shouldEncode` closure's signature 21 | internal typealias ShouldEncodeSignature = (Root) -> Bool 22 | 23 | /** 24 | Stored implementation for the `shouldEncode` function defined in an extension, to allow for proper type erasure 25 | 26 | This closure captures any information it needs to write the correct value type into the root object, without having the node object be generic on the type (which will cause issues with storing collections of nodes with different value types). 27 | 28 | While it's not semantically ideal that the bare (non-`Decodable`/`Encodable` struct) has this property, we can't add a stored property in an extension so we have to declare the storage here. 29 | */ 30 | internal let shouldEncodeImplementation: ShouldEncodeSignature? 31 | 32 | 33 | /** 34 | Shortcut alias for the container used to decode 35 | 36 | This has to be `internal` since it's used in a method used by other types in this module. 37 | */ 38 | internal typealias DecodingContainer = Tree.DecodingContainer 39 | /// Shortcut alias for the decode closure's signature 40 | internal typealias DecodeSignature = (DecodingContainer, inout Root) throws -> () 41 | 42 | /** 43 | Stored implementation for the `decode` function defined in an extension, to allow for proper type erasure 44 | 45 | This closure captures any information it needs to write the correct value type into the root object, without having the node object be generic on the type (which will cause issues with storing collections of nodes with different value types). 46 | 47 | While it's not semantically ideal that the bare (non-`Decodable`/`Encodable` struct) has this property, we can't add a stored property in an extension so we have to declare the storage here. 48 | */ 49 | internal let decodeImplementation: DecodeSignature? 50 | 51 | 52 | /** 53 | Shortcut alias for the container used to encode 54 | 55 | This has to be `public` since it's used in a public-facing method. 56 | */ 57 | internal typealias EncodingContainer = KeyedEncodingContainer 58 | /// Shortcut alias for the encode closure's signature 59 | internal typealias EncodeSignature = (inout EncodingContainer, Root) throws -> () 60 | 61 | /** 62 | Stored implementation for the `encode` function defined in an extension, to allow for proper type erasure 63 | 64 | This closure captures any information it needs to write the correct value type into the encoder, without having the node object be generic on the type (which will cause issues with storing collections of nodes with different value types). 65 | 66 | While it's not semantically ideal that the bare (non-`Decodable`/`Encodable` struct) has this property, we can't add a stored property in an extension so we have to declare the storage here. 67 | */ 68 | internal let encodeImplementation: EncodeSignature? 69 | } 70 | 71 | 72 | public extension _DeepCodingTreeDefiner { 73 | /** 74 | Shortcut type alias to allow `Key` to be used in the result builder instead of the full type name 75 | 76 | This also has the advantage of scoping this short type name to within the conforming type, rather than polluting the global namespace. 77 | */ 78 | typealias Key = DeepCodingNode 79 | } 80 | -------------------------------------------------------------------------------- /Sources/DeepCodable/Tree.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Object containing the tree structure representing the encoded representation to be decoded into the containing type 3 | 4 | Designed to be instantiated using result builder syntax; for example: 5 | ```swift 6 | static let codingTree = CodingTree { 7 | Key("someRootKey") { 8 | Key("someIntermediateKey") { 9 | Key("someLeafKey", containing: \._someProperty) 10 | } 11 | } 12 | 13 | Key("otherRootKey", containing: \._otherProperty) 14 | } 15 | ``` 16 | */ 17 | public struct DeepCodingTree { 18 | /// Helper object to enable result builder syntax for defining a coding tree 19 | @resultBuilder 20 | public struct TreeBuilder { 21 | /// Shortcut alias for the type of a node in the tree 22 | public typealias Node = DeepCodingNode 23 | 24 | /** 25 | Aggregate result builder-defined nodes into a list for initializing a parent node (or tree root). 26 | 27 | - Parameter nodes: nodes representing the keys at a given level of hierarchy on the coding tree 28 | - Returns: A list of nodes to be stored as children on the parent node 29 | */ 30 | public static func buildBlock(_ nodes: Node...) -> [Node] { 31 | return nodes 32 | } 33 | } 34 | 35 | /// Shortcut alias for the type of the result builder helper struct 36 | public typealias Builder = TreeBuilder 37 | /// Shortcut alias for the type of the node in the coding tree 38 | public typealias Node = Builder.Node 39 | /// Shortcut alias for the type of the container used in decoding 40 | public typealias DecodingContainer = KeyedDecodingContainer 41 | 42 | 43 | /// Top-level nodes in the tree 44 | internal let nodes: [Node] 45 | 46 | /** 47 | Initialize an instance from the output of a result builder defining the top-level nodes of the tree. 48 | 49 | - Parameter builder: closure representing the output of a result builder block containing the top-level nodes 50 | */ 51 | public init(@Builder _ builder: () -> [Node]) { 52 | self.init(nodes: builder()) 53 | } 54 | 55 | /** 56 | Initialize an instance from an array of child nodes. 57 | 58 | - Parameter nodes: array of direct child nodes 59 | */ 60 | public init(nodes: [Node]) { 61 | self.nodes = nodes 62 | } 63 | } 64 | 65 | 66 | /** 67 | Simple stub protocol defining the `codingTree` requirement used in `DeepDecodable` and `DeepEncodable`, for centralization purposes 68 | 69 | Not intended for public use (hence the underscore prefix), but must be `public` since other public protocols inherit from it. 70 | */ 71 | public protocol _DeepCodingTreeDefiner { 72 | /** 73 | Shortcut type alias to allow `CodingTree` to be used in the users' type declarations instead of the full type name 74 | 75 | This also has the advantage of scoping this short type name to within the conforming type, rather than polluting the global namespace. 76 | */ 77 | typealias CodingTree = DeepCodingTree 78 | 79 | /** 80 | Tree representing the mapping from encoded keys to this type's properties 81 | 82 | Designed to be instantiated using result builder syntax; for example: 83 | ```swift 84 | static let codingTree = CodingTree { 85 | Key("someRootKey") { 86 | Key("someIntermediateKey") { 87 | Key("someLeafKey", containing: \._someProperty) 88 | } 89 | } 90 | 91 | Key("otherRootKey", containing: \._otherProperty) 92 | } 93 | ``` 94 | */ 95 | static var codingTree: CodingTree { get } 96 | } 97 | 98 | 99 | /** 100 | A fully dynamic coding key implementation that simply stores the string value it's initialized with 101 | 102 | This allows us to map strings provided by the result builder input into keys accepted by the actual `Codable` implementation. 103 | 104 | This has to be `internal` since it's used in methods used throughout this module. 105 | 106 | Derived from: https://swiftsenpai.com/swift/decode-dynamic-keys-json 107 | */ 108 | public struct DynamicStringCodingKey: CodingKey { 109 | public let stringValue: String 110 | public init(stringValue: String) { 111 | self.stringValue = stringValue 112 | } 113 | 114 | // This is a protocol requirement, but this is only intended to hold strings so just return `nil` for everything `Int`-related. 115 | public let intValue: Int? = nil 116 | public init?(intValue: Int) { 117 | return nil 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/DeepCodable/Value.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Wrapper type for coding values, to allow users to use arbitrary underlying types for the actual values. 3 | 4 | If not wrapped, decoding would require the values to be optional (even if the user wanted the value to be required and not have to juggle optionals. 5 | */ 6 | @propertyWrapper 7 | public struct DeepCodingValue { 8 | /** 9 | Simple reference-semantics storage container for the property wrapper, to sidestep `KeyPath` limitations 10 | 11 | Without this indirection, the wrapped value would have to be stored directly on the property wrapper struct, and thus we'd need a `WritableKeyPath`. 12 | When users are constructing coding trees, key paths to the projected values only come across as (non-writable) `KeyPath`s, so we need to be able to mutate the wrapped value with a read-only key path. 13 | 14 | By storing a reference to a class (and declaring the `set` implementation on `wrappedValue` as `nonmutating`), we can successfully write values to the underlying storage without mutating the actual wrapper struct, thus giving our users a simpler interface for declaring values to decode. 15 | */ 16 | fileprivate class Storage { 17 | /// Simple enum indicating the storage's initialization state 18 | fileprivate enum Wrapped { 19 | case empty 20 | case value(Value) 21 | } 22 | 23 | /// Actual storage for the value being wrapped in the parent property wrapper, contained in an enum indicating if the value has been initialized yet 24 | fileprivate var _value: Wrapped 25 | 26 | /// Initialize to the default "empty" state. 27 | fileprivate init() { 28 | self._value = .empty 29 | } 30 | 31 | 32 | /// Value being stored by this container 33 | var value: Value { 34 | get { 35 | switch self._value { 36 | case .empty: 37 | // We should never get here in normal usage, but we need to have something here that terminates execution in order to compile. 38 | // The only alternative is to make this getter throwing, but that seems nasty since it will impact every access in user code to a wrapped property. 39 | fatalError("Unexpected empty storage") 40 | 41 | case .value(let value): 42 | return value 43 | } 44 | } 45 | 46 | set { 47 | self._value = .value(newValue) 48 | } 49 | } 50 | } 51 | 52 | /// Storage instance with reference semantics to allow mutating the wrapped value without mutating this struct itself 53 | private let storage: Storage = .init() 54 | 55 | 56 | /** 57 | Initialize this property wrapper with a default value. 58 | 59 | This initializer will be called when the wrapper is declared like: 60 | ```swift 61 | @CodingValue var example: String = "example" 62 | ``` 63 | 64 | - Parameter wrappedValue: value being wrapped by this instance 65 | */ 66 | public init(wrappedValue: Value) { 67 | self.storage.value = wrappedValue 68 | } 69 | 70 | /** 71 | Initialize this property wrapper with no default value. 72 | 73 | This initializer will be called when the wrapper is declared like: 74 | ```swift 75 | @CodingValue var example: String 76 | ``` 77 | 78 | After invoking this initializer, the property wrapped will be in an uninitialized state and any attempts to read the wrapped value until it's set elsewhere will result in a `fatalError`. 79 | */ 80 | public init() {} 81 | 82 | /** 83 | Underlying value contained by this wrapper 84 | 85 | Note that if the wrapped value is attempted to be read before initialized with an actual value, a `fatalError` will result. 86 | In normal usage, this should not occur, since one of the following scenarios should always occur: 87 | - The wrapped property has a default value, and we never have an uninitialized state 88 | - The wrapped property is optional and implicitly given a default of `nil` 89 | - The wrapped property is successfully decoded from the serialized representation 90 | - The wrapped property is not successfully decoded from the serialized representation, but an error is thrown by the decoding container at that point so this value can never have an access attempt anyway 91 | 92 | This architecture is inspired by [the equivalent handling in `ArgumentParser`](https://github.com/apple/swift-argument-parser/blob/df9ee6676cd5b3bf5b330ec7568a5644f547201b/Sources/ArgumentParser/Parsable%20Properties/Option.swift#L76-L87), which deals with the same set of problems. 93 | */ 94 | public var wrappedValue: Value { 95 | get { self.storage.value } 96 | 97 | // By declaring this `nonmutating`, we can use normal variable set syntax without needing a `WritableKeyPath`, since the compiler knows this doesn't mutate the actual struct. 98 | nonmutating set { 99 | self.storage.value = newValue 100 | } 101 | } 102 | } 103 | 104 | 105 | /** 106 | Simple stub protocol to allow conditional extensions to check if a given value is `Optional` 107 | 108 | This can be used, for instance, to replicate implicit-default-`nil` behavior for a property wrapper that wraps an optional, which would otherwise be lost. 109 | 110 | Not intended for public use (hence the underscore prefix), but must be `public` since other public types use it in public extensions. 111 | */ 112 | public protocol _OptionalValue: ExpressibleByNilLiteral {} 113 | 114 | /* 115 | Add an empty protocol to `Optional`, to allow us to add conditional extensions when the wrapped value is optional. 116 | We don't want to just check for `ExpressibleByNilLiteral` conformance, since true optionals are the only type that gets an implicit default of `nil` (which is the behavior we're actually trying to replicate). 117 | */ 118 | extension Optional: _OptionalValue {} 119 | 120 | /* 121 | If the value being wrapped is optional, set the value to `nil` upon initialization. 122 | This replicates the normal behavior that optional values implicitly get `nil` as a default, and cuts off further cases where the `fatalError` in getting the wrapped value could be triggered. 123 | */ 124 | extension DeepCodingValue where Value: _OptionalValue { 125 | public init() { 126 | self.storage.value = nil 127 | } 128 | } 129 | 130 | 131 | // Pass through `Decodable` behavior and conformance from the wrapped type. 132 | extension DeepCodingValue: Decodable where Value: Decodable { 133 | public init(from decoder: Decoder) throws { 134 | self.wrappedValue = try .init(from: decoder) 135 | } 136 | } 137 | 138 | // Pass through `Encodable` behavior and conformance from the wrapped type. 139 | extension DeepCodingValue: Encodable where Value: Encodable { 140 | public func encode(to encoder: Encoder) throws { 141 | return try self.wrappedValue.encode(to: encoder) 142 | } 143 | } 144 | 145 | 146 | public extension _DeepCodingTreeDefiner { 147 | /** 148 | Shortcut type alias to allow `Value` to be used in the property wrapper instead of the full type name 149 | 150 | This also has the advantage of scoping this short type name to within the conforming type, rather than polluting the global namespace. 151 | */ 152 | typealias Value = DeepCodingValue 153 | } 154 | -------------------------------------------------------------------------------- /Tests/DeepCodableTests/BareEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DeepCodable 4 | 5 | 6 | /// Test encoding types containing only "bare" values (no `@Value` wrappers). 7 | final class BareEncodingTests: XCTestCase { 8 | /// Helper function to decode from a string to a type instance 9 | func decode(_ type: Type.Type, from json: String) throws -> Type { 10 | return try JSONDecoder().decode(type.self, from: json.data(using: .utf8)!) 11 | } 12 | 13 | /// Helper function to encode a type instance to a string 14 | func encode(_ instance: Type) throws -> String { 15 | return String(data: try JSONEncoder().encode(instance), encoding: .utf8)! 16 | } 17 | 18 | 19 | /// Test that encoding a simple one-level JSON body encodes the correct value. 20 | func testTopLevelEncoding() throws { 21 | struct TopLevelEncoding: DeepEncodable { 22 | static let codingTree = CodingTree { 23 | Key("top", containing: \.key) 24 | } 25 | 26 | let key: String 27 | } 28 | 29 | let encoded = try encode(TopLevelEncoding(key: "topValue")) 30 | 31 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 32 | let dict = try decode([String: String].self, from: encoded) 33 | XCTAssertEqual("topValue", dict["top"]) 34 | } 35 | 36 | /// Test that encoding a simple one-level JSON body with two keys encodes the correct values. 37 | func testBranchedTopLevelEncoding() throws { 38 | struct BranchedTopLevelEncoding: DeepEncodable { 39 | static let codingTree = CodingTree { 40 | Key("top1", containing: \.key1) 41 | Key("top2", containing: \.key2) 42 | } 43 | 44 | let key1: String 45 | let key2: String 46 | } 47 | 48 | let encoded = try encode(BranchedTopLevelEncoding(key1: "top1Value", key2: "top2Value")) 49 | 50 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 51 | let dict = try decode([String: String].self, from: encoded) 52 | XCTAssertEqual("top1Value", dict["top1"]) 53 | XCTAssertEqual("top2Value", dict["top2"]) 54 | } 55 | 56 | 57 | /// Test that encoding a two-level JSON body encodes the correct value. 58 | func testSecondLevelEncoding() throws { 59 | struct SecondLevelEncoding: DeepEncodable { 60 | static let codingTree = CodingTree { 61 | Key("top") { 62 | Key("second", containing: \.key) 63 | } 64 | } 65 | 66 | let key: String 67 | } 68 | 69 | let encoded = try encode(SecondLevelEncoding(key: "secondValue")) 70 | 71 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 72 | let dict = try decode([String: [String: String]].self, from: encoded) 73 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 74 | } 75 | 76 | 77 | /// Test that encoding a two-level JSON body with two keys in different branches encodes the correct values. 78 | func testBranchedSecondLevelEncoding() throws { 79 | struct BranchedSecondLevelEncoding: DeepEncodable { 80 | static let codingTree = CodingTree { 81 | Key("top1") { 82 | Key("second1", containing: \.key1) 83 | } 84 | 85 | Key("top2") { 86 | Key("second2", containing: \.key2) 87 | } 88 | } 89 | 90 | 91 | let key1: String 92 | let key2: String 93 | } 94 | 95 | let encoded = try encode(BranchedSecondLevelEncoding(key1: "second1Value", key2: "second2Value")) 96 | 97 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 98 | let dict = try decode([String: [String: String]].self, from: encoded) 99 | XCTAssertEqual("second1Value", dict["top1"]?["second1"]) 100 | XCTAssertEqual("second2Value", dict["top2"]?["second2"]) 101 | } 102 | 103 | 104 | /// Test that encoding a deep ten-level JSON body encodes the correct value. 105 | func testExcessivelyDeepEncoding() throws { 106 | struct ExcessivelyDeepEncoding: DeepEncodable { 107 | static let codingTree = CodingTree { 108 | Key("top") { 109 | Key("second") { 110 | Key("third") { 111 | Key("fourth") { 112 | Key("fifth") { 113 | Key("sixth") { 114 | Key("seventh") { 115 | Key("eighth") { 116 | Key("ninth") { 117 | Key("tenth", containing: \.key) 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | let key: String 130 | } 131 | 132 | let encoded = try encode(ExcessivelyDeepEncoding(key: "tenthValue")) 133 | 134 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 135 | let dict = try decode( 136 | [ 137 | String: [ 138 | String: [ 139 | String: [ 140 | String: [ 141 | String: [ 142 | String: [ 143 | String: [ 144 | String: [ 145 | String: [ 146 | String: String 147 | ] 148 | ] 149 | ] 150 | ] 151 | ] 152 | ] 153 | ] 154 | ] 155 | ] 156 | ].self, 157 | from: encoded 158 | ) 159 | XCTAssertEqual("tenthValue", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth"]?["seventh"]?["eighth"]?["ninth"]?["tenth"]) 160 | } 161 | 162 | /// Test that encoding a deep ten-level JSON body using a flattened coding tree encodes the correct value. 163 | func testFlattenedExcessivelyDeepEncoding() throws { 164 | struct FlattenedExcessivelyDeepEncoding: DeepEncodable { 165 | static let codingTree = CodingTree { 166 | Key("top", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", containing: \.key) 167 | } 168 | 169 | let key: String 170 | } 171 | 172 | let encoded = try encode(FlattenedExcessivelyDeepEncoding(key: "tenthValue")) 173 | 174 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 175 | let dict = try decode( 176 | [ 177 | String: [ 178 | String: [ 179 | String: [ 180 | String: [ 181 | String: [ 182 | String: [ 183 | String: [ 184 | String: [ 185 | String: [ 186 | String: String 187 | ] 188 | ] 189 | ] 190 | ] 191 | ] 192 | ] 193 | ] 194 | ] 195 | ] 196 | ].self, 197 | from: encoded 198 | ) 199 | XCTAssertEqual("tenthValue", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth"]?["seventh"]?["eighth"]?["ninth"]?["tenth"]) 200 | } 201 | 202 | /// Test that encoding a deep ten-level JSON body with two keys in different branches encodes the correct values. 203 | func testBranchedExcessivelyDeepEncoding() throws { 204 | struct BranchedExcessivelyDeepEncoding: DeepEncodable { 205 | static let codingTree = CodingTree { 206 | Key("top") { 207 | Key("second") { 208 | Key("third") { 209 | Key("fourth") { 210 | Key("fifth") { 211 | Key("sixth1") { 212 | Key("seventh1") { 213 | Key("eighth1") { 214 | Key("ninth1") { 215 | Key("tenth1", containing: \.key1) 216 | } 217 | } 218 | } 219 | } 220 | 221 | Key("sixth2") { 222 | Key("seventh2") { 223 | Key("eighth2") { 224 | Key("ninth2") { 225 | Key("tenth2", containing: \.key2) 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | } 236 | 237 | let key1: String 238 | let key2: String 239 | } 240 | 241 | let encoded = try encode(BranchedExcessivelyDeepEncoding(key1: "tenth1Value", key2: "tenth2Value")) 242 | 243 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 244 | let dict = try decode( 245 | [ 246 | String: [ 247 | String: [ 248 | String: [ 249 | String: [ 250 | String: [ 251 | String: [ 252 | String: [ 253 | String: [ 254 | String: [ 255 | String: String 256 | ] 257 | ] 258 | ] 259 | ] 260 | ] 261 | ] 262 | ] 263 | ] 264 | ] 265 | ].self, 266 | from: encoded 267 | ) 268 | XCTAssertEqual("tenth1Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth1"]?["seventh1"]?["eighth1"]?["ninth1"]?["tenth1"]) 269 | XCTAssertEqual("tenth2Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth2"]?["seventh2"]?["eighth2"]?["ninth2"]?["tenth2"]) 270 | } 271 | 272 | /// Test that encoding a deep ten-level JSON body with two keys in different branches using a flattened coding tree encodes the correct values. 273 | func testFlattenedBranchedExcessivelyDeepEncoding() throws { 274 | struct FlattenedBranchedExcessivelyDeepEncoding: DeepEncodable { 275 | static let codingTree = CodingTree { 276 | Key("top", "second", "third", "fourth", "fifth") { 277 | Key("sixth1", "seventh1", "eighth1", "ninth1", "tenth1", containing: \.key1) 278 | Key("sixth2", "seventh2", "eighth2", "ninth2", "tenth2", containing: \.key2) 279 | } 280 | } 281 | 282 | let key1: String 283 | let key2: String 284 | } 285 | 286 | let encoded = try encode(FlattenedBranchedExcessivelyDeepEncoding(key1: "tenth1Value", key2: "tenth2Value")) 287 | 288 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 289 | let dict = try decode( 290 | [ 291 | String: [ 292 | String: [ 293 | String: [ 294 | String: [ 295 | String: [ 296 | String: [ 297 | String: [ 298 | String: [ 299 | String: [ 300 | String: String 301 | ] 302 | ] 303 | ] 304 | ] 305 | ] 306 | ] 307 | ] 308 | ] 309 | ] 310 | ].self, 311 | from: encoded 312 | ) 313 | XCTAssertEqual("tenth1Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth1"]?["seventh1"]?["eighth1"]?["ninth1"]?["tenth1"]) 314 | XCTAssertEqual("tenth2Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth2"]?["seventh2"]?["eighth2"]?["ninth2"]?["tenth2"]) 315 | } 316 | 317 | 318 | /// Test that optionals encode correctly when provided an actual value. 319 | func testOptionalEncodingToValue() throws { 320 | struct OptionalEncodingToValue: DeepEncodable { 321 | static let codingTree = CodingTree { 322 | Key("top") { 323 | Key("second", containing: \.key) 324 | } 325 | } 326 | 327 | let key: String? 328 | } 329 | 330 | let encoded = try encode(OptionalEncodingToValue(key: "secondValue")) 331 | 332 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 333 | let dict = try decode([String: [String: String]].self, from: encoded) 334 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 335 | } 336 | 337 | /// Test that optionals encode correctly when not provided an actual value. 338 | func testOptionalEncodingToNil() throws { 339 | struct OptionalEncodingToNil: DeepEncodable { 340 | static let codingTree = CodingTree { 341 | Key("top") { 342 | Key("second", containing: \.key) 343 | } 344 | } 345 | 346 | let key: String? 347 | } 348 | 349 | let expected = "{}" 350 | let actual = try encode(OptionalEncodingToNil(key: nil)) 351 | XCTAssertEqual(expected, actual) 352 | } 353 | 354 | 355 | /// Test that optionals encode correctly using a flattened coding tree when provided an actual value. 356 | func testFlattenedOptionalEncodingToValue() throws { 357 | struct FlattenedOptionalEncodingToValue: DeepEncodable { 358 | static let codingTree = CodingTree { 359 | Key("top", "second", containing: \.key) 360 | } 361 | 362 | let key: String? 363 | } 364 | 365 | let encoded = try encode(FlattenedOptionalEncodingToValue(key: "secondValue")) 366 | 367 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 368 | let dict = try decode([String: [String: String]].self, from: encoded) 369 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 370 | } 371 | 372 | /// Test that optionals encode correctly using a flattened coding tree when not provided an actual value. 373 | func testFlattenedOptionalEncodingToNil() throws { 374 | struct FlattenedOptionalEncodingToNil: DeepEncodable { 375 | static let codingTree = CodingTree { 376 | Key("top", "second", containing: \.key) 377 | } 378 | 379 | let key: String? 380 | } 381 | 382 | let expected = "{}" 383 | let actual = try encode(FlattenedOptionalEncodingToNil(key: nil)) 384 | XCTAssertEqual(expected, actual) 385 | } 386 | 387 | 388 | /// Test that integers encode correctly. 389 | func testIntEncoding() throws { 390 | struct IntEncoding: DeepEncodable { 391 | static let codingTree = CodingTree { 392 | Key("top") { 393 | Key("second", containing: \.key) 394 | } 395 | } 396 | 397 | let key: Int 398 | } 399 | 400 | let encoded = try encode(IntEncoding(key: 17)) 401 | 402 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 403 | let dict = try decode([String: [String: Int]].self, from: encoded) 404 | XCTAssertEqual(17, dict["top"]?["second"]) 405 | } 406 | 407 | /// Test that arrays encode correctly. 408 | func testArrayEncoding() throws { 409 | struct ArrayEncoding: DeepEncodable { 410 | static let codingTree = CodingTree { 411 | Key("top") { 412 | Key("second", containing: \.key) 413 | } 414 | } 415 | 416 | let key: [String] 417 | } 418 | 419 | let encoded = try encode(ArrayEncoding(key: ["secondValue1", "secondValue2"])) 420 | 421 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 422 | let dict = try decode([String: [String: [String]]].self, from: encoded) 423 | XCTAssertEqual(["secondValue1", "secondValue2"], dict["top"]?["second"]) 424 | } 425 | 426 | /// Test that an arbitrary `Codable` struct encodes correctly. 427 | func testStructEncoding() throws { 428 | struct StructEncoding: DeepEncodable { 429 | static let codingTree = CodingTree { 430 | Key("top") { 431 | Key("second", containing: \.key) 432 | } 433 | } 434 | 435 | 436 | struct InternalStruct: Codable { 437 | let key: String 438 | } 439 | 440 | let key: InternalStruct 441 | 442 | init(key: InternalStruct) { 443 | self.key = key 444 | } 445 | } 446 | 447 | 448 | let encoded = try encode(StructEncoding(key: .init(key: "nestedValue"))) 449 | 450 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 451 | let dict = try decode([String: [String: StructEncoding.InternalStruct]].self, from: encoded) 452 | XCTAssertEqual("nestedValue", dict["top"]?["second"]?.key) 453 | } 454 | 455 | 456 | /// Test that normal `Decodable` behavior is unchanged while deep encoding behaves as expected. 457 | func testDecodableNonInterference() throws { 458 | struct DeepEncodableDecoding: DeepEncodable, Decodable { 459 | static let codingTree = CodingTree { 460 | Key("top") { 461 | Key("second") { 462 | Key("third", containing: \.key) 463 | } 464 | } 465 | } 466 | 467 | let key: String 468 | } 469 | 470 | 471 | let encoded = try encode(DeepEncodableDecoding(key: "thirdValue")) 472 | 473 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 474 | let dict = try decode([String: [String: [String: String]]].self, from: encoded) 475 | XCTAssertEqual("thirdValue", dict["top"]?["second"]?["third"]) 476 | 477 | 478 | let json = """ 479 | { 480 | "key": "thirdValue" 481 | } 482 | """ 483 | let decoded = try decode(DeepEncodableDecoding.self, from: json) 484 | 485 | XCTAssertEqual("thirdValue", decoded.key) 486 | } 487 | 488 | 489 | /// Test that providing all `nil` values to a struct with only optional properties will result in an empty encoding 490 | func testOptionalValuesEncodeEmpty() throws { 491 | struct OptionalValuesEncodeEmpty: DeepEncodable { 492 | static let codingTree = CodingTree { 493 | Key("top") { 494 | Key("second1") { 495 | Key("third1", containing: \.key1) 496 | } 497 | 498 | Key("second2") { 499 | Key("third2") { 500 | Key("fourth2", containing: \.key2) 501 | } 502 | } 503 | } 504 | } 505 | 506 | let key1: String? = nil 507 | let key2: String? = nil 508 | } 509 | 510 | 511 | let expected = "{}" 512 | let actual = try encode(OptionalValuesEncodeEmpty()) 513 | XCTAssertEqual(expected, actual) 514 | } 515 | 516 | 517 | /** 518 | Test that optionals encode correctly when the same type is encoded as `nil`, then with some value. 519 | 520 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 521 | */ 522 | func testOptionalEncodingSameTypeNilThenValue() throws { 523 | struct OptionalEncodingSameTypeNilThenValue: DeepEncodable { 524 | static let codingTree = CodingTree { 525 | Key("top") { 526 | Key("second", containing: \.key) 527 | } 528 | } 529 | 530 | let key: String? 531 | } 532 | 533 | 534 | let encoded1 = try encode(OptionalEncodingSameTypeNilThenValue(key: nil)) 535 | XCTAssertEqual("{}", encoded1) 536 | 537 | 538 | let encoded2 = try encode(OptionalEncodingSameTypeNilThenValue(key: "secondValue")) 539 | 540 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 541 | let dict = try decode([String: [String: String]].self, from: encoded2) 542 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 543 | } 544 | 545 | /** 546 | Test that optionals encode correctly when the same type is encoded with some value, then as `nil`. 547 | 548 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 549 | */ 550 | func testOptionalEncodingSameTypeValueThenNil() throws { 551 | struct OptionalEncodingSameTypeValueThenNil: DeepEncodable { 552 | static let codingTree = CodingTree { 553 | Key("top") { 554 | Key("second", containing: \.key) 555 | } 556 | } 557 | 558 | let key: String? 559 | } 560 | 561 | 562 | let encoded1 = try encode(OptionalEncodingSameTypeValueThenNil(key: "secondValue")) 563 | 564 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 565 | let dict = try decode([String: [String: String]].self, from: encoded1) 566 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 567 | 568 | 569 | let encoded2 = try encode(OptionalEncodingSameTypeValueThenNil(key: nil)) 570 | XCTAssertEqual("{}", encoded2) 571 | } 572 | 573 | 574 | /** 575 | Test that optionals encode correctly when the same mutable instance is encoded as `nil`, then with some value. 576 | 577 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 578 | */ 579 | func testOptionalEncodingSameInstanceNilThenValue() throws { 580 | struct OptionalEncodingSameInstanceNilThenValue: DeepEncodable { 581 | static let codingTree = CodingTree { 582 | Key("top") { 583 | Key("second", containing: \.key) 584 | } 585 | } 586 | 587 | var key: String? 588 | } 589 | 590 | 591 | var instance = OptionalEncodingSameInstanceNilThenValue(key: nil) 592 | 593 | let encoded1 = try encode(instance) 594 | XCTAssertEqual("{}", encoded1) 595 | 596 | 597 | instance.key = "secondValue" 598 | 599 | let encoded2 = try encode(instance) 600 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 601 | let dict = try decode([String: [String: String]].self, from: encoded2) 602 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 603 | } 604 | 605 | /** 606 | Test that optionals encode correctly when the same mutable instance is encoded with some value, then as `nil`. 607 | 608 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 609 | */ 610 | func testOptionalEncodingSameInstanceValueThenNil() throws { 611 | struct OptionalEncodingSameInstanceValueThenNil: DeepEncodable { 612 | static let codingTree = CodingTree { 613 | Key("top") { 614 | Key("second", containing: \.key) 615 | } 616 | } 617 | 618 | var key: String? 619 | } 620 | 621 | 622 | var instance = OptionalEncodingSameInstanceValueThenNil(key: "secondValue") 623 | 624 | let encoded1 = try encode(instance) 625 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 626 | let dict = try decode([String: [String: String]].self, from: encoded1) 627 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 628 | 629 | instance.key = nil 630 | 631 | let encoded2 = try encode(instance) 632 | XCTAssertEqual("{}", encoded2) 633 | } 634 | } 635 | -------------------------------------------------------------------------------- /Tests/DeepCodableTests/DecodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DeepCodable 4 | 5 | 6 | /// Test decoding deeply nested serialized representations. 7 | final class DecodingTests: XCTestCase { 8 | /// Helper function to decode from a string to a type instance 9 | func decode(_ type: Type.Type, from json: String) throws -> Type { 10 | return try JSONDecoder().decode(type.self, from: json.data(using: .utf8)!) 11 | } 12 | 13 | /// Helper function to encode a type instance to a string 14 | func encode(_ instance: Type) throws -> String { 15 | return String(data: try JSONEncoder().encode(instance), encoding: .utf8)! 16 | } 17 | 18 | 19 | /// Test that decoding a simple one-level JSON body decodes the correct value. 20 | func testTopLevelDecoding() throws { 21 | struct TopLevelDecoding: DeepDecodable { 22 | static let codingTree = CodingTree { 23 | Key("top", containing: \._key) 24 | } 25 | 26 | @Value var key: String 27 | } 28 | 29 | let json = """ 30 | { 31 | "top": "topValue" 32 | } 33 | """ 34 | let decoded = try decode(TopLevelDecoding.self, from: json) 35 | 36 | XCTAssertEqual("topValue", decoded.key) 37 | } 38 | 39 | /// Test that decoding a simple one-level JSON body with two keys decodes the correct values. 40 | func testBranchedTopLevelDecoding() throws { 41 | struct BranchedTopLevelDecoding: DeepDecodable { 42 | static let codingTree = CodingTree { 43 | Key("top1", containing: \._key1) 44 | Key("top2", containing: \._key2) 45 | } 46 | 47 | @Value var key1: String 48 | @Value var key2: String 49 | } 50 | 51 | let json = """ 52 | { 53 | "top1": "top1Value", 54 | "top2": "top2Value" 55 | } 56 | """ 57 | let decoded = try decode(BranchedTopLevelDecoding.self, from: json) 58 | 59 | XCTAssertEqual("top1Value", decoded.key1) 60 | XCTAssertEqual("top2Value", decoded.key2) 61 | } 62 | 63 | 64 | /// Test that decoding a two-level JSON body decodes the correct value. 65 | func testSecondLevelDecoding() throws { 66 | struct SecondLevelDecoding: DeepDecodable { 67 | static let codingTree = CodingTree { 68 | Key("top") { 69 | Key("second", containing: \._key) 70 | } 71 | } 72 | 73 | @Value var key: String 74 | } 75 | 76 | let json = """ 77 | { 78 | "top": { 79 | "second": "secondValue" 80 | } 81 | } 82 | """ 83 | let decoded = try decode(SecondLevelDecoding.self, from: json) 84 | 85 | XCTAssertEqual("secondValue", decoded.key) 86 | } 87 | 88 | /// Test that decoding a two-level JSON body with two keys in different branches decodes the correct values. 89 | func testBranchedSecondLevelDecoding() throws { 90 | struct BranchedSecondLevelDecoding: DeepDecodable { 91 | static let codingTree = CodingTree { 92 | Key("top1") { 93 | Key("second1", containing: \._key1) 94 | } 95 | 96 | Key("top2") { 97 | Key("second2", containing: \._key2) 98 | } 99 | } 100 | 101 | @Value var key1: String 102 | @Value var key2: String 103 | } 104 | 105 | let json = """ 106 | { 107 | "top1": { 108 | "second1": "second1Value" 109 | }, 110 | "top2": { 111 | "second2": "second2Value" 112 | } 113 | } 114 | """ 115 | let decoded = try decode(BranchedSecondLevelDecoding.self, from: json) 116 | 117 | XCTAssertEqual("second1Value", decoded.key1) 118 | XCTAssertEqual("second2Value", decoded.key2) 119 | } 120 | 121 | 122 | /// Test that decoding a deep ten-level JSON body decodes the correct value. 123 | func testExcessivelyDeepDecoding() throws { 124 | struct ExcessivelyDeepDecoding: DeepDecodable { 125 | static let codingTree = CodingTree { 126 | Key("top") { 127 | Key("second") { 128 | Key("third") { 129 | Key("fourth") { 130 | Key("fifth") { 131 | Key("sixth") { 132 | Key("seventh") { 133 | Key("eighth") { 134 | Key("ninth") { 135 | Key("tenth", containing: \._key) 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | @Value var key: String 148 | } 149 | 150 | let json = """ 151 | { 152 | "top": { 153 | "second": { 154 | "third": { 155 | "fourth": { 156 | "fifth": { 157 | "sixth": { 158 | "seventh": { 159 | "eighth": { 160 | "ninth": { 161 | "tenth": "tenthValue" 162 | } 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | } 172 | """ 173 | 174 | let decoded = try decode(ExcessivelyDeepDecoding.self, from: json) 175 | 176 | XCTAssertEqual("tenthValue", decoded.key) 177 | } 178 | 179 | /// Test that decoding a deep ten-level JSON body using a flattened coding tree decodes the correct value. 180 | func testFlattenedExcessivelyDeepDecoding() throws { 181 | struct FlattenedExcessivelyDeepDecoding: DeepDecodable { 182 | static let codingTree = CodingTree { 183 | Key("top", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", containing: \._key) 184 | } 185 | 186 | @Value var key: String 187 | } 188 | 189 | let json = """ 190 | { 191 | "top": { 192 | "second": { 193 | "third": { 194 | "fourth": { 195 | "fifth": { 196 | "sixth": { 197 | "seventh": { 198 | "eighth": { 199 | "ninth": { 200 | "tenth": "tenthValue" 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | """ 212 | 213 | let decoded = try decode(FlattenedExcessivelyDeepDecoding.self, from: json) 214 | 215 | XCTAssertEqual("tenthValue", decoded.key) 216 | } 217 | 218 | /// Test that decoding a deep ten-level JSON body with two keys in different branches decodes the correct values. 219 | func testBranchedExcessivelyDeepDecoding() throws { 220 | struct BranchedExcessivelyDeepDecoding: DeepDecodable { 221 | static let codingTree = CodingTree { 222 | Key("top") { 223 | Key("second") { 224 | Key("third") { 225 | Key("fourth") { 226 | Key("fifth") { 227 | Key("sixth1") { 228 | Key("seventh1") { 229 | Key("eighth1") { 230 | Key("ninth1") { 231 | Key("tenth1", containing: \._key1) 232 | } 233 | } 234 | } 235 | } 236 | 237 | Key("sixth2") { 238 | Key("seventh2") { 239 | Key("eighth2") { 240 | Key("ninth2", containing: \._key2) 241 | } 242 | } 243 | } 244 | } 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | @Value var key1: String 252 | @Value var key2: String 253 | } 254 | 255 | let json = """ 256 | { 257 | "top": { 258 | "second": { 259 | "third": { 260 | "fourth": { 261 | "fifth": { 262 | "sixth1": { 263 | "seventh1": { 264 | "eighth1": { 265 | "ninth1": { 266 | "tenth1": "tenth1Value" 267 | } 268 | } 269 | } 270 | }, 271 | "sixth2": { 272 | "seventh2": { 273 | "eighth2": { 274 | "ninth2": "ninth2Value" 275 | } 276 | } 277 | } 278 | } 279 | } 280 | } 281 | } 282 | } 283 | } 284 | """ 285 | 286 | let decoded = try decode(BranchedExcessivelyDeepDecoding.self, from: json) 287 | 288 | XCTAssertEqual("tenth1Value", decoded.key1) 289 | XCTAssertEqual("ninth2Value", decoded.key2) 290 | } 291 | 292 | /// Test that decoding a deep ten-level JSON body with two keys in different branches using a flattened coding tree decodes the correct values. 293 | func testFlattenedBranchedExcessivelyDeepDecoding() throws { 294 | struct FlattenedBranchedExcessivelyDeepDecoding: DeepDecodable { 295 | static let codingTree = CodingTree { 296 | Key("top", "second", "third", "fourth", "fifth") { 297 | Key("sixth1", "seventh1", "eighth1", "ninth1", "tenth1", containing: \._key1) 298 | Key("sixth2", "seventh2", "eighth2", "ninth2", containing: \._key2) 299 | } 300 | } 301 | 302 | @Value var key1: String 303 | @Value var key2: String 304 | } 305 | 306 | let json = """ 307 | { 308 | "top": { 309 | "second": { 310 | "third": { 311 | "fourth": { 312 | "fifth": { 313 | "sixth1": { 314 | "seventh1": { 315 | "eighth1": { 316 | "ninth1": { 317 | "tenth1": "tenth1Value" 318 | } 319 | } 320 | } 321 | }, 322 | "sixth2": { 323 | "seventh2": { 324 | "eighth2": { 325 | "ninth2": "ninth2Value" 326 | } 327 | } 328 | } 329 | } 330 | } 331 | } 332 | } 333 | } 334 | } 335 | """ 336 | 337 | let decoded = try decode(FlattenedBranchedExcessivelyDeepDecoding.self, from: json) 338 | 339 | XCTAssertEqual("tenth1Value", decoded.key1) 340 | XCTAssertEqual("ninth2Value", decoded.key2) 341 | } 342 | 343 | 344 | /// Test that optionals decode correctly when provided an actual value. 345 | func testOptionalDecodingToValue() throws { 346 | struct OptionalDecodingToValue: DeepDecodable { 347 | static let codingTree = CodingTree { 348 | Key("top") { 349 | Key("second", containing: \._key) 350 | } 351 | } 352 | 353 | @Value var key: String? 354 | } 355 | 356 | let json = """ 357 | { 358 | "top": { 359 | "second": "secondValue" 360 | } 361 | } 362 | """ 363 | let decoded = try decode(OptionalDecodingToValue.self, from: json) 364 | 365 | XCTAssertEqual("secondValue", decoded.key) 366 | } 367 | 368 | /// Test that optionals decode correctly when not provided an actual value. 369 | func testOptionalDecodingToNil() throws { 370 | struct OptionalDecodingToNil: DeepDecodable { 371 | static let codingTree = CodingTree { 372 | Key("top") { 373 | Key("second", containing: \._key) 374 | } 375 | } 376 | 377 | @Value var key: String? 378 | } 379 | 380 | let json = """ 381 | {} 382 | """ 383 | let decoded = try decode(OptionalDecodingToNil.self, from: json) 384 | 385 | XCTAssertEqual(nil, decoded.key) 386 | } 387 | 388 | 389 | /// Test that optionals decode correctly using a flattened coding tree when provided an actual value. 390 | func testFlattenedOptionalDecodingToValue() throws { 391 | struct FlattenedOptionalDecodingToValue: DeepDecodable { 392 | static let codingTree = CodingTree { 393 | Key("top", "second", containing: \._key) 394 | } 395 | 396 | @Value var key: String? 397 | } 398 | 399 | let json = """ 400 | { 401 | "top": { 402 | "second": "secondValue" 403 | } 404 | } 405 | """ 406 | let decoded = try decode(FlattenedOptionalDecodingToValue.self, from: json) 407 | 408 | XCTAssertEqual("secondValue", decoded.key) 409 | } 410 | 411 | /// Test that optionals decode correctly using a flattened coding tree when not provided an actual value. 412 | func testFlattenedOptionalDecodingToNil() throws { 413 | struct FlattenedOptionalDecodingToNil: DeepDecodable { 414 | static let codingTree = CodingTree { 415 | Key("top", "second", containing: \._key) 416 | } 417 | 418 | @Value var key: String? 419 | } 420 | 421 | let json = """ 422 | {} 423 | """ 424 | let decoded = try decode(FlattenedOptionalDecodingToNil.self, from: json) 425 | 426 | XCTAssertEqual(nil, decoded.key) 427 | } 428 | 429 | 430 | /// Test that integers decode correctly. 431 | func testIntDecoding() throws { 432 | struct IntDecoding: DeepDecodable { 433 | static let codingTree = CodingTree { 434 | Key("top") { 435 | Key("second", containing: \._key) 436 | } 437 | } 438 | 439 | @Value var key: Int 440 | } 441 | 442 | let json = """ 443 | { 444 | "top": { 445 | "second": 17 446 | } 447 | } 448 | """ 449 | let decoded = try decode(IntDecoding.self, from: json) 450 | 451 | XCTAssertEqual(17, decoded.key) 452 | } 453 | 454 | /// Test that arrays decode correctly. 455 | func testArrayDecoding() throws { 456 | struct ArrayDecoding: DeepDecodable { 457 | static let codingTree = CodingTree { 458 | Key("top") { 459 | Key("second", containing: \._key) 460 | } 461 | } 462 | 463 | @Value var key: [String] 464 | } 465 | 466 | let json = """ 467 | { 468 | "top": { 469 | "second": [ 470 | "secondValue1", 471 | "secondValue2" 472 | ] 473 | } 474 | } 475 | """ 476 | let decoded = try decode(ArrayDecoding.self, from: json) 477 | 478 | XCTAssertEqual(["secondValue1", "secondValue2"], decoded.key) 479 | } 480 | 481 | /// Test that an arbitrary `Decodable` struct decodes correctly. 482 | func testStructDecoding() throws { 483 | struct StructDecoding: DeepDecodable { 484 | static let codingTree = CodingTree { 485 | Key("top") { 486 | Key("second", containing: \._key) 487 | } 488 | } 489 | 490 | 491 | struct InternalStruct: Decodable { 492 | let key: String 493 | } 494 | 495 | @Value var key: InternalStruct 496 | } 497 | 498 | let json = """ 499 | { 500 | "top": { 501 | "second": { 502 | "key": "nestedValue" 503 | } 504 | } 505 | } 506 | """ 507 | let decoded = try decode(StructDecoding.self, from: json) 508 | 509 | XCTAssertEqual("nestedValue", decoded.key.key) 510 | } 511 | 512 | 513 | /// Test that normal `Encodable` behavior is unchanged while deep decoding behaves as expected. 514 | func testEncodableNonInterference() throws { 515 | struct DeepDecodableEncoding: DeepDecodable, Encodable { 516 | static let codingTree = CodingTree { 517 | Key("top") { 518 | Key("second") { 519 | Key("third", containing: \._key) 520 | } 521 | } 522 | } 523 | 524 | @Value var key: String 525 | } 526 | 527 | let json = """ 528 | { 529 | "top": { 530 | "second": { 531 | "third": "thirdValue" 532 | } 533 | } 534 | } 535 | """ 536 | let decoded = try decode(DeepDecodableEncoding.self, from: json) 537 | 538 | XCTAssertEqual("thirdValue", decoded.key) 539 | 540 | 541 | let expected = """ 542 | {"key":"thirdValue"} 543 | """ 544 | let actual = try encode(decoded) 545 | XCTAssertEqual(expected, actual) 546 | } 547 | 548 | 549 | /// Test that omitting a node in the serialized representation (that should have decoded into a non-optional value) throws. 550 | func testMissingNodeThrows() throws { 551 | struct MissingNodeThrows: DeepDecodable { 552 | static let codingTree = CodingTree { 553 | Key("top") { 554 | Key("second") { 555 | Key("third", containing: \._key) 556 | } 557 | } 558 | } 559 | 560 | @Value var key: String 561 | } 562 | 563 | let json = """ 564 | { 565 | "top": { 566 | "second": "secondValue" 567 | } 568 | } 569 | """ 570 | XCTAssertThrowsError(dump(try decode(MissingNodeThrows.self, from: json))) 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /Tests/DeepCodableTests/RealWorldDecodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DeepCodable 4 | 5 | 6 | /// Test some examples pulled from real-world usage, to better catch interesting corner-cases than contrived examples. 7 | final class RealWorldDecodingTests: XCTestCase { 8 | /// Helper function to decode from a string to a type instance 9 | func decode(_ type: Type.Type, from json: String) throws -> Type { 10 | return try JSONDecoder().decode(type.self, from: json.data(using: .utf8)!) 11 | } 12 | 13 | 14 | /// Test that decoding an example response from the GitHub GraphQL API produces the expected results. 15 | func testGithubGraphqlResponse() throws { 16 | struct GithubGraphqlResponse: DeepDecodable { 17 | static let codingTree = CodingTree { 18 | Key("data") { 19 | Key("node") { 20 | Key("content") { 21 | Key("__typename", containing: \._type) 22 | Key("title", containing: \._title) 23 | } 24 | 25 | Key("fieldValues") { 26 | Key("nodes", containing: \._nodes) 27 | } 28 | } 29 | } 30 | } 31 | 32 | 33 | enum TypeName: String, Decodable { 34 | case example = "Example type" 35 | } 36 | 37 | struct Node: DeepDecodable { 38 | static let codingTree = CodingTree { 39 | Key("name", containing: \._name) 40 | 41 | Key("field") { 42 | Key("name", containing: \._fieldName) 43 | } 44 | } 45 | 46 | 47 | @Value var name: String? 48 | @Value var fieldName: String? 49 | } 50 | 51 | @Value var title: String 52 | @Value var type: TypeName 53 | @Value var nodes: [Node] 54 | } 55 | 56 | let json = """ 57 | { 58 | "data": { 59 | "node": { 60 | "content": { 61 | "__typename": "Example type", 62 | "title": "Example title" 63 | }, 64 | "fieldValues": { 65 | "nodes": [ 66 | {}, 67 | {}, 68 | { 69 | "name": "Example node name", 70 | "field": { 71 | "name": "Example field name" 72 | } 73 | } 74 | ] 75 | } 76 | } 77 | } 78 | } 79 | """ 80 | 81 | let decoded = try decode(GithubGraphqlResponse.self, from: json) 82 | 83 | XCTAssertEqual("Example title", decoded.title) 84 | XCTAssertEqual(.example, decoded.type) 85 | 86 | XCTAssertTrue(decoded.nodes.indices.contains(0)) 87 | let emptyNode1 = decoded.nodes[0] 88 | XCTAssertNil(emptyNode1.name) 89 | XCTAssertNil(emptyNode1.fieldName) 90 | 91 | XCTAssertTrue(decoded.nodes.indices.contains(1)) 92 | let emptyNode2 = decoded.nodes[1] 93 | XCTAssertNil(emptyNode2.name) 94 | XCTAssertNil(emptyNode2.fieldName) 95 | 96 | XCTAssertTrue(decoded.nodes.indices.contains(2)) 97 | let validNode = decoded.nodes[2] 98 | XCTAssertEqual("Example node name", validNode.name) 99 | XCTAssertEqual("Example field name", validNode.fieldName) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/DeepCodableTests/WrappedEncodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DeepCodable 4 | 5 | 6 | /// Test encoding types containing only values wrapped in `@Value` property wrappers. 7 | final class WrappedEncodingTests: XCTestCase { 8 | /// Helper function to decode from a string to a type instance 9 | func decode(_ type: Type.Type, from json: String) throws -> Type { 10 | return try JSONDecoder().decode(type.self, from: json.data(using: .utf8)!) 11 | } 12 | 13 | /// Helper function to encode a type instance to a string 14 | func encode(_ instance: Type) throws -> String { 15 | return String(data: try JSONEncoder().encode(instance), encoding: .utf8)! 16 | } 17 | 18 | 19 | /// Test that encoding a simple one-level JSON body encodes the correct value. 20 | func testTopLevelEncoding() throws { 21 | struct TopLevelEncoding: DeepEncodable { 22 | static let codingTree = CodingTree { 23 | Key("top", containing: \._key) 24 | } 25 | 26 | 27 | @Value var key: String 28 | 29 | init(key: String) { 30 | self.key = key 31 | } 32 | } 33 | 34 | let encoded = try encode(TopLevelEncoding(key: "topValue")) 35 | 36 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 37 | let dict = try decode([String: String].self, from: encoded) 38 | XCTAssertEqual("topValue", dict["top"]) 39 | } 40 | 41 | /// Test that encoding a simple one-level JSON body with two keys encodes the correct values. 42 | func testBranchedTopLevelEncoding() throws { 43 | struct BranchedTopLevelEncoding: DeepEncodable { 44 | static let codingTree = CodingTree { 45 | Key("top1", containing: \._key1) 46 | Key("top2", containing: \._key2) 47 | } 48 | 49 | 50 | @Value var key1: String 51 | @Value var key2: String 52 | 53 | init(key1: String, key2: String) { 54 | self.key1 = key1 55 | self.key2 = key2 56 | } 57 | } 58 | 59 | let encoded = try encode(BranchedTopLevelEncoding(key1: "top1Value", key2: "top2Value")) 60 | 61 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 62 | let dict = try decode([String: String].self, from: encoded) 63 | XCTAssertEqual("top1Value", dict["top1"]) 64 | XCTAssertEqual("top2Value", dict["top2"]) 65 | } 66 | 67 | 68 | /// Test that encoding a two-level JSON body encodes the correct value. 69 | func testSecondLevelEncoding() throws { 70 | struct SecondLevelEncoding: DeepEncodable { 71 | static let codingTree = CodingTree { 72 | Key("top") { 73 | Key("second", containing: \._key) 74 | } 75 | } 76 | 77 | 78 | @Value var key: String 79 | 80 | init(key: String) { 81 | self.key = key 82 | } 83 | } 84 | 85 | let encoded = try encode(SecondLevelEncoding(key: "secondValue")) 86 | 87 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 88 | let dict = try decode([String: [String: String]].self, from: encoded) 89 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 90 | } 91 | 92 | 93 | /// Test that encoding a two-level JSON body with two keys in different branches encodes the correct values. 94 | func testBranchedSecondLevelEncoding() throws { 95 | struct BranchedSecondLevelEncoding: DeepEncodable { 96 | static let codingTree = CodingTree { 97 | Key("top1") { 98 | Key("second1", containing: \._key1) 99 | } 100 | 101 | Key("top2") { 102 | Key("second2", containing: \._key2) 103 | } 104 | } 105 | 106 | 107 | @Value var key1: String 108 | @Value var key2: String 109 | 110 | init(key1: String, key2: String) { 111 | self.key1 = key1 112 | self.key2 = key2 113 | } 114 | } 115 | 116 | let encoded = try encode(BranchedSecondLevelEncoding(key1: "second1Value", key2: "second2Value")) 117 | 118 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 119 | let dict = try decode([String: [String: String]].self, from: encoded) 120 | XCTAssertEqual("second1Value", dict["top1"]?["second1"]) 121 | XCTAssertEqual("second2Value", dict["top2"]?["second2"]) 122 | } 123 | 124 | 125 | /// Test that encoding a deep ten-level JSON body encodes the correct value. 126 | func testExcessivelyDeepEncoding() throws { 127 | struct ExcessivelyDeepEncoding: DeepEncodable { 128 | static let codingTree = CodingTree { 129 | Key("top") { 130 | Key("second") { 131 | Key("third") { 132 | Key("fourth") { 133 | Key("fifth") { 134 | Key("sixth") { 135 | Key("seventh") { 136 | Key("eighth") { 137 | Key("ninth") { 138 | Key("tenth", containing: \._key) 139 | } 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | 151 | @Value var key: String 152 | 153 | init(key: String) { 154 | self.key = key 155 | } 156 | } 157 | 158 | let encoded = try encode(ExcessivelyDeepEncoding(key: "tenthValue")) 159 | 160 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 161 | let dict = try decode( 162 | [ 163 | String: [ 164 | String: [ 165 | String: [ 166 | String: [ 167 | String: [ 168 | String: [ 169 | String: [ 170 | String: [ 171 | String: [ 172 | String: String 173 | ] 174 | ] 175 | ] 176 | ] 177 | ] 178 | ] 179 | ] 180 | ] 181 | ] 182 | ].self, 183 | from: encoded 184 | ) 185 | XCTAssertEqual("tenthValue", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth"]?["seventh"]?["eighth"]?["ninth"]?["tenth"]) 186 | } 187 | 188 | /// Test that encoding a deep ten-level JSON body using a flattened coding tree encodes the correct value. 189 | func testFlattenedExcessivelyDeepEncoding() throws { 190 | struct FlattenedExcessivelyDeepEncoding: DeepEncodable { 191 | static let codingTree = CodingTree { 192 | Key("top", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", containing: \._key) 193 | } 194 | 195 | 196 | @Value var key: String 197 | 198 | init(key: String) { 199 | self.key = key 200 | } 201 | } 202 | 203 | let encoded = try encode(FlattenedExcessivelyDeepEncoding(key: "tenthValue")) 204 | 205 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 206 | let dict = try decode( 207 | [ 208 | String: [ 209 | String: [ 210 | String: [ 211 | String: [ 212 | String: [ 213 | String: [ 214 | String: [ 215 | String: [ 216 | String: [ 217 | String: String 218 | ] 219 | ] 220 | ] 221 | ] 222 | ] 223 | ] 224 | ] 225 | ] 226 | ] 227 | ].self, 228 | from: encoded 229 | ) 230 | XCTAssertEqual("tenthValue", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth"]?["seventh"]?["eighth"]?["ninth"]?["tenth"]) 231 | } 232 | 233 | /// Test that encoding a deep ten-level JSON body with two keys in different branches encodes the correct values. 234 | func testBranchedExcessivelyDeepEncoding() throws { 235 | struct BranchedExcessivelyDeepEncoding: DeepEncodable { 236 | static let codingTree = CodingTree { 237 | Key("top") { 238 | Key("second") { 239 | Key("third") { 240 | Key("fourth") { 241 | Key("fifth") { 242 | Key("sixth1") { 243 | Key("seventh1") { 244 | Key("eighth1") { 245 | Key("ninth1") { 246 | Key("tenth1", containing: \._key1) 247 | } 248 | } 249 | } 250 | } 251 | 252 | Key("sixth2") { 253 | Key("seventh2") { 254 | Key("eighth2") { 255 | Key("ninth2") { 256 | Key("tenth2", containing: \._key2) 257 | } 258 | } 259 | } 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | 268 | 269 | @Value var key1: String 270 | @Value var key2: String 271 | 272 | init(key1: String, key2: String) { 273 | self.key1 = key1 274 | self.key2 = key2 275 | } 276 | } 277 | 278 | let encoded = try encode(BranchedExcessivelyDeepEncoding(key1: "tenth1Value", key2: "tenth2Value")) 279 | 280 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 281 | let dict = try decode( 282 | [ 283 | String: [ 284 | String: [ 285 | String: [ 286 | String: [ 287 | String: [ 288 | String: [ 289 | String: [ 290 | String: [ 291 | String: [ 292 | String: String 293 | ] 294 | ] 295 | ] 296 | ] 297 | ] 298 | ] 299 | ] 300 | ] 301 | ] 302 | ].self, 303 | from: encoded 304 | ) 305 | XCTAssertEqual("tenth1Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth1"]?["seventh1"]?["eighth1"]?["ninth1"]?["tenth1"]) 306 | XCTAssertEqual("tenth2Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth2"]?["seventh2"]?["eighth2"]?["ninth2"]?["tenth2"]) 307 | } 308 | 309 | /// Test that encoding a deep ten-level JSON body with two keys in different branches using a flattened coding tree encodes the correct values. 310 | func testFlattenedBranchedExcessivelyDeepEncoding() throws { 311 | struct FlattenedBranchedExcessivelyDeepEncoding: DeepEncodable { 312 | static let codingTree = CodingTree { 313 | Key("top", "second", "third", "fourth", "fifth") { 314 | Key("sixth1", "seventh1", "eighth1", "ninth1", "tenth1", containing: \._key1) 315 | Key("sixth2", "seventh2", "eighth2", "ninth2", "tenth2", containing: \._key2) 316 | } 317 | } 318 | 319 | 320 | @Value var key1: String 321 | @Value var key2: String 322 | 323 | init(key1: String, key2: String) { 324 | self.key1 = key1 325 | self.key2 = key2 326 | } 327 | } 328 | 329 | let encoded = try encode(FlattenedBranchedExcessivelyDeepEncoding(key1: "tenth1Value", key2: "tenth2Value")) 330 | 331 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 332 | let dict = try decode( 333 | [ 334 | String: [ 335 | String: [ 336 | String: [ 337 | String: [ 338 | String: [ 339 | String: [ 340 | String: [ 341 | String: [ 342 | String: [ 343 | String: String 344 | ] 345 | ] 346 | ] 347 | ] 348 | ] 349 | ] 350 | ] 351 | ] 352 | ] 353 | ].self, 354 | from: encoded 355 | ) 356 | XCTAssertEqual("tenth1Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth1"]?["seventh1"]?["eighth1"]?["ninth1"]?["tenth1"]) 357 | XCTAssertEqual("tenth2Value", dict["top"]?["second"]?["third"]?["fourth"]?["fifth"]?["sixth2"]?["seventh2"]?["eighth2"]?["ninth2"]?["tenth2"]) 358 | } 359 | 360 | 361 | /// Test that optionals encode correctly when provided an actual value. 362 | func testOptionalEncodingToValue() throws { 363 | struct OptionalEncodingToValue: DeepEncodable { 364 | static let codingTree = CodingTree { 365 | Key("top") { 366 | Key("second", containing: \._key) 367 | } 368 | } 369 | 370 | 371 | @Value var key: String? 372 | 373 | init(key: String? = nil) { 374 | self.key = key 375 | } 376 | } 377 | 378 | 379 | let encoded = try encode(OptionalEncodingToValue(key: "secondValue")) 380 | 381 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 382 | let dict = try decode([String: [String: String]].self, from: encoded) 383 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 384 | } 385 | 386 | /// Test that optionals encode correctly using a flattened coding tree when provided an actual value. 387 | func testFlattenedOptionalEncodingToValue() throws { 388 | struct FlattenedOptionalEncodingToValue: DeepEncodable { 389 | static let codingTree = CodingTree { 390 | Key("top", "second", containing: \._key) 391 | } 392 | 393 | 394 | @Value var key: String? 395 | 396 | init(key: String? = nil) { 397 | self.key = key 398 | } 399 | } 400 | 401 | 402 | let encoded = try encode(FlattenedOptionalEncodingToValue(key: "secondValue")) 403 | 404 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 405 | let dict = try decode([String: [String: String]].self, from: encoded) 406 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 407 | } 408 | 409 | 410 | /// Test that optionals encode correctly when not provided an actual value. 411 | func testOptionalEncodingToNil() throws { 412 | struct OptionalEncodingToNil: DeepEncodable { 413 | static let codingTree = CodingTree { 414 | Key("top") { 415 | Key("second", containing: \._key) 416 | } 417 | } 418 | 419 | 420 | @Value var key: String? 421 | 422 | init(key: String? = nil) { 423 | self.key = key 424 | } 425 | } 426 | 427 | 428 | let expected = "{}" 429 | let actual = try encode(OptionalEncodingToNil(key: nil)) 430 | XCTAssertEqual(expected, actual) 431 | } 432 | 433 | /// Test that optionals encode correctly using a flattened coding tree when not provided an actual value. 434 | func testFlattenedOptionalEncodingToNil() throws { 435 | struct FlattenedOptionalEncodingToNil: DeepEncodable { 436 | static let codingTree = CodingTree { 437 | Key("top", "second", containing: \._key) 438 | } 439 | 440 | 441 | @Value var key: String? 442 | 443 | init(key: String? = nil) { 444 | self.key = key 445 | } 446 | } 447 | 448 | 449 | let expected = "{}" 450 | let actual = try encode(FlattenedOptionalEncodingToNil(key: nil)) 451 | XCTAssertEqual(expected, actual) 452 | } 453 | 454 | 455 | /// Test that integers encode correctly. 456 | func testIntEncoding() throws { 457 | struct IntEncoding: DeepEncodable { 458 | static let codingTree = CodingTree { 459 | Key("top") { 460 | Key("second", containing: \._key) 461 | } 462 | } 463 | 464 | 465 | @Value var key: Int 466 | 467 | init(key: Int) { 468 | self.key = key 469 | } 470 | } 471 | 472 | let encoded = try encode(IntEncoding(key: 17)) 473 | 474 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 475 | let dict = try decode([String: [String: Int]].self, from: encoded) 476 | XCTAssertEqual(17, dict["top"]?["second"]) 477 | } 478 | 479 | /// Test that arrays encode correctly. 480 | func testArrayEncoding() throws { 481 | struct ArrayEncoding: DeepEncodable { 482 | static let codingTree = CodingTree { 483 | Key("top") { 484 | Key("second", containing: \._key) 485 | } 486 | } 487 | 488 | 489 | @Value var key: [String] 490 | 491 | init(key: [String]) { 492 | self.key = key 493 | } 494 | } 495 | 496 | let encoded = try encode(ArrayEncoding(key: ["secondValue1", "secondValue2"])) 497 | 498 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 499 | let dict = try decode([String: [String: [String]]].self, from: encoded) 500 | XCTAssertEqual(["secondValue1", "secondValue2"], dict["top"]?["second"]) 501 | } 502 | 503 | /// Test that an arbitrary `Codable` struct encodes correctly. 504 | func testStructEncoding() throws { 505 | struct StructEncoding: DeepEncodable { 506 | static let codingTree = CodingTree { 507 | Key("top") { 508 | Key("second", containing: \._key) 509 | } 510 | } 511 | 512 | 513 | struct InternalStruct: Codable { 514 | let key: String 515 | } 516 | 517 | @Value var key: InternalStruct 518 | 519 | init(key: InternalStruct) { 520 | self.key = key 521 | } 522 | } 523 | 524 | 525 | let encoded = try encode(StructEncoding(key: .init(key: "nestedValue"))) 526 | 527 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 528 | let dict = try decode([String: [String: StructEncoding.InternalStruct]].self, from: encoded) 529 | XCTAssertEqual("nestedValue", dict["top"]?["second"]?.key) 530 | } 531 | 532 | 533 | /// Test that normal `Decodable` behavior is unchanged while deep encoding behaves as expected. 534 | func testDecodableNonInterference() throws { 535 | struct DeepEncodableDecoding: DeepEncodable, Decodable { 536 | static let codingTree = CodingTree { 537 | Key("top") { 538 | Key("second") { 539 | Key("third", containing: \._key) 540 | } 541 | } 542 | } 543 | 544 | @Value var key: String 545 | 546 | init(key: String) { 547 | self.key = key 548 | } 549 | } 550 | 551 | 552 | let encoded = try encode(DeepEncodableDecoding(key: "thirdValue")) 553 | 554 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 555 | let dict = try decode([String: [String: [String: String]]].self, from: encoded) 556 | XCTAssertEqual("thirdValue", dict["top"]?["second"]?["third"]) 557 | 558 | 559 | let json = """ 560 | { 561 | "key": "thirdValue" 562 | } 563 | """ 564 | let decoded = try decode(DeepEncodableDecoding.self, from: json) 565 | 566 | XCTAssertEqual("thirdValue", decoded.key) 567 | } 568 | 569 | 570 | /// Test that providing all `nil` values to a struct with only optional properties will result in an empty encoding. 571 | func testOptionalValuesEncodeEmpty() throws { 572 | struct OptionalValuesEncodeEmpty: DeepEncodable { 573 | static let codingTree = CodingTree { 574 | Key("top") { 575 | Key("second1") { 576 | Key("third1", containing: \._key1) 577 | } 578 | 579 | Key("second2") { 580 | Key("third2") { 581 | Key("fourth2", containing: \._key2) 582 | } 583 | } 584 | } 585 | } 586 | 587 | @Value var key1: String? 588 | @Value var key2: String? 589 | } 590 | 591 | 592 | let expected = "{}" 593 | let actual = try encode(OptionalValuesEncodeEmpty()) 594 | XCTAssertEqual(expected, actual) 595 | } 596 | 597 | 598 | /** 599 | Test that optionals encode correctly when the same type is encoded as `nil`, then with some value. 600 | 601 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 602 | */ 603 | func testOptionalEncodingSameTypeNilThenValue() throws { 604 | struct OptionalEncodingSameTypeNilThenValue: DeepEncodable { 605 | static let codingTree = CodingTree { 606 | Key("top") { 607 | Key("second", containing: \._key) 608 | } 609 | } 610 | 611 | 612 | @Value var key: String? 613 | 614 | init(key: String? = nil) { 615 | self.key = key 616 | } 617 | } 618 | 619 | 620 | let encoded1 = try encode(OptionalEncodingSameTypeNilThenValue(key: nil)) 621 | XCTAssertEqual("{}", encoded1) 622 | 623 | 624 | let encoded2 = try encode(OptionalEncodingSameTypeNilThenValue(key: "secondValue")) 625 | 626 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 627 | let dict = try decode([String: [String: String]].self, from: encoded2) 628 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 629 | } 630 | 631 | /** 632 | Test that optionals encode correctly when the same type is encoded with some value, then as `nil`. 633 | 634 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 635 | */ 636 | func testOptionalEncodingSameTypeValueThenNil() throws { 637 | struct OptionalEncodingSameTypeValueThenNil: DeepEncodable { 638 | static let codingTree = CodingTree { 639 | Key("top") { 640 | Key("second", containing: \._key) 641 | } 642 | } 643 | 644 | 645 | @Value var key: String? 646 | 647 | init(key: String? = nil) { 648 | self.key = key 649 | } 650 | } 651 | 652 | 653 | let encoded1 = try encode(OptionalEncodingSameTypeValueThenNil(key: "secondValue")) 654 | 655 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 656 | let dict = try decode([String: [String: String]].self, from: encoded1) 657 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 658 | 659 | 660 | let encoded2 = try encode(OptionalEncodingSameTypeValueThenNil(key: nil)) 661 | XCTAssertEqual("{}", encoded2) 662 | } 663 | 664 | 665 | /** 666 | Test that optionals encode correctly when the same mutable instance is encoded as `nil`, then with some value. 667 | 668 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 669 | */ 670 | func testOptionalEncodingSameInstanceNilThenValue() throws { 671 | struct OptionalEncodingSameInstanceNilThenValue: DeepEncodable { 672 | static let codingTree = CodingTree { 673 | Key("top") { 674 | Key("second", containing: \._key) 675 | } 676 | } 677 | 678 | 679 | @Value var key: String? 680 | 681 | init(key: String? = nil) { 682 | self.key = key 683 | } 684 | } 685 | 686 | 687 | let instance = OptionalEncodingSameInstanceNilThenValue(key: nil) 688 | 689 | let encoded1 = try encode(instance) 690 | XCTAssertEqual("{}", encoded1) 691 | 692 | 693 | instance.key = "secondValue" 694 | 695 | let encoded2 = try encode(instance) 696 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 697 | let dict = try decode([String: [String: String]].self, from: encoded2) 698 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 699 | } 700 | 701 | /** 702 | Test that optionals encode correctly when the same mutable instance is encoded with some value, then as `nil`. 703 | 704 | It's been removed now, but during development there was some caching added to recursing operations in encoding that was accidentally retained across encodings and resulted in values being omitted on the next encode, so this is here to make sure that doesn't happen again. 705 | */ 706 | func testOptionalEncodingSameInstanceValueThenNil() throws { 707 | struct OptionalEncodingSameInstanceValueThenNil: DeepEncodable { 708 | static let codingTree = CodingTree { 709 | Key("top") { 710 | Key("second", containing: \._key) 711 | } 712 | } 713 | 714 | 715 | @Value var key: String? 716 | 717 | init(key: String? = nil) { 718 | self.key = key 719 | } 720 | } 721 | 722 | 723 | let instance = OptionalEncodingSameInstanceValueThenNil(key: "secondValue") 724 | 725 | let encoded1 = try encode(instance) 726 | // We can't compare JSON strings here since key ordering is non-deterministic, so decode to a `Dictionary` instead. 727 | let dict = try decode([String: [String: String]].self, from: encoded1) 728 | XCTAssertEqual("secondValue", dict["top"]?["second"]) 729 | 730 | instance.key = nil 731 | 732 | let encoded2 = try encode(instance) 733 | XCTAssertEqual("{}", encoded2) 734 | } 735 | } 736 | --------------------------------------------------------------------------------