Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
PropertyWrapper: Dealing with heterogenous arrays (non-property wrapper approach as well)
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)])
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)]
@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)
}
}
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()]
}
}
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"))
}
}
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"))
}
}
@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)
}
}
}
@IanKeen
Copy link
Author

IanKeen commented Apr 18, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment