Last active
September 3, 2021 13:25
-
-
Save IanKeen/a2f461d975152ab234de14e7d0ca19aa to your computer and use it in GitHub Desktop.
PropertyWrapper: Dealing with heterogenous arrays (non-property wrapper approach as well)
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
struct Foo: Codable { | |
let type: String | |
let value: String | |
} | |
struct Bar: Codable { | |
let type: String | |
let value: Int | |
} | |
protocol Item { } | |
extension Foo: Item { } | |
extension Bar: Item { } | |
enum Items: CodableElementSet { | |
static var decoders: [DecodingRoutine<Item>] { | |
return [ | |
.item(Foo.init, when: "type", equals: "foo"), | |
.item(Bar.init, when: "type", equals: "bar") | |
] | |
} | |
static var encoders: [EncodingRoutine<Item>] { | |
return [ | |
.item(Foo.self), | |
.item(Bar.self) | |
] | |
} | |
} | |
struct Model: Codable { | |
@ManyOf<Items> var items: [Item] | |
} | |
let model = Model(items: [ | |
Foo(type: "foo", value: "hello world"), | |
Bar(type: "bar", value: 42) | |
]) | |
let data = try! JSONEncoder().encode(model) | |
print(String(data: data, encoding: .utf8)!) // {"items":[{"type":"foo","value":"hello world"},{"type":"bar","value":42}]} | |
let decoded = try! JSONDecoder().decode(Model.self, from: data) | |
print(decoded) // Model(items: [Foo(type: "foo", value: "hello world"), Bar(type: "foo", value: 42)]) |
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
struct Foo: Codable { let value: String } | |
enum Multi { | |
case foo(Foo) | |
case bar(Int) | |
} | |
extension Multi: Decodable { | |
init(from decoder: Decoder) throws { | |
self = try decoder.decodeFirst(from: [ // these routines will be attempted in order | |
.case(Multi.bar), | |
.case(Multi.foo, when: "type", equals: "foo"), | |
.default(.bar(100)) // used if all the above fail | |
]) | |
} | |
} | |
extension Multi: Encodable { | |
func encode(to encoder: Encoder) throws { | |
switch self { | |
case .foo(let value): | |
var container = encoder.container(keyedBy: AnyCodingKey.self) | |
try container.encode("foo", forKey: "type") | |
try value.encode(to: encoder) | |
case .bar(let value): | |
try value.encode(to: encoder) | |
} | |
} | |
} | |
let items: [Multi] = [.foo(.init(value: "hello world")), .bar(42)] | |
let data = try! JSONEncoder().encode(items) | |
print(String(data: data, encoding: .utf8)!) // [{"foo":"type","value":{"value":"hello world"}},42] | |
let decoded = try! JSONDecoder().decode([Multi].self, from: data) | |
print(decoded) // [Multi.foo(Foo(value: "hello world")), Multi.bar(42)] |
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 | |
public struct AnyOf<T: CodableElementSet>: Codable { | |
public var wrappedValue: T.Element | |
public init(wrappedValue: T.Element) { | |
self.wrappedValue = wrappedValue | |
} | |
public init(from decoder: Decoder) throws { | |
self.wrappedValue = try decoder.decodeFirst(from: T.decoders) | |
} | |
public func encode(to encoder: Encoder) throws { | |
try encoder.encode(wrappedValue, from: T.encoders) | |
} | |
} | |
extension KeyedDecodingContainer { | |
public func decode<T>(_ type: AnyOf<Optional<T>>.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> AnyOf<Optional<T>> { | |
do { | |
if (try? decodeNil(forKey: key)) == true { | |
return AnyOf<Optional<T>>(wrappedValue: nil) | |
} else { | |
let unwrapped = try decode(AnyOf<T>.self, forKey: key) | |
return AnyOf<Optional<T>>(wrappedValue: unwrapped.wrappedValue) | |
} | |
} catch DecodingError.keyNotFound { | |
return AnyOf<Optional<T>>(wrappedValue: nil) | |
} catch let error { | |
throw error | |
} | |
} | |
} | |
extension KeyedEncodingContainer { | |
public mutating func encode<T>(_ value: AnyOf<Optional<T>>, forKey key: KeyedEncodingContainer<K>.Key) throws { | |
guard let inner = value.wrappedValue else { return } | |
let unwrapped = AnyOf<T>(wrappedValue: inner) | |
try encode(unwrapped, forKey: 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
public protocol CodableElementSet { | |
associatedtype Element | |
static var decoders: [DecodingRoutine<Element>] { get } | |
static var encoders: [EncodingRoutine<Element>] { get } | |
} | |
extension Optional: CodableElementSet where Wrapped: CodableElementSet { | |
public static var decoders: [DecodingRoutine<Optional<Wrapped.Element>>] { | |
return Wrapped.decoders.map({ $0.optional() }) + [.default(nil)] | |
} | |
public static var encoders: [EncodingRoutine<Optional<Wrapped.Element>>] { | |
return Wrapped.encoders.map({ $0.optional() }) + [.null()] | |
} | |
} |
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
enum DecodingRoutineError: Error { case noMatch } | |
public struct DecodingRoutine<T> { | |
public let decode: (Decoder) throws -> T | |
public init(decode: @escaping (Decoder) throws -> T) { | |
self.decode = decode | |
} | |
} | |
extension DecodingRoutine { | |
public func optional() -> DecodingRoutine<T?> { | |
return .init { try self.decode($0) } | |
} | |
} | |
extension DecodingRoutine { | |
public static func `default`(_ value: @autoclosure @escaping () throws -> T) -> DecodingRoutine { | |
return .init { _ in try value() } | |
} | |
} | |
extension DecodingRoutine { | |
public static func item(_ factory: @escaping (Decoder) throws -> T) -> DecodingRoutine { | |
return .init { decoder in | |
guard let value = try? factory(decoder) else { throw DecodingRoutineError.noMatch } | |
return value | |
} | |
} | |
} | |
extension DecodingRoutine { | |
public static func item<Key: Decodable & Equatable>(_ factory: @escaping (Decoder) throws -> T, when key: String, equals value: Key) -> DecodingRoutine { | |
return .init { decoder in | |
guard | |
let keyContainer = try? decoder.container(keyedBy: AnyCodingKey.self), | |
let key = try? keyContainer.decode(Key.self, forKey: AnyCodingKey(key)), | |
key == value | |
else { throw DecodingRoutineError.noMatch } | |
return try factory(decoder) | |
} | |
} | |
} | |
extension DecodingRoutine { | |
public static func `case`<Case: Decodable>(_ function: @escaping (Case) -> T) -> DecodingRoutine { | |
return .init { decoder in | |
guard let value = try? Case(from: decoder) else { throw DecodingRoutineError.noMatch } | |
return function(value) | |
} | |
} | |
} | |
extension DecodingRoutine { | |
public static func `case`<Key: Decodable & Equatable, Case: Decodable>(_ function: @escaping (Case) -> T, when key: String, equals value: Key) -> DecodingRoutine { | |
return .init { decoder in | |
guard | |
let keyContainer = try? decoder.container(keyedBy: AnyCodingKey.self), | |
let key = try? keyContainer.decode(Key.self, forKey: AnyCodingKey(key)), | |
key == value | |
else { throw DecodingRoutineError.noMatch } | |
return try function(Case(from: decoder)) | |
} | |
} | |
} | |
extension Decoder { | |
public func decodeFirst<T>(from routines: [DecodingRoutine<T>]) throws -> T { | |
for routine in routines { | |
do { | |
return try routine.decode(self) | |
} catch DecodingRoutineError.noMatch { | |
continue | |
} catch let error { | |
throw error | |
} | |
} | |
throw DecodingError.typeMismatch(T.self, .init(codingPath: codingPath, debugDescription: "None of the provided options were able to decode the data")) | |
} | |
} |
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
enum EncodingRoutineError: Error { case noMatch } | |
public struct EncodingRoutine<T> { | |
public let encode: (T, Encoder) throws -> Void | |
public init(encode: @escaping (T, Encoder) throws -> Void) { | |
self.encode = encode | |
} | |
} | |
extension EncodingRoutine { | |
public func optional() -> EncodingRoutine<T?> { | |
return .init { value, encoder in | |
guard let value = value else { throw EncodingRoutineError.noMatch } | |
try self.encode(value, encoder) | |
} | |
} | |
} | |
extension EncodingRoutine { | |
public static func null() -> EncodingRoutine { | |
// this type can be anything.. we are just encoding nil | |
return .init { try Optional<Bool>.none.encode(to: $1) } | |
} | |
} | |
extension EncodingRoutine { | |
public static func item<U: Encodable>(_: U.Type) -> EncodingRoutine { | |
return .init { value, encoder in | |
guard let encodable = value as? U else { throw EncodingRoutineError.noMatch } | |
try encodable.encode(to: encoder) | |
} | |
} | |
} | |
extension Encoder { | |
public func encode<T>(_ value: T, from routines: [EncodingRoutine<T>]) throws { | |
for routine in routines { | |
do { | |
return try routine.encode(value, self) | |
} catch EncodingRoutineError.noMatch { | |
continue | |
} catch let error { | |
throw error | |
} | |
} | |
throw EncodingError.invalidValue(value, .init(codingPath: codingPath, debugDescription: "None of the provided options were able to encode the data")) | |
} | |
} |
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 | |
public struct ManyOf<T: CodableElementSet>: Codable { | |
public var wrappedValue: [T.Element] | |
public init(wrappedValue: [T.Element]) { | |
self.wrappedValue = wrappedValue | |
} | |
public init(from decoder: Decoder) throws { | |
var container = try decoder.unkeyedContainer() | |
var results: [T.Element] = [] | |
while !container.isAtEnd { | |
let nested = try container.superDecoder() | |
try results.append(nested.decodeFirst(from: T.decoders)) | |
} | |
self.wrappedValue = results | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.unkeyedContainer() | |
for element in wrappedValue { | |
let nested = container.superEncoder() | |
try nested.encode(element, from: T.encoders) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
AnyCodingKey
: https://gist.github.com/IanKeen/3d226854c8c59a17e151a0022b71f6bb