Skip to content

Commit 91df521

Browse files
committed
[TSCUtility] Introduce PolymorphicCodable property wrapper
1 parent 4022ff4 commit 91df521

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
/// Allows encoding and decoding known polymorphic types.
12+
public protocol PolymorphicCodableProtocol: Codable {
13+
static var implementations: [PolymorphicCodableProtocol.Type] { get }
14+
}
15+
16+
@propertyWrapper
17+
public struct PolymorphicCodable<T: PolymorphicCodableProtocol>: Codable {
18+
public let value: T
19+
20+
public init(wrappedValue value: T) {
21+
self.value = value
22+
}
23+
24+
public var wrappedValue: T {
25+
return value
26+
}
27+
28+
public func encode(to encoder: Encoder) throws {
29+
var container = encoder.unkeyedContainer()
30+
try container.encode(String(reflecting: type(of: value)))
31+
try container.encode(value)
32+
}
33+
34+
public init(from decoder: Decoder) throws {
35+
var container = try decoder.unkeyedContainer()
36+
let typeCode = try container.decode(String.self)
37+
guard let klass = T.implementations.first(where: { String(reflecting: $0) == typeCode }) else {
38+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unexpected Codable type code for concrete '\(type(of: T.self))': \(typeCode)")
39+
}
40+
41+
self.value = try klass.init(from: container.superDecoder()) as! T
42+
}
43+
}
44+
45+
extension Array: PolymorphicCodableProtocol where Element: PolymorphicCodableProtocol {
46+
public static var implementations: [PolymorphicCodableProtocol.Type] {
47+
return [Array<Element>.self]
48+
}
49+
50+
public func encode(to encoder: Encoder) throws {
51+
try self.map{ PolymorphicCodable(wrappedValue: $0) }.encode(to: encoder)
52+
}
53+
54+
public init(from decoder: Decoder) throws {
55+
var container = try decoder.unkeyedContainer()
56+
var items: [PolymorphicCodable<Element>] = []
57+
while !container.isAtEnd {
58+
items.append(try container.decode(PolymorphicCodable<Element>.self))
59+
}
60+
self = items.map{ $0.value }
61+
}
62+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import XCTest
13+
14+
import TSCBasic
15+
import TSCUtility
16+
17+
class Animal: PolymorphicCodableProtocol {
18+
static var implementations: [PolymorphicCodableProtocol.Type] = [
19+
Dog.self,
20+
Cat.self,
21+
]
22+
23+
let age: Int
24+
25+
init(age: Int) {
26+
self.age = age
27+
}
28+
}
29+
30+
struct Animals: Codable {
31+
@PolymorphicCodable
32+
var animal1: Animal
33+
34+
@PolymorphicCodable
35+
var animal2: Animal
36+
37+
@PolymorphicCodable
38+
var animals: [Animal]
39+
}
40+
41+
final class PolymorphicCodableTests: XCTestCase {
42+
43+
func testBasic() throws {
44+
let dog = Dog(age: 5, dogCandy: "bone")
45+
let cat = Cat(age: 3, catToy: "wool")
46+
47+
let animals = Animals(animal1: dog, animal2: cat, animals: [dog, cat])
48+
let encoded = try JSONEncoder().encode(animals)
49+
let decoded = try JSONDecoder().decode(Animals.self, from: encoded)
50+
51+
let animal1 = try XCTUnwrap(decoded.animal1 as? Dog)
52+
XCTAssertEqual(animal1.age, 5)
53+
XCTAssertEqual(animal1.dogCandy, "bone")
54+
55+
let animal2 = try XCTUnwrap(decoded.animal2 as? Cat)
56+
XCTAssertEqual(animal2.age, 3)
57+
XCTAssertEqual(animal2.catToy, "wool")
58+
59+
XCTAssertEqual(decoded.animals.count, 2)
60+
XCTAssertEqual(decoded.animals.map{ $0.age }, [5, 3])
61+
}
62+
}
63+
64+
// MARK:- Subclasses
65+
66+
class Dog: Animal {
67+
let dogCandy: String
68+
69+
init(age: Int, dogCandy: String) {
70+
self.dogCandy = dogCandy
71+
super.init(age: age)
72+
}
73+
74+
enum CodingKeys: CodingKey {
75+
case dogCandy
76+
}
77+
78+
public override func encode(to encoder: Encoder) throws {
79+
try super.encode(to: encoder)
80+
var container = encoder.container(keyedBy: CodingKeys.self)
81+
try container.encode(dogCandy, forKey: .dogCandy)
82+
}
83+
84+
required init(from decoder: Decoder) throws {
85+
let container = try decoder.container(keyedBy: CodingKeys.self)
86+
self.dogCandy = try container.decode(String.self, forKey: .dogCandy)
87+
try super.init(from: decoder)
88+
}
89+
}
90+
91+
class Cat: Animal {
92+
let catToy: String
93+
94+
init(age: Int, catToy: String) {
95+
self.catToy = catToy
96+
super.init(age: age)
97+
}
98+
99+
enum CodingKeys: CodingKey {
100+
case catToy
101+
}
102+
103+
public override func encode(to encoder: Encoder) throws {
104+
try super.encode(to: encoder)
105+
var container = encoder.container(keyedBy: CodingKeys.self)
106+
try container.encode(catToy, forKey: .catToy)
107+
}
108+
109+
required init(from decoder: Decoder) throws {
110+
let container = try decoder.container(keyedBy: CodingKeys.self)
111+
self.catToy = try container.decode(String.self, forKey: .catToy)
112+
try super.init(from: decoder)
113+
}
114+
}

0 commit comments

Comments
 (0)