Last active
November 17, 2021 16:30
-
-
Save jayrhynas/1e24f435987cbc4ac773d2dcff109bf5 to your computer and use it in GitHub Desktop.
Enforce a constant value for decoded fields. Heavliy inspired by: https://gist.github.com/IanKeen/4f7717d7afd266137042b86983d04bc1
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 | |
struct Test: ValidatingDecodable, CustomStringConvertible, Encodable { | |
@ConstantDecodable var value = 10 | |
@VariableDecodable var string: String | |
var description: String { | |
#"Test(value: \#(value), string: "\#(string)")"# | |
} | |
} | |
do { | |
let json = """ | |
{ | |
"value": 10, | |
"string": "value" | |
} | |
""".data(using: .utf8)! | |
let t = try JSONDecoder().decode(Test.self, from: json) | |
print(t) | |
try print(String(data: JSONEncoder().encode(t), encoding: .utf8)!) | |
} catch { | |
print(error) | |
} |
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
protocol DecodableType { | |
func decode(container: KeyedDecodingContainer<AnyCodingKey>, key: String) throws | |
} | |
protocol ValidatingDecodable: Decodable { | |
init() | |
} | |
extension ValidatingDecodable { | |
init(from decoder: Decoder) throws { | |
self.init() | |
let container = try decoder.container(keyedBy: AnyCodingKey.self) | |
let mirror = Mirror(reflecting: self) | |
for (label, value) in mirror.children { | |
guard let label = label else { | |
throw DecodingError.keyNotFound(AnyCodingKey(""), .init(codingPath: container.codingPath, debugDescription: "Not a KeyedContainer")) | |
} | |
guard let property = value as? DecodableType else { | |
throw DecodingError.keyNotFound(AnyCodingKey(label), .init(codingPath: container.codingPath, debugDescription: "All keys must use a @ConstantDecodable or @VariableDecodable property wrapper")) | |
} | |
try property.decode(container: container, key: String(label.dropFirst())) | |
} | |
} | |
} | |
@propertyWrapper | |
struct ConstantDecodable<Value: Decodable>: Decodable, DecodableType where Value: Equatable { | |
private(set) var wrappedValue: Value | |
init(wrappedValue: Value) { self.wrappedValue = wrappedValue } | |
init(from decoder: Decoder) throws { | |
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath.dropLast(), debugDescription: "Type must conform to ValidatingDecodable", underlyingError: nil)) | |
} | |
func decode(container: KeyedDecodingContainer<AnyCodingKey>, key: String) throws { | |
let value = try container.decode(Value.self, forKey: AnyCodingKey(key)) | |
if value != self.wrappedValue { | |
throw DecodingError.dataCorruptedError(forKey: AnyCodingKey(key), in: container, debugDescription: "Invalid value: \(value) (expected: \(wrappedValue))") | |
} | |
} | |
} | |
extension ConstantDecodable: Encodable where Value: Encodable { | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(self.wrappedValue) | |
} | |
} | |
@propertyWrapper | |
final class VariableDecodable<Value: Decodable>: Decodable, DecodableType { | |
private var value: Value? | |
var wrappedValue: Value { | |
get { | |
guard let value = value else { fatalError("Unable to access value before it is set") } | |
return value | |
} | |
set { value = newValue } | |
} | |
init() {} | |
init(wrappedValue: Value) { self.value = wrappedValue } | |
init(from decoder: Decoder) throws { | |
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath.dropLast(), debugDescription: "Type must conform to ValidatingDecodable", underlyingError: nil)) | |
} | |
func decode(container: KeyedDecodingContainer<AnyCodingKey>, key: String) throws { | |
self.value = try container.decode(Value.self, forKey: AnyCodingKey(key)) | |
} | |
} | |
extension VariableDecodable: Encodable where Value: Encodable { | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(self.wrappedValue) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
AnyCodingKey
by @IanKeenhttps://gist.github.com/IanKeen/3d226854c8c59a17e151a0022b71f6bb