-
-
Save jayrhynas/e4ea2dc6cd6173b28f4c07d89afc4ee9 to your computer and use it in GitHub Desktop.
PropertyWrapper: CustomKeyCodable allows defining the keys for decoding _per property_
This file contains 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
protocol CustomKeyCodable: Codable { | |
static var keyEncodingStrategy: ([CodingKey]) -> CodingKey { get } | |
static var keyDecodingStrategy: ([CodingKey]) -> CodingKey { get } | |
init() | |
} | |
extension CustomKeyCodable { | |
init(from decoder: Decoder) throws { | |
self.init() | |
let container = try decoder.container(keyedBy: AnyCodingKey.self) | |
let mirror = Mirror(reflecting: self) | |
for (label, value) in mirror.children { | |
guard let label = label else { | |
throw DecodingError.keyNotFound(AnyCodingKey(""), .init(codingPath: container.codingPath, debugDescription: "Not an KeyedContainer")) | |
} | |
guard let key = value as? KeyType else { | |
throw DecodingError.keyNotFound(AnyCodingKey(label), .init(codingPath: container.codingPath, debugDescription: "All keys must use @Key property wrapper")) | |
} | |
var newKey: String { | |
let path = decoder.codingPath + [AnyCodingKey(String(label.dropFirst()))] | |
return Self.keyDecodingStrategy(path).stringValue | |
} | |
try key.decode(container: container, key: key.name ?? newKey) | |
} | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: AnyCodingKey.self) | |
let mirror = Mirror(reflecting: self) | |
for (label, value) in mirror.children { | |
guard let label = label else { | |
throw EncodingError.invalidValue(self, .init(codingPath: container.codingPath, debugDescription: "Not an KeyedContainer")) | |
} | |
guard let key = value as? KeyType else { | |
throw EncodingError.invalidValue(value, .init(codingPath: container.codingPath, debugDescription: "All keys must use @Key property wrapper")) | |
} | |
var newKey: String { | |
let path = encoder.codingPath + [AnyCodingKey(String(label.dropFirst()))] | |
return Self.keyEncodingStrategy(path).stringValue | |
} | |
try key.encode(container: &container, key: key.name ?? newKey) | |
} | |
} | |
} | |
private protocol KeyType { | |
var name: String? { get } | |
func decode(container: KeyedDecodingContainer<AnyCodingKey>, key: String) throws | |
func encode(container: inout KeyedEncodingContainer<AnyCodingKey>, key: String) throws | |
} | |
extension Key: KeyType {} | |
@propertyWrapper | |
public final class Key<T: Codable>: Codable { | |
public var name: String? | |
public var wrappedValue: T { | |
get { | |
guard let value = value else { fatalError("Unable to access value before it is set") } | |
return value | |
} | |
set { value = newValue } | |
} | |
private var value: T? | |
public init() { } | |
public init(_ name: String) { | |
self.name = name | |
} | |
public init(wrappedValue: T) { | |
self.value = wrappedValue | |
} | |
public init(wrappedValue: T, _ name: String) { | |
self.name = name | |
self.value = wrappedValue | |
} | |
func decode(container: KeyedDecodingContainer<AnyCodingKey>, key: String) throws { | |
self.name = key | |
self.value = try container.decode(T.self, forKey: AnyCodingKey(key)) | |
} | |
func encode(container: inout KeyedEncodingContainer<AnyCodingKey>, key: String) throws { | |
try container.encode(value, forKey: AnyCodingKey(key)) | |
} | |
} |
This file contains 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
// Note: All properties must be marked with `@Key` | |
// provide a value if you are specifying a key, otherwise you can omit it | |
extension String { | |
func toPascalCase() -> String { | |
guard let first = self.first else { return "" } | |
return first.uppercased() + self.dropFirst() | |
} | |
func fromPascalCase() -> String { | |
guard let first = self.first else { return "" } | |
return first.lowercased() + self.dropFirst() | |
} | |
} | |
struct User: CustomKeyCodable { | |
static let keyEncodingStrategy: ([CodingKey]) -> CodingKey = { path in | |
AnyCodingKey(path.last!.stringValue.fromPascalCase()) | |
} | |
static let keyDecodingStrategy: ([CodingKey]) -> CodingKey = { path in | |
AnyCodingKey(path.last!.stringValue.toPascalCase()) | |
} | |
@Key("first_name") var firstName: String | |
@Key("last_name") var lastName: String | |
@Key var age: Int | |
init() { } | |
} | |
let json = Data(#"{"first_name":"Ian", "last_name": "Keen", "Age":42}"#.utf8) | |
var model = try! JSONDecoder().decode(User.self, from: json) | |
print(model.firstName) // Ian | |
print(model.lastName) // Keen | |
print(model.age) // 42 | |
try print(String(data: JSONEncoder().encode(model), encoding: .utf8)!) | |
//{"Age":42,"first_name":"Ian","last_name":"Keen"} | |
model.lastName = "Smith" | |
model.age = 123 | |
try print(String(data: JSONEncoder().encode(model), encoding: .utf8)!) | |
//{"Age":123,"first_name":"Ian","last_name":"Smith"} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment