Automatic Codable conformance for enums with associated values that themselves conform to Codable

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 :stuck_out_tongue:

cc @anandabits @itaiferber