-
-
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 😊