Skip to content

Instantly share code, notes, and snippets.

@maximkrouk
Created June 14, 2020 21:16
Show Gist options
  • Save maximkrouk/1f42b914879aa2d24af471dfc52c78cb to your computer and use it in GitHub Desktop.
Save maximkrouk/1f42b914879aa2d24af471dfc52c78cb to your computer and use it in GitHub Desktop.
// Based on https://ilya.puchka.me/decoding-nested-values-with-property-wrappers/
import Foundation
public struct CodingProxy: Codable, Equatable {
init() {}
public init(from decoder: Decoder) {}
public func encode(to encoder: Encoder) throws {}
public static func ==(lhs: Self, rhs: Self) -> Bool { true }
}
public protocol _NestedDecodable: Decodable {
associatedtype Value: Decodable
associatedtype NestedKeys: CodingKey & CaseIterable
init(wrappedValue: Value)
}
@propertyWrapper
public struct NestedDecodable<NestedKeys, Value>: _NestedDecodable
where Value: Decodable, NestedKeys: CodingKey & CaseIterable {
public var wrappedValue: Value
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
public protocol _NestedEncodable: Encodable {
associatedtype Value: Encodable
associatedtype NestedKeys: CodingKey & CaseIterable
var wrappedValue: Value { get }
}
@propertyWrapper
public struct NestedEncodable<NestedKeys, Value>: _NestedEncodable
where Value: Encodable, NestedKeys: CodingKey & CaseIterable {
public var wrappedValue: Value
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
@propertyWrapper
public struct NestedCodable<NestedKeys, Value>: _NestedDecodable, _NestedEncodable
where Value: Codable, NestedKeys: CodingKey & CaseIterable {
public var wrappedValue: Value
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
extension NestedDecodable: Equatable where Value: Equatable {}
extension NestedEncodable: Equatable where Value: Equatable {}
extension NestedCodable: Equatable where Value: Equatable {}
extension KeyedDecodingContainer {
public func decode(_: CodingProxy.Type, forKey key: Key) throws -> CodingProxy { CodingProxy() }
public func decode<T: _NestedDecodable>(_: T.Type, forKey key: Key) throws -> T {
guard T.NestedKeys.allCases.isEmpty == false else {
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)"))
}
let wrappedValue = try containerForNestedKey()
.decode(T.Value.self, forKey: T.NestedKeys.lastCase!)
return T(wrappedValue: wrappedValue)
}
public func decode<T: _NestedDecodable, Value>(_: T.Type, forKey key: Key)
throws -> T where T.Value == Value? {
guard T.NestedKeys.allCases.isEmpty == false else {
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)"))
}
let wrappedValue = try containerForNestedKey()
.decodeIfPresent(Value.self, forKey: T.NestedKeys.lastCase!)
return T(wrappedValue: wrappedValue)
}
private func containerForNestedKey<K: CodingKey & CaseIterable>()
throws -> KeyedDecodingContainer<K> {
guard let rootKey = Key(stringValue: K.allCases.first!.stringValue) else {
throw DecodingError.valueNotFound(Key.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No root key in \(Key.self) with string value `\(K.allCases.first!.stringValue)`"))
}
var container = try self.nestedContainer(keyedBy: K.self, forKey: rootKey)
if K.allCases.count > 1 {
try K.allCases.dropFirst().dropLast().forEach { key in
container = try container.nestedContainer(keyedBy: K.self, forKey: key)
}
}
return container
}
}
extension KeyedEncodingContainer {
public mutating func encode(_ value: CodingProxy, forKey key: Key) throws {}
public mutating func encode<T: _NestedEncodable>(_ value: T, forKey key: Key) throws {
guard T.NestedKeys.allCases.isEmpty == false else {
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)"))
}
var container: KeyedEncodingContainer<T.NestedKeys> = try containerForNestedKey()
try container.encode(value.wrappedValue, forKey: T.NestedKeys.lastCase!)
}
public mutating func encode<T: _NestedEncodable, Value>(_ value: T, forKey key: Key)
throws where T.Value == Value? {
guard T.NestedKeys.allCases.isEmpty == false else {
throw DecodingError.valueNotFound(T.NestedKeys.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No keys defined in \(K.self)"))
}
var container: KeyedEncodingContainer<T.NestedKeys> = try containerForNestedKey()
try container.encodeIfPresent(value.wrappedValue, forKey: T.NestedKeys.lastCase!)
}
private mutating func containerForNestedKey<K: CodingKey & CaseIterable>()
throws -> KeyedEncodingContainer<K> {
guard let rootKey = Key(stringValue: K.allCases.first!.stringValue) else {
throw DecodingError.valueNotFound(Key.self, DecodingError.Context(codingPath: codingPath, debugDescription: "No root key in \(Key.self) with string value `\(K.allCases.first!.stringValue)`"))
}
var container = self.nestedContainer(keyedBy: K.self, forKey: rootKey)
if K.allCases.count > 1 {
K.allCases.dropFirst().dropLast().forEach { (key) in
container = container.nestedContainer(keyedBy: K.self, forKey: key)
}
}
return container
}
}
extension CodingKey where Self: CaseIterable {
static var lastCase: Self? {
guard allCases.isEmpty == false else { return nil }
let lastIndex = allCases.index(allCases.endIndex, offsetBy: -1)
return allCases[lastIndex]
}
}
@maximkrouk
Copy link
Author

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