Last active
July 13, 2022 23:57
-
-
Save IanKeen/9e77041c386cd5ba0128330d6149c5a4 to your computer and use it in GitHub Desktop.
PropertyWrapper: NestedKey - deal with en/de-coding values within nested data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let json = """ | |
{ | |
"name": { | |
"first": "Ian", | |
"last": "Keen" | |
}, | |
"age": 42, | |
"settings": { | |
"permissions": { | |
"admin": true | |
} | |
} | |
} | |
""" | |
struct User: Codable { | |
private enum CodingKeys: String, NestedCodingKey { | |
case firstName = "name.first" | |
case lastName = "name.last" | |
case age | |
case isAdmin = "setting.permissions.admin" | |
} | |
@NestedKey var firstName: String | |
@NestedKey var lastName: String | |
var age: Int | |
@NestedKey var isAdmin: Bool | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public protocol NestedCodingKey: CodingKey { | |
var nestedKeys: [String] { get } | |
} | |
@propertyWrapper | |
public struct NestedKey<T: Codable>: Codable { | |
public var wrappedValue: T | |
public init(wrappedValue: T) { | |
self.wrappedValue = wrappedValue | |
} | |
public init(from decoder: Decoder) throws { | |
let codingKey = decoder.codingPath.last! | |
guard let key = codingKey as? NestedCodingKey else { | |
throw DecodingError.keyNotFound(codingKey, .init(codingPath: decoder.codingPath, debugDescription: "CodingKeys must conform to NestedCodingKey")) | |
} | |
let last = key.nestedKeys.last! | |
let container = try key.nestedKeys.dropFirst().dropLast().reduce(decoder.container(keyedBy: AnyCodingKey.self)) { container, key in | |
return try container.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(key)) | |
} | |
self.wrappedValue = try container.decode(T.self, forKey: AnyCodingKey(last)) | |
} | |
public func encode(to encoder: Encoder) throws { | |
let codingKey = encoder.codingPath.last! | |
guard let key = codingKey as? NestedCodingKey else { | |
throw EncodingError.invalidValue(codingKey, .init(codingPath: encoder.codingPath, debugDescription: "CodingKeys must conform to NestedCodingKey")) | |
} | |
let last = key.nestedKeys.last! | |
var container = encoder.container(keyedBy: AnyCodingKey.self) | |
for key in key.nestedKeys.dropFirst().dropLast() { | |
container = container.nestedContainer(keyedBy: AnyCodingKey.self, forKey: .init(key)) | |
} | |
try container.encode(wrappedValue, forKey: AnyCodingKey(last)) | |
} | |
} | |
extension NestedKey: Equatable where T: Equatable { } | |
extension KeyedDecodingContainer { | |
public func decode<T>(_ type: NestedKey<T?>.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> NestedKey<T?> { | |
return NestedKey<T?>(wrappedValue: try? decode(NestedKey<T>.self, forKey: key).wrappedValue) | |
} | |
} | |
extension NestedCodingKey where Self: RawRepresentable, RawValue == String { | |
public init?(stringValue: String) { self.init(rawValue: stringValue) } | |
public init?(intValue: Int) { fatalError() } | |
public var intValue: Int? { nil } | |
public var stringValue: String { nestedKeys.first! } | |
public var nestedKeys: [String] { rawValue.components(separatedBy: ".") } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
AnyCodingKey
: https://gist.github.com/IanKeen/3d226854c8c59a17e151a0022b71f6bbOriginal inspiration from @AliSoftware : https://gist.github.com/AliSoftware/89b275d7259d23ebf12d377b6ffe15cd