Created
December 10, 2019 09:55
-
-
Save ole/5c31ca9e2919c815029784ef3b8fdc0d to your computer and use it in GitHub Desktop.
A property wrapper for dictionaries with keys that are raw-representable as strings. It modifies the wrapped dictionary's encoding/decoding behavior such that the dictionary is encoded as a dictionary (unkeyed container) rather than as an array (keyed container). For context, see https://oleb.net/blog/2017/12/dictionary-codable-array/
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
import Foundation | |
// MARK: - KeyedContainer | |
/// A property wrapper for dictionaries with keys that are raw-representable as strings. | |
/// It modifies the wrapped dictionary's encoding/decoding behavior such that the dictionary | |
/// is encoded as a dictionary (unkeyed container) rather than as an array (keyed container). | |
/// | |
/// For context, see <https://oleb.net/blog/2017/12/dictionary-codable-array/>. | |
@propertyWrapper | |
struct KeyedContainer<Key, Value> where Key: Hashable & RawRepresentable, Key.RawValue == String { | |
var wrappedValue: [Key: Value] | |
/// Copied from the standard library (`_DictionaryCodingKey`). | |
private struct CodingKeys: CodingKey { | |
let stringValue: String | |
let intValue: Int? | |
init?(stringValue: String) { | |
self.stringValue = stringValue | |
self.intValue = Int(stringValue) | |
} | |
init?(intValue: Int) { | |
self.stringValue = "\(intValue)" | |
self.intValue = intValue | |
} | |
} | |
} | |
extension KeyedContainer: Equatable where Key: Equatable, Value: Equatable {} | |
extension KeyedContainer: Encodable where Key: Encodable, Value: Encodable { | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
for (key, value) in wrappedValue { | |
let codingKey = CodingKeys(stringValue: key.rawValue)! | |
try container.encode(value, forKey: codingKey) | |
} | |
} | |
} | |
extension KeyedContainer: Decodable where Key: Decodable, Value: Decodable { | |
init(from decoder: Decoder) throws { | |
wrappedValue = [:] | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
for key in container.allKeys { | |
let value = try container.decode(Value.self, forKey: key) | |
wrappedValue[Key(rawValue: key.stringValue)!] = value | |
} | |
} | |
} | |
// MARK: - Test | |
enum Color: String, Codable { | |
case red | |
case green | |
case blue | |
} | |
struct S: Equatable, Codable { | |
var unannotatedDict: [Color: String] = [ | |
.red: "ff0000", | |
.green: "00ff00", | |
.blue: "0000ff" | |
] | |
@KeyedContainer var annotatedDict: [Color: String] = [ | |
.red: "ff0000", | |
.green: "00ff00", | |
.blue: "0000ff" | |
] | |
} | |
let input = S() | |
let encoder = JSONEncoder() | |
encoder.outputFormatting = [.prettyPrinted, .sortedKeys] | |
let jsonData = try encoder.encode(input) | |
let jsonText = String(decoding: jsonData, as: UTF8.self) | |
print(jsonText) | |
let decoder = JSONDecoder() | |
let decoded = try decoder.decode(S.self, from: jsonData) | |
assert(decoded == input) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment