Skip to content

Commit

Permalink
[TSCUtility] Introduce PolymorphicCodable property wrapper (#67)
Browse files Browse the repository at this point in the history
* [TSCUtility] Introduce PolymorphicCodable property wrapper

This allows encoding and decoding polymorphic types without writing
a bunch of boilerplate code.

* [TSCUtility] Conditionally conform Array to PolymorphicCodable
  • Loading branch information
aciidgh authored Apr 20, 2020
1 parent ecc9fc1 commit 39fb181
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
62 changes: 62 additions & 0 deletions Sources/TSCUtility/PolymorphicCodable.swift
Original file line number Diff line number Diff line change
@@ -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<T: PolymorphicCodableProtocol>: 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<Element>.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<Element>] = []
while !container.isAtEnd {
items.append(try container.decode(PolymorphicCodable<Element>.self))
}
self = items.map{ $0.value }
}
}
114 changes: 114 additions & 0 deletions Tests/TSCUtilityTests/PolymorphicCodableTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 39fb181

Please sign in to comment.