Skip to content

Instantly share code, notes, and snippets.

@lukeredpath
Last active June 7, 2023 13:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukeredpath/186022efcd7377c7e42329e576cbccd2 to your computer and use it in GitHub Desktop.
Save lukeredpath/186022efcd7377c7e42329e576cbccd2 to your computer and use it in GitHub Desktop.
A wrapper for raw representable values when decoding them from JSON
let jsonPayload = """
{
"category": "animal"
}
"""
struct JsonValue: Codable {
enum Category: String, UnknownRawValueRepresenting {
case animal, mineral, vegetable, unknownValue
}
@UnknownRepresentableValueHandling
var category: Category
}
let decoder = JSONDecoder()
let value = try decoder.decode(jsonPayload.data(using: .utf8)!, as: JsonValue.self)
print(value.$category) // => "animal"
print(value.category) // => JsonValue.Category.animal
Copyright 2023 Luke Redpath
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Foundation
/// A raw representable value that can provide a fallback for unknown raw values.
public protocol UnknownRawValueRepresenting: RawRepresentable {
/// The value to use when given an unknown raw value.
static var unknownValue: Self { get }
}
/// A property wrapper that wraps raw values that can be used as the raw value for a raw representable type.
///
/// This is useful for codable types that have some raw value (usually a `String`) in the JSON payload that
/// you want to decode as some kind of raw representable type, like an enum. Decoding directly to the enum
/// can be prone to failure if new raw values are returned from the API that the enum doesn't know about - this
/// would cause the decoding for the entire JSON payload to fail.
///
/// By storing and decoding the raw value and computing the enum value at runtime, we can be sure that any
/// future values will not break JSON decoding. This property wrapper provides access to the original raw value
/// through it's projected value if you still need to inspect it, but will always return the `unknownValue` when
/// given an unknown raw value.
///
/// - Note: this property wrapper implements equatability and hashability based on the original raw value,
/// so two different unknown values will not be the same even if they have the same computed wrapped value.
@propertyWrapper
public struct UnknownRepresentableValueHandling<ValueType: UnknownRawValueRepresenting> {
private var _wrappedValue: ValueType
public var rawValue: ValueType.RawValue
/// Returns the computed wrapped value - this will be the value's unknown value if the raw value is unknown.
public var wrappedValue: ValueType {
get { _wrappedValue }
set {
_wrappedValue = newValue
rawValue = _wrappedValue.rawValue
}
}
/// Returns the original raw value, even if unknown.
public var projectedValue: ValueType.RawValue {
get { rawValue }
}
/// Initializes the property wrapper with an existing wrapped value.
public init(wrappedValue: ValueType) {
_wrappedValue = wrappedValue
rawValue = wrappedValue.rawValue
}
}
extension UnknownRepresentableValueHandling: RawRepresentable {
/// Initializes the wrapped value from a raw value, or uses the unknown value if not known.
public init(rawValue: ValueType.RawValue) {
self.rawValue = rawValue
_wrappedValue = .init(rawValue: rawValue) ?? ValueType.unknownValue
}
}
extension UnknownRepresentableValueHandling: Hashable where ValueType.RawValue: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}
}
extension UnknownRepresentableValueHandling: Equatable where ValueType.RawValue: Equatable {
public static func == (lhs: UnknownRepresentableValueHandling<ValueType>, rhs: UnknownRepresentableValueHandling<ValueType>) -> Bool {
lhs.rawValue == rhs.rawValue
}
}
extension UnknownRepresentableValueHandling: Encodable where ValueType.RawValue: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}
extension UnknownRepresentableValueHandling: Decodable where ValueType.RawValue: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
rawValue = try container.decode(ValueType.RawValue.self)
_wrappedValue = .init(rawValue: rawValue) ?? ValueType.unknownValue
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment