diff --git a/Sources/TSCUtility/PolymorphicCodable.swift b/Sources/TSCUtility/PolymorphicCodable.swift new file mode 100644 index 00000000..35676974 --- /dev/null +++ b/Sources/TSCUtility/PolymorphicCodable.swift @@ -0,0 +1,62 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +/// Allows encoding and decoding known polymorphic types. +public protocol PolymorphicCodableProtocol: Codable { + static var implementations: [PolymorphicCodableProtocol.Type] { get } +} + +@propertyWrapper +public struct PolymorphicCodable: Codable { + public let value: T + + public init(wrappedValue value: T) { + self.value = value + } + + public var wrappedValue: T { + return value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(String(reflecting: type(of: value))) + try container.encode(value) + } + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let typeCode = try container.decode(String.self) + guard let klass = T.implementations.first(where: { String(reflecting: $0) == typeCode }) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unexpected Codable type code for concrete '\(type(of: T.self))': \(typeCode)") + } + + self.value = try klass.init(from: container.superDecoder()) as! T + } +} + +extension Array: PolymorphicCodableProtocol where Element: PolymorphicCodableProtocol { + public static var implementations: [PolymorphicCodableProtocol.Type] { + return [Array.self] + } + + public func encode(to encoder: Encoder) throws { + try self.map{ PolymorphicCodable(wrappedValue: $0) }.encode(to: encoder) + } + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var items: [PolymorphicCodable] = [] + while !container.isAtEnd { + items.append(try container.decode(PolymorphicCodable.self)) + } + self = items.map{ $0.value } + } +} diff --git a/Tests/TSCUtilityTests/PolymorphicCodableTests.swift b/Tests/TSCUtilityTests/PolymorphicCodableTests.swift new file mode 100644 index 00000000..d855d8ad --- /dev/null +++ b/Tests/TSCUtilityTests/PolymorphicCodableTests.swift @@ -0,0 +1,114 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation +import XCTest + +import TSCBasic +import TSCUtility + +class Animal: PolymorphicCodableProtocol { + static var implementations: [PolymorphicCodableProtocol.Type] = [ + Dog.self, + Cat.self, + ] + + let age: Int + + init(age: Int) { + self.age = age + } +} + +struct Animals: Codable { + @PolymorphicCodable + var animal1: Animal + + @PolymorphicCodable + var animal2: Animal + + @PolymorphicCodable + var animals: [Animal] +} + +final class PolymorphicCodableTests: XCTestCase { + + func testBasic() throws { + let dog = Dog(age: 5, dogCandy: "bone") + let cat = Cat(age: 3, catToy: "wool") + + let animals = Animals(animal1: dog, animal2: cat, animals: [dog, cat]) + let encoded = try JSONEncoder().encode(animals) + let decoded = try JSONDecoder().decode(Animals.self, from: encoded) + + let animal1 = try XCTUnwrap(decoded.animal1 as? Dog) + XCTAssertEqual(animal1.age, 5) + XCTAssertEqual(animal1.dogCandy, "bone") + + let animal2 = try XCTUnwrap(decoded.animal2 as? Cat) + XCTAssertEqual(animal2.age, 3) + XCTAssertEqual(animal2.catToy, "wool") + + XCTAssertEqual(decoded.animals.count, 2) + XCTAssertEqual(decoded.animals.map{ $0.age }, [5, 3]) + } +} + +// MARK:- Subclasses + +class Dog: Animal { + let dogCandy: String + + init(age: Int, dogCandy: String) { + self.dogCandy = dogCandy + super.init(age: age) + } + + enum CodingKeys: CodingKey { + case dogCandy + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(dogCandy, forKey: .dogCandy) + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.dogCandy = try container.decode(String.self, forKey: .dogCandy) + try super.init(from: decoder) + } +} + +class Cat: Animal { + let catToy: String + + init(age: Int, catToy: String) { + self.catToy = catToy + super.init(age: age) + } + + enum CodingKeys: CodingKey { + case catToy + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(catToy, forKey: .catToy) + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.catToy = try container.decode(String.self, forKey: .catToy) + try super.init(from: decoder) + } +}