Skip to content

Commit cf45d10

Browse files
authored
Fix Swift mapping of ints within unions (#47)
This fixes an issue where a union with an int member cannot decode any value that is out of the Int8 bounds. Also: make SingleValueDecodingContainer throw `typeMismatch` errors when the expected type doesn't match.
1 parent c514843 commit cf45d10

File tree

10 files changed

+308
-80
lines changed

10 files changed

+308
-80
lines changed

Sources/PklSwift/Decoder/PklSingleValueDecodingContainer.swift

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,47 +37,55 @@ class PklSingleValueDecodingContainer: SingleValueDecodingContainer {
3737
switch self.value {
3838
case .bool(let value): return value
3939
default:
40-
throw DecodingError.dataCorrupted(
40+
throw DecodingError.typeMismatch(
41+
Bool.self,
4142
.init(
4243
codingPath: self.codingPath,
4344
debugDescription: "Expected a boolean, but got \(self.value.debugDataTypeDescription)"
44-
))
45+
)
46+
)
4547
}
4648
}
4749

4850
func decode(_: String.Type) throws -> String {
4951
switch self.value {
5052
case .string(let value): return value
5153
default:
52-
throw DecodingError.dataCorrupted(
54+
throw DecodingError.typeMismatch(
55+
String.self,
5356
.init(
5457
codingPath: self.codingPath,
5558
debugDescription: "Expected a string, but got \(self.value.debugDataTypeDescription)"
56-
))
59+
)
60+
)
5761
}
5862
}
5963

6064
func decode(_: Double.Type) throws -> Double {
6165
switch self.value {
6266
case .float(let value): return Double(value)
6367
default:
64-
throw DecodingError.dataCorrupted(
68+
throw DecodingError.typeMismatch(
69+
Double.self,
6570
.init(
6671
codingPath: self.codingPath,
6772
debugDescription: "Expected a float, but got \(self.value.debugDataTypeDescription)"
68-
))
73+
)
74+
)
6975
}
7076
}
7177

7278
func decode(_: Float.Type) throws -> Float {
7379
switch self.value {
7480
case .float(let value): return Float(value)
7581
default:
76-
throw DecodingError.dataCorrupted(
82+
throw DecodingError.typeMismatch(
83+
Float.self,
7784
.init(
7885
codingPath: self.codingPath,
7986
debugDescription: "Expected a float, but got \(self.value.debugDataTypeDescription)"
80-
))
87+
)
88+
)
8189
}
8290
}
8391

@@ -124,7 +132,7 @@ class PklSingleValueDecodingContainer: SingleValueDecodingContainer {
124132
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
125133
if type == PklAny.self {
126134
guard let result = try _PklDecoder.decodePolymorphic(value, codingPath: codingPath) else {
127-
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Tried to decode but got nil"))
135+
throw DecodingError.typeMismatch(T.self, .init(codingPath: self.codingPath, debugDescription: "Tried to decode but got nil"))
128136
}
129137
return result as! T
130138
}
@@ -135,13 +143,25 @@ class PklSingleValueDecodingContainer: SingleValueDecodingContainer {
135143

136144
private func decodeBinaryInteger<T>() throws -> T where T: BinaryInteger {
137145
switch self.value {
138-
case .int(let value): return T(value)
146+
case .int(let value):
147+
guard let result = T(exactly: value) else {
148+
throw DecodingError.typeMismatch(
149+
T.self,
150+
.init(
151+
codingPath: self.codingPath,
152+
debugDescription: "Cannot fit \(value) into \(String(describing: T.self))"
153+
)
154+
)
155+
}
156+
return result
139157
default:
140-
throw DecodingError.dataCorrupted(
158+
throw DecodingError.typeMismatch(
159+
T.self,
141160
.init(
142161
codingPath: self.codingPath,
143162
debugDescription: "Expected an int, but got \(self.value.debugDataTypeDescription)"
144-
))
163+
)
164+
)
145165
}
146166
}
147167
}

Tests/PklSwiftTests/Decoder/PklDecoderTest.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ final class PklDecoderTests: XCTestCase {
4343
XCTAssertEqual(value, 42)
4444
}
4545

46+
func testDecodeInt8Overflow() throws {
47+
let bytes: [UInt8] = [0xD2, 0xFF, 0x00, 0x00, 0x00]
48+
XCTAssertThrowsError(try PklDecoder.decode(Int8.self, from: bytes), "Cannot fit -16777216 into Int8")
49+
}
50+
4651
func testDecodeString() throws {
4752
let bytes: [UInt8] = [
4853
0xD9, 0x27, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x69, 0x63, 0x6B, 0x20, 0x66, 0x6F, 0x78,

Tests/PklSwiftTests/Fixtures/Generated/UnionTypes.pkl.swift

Lines changed: 143 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ public protocol UnionTypes_Animal: PklRegisteredType, DynamicallyEquatable, Hash
77
var name: String { get }
88
}
99

10+
public protocol UnionTypes_Shape: PklRegisteredType, DynamicallyEquatable, Hashable {
11+
}
12+
1013
extension UnionTypes {
1114
public enum Fruit: Decodable, Hashable {
1215
case banana(Banana)
1316
case grape(Grape)
1417
case apple(Apple)
1518

1619
public init(from decoder: Decoder) throws {
17-
let decoded = try decoder.singleValueContainer().decode(PklSwift.PklAny.self).value
18-
switch decoded?.base {
20+
let container = try decoder.singleValueContainer()
21+
let value = try container.decode(PklSwift.PklAny.self).value
22+
switch value?.base {
1923
case let decoded as Banana:
2024
self = Fruit.banana(decoded)
2125
case let decoded as Grape:
@@ -27,7 +31,7 @@ extension UnionTypes {
2731
Fruit.self,
2832
.init(
2933
codingPath: decoder.codingPath,
30-
debugDescription: "Expected type Fruit, but got \(String(describing: decoded))"
34+
debugDescription: "Expected type Fruit, but got \(String(describing: value))"
3135
)
3236
)
3337
}
@@ -46,8 +50,9 @@ extension UnionTypes {
4650
case donkey(Donkey)
4751

4852
public init(from decoder: Decoder) throws {
49-
let decoded = try decoder.singleValueContainer().decode(PklSwift.PklAny.self).value
50-
switch decoded?.base {
53+
let container = try decoder.singleValueContainer()
54+
let value = try container.decode(PklSwift.PklAny.self).value
55+
switch value?.base {
5156
case let decoded as Zebra:
5257
self = ZebraOrDonkey.zebra(decoded)
5358
case let decoded as Donkey:
@@ -57,7 +62,7 @@ extension UnionTypes {
5762
ZebraOrDonkey.self,
5863
.init(
5964
codingPath: decoder.codingPath,
60-
debugDescription: "Expected type ZebraOrDonkey, but got \(String(describing: decoded))"
65+
debugDescription: "Expected type ZebraOrDonkey, but got \(String(describing: value))"
6166
)
6267
)
6368
}
@@ -80,8 +85,9 @@ extension UnionTypes {
8085
}
8186

8287
public init(from decoder: Decoder) throws {
83-
let decoded = try decoder.singleValueContainer().decode(PklSwift.PklAny.self).value
84-
switch decoded?.base {
88+
let container = try decoder.singleValueContainer()
89+
let value = try container.decode(PklSwift.PklAny.self).value
90+
switch value?.base {
8591
case let decoded as any Animal:
8692
self = AnimalOrString.animal(decoded)
8793
case let decoded as String:
@@ -91,7 +97,7 @@ extension UnionTypes {
9197
AnimalOrString.self,
9298
.init(
9399
codingPath: decoder.codingPath,
94-
debugDescription: "Expected type AnimalOrString, but got \(String(describing: decoded))"
100+
debugDescription: "Expected type AnimalOrString, but got \(String(describing: value))"
95101
)
96102
)
97103
}
@@ -111,29 +117,108 @@ extension UnionTypes {
111117
case int(Int)
112118
case float64(Float64)
113119

120+
private static func decodeNumeric(from decoder: Decoder, _ container: any SingleValueDecodingContainer) -> IntOrFloat? {
121+
return (try? .int(container.decode(Int.self)))
122+
?? (try? .float64(container.decode(Float64.self)))
123+
}
124+
125+
public init(from decoder: Decoder) throws {
126+
let container = try decoder.singleValueContainer()
127+
let decoded = IntOrFloat.decodeNumeric(from: decoder, container)
128+
if decoded != nil {
129+
self = decoded!
130+
return
131+
}
132+
let value = try container.decode(PklSwift.PklAny.self).value
133+
throw DecodingError.typeMismatch(
134+
IntOrFloat.self,
135+
.init(
136+
codingPath: decoder.codingPath,
137+
debugDescription: "Expected type IntOrFloat, but got \(String(describing: value))"
138+
)
139+
)
140+
}
141+
}
142+
143+
public enum Environment: String, CaseIterable, CodingKeyRepresentable, Decodable, Hashable {
144+
case dev = "dev"
145+
case prod = "prod"
146+
case qa = "qa"
147+
}
148+
149+
public enum AnimalOrShape: Decodable, Hashable {
150+
case animal(any Animal)
151+
case shape(any Shape)
152+
153+
public static func ==(lhs: AnimalOrShape, rhs: AnimalOrShape) -> Bool {
154+
switch (lhs, rhs) {
155+
case let (.animal(a), .animal(b)):
156+
return a.isDynamicallyEqual(to: b)
157+
case let (.shape(a), .shape(b)):
158+
return a.isDynamicallyEqual(to: b)
159+
default:
160+
return false
161+
}
162+
}
163+
114164
public init(from decoder: Decoder) throws {
115-
let decoded = try decoder.singleValueContainer().decode(PklSwift.PklAny.self).value
116-
switch decoded?.base {
117-
case let decoded as Int:
118-
self = IntOrFloat.int(decoded)
119-
case let decoded as Float64:
120-
self = IntOrFloat.float64(decoded)
165+
let container = try decoder.singleValueContainer()
166+
let value = try container.decode(PklSwift.PklAny.self).value
167+
switch value?.base {
168+
case let decoded as any Animal:
169+
self = AnimalOrShape.animal(decoded)
170+
case let decoded as any Shape:
171+
self = AnimalOrShape.shape(decoded)
121172
default:
122173
throw DecodingError.typeMismatch(
123-
IntOrFloat.self,
174+
AnimalOrShape.self,
124175
.init(
125176
codingPath: decoder.codingPath,
126-
debugDescription: "Expected type IntOrFloat, but got \(String(describing: decoded))"
177+
debugDescription: "Expected type AnimalOrShape, but got \(String(describing: value))"
127178
)
128179
)
129180
}
130181
}
182+
183+
public func hash(into hasher: inout Hasher) {
184+
switch self {
185+
case let .animal(value):
186+
hasher.combine(value)
187+
case let .shape(value):
188+
hasher.combine(value)
189+
}
190+
}
131191
}
132192

133-
public enum Environment: String, CaseIterable, CodingKeyRepresentable, Decodable, Hashable {
134-
case dev = "dev"
135-
case prod = "prod"
136-
case qa = "qa"
193+
public enum Numbers: Decodable, Hashable {
194+
case int8(Int8)
195+
case int16(Int16)
196+
case int32(Int32)
197+
case int(Int)
198+
199+
private static func decodeNumeric(from decoder: Decoder, _ container: any SingleValueDecodingContainer) -> Numbers? {
200+
return (try? .int8(container.decode(Int8.self)))
201+
?? (try? .int16(container.decode(Int16.self)))
202+
?? (try? .int32(container.decode(Int32.self)))
203+
?? (try? .int(container.decode(Int.self)))
204+
}
205+
206+
public init(from decoder: Decoder) throws {
207+
let container = try decoder.singleValueContainer()
208+
let decoded = Numbers.decodeNumeric(from: decoder, container)
209+
if decoded != nil {
210+
self = decoded!
211+
return
212+
}
213+
let value = try container.decode(PklSwift.PklAny.self).value
214+
throw DecodingError.typeMismatch(
215+
Numbers.self,
216+
.init(
217+
codingPath: decoder.codingPath,
218+
debugDescription: "Expected type Numbers, but got \(String(describing: value))"
219+
)
220+
)
221+
}
137222
}
138223

139224
public struct Module: PklRegisteredType, Decodable, Hashable {
@@ -169,6 +254,18 @@ extension UnionTypes {
169254

170255
public var config: [Environment: String]
171256

257+
public var animalOrShape1: AnimalOrShape
258+
259+
public var animalOrShape2: AnimalOrShape
260+
261+
public var numbers1: Numbers
262+
263+
public var numbers2: Numbers
264+
265+
public var numbers3: Numbers
266+
267+
public var numbers4: Numbers
268+
172269
public init(
173270
fruit1: Fruit,
174271
fruit2: Fruit,
@@ -184,7 +281,13 @@ extension UnionTypes {
184281
intOrFloat1: IntOrFloat,
185282
intOrFloat2: IntOrFloat,
186283
intOrFloat3: IntOrFloat,
187-
config: [Environment: String]
284+
config: [Environment: String],
285+
animalOrShape1: AnimalOrShape,
286+
animalOrShape2: AnimalOrShape,
287+
numbers1: Numbers,
288+
numbers2: Numbers,
289+
numbers3: Numbers,
290+
numbers4: Numbers
188291
) {
189292
self.fruit1 = fruit1
190293
self.fruit2 = fruit2
@@ -201,6 +304,12 @@ extension UnionTypes {
201304
self.intOrFloat2 = intOrFloat2
202305
self.intOrFloat3 = intOrFloat3
203306
self.config = config
307+
self.animalOrShape1 = animalOrShape1
308+
self.animalOrShape2 = animalOrShape2
309+
self.numbers1 = numbers1
310+
self.numbers2 = numbers2
311+
self.numbers3 = numbers3
312+
self.numbers4 = numbers4
204313
}
205314
}
206315

@@ -236,6 +345,18 @@ extension UnionTypes {
236345

237346
public typealias Animal = UnionTypes_Animal
238347

348+
public typealias Shape = UnionTypes_Shape
349+
350+
public struct Square: Shape {
351+
public static let registeredIdentifier: String = "UnionTypes#Square"
352+
353+
public var corners: Int
354+
355+
public init(corners: Int) {
356+
self.corners = corners
357+
}
358+
}
359+
239360
public struct Zebra: Animal {
240361
public static let registeredIdentifier: String = "UnionTypes#Zebra"
241362

0 commit comments

Comments
 (0)