-
-
Save AliSoftware/89b275d7259d23ebf12d377b6ffe15cd to your computer and use it in GitHub Desktop.
| struct Contact: Decodable, CustomStringConvertible { | |
| var id: String | |
| @NestedKey | |
| var firstname: String | |
| @NestedKey | |
| var lastname: String | |
| @NestedKey | |
| var address: String | |
| enum CodingKeys: String, NestableCodingKey { | |
| case id | |
| case firstname = "nested/data/user/firstname" | |
| case lastname = "nested/data/user/lastname" | |
| case address = "nested/data/address" | |
| } | |
| var description: String { | |
| "Contact(firstname: \(firstname), lastname: \(lastname), address: \(address))" | |
| } | |
| } | |
| let json = """ | |
| [ | |
| { | |
| "id": "1", | |
| "nested": { "data": { | |
| "user": { "firstname": "Alice", "lastname": "Wonderland" }, | |
| "address": "Through the looking glass" | |
| } } | |
| }, | |
| { | |
| "id": "2", | |
| "nested": { "data": { | |
| "user": { "firstname": "Bob", "lastname": "Builder" }, | |
| "address": "1, NewRoad" | |
| } } | |
| } | |
| ] | |
| """.data(using: .utf8)! | |
| let decoder = JSONDecoder() | |
| let list = try decoder.decode([Contact].self, from: json) | |
| // [Contact(firstname: Alice, lastname: Wonderland, address: Through the looking glass), Contact(firstname: Bob, lastname: Builder, address: 1, NewRoad)] | |
| import Foundation | |
| //: # NestedKey | |
| /// | |
| /// Use this to annotate the properties that require a depth traversal during decoding. | |
| /// The corresponding `CodingKey` for this property must be a `NestableCodingKey` | |
| @propertyWrapper | |
| struct NestedKey<T: Decodable>: Decodable { | |
| var wrappedValue: T | |
| struct AnyCodingKey: CodingKey { | |
| let stringValue: String | |
| let intValue: Int? | |
| init(stringValue: String) { | |
| self.stringValue = stringValue | |
| self.intValue = nil | |
| } | |
| init?(intValue: Int) { | |
| self.stringValue = "\(intValue)" | |
| self.intValue = intValue | |
| } | |
| } | |
| init(from decoder: Decoder) throws { | |
| let key = decoder.codingPath.last! | |
| guard let nestedKey = key as? NestableCodingKey else { | |
| throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Key \(key) is not a NestableCodingKey")) | |
| } | |
| let nextKeys = nestedKey.path.dropFirst() | |
| // key descent | |
| let container = try decoder.container(keyedBy: AnyCodingKey.self) | |
| let lastLeaf = try nextKeys.indices.dropLast().reduce(container) { (nestedContainer, keyIdx) in | |
| do { | |
| return try nestedContainer.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue: nextKeys[keyIdx])) | |
| } catch DecodingError.keyNotFound(let key, let ctx) { | |
| try NestedKey.keyNotFound(key: key, ctx: ctx, container: container, nextKeys: nextKeys[..<keyIdx]) | |
| } | |
| } | |
| // key leaf | |
| do { | |
| self.wrappedValue = try lastLeaf.decode(T.self, forKey: AnyCodingKey(stringValue: nextKeys.last!)) | |
| } catch DecodingError.keyNotFound(let key, let ctx) { | |
| try NestedKey.keyNotFound(key: key, ctx: ctx, container: container, nextKeys: nextKeys.dropLast()) | |
| } | |
| } | |
| private static func keyNotFound<C: Collection>( | |
| key: CodingKey, ctx: DecodingError.Context, | |
| container: KeyedDecodingContainer<AnyCodingKey>, nextKeys: C) throws -> Never | |
| where C.Element == String | |
| { | |
| throw DecodingError.keyNotFound(key, DecodingError.Context( | |
| codingPath: container.codingPath + nextKeys.map(AnyCodingKey.init(stringValue:)), | |
| debugDescription: "NestedKey: No value associated with key \"\(key.stringValue)\"", | |
| underlyingError: ctx.underlyingError | |
| )) | |
| } | |
| } | |
| //: # NestableCodingKey | |
| /// Use this instead of `CodingKey` to annotate your `enum CodingKeys: String, NestableCodingKey`. | |
| /// Use a `/` to separate the components of the path to nested keys | |
| protocol NestableCodingKey: CodingKey { | |
| var path: [String] { get } | |
| } | |
| extension NestableCodingKey where Self: RawRepresentable, Self.RawValue == String { | |
| init?(stringValue: String) { | |
| self.init(rawValue: stringValue) | |
| } | |
| var stringValue: String { | |
| path.first! | |
| } | |
| init?(intValue: Int) { | |
| fatalError() | |
| } | |
| var intValue: Int? { nil } | |
| var path: [String] { | |
| self.rawValue.components(separatedBy: "/") | |
| } | |
| } |
would you consider creating a repo for this, so it can be wrapped in a community swift package? Would like to consume it in our enterprise open source offering.
@sstadelman would be nice, but for now this is just a gist I hacked in one evening, not battle tested. To make it into a proper OSS package that I'd be confident sharing it would first require some tests; and would require some maintenance time that I don't have anymore (I mean, I already have quite a lot of work waiting in my backlog to get back on SwiftGen an other existing repos, and making a package kind of entitles me to maintain it but if I don't have time better not make promise that official)
I'd love this idea to become part of an existing lib though which have maintainers and already a setup for unit tests and all. @marksands interested in including the idea behind that gist into BetterCodable?
@AliSoftware Absolutely! I was following along last night and didn't want to steal your thunder. I've been trying to crib your idea to cook up some other nice-to-haves, so I'll see what comes out of it.
👍
That is awesome! Just a quick style comment if I may, I think Data(str.utf8) is more elegant than str.data(using: .utf8)! because it avoids the force unwrap.
Otherwise I’ll probably use this truck in a project of my own 😊