Skip to content

Instantly share code, notes, and snippets.

@jayrhynas
Last active November 17, 2021 16:30
Show Gist options
  • Save jayrhynas/1e24f435987cbc4ac773d2dcff109bf5 to your computer and use it in GitHub Desktop.
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
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)
}
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)
}
}
@jayrhynas
Copy link
Author

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