Skip to content

Instantly share code, notes, and snippets.

@adam-fowler
Last active October 26, 2021 10:21
Show Gist options
  • Save adam-fowler/28f7a5d5acc1c251a34a02b7ee702dac to your computer and use it in GitHub Desktop.
Save adam-fowler/28f7a5d5acc1c251a34a02b7ee702dac to your computer and use it in GitHub Desktop.
Decoding/Encoding of Polymorphic payloads (taken from Job implementation in https://github.com/hummingbird-project/hummingbird)
/// For a job to be decodable, it has to be registered. Call `MyJob.register()` to register a job.
public protocol HBJob: Codable {
    /// Unique Job name
    static var name: String { get }
}

extension HBJob {
    /// register job
    public static func register() {
        HBJobRegister.register(job: Self.self)
    }
}

/// Register Jobs, for decoding and encoding
enum HBJobRegister {
    static func decode(from decoder: Decoder) throws -> HBJob {
        let container = try decoder.container(keyedBy: _HBJobCodingKey.self)
        // get key and use as lookup into JobRegister
        guard let key = container.allKeys.first,
              let jobType = HBJobRegister.nameTypeMap[key.stringValue] else { throw JobQueueError.decodeJobFailed }
        let childDecoder = try container.superDecoder(forKey: key)
        return try jobType.init(from: childDecoder)
    }

    static func encode(job: HBJob, to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: _HBJobCodingKey.self)
        let childEncoder = container.superEncoder(forKey: .init(stringValue: type(of: job).name, intValue: nil))
        try job.encode(to: childEncoder)
    }

    static func register(job: HBJob.Type) {
        self.nameTypeMap[job.name] = job
    }

    static var nameTypeMap: [String: HBJob.Type] = [:]
}

internal struct _HBJobCodingKey: CodingKey {
    public var stringValue: String
    public var intValue: Int?

    public init?(stringValue: String) {
        self.stringValue = stringValue
        self.intValue = nil
    }

    public init?(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }

    public init(stringValue: String, intValue: Int?) {
        self.stringValue = stringValue
        self.intValue = intValue
    }

    internal init(index: Int) {
        self.stringValue = "Index \(index)"
        self.intValue = index
    }
}

/// To encode/decode Job needs to be in a container type because you cannot decode `HBJob` as it does conform to `HBJob`.
public struct HBJobContainer: Codable {
    /// Job data
    public let job: HBJob

    /// Initialize a queue job
    init(_ job: HBJob) {
        self.job = job
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let jobDecoder = try container.superDecoder(forKey: .job)
        self.job = try HBJobRegister.decode(from: jobDecoder)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let jobEncoder = container.superEncoder(forKey: .job)
        try HBJobRegister.encode(job: self.job, to: jobEncoder)
    }

    private enum CodingKeys: String, CodingKey {
        case job
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment