Here's a possible solution that will be able to cover all cases, as opposed to a few hard-coded ones in SynthesizedEnumDiscriminatorType
: (sorry for ugly/inconsistent parameter order and naming)
protocol SynthesisedEnumDiscriminatorEncoder {
// special case, effectively "single value container"
static func encodeCase<CaseKey: CodingKey>(with encoder: Encoder, _ name: CaseKey, _ value: AnyEncodable) throws
// "keyed container"
static func encodeCase<CaseKey: CodingKey, ValueKey: CodingKey>(with encoder: Encoder, _ name: CaseKey, _ values: [ValueKey : AnyEncodable]) throws
// maybe an "unkeyed container" as well?
// static func encodeCase<CaseKey: CodingKey>(with encoder: Encoder, _ name: CaseKey, _ values: [AnyEncodable]) throws
}
protocol SynthesisedEnumDiscriminatorDecoder {
// first decode the discriminator to figure out what to do
static func decodeCase<CaseKey: CodingKey>(with decoder: Decoder, ofType type: CaseKey.Type) throws -> CaseKey
// decode from "single value container"
static func decodeCaseValue<CaseKey: CodingKey, T: Decodable>(with decoder: Decoder, forCase case: CaseKey, ofType type: T.Type) throws -> T
// decode an associated value by key
static func decodeCaseValue<CaseKey: CodingKey, ValueKey: CodingKey, T: Decodable>(with decoder: Decoder, forCase case: CaseKey, forKey: ValueKey, ofType type: T.Type) throws -> T
// "unkeyed container" would require a mutating function to store implicit state, which isn't possible
// mutating static func decodeNextCaseValue<CaseKey: CodingKey, T: Decodable>(with decoder: Decoder, forCase case: CaseKey, ofType type: T.Type) throws -> T
}
(note: I'm pretending AnyEncodable
exists here to simplify the encoding API, but it'd be possible to come up with a similar thing that doesn't need it — variadic generics might help here)
Each "strategy" would then implement these protocols. Here's an example for an "out of line" discriminator:
struct SynthesisedEnumOutOfLineDiscriminatorCoder: SynthesisedEnumDiscriminatorEncoder, SynthesisedEnumDiscriminatorDecoder {
enum CodingKeys: CodingKey {
case type
case value
}
static func encodeCase<CaseKey: CodingKey>(with encoder: Encoder, _ name: CaseKey, _ value: AnyEncodable) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name.stringValue, forKey: .type)
try container.encode(value, forKey: .value)
}
static func encodeCase<CaseKey: CodingKey, ValueKey: CodingKey>(with encoder: Encoder, _ name: CaseKey, _ values: [ValueKey : AnyEncodable]) throws {
// similarly to above
}
static func decodeCase<CaseKey: CodingKey>(with decoder: Decoder, ofType type: CaseKey.Type) throws -> CaseKey {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let name = type.init(stringValue: try container.decode(String.self, forKey: .type)) else { fatalError() }
return name
}
static func decodeCaseValue<CaseKey: CodingKey, T: Decodable>(with decoder: Decoder, forCase case: CaseKey, ofType type: T.Type) throws -> T {
let container = try decoder.container(keyedBy: CodingKeys.self)
return try container.decode(T.self, forKey: .value)
}
static func decodeCaseValue<CaseKey: CodingKey, ValueKey: CodingKey, T: Decodable>(with decoder: Decoder, forCase case: CaseKey, forKey: ValueKey, ofType type: T.Type) throws -> T {
// similarly to above
}
}
Then a type that wants to use a strategy has to do a whole lot less work:
enum IntOrString: Codable {
case int(Int)
case string(String)
typealias SynthesisedDiscriminatorEncoder = SynthesisedEnumOutOfLineDiscriminatorCoder
typealias SynthesisedDiscriminatorDecoder = SynthesisedEnumOutOfLineDiscriminatorCoder
// SYNTHESISED STUFF BELOW
enum Discriminator: String, CodingKey {
case int
case string
}
init(from decoder: Decoder) throws {
let discriminator = try SynthesisedDiscriminatorDecoder.decodeCase(with: decoder, ofType: Discriminator.self)
switch discriminator {
case .int:
// the associated value for this case is exactly one, unlabelled Int, the "single value" function is used
self = .int(try SynthesisedDiscriminatorDecoder.decodeCaseValue(with: decoder, forCase: discriminator, ofType: Int.self))
case .string:
self = .string(try SynthesisedDiscriminatorDecoder.decodeCaseValue(with: decoder, forCase: discriminator, ofType: String.self))
}
}
func encode(to encoder: Encoder) throws {
switch self {
case .int(let value):
try SynthesisedDiscriminatorEncoder.encodeCase(with: encoder, Discriminator.int, AnyEncodable(value))
case .string(let value):
try SynthesisedDiscriminatorEncoder.encodeCase(with: encoder, Discriminator.string, AnyEncodable(value))
}
}
}
Two simple typealiases, and boom, you have synthesised Codable
conformance.
Of course, if you want to use a custom strategy, the amount of code to implement a new strategy is probably a bit more than just implementing Codable
directly, but the idea here is that the Swift standard library would provide implementations for several common ones