Last active
August 12, 2021 20:13
-
-
Save IanKeen/c70657e84bd3b8108455ffb18e6e6852 to your computer and use it in GitHub Desktop.
PropertyWrapper: Allow en/de-coding of String keys to non-String types
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
let json = """ | |
{ | |
"value": { | |
"true": "foo", | |
"false": "bar" | |
} | |
} | |
""" | |
struct Model: Codable { | |
@LosslessKey var value: [Bool: String] | |
} |
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
@propertyWrapper | |
struct LosslessKey<T: Hashable & LosslessStringConvertible, U> { | |
var wrappedValue: [T: U] | |
} | |
extension LosslessKey: Codable where T: Codable, U: Codable { | |
init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
var result: [T: U] = [:] | |
for (key, value) in try container.decode([String: U].self) { | |
guard let key = T.init(key) else { | |
throw DecodingError.typeMismatch(T.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode value for key '\(key)'")) | |
} | |
result[key] = value | |
} | |
self.wrappedValue = result | |
} | |
func encode(to encoder: Encoder) throws { | |
var dict: [String: U] = [:] | |
for (key, value) in wrappedValue { | |
dict[key.description] = value | |
} | |
try dict.encode(to: encoder) | |
} | |
} |
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
/* | |
This variant requires conforming the key type to a protocol in case you need something that can't (or shouldn't) be LosslessStringConvertible | |
**/ | |
protocol StringKeyCodable { | |
static func decode(from string: String, decoder: Decoder) -> Self? | |
func encode(encoder: Encoder) -> String | |
} | |
extension StringKeyCodable where Self: LosslessStringConvertible { | |
static func decode(from string: String, decoder: Decoder) -> Self? { .init(string) } | |
func encode(encoder: Encoder) -> String { description } | |
} | |
@propertyWrapper | |
struct StringKey<T: Hashable & StringKeyCodable, U> { | |
var wrappedValue: [T: U] | |
} | |
extension StringKey: Codable where T: Codable, U: Codable { | |
init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
var result: [T: U] = [:] | |
for (key, value) in try container.decode([String: U].self) { | |
guard let key = T.decode(from: key, decoder: decoder) else { | |
throw DecodingError.typeMismatch(T.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode value for key '\(key)'")) | |
} | |
result[key] = value | |
} | |
self.wrappedValue = result | |
} | |
func encode(to encoder: Encoder) throws { | |
var dict: [String: U] = [:] | |
for (key, value) in wrappedValue { | |
dict[key.encode(encoder: encoder)] = value | |
} | |
try dict.encode(to: encoder) | |
} | |
} | |
/// MARK: - Example | |
let json = """ | |
{ | |
"value": { | |
"1628798923.073723": "foo", | |
"1628798920": "bar" | |
} | |
} | |
""" | |
extension Date: StringKeyCodable { | |
static func decode(from string: String, decoder: Decoder) -> Date? { | |
TimeInterval(string).map(Date.init(timeIntervalSince1970:)) | |
} | |
func encode(encoder: Encoder) -> String { | |
"\(timeIntervalSince1970)" | |
} | |
} | |
struct Model: Codable { | |
@StringKey var value: [Date: String] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment