Skip to content

Commit 2a2a882

Browse files
authored
Merge pull request #8 from swhitty/int-decoding-strategy
IntDecodingStrategy
2 parents a95e116 + 56fcfe1 commit 2a2a882

File tree

2 files changed

+152
-26
lines changed

2 files changed

+152
-26
lines changed

Sources/KeyValueDecoder.swift

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ public final class KeyValueDecoder {
4141
/// The strategy to use for decoding `nil`. Defaults to `Optional<Any>.none` which can be decoded to any optional type.
4242
public var nilDecodingStrategy: NilDecodingStrategy = .default
4343

44-
/// Initializes `self` with default strategies.
44+
/// The strategy to use for decoding BinaryInteger types. Defaults to `.exact` for lossless conversion between types.
45+
public var intDecodingStrategy: IntDecodingStrategy = .exact
46+
47+
/// Initializes `self` with default strategy.
4548
public init () {
4649
self.userInfo = [:]
4750
}
@@ -60,13 +63,21 @@ public final class KeyValueDecoder {
6063
value: value,
6164
codingPath: [],
6265
userInfo: userInfo,
63-
nilDecodingStrategy: nilDecodingStrategy
66+
strategy: strategy
6467
)
6568
return try container.decode(type)
6669
}
6770

6871
/// Strategy used to decode nil values.
6972
public typealias NilDecodingStrategy = NilCodingStrategy
73+
74+
public enum IntDecodingStrategy {
75+
/// Decodes all number types with lossless conversion or throws error.
76+
case exact
77+
78+
/// Decodes all floating point numbers using the provided rounding rule.
79+
case rounded(rule: FloatingPointRoundingRule)
80+
}
7081
}
7182

7283
extension KeyValueDecoder {
@@ -80,6 +91,18 @@ extension KeyValueDecoder {
8091

8192
private extension KeyValueDecoder {
8293

94+
struct DecodingStrategy {
95+
var optionals: NilDecodingStrategy
96+
var integers: IntDecodingStrategy
97+
}
98+
99+
var strategy: DecodingStrategy {
100+
DecodingStrategy(
101+
optionals: nilDecodingStrategy,
102+
integers: intDecodingStrategy
103+
)
104+
}
105+
83106
struct Decoder: Swift.Decoder {
84107

85108
private let container: SingleContainer
@@ -97,7 +120,7 @@ private extension KeyValueDecoder {
97120
codingPath: codingPath,
98121
storage: container.decode([String: Any].self),
99122
userInfo: userInfo,
100-
nilDecodingStrategy: container.nilDecodingStrategy
123+
strategy: container.strategy
101124
)
102125
return KeyedDecodingContainer(keyed)
103126
}
@@ -108,7 +131,7 @@ private extension KeyValueDecoder {
108131
codingPath: codingPath,
109132
storage: storage,
110133
userInfo: userInfo,
111-
nilDecodingStrategy: container.nilDecodingStrategy
134+
strategy: container.strategy
112135
)
113136
}
114137

@@ -121,28 +144,28 @@ private extension KeyValueDecoder {
121144

122145
let codingPath: [any CodingKey]
123146
let userInfo: [CodingUserInfoKey: Any]
124-
let nilDecodingStrategy: NilDecodingStrategy
147+
let strategy: DecodingStrategy
125148

126149
private var value: Any
127150

128151
init(
129152
value: Any,
130153
codingPath: [any CodingKey],
131154
userInfo: [CodingUserInfoKey: Any],
132-
nilDecodingStrategy: NilDecodingStrategy
155+
strategy: DecodingStrategy
133156
) {
134157
self.value = value
135158
self.codingPath = codingPath
136159
self.userInfo = userInfo
137-
self.nilDecodingStrategy = nilDecodingStrategy
160+
self.strategy = strategy
138161
}
139162

140163
func decodeNil() -> Bool {
141-
nilDecodingStrategy.isNull(value)
164+
strategy.optionals.isNull(value)
142165
}
143166

144167
private var valueDescription: String {
145-
nilDecodingStrategy.isNull(value) ? "nil" : String(describing: type(of: value))
168+
strategy.optionals.isNull(value) ? "nil" : String(describing: type(of: value))
146169
}
147170

148171
func getValue<T>(of type: T.Type = T.self) throws -> T {
@@ -170,15 +193,13 @@ private extension KeyValueDecoder {
170193
throw DecodingError.typeMismatch(type, context)
171194
}
172195
return val
173-
} else if let double = (value as? NSNumber)?.getDoubleValue() {
174-
let val = T(double)
175-
guard Double(val) == double else {
196+
} else if let double = getDoubleValue(from: value, using: strategy.integers) {
197+
guard let val = T(exactly: double) else {
176198
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "\(valueDescription) at \(codingPath.makeKeyPath()), cannot be exactly represented by \(type)")
177199
throw DecodingError.typeMismatch(type, context)
178200
}
179201
return val
180-
}
181-
else {
202+
} else {
182203
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Expected BinaryInteger at \(codingPath.makeKeyPath()), found \(valueDescription)")
183204
if decodeNil() {
184205
throw DecodingError.valueNotFound(type, context)
@@ -188,6 +209,18 @@ private extension KeyValueDecoder {
188209
}
189210
}
190211

212+
func getDoubleValue(from value: Any, using strategy: IntDecodingStrategy) -> Double? {
213+
guard let double = (value as? NSNumber)?.getDoubleValue() else {
214+
return nil
215+
}
216+
switch strategy {
217+
case .exact:
218+
return double
219+
case .rounded(rule: let rule):
220+
return double.rounded(rule)
221+
}
222+
}
223+
191224
func decode(_ type: Bool.Type) throws -> Bool {
192225
try getValue()
193226
}
@@ -322,18 +355,18 @@ private extension KeyValueDecoder {
322355
let storage: [String: Any]
323356
let codingPath: [any CodingKey]
324357
private let userInfo: [CodingUserInfoKey: Any]
325-
private let nilDecodingStrategy: NilDecodingStrategy
358+
private let strategy: DecodingStrategy
326359

327360
init(
328361
codingPath: [any CodingKey],
329362
storage: [String: Any],
330363
userInfo: [CodingUserInfoKey: Any],
331-
nilDecodingStrategy: NilDecodingStrategy
364+
strategy: DecodingStrategy
332365
) {
333366
self.codingPath = codingPath
334367
self.storage = storage
335368
self.userInfo = userInfo
336-
self.nilDecodingStrategy = nilDecodingStrategy
369+
self.strategy = strategy
337370
}
338371

339372
var allKeys: [Key] {
@@ -357,7 +390,7 @@ private extension KeyValueDecoder {
357390
value: value,
358391
codingPath: path,
359392
userInfo: userInfo,
360-
nilDecodingStrategy: nilDecodingStrategy
393+
strategy: strategy
361394
)
362395
}
363396

@@ -435,7 +468,7 @@ private extension KeyValueDecoder {
435468
codingPath: container.codingPath,
436469
storage: container.decode([String: Any].self),
437470
userInfo: userInfo,
438-
nilDecodingStrategy: nilDecodingStrategy
471+
strategy: strategy
439472
)
440473
return KeyedDecodingContainer<NestedKey>(keyed)
441474
}
@@ -446,7 +479,7 @@ private extension KeyValueDecoder {
446479
codingPath: container.codingPath,
447480
storage: container.decode([Any].self),
448481
userInfo: userInfo,
449-
nilDecodingStrategy: nilDecodingStrategy
482+
strategy: strategy
450483
)
451484
}
452485

@@ -455,7 +488,7 @@ private extension KeyValueDecoder {
455488
value: storage,
456489
codingPath: codingPath,
457490
userInfo: userInfo,
458-
nilDecodingStrategy: nilDecodingStrategy
491+
strategy: strategy
459492
)
460493
return Decoder(container: container)
461494
}
@@ -471,18 +504,18 @@ private extension KeyValueDecoder {
471504

472505
let storage: [Any]
473506
private let userInfo: [CodingUserInfoKey: Any]
474-
private let nilDecodingStrategy: NilDecodingStrategy
507+
private let strategy: DecodingStrategy
475508

476509
init(
477510
codingPath: [any CodingKey],
478511
storage: [Any],
479512
userInfo: [CodingUserInfoKey: Any],
480-
nilDecodingStrategy: NilDecodingStrategy
513+
strategy: DecodingStrategy
481514
) {
482515
self.codingPath = codingPath
483516
self.storage = storage
484517
self.userInfo = userInfo
485-
self.nilDecodingStrategy = nilDecodingStrategy
518+
self.strategy = strategy
486519
self.currentIndex = storage.startIndex
487520
}
488521

@@ -507,7 +540,7 @@ private extension KeyValueDecoder {
507540
value: storage[currentIndex],
508541
codingPath: path,
509542
userInfo: userInfo,
510-
nilDecodingStrategy: nilDecodingStrategy
543+
strategy: strategy
511544
)
512545
}
513546

@@ -600,7 +633,7 @@ private extension KeyValueDecoder {
600633
value: storage,
601634
codingPath: codingPath,
602635
userInfo: userInfo,
603-
nilDecodingStrategy: nilDecodingStrategy
636+
strategy: strategy
604637
)
605638
return Decoder(container: container)
606639
}

Tests/KeyValueDecoderTests.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,53 @@ final class KeyValueDecoderTests: XCTestCase {
134134
)
135135
}
136136

137+
func testDecodesRounded_Ints() {
138+
let decoder = KeyValueDecoder()
139+
decoder.intDecodingStrategy = .rounded(rule: .toNearestOrAwayFromZero)
140+
141+
XCTAssertEqual(
142+
try decoder.decode(Int16.self, from: 10.0),
143+
10
144+
)
145+
XCTAssertEqual(
146+
try decoder.decode(Int16.self, from: 10.00001),
147+
10
148+
)
149+
XCTAssertEqual(
150+
try decoder.decode(Int16.self, from: 10.1),
151+
10
152+
)
153+
XCTAssertEqual(
154+
try decoder.decode(Int16.self, from: 10.5),
155+
11
156+
)
157+
XCTAssertEqual(
158+
try decoder.decode(Int16.self, from: -10.5),
159+
-11
160+
)
161+
XCTAssertEqual(
162+
try decoder.decode(Int16.self, from: NSNumber(10.5)),
163+
11
164+
)
165+
XCTAssertEqual(
166+
try decoder.decode([Int].self, from: [10.1, -20.9, 50.00001]),
167+
[10, -21, 50]
168+
)
169+
170+
XCTAssertThrowsError(
171+
try KeyValueDecoder.decode(Int8.self, from: Double(Int16.max))
172+
)
173+
XCTAssertThrowsError(
174+
try KeyValueDecoder.decode(Int8.self, from: NSNumber(value: Double(Int16.max)))
175+
)
176+
XCTAssertThrowsError(
177+
try KeyValueDecoder.decode(Int16.self, from: Optional<Double>.none)
178+
)
179+
XCTAssertThrowsError(
180+
try KeyValueDecoder.decode(Int16.self, from: NSNull())
181+
)
182+
}
183+
137184
func testDecodes_UInts() {
138185
XCTAssertEqual(
139186
try KeyValueDecoder.decode(UInt16.self, from: UInt16.max),
@@ -171,6 +218,52 @@ final class KeyValueDecoderTests: XCTestCase {
171218
)
172219
}
173220

221+
func testDecodesRounded_UInts() {
222+
let decoder = KeyValueDecoder()
223+
decoder.intDecodingStrategy = .rounded(rule: .toNearestOrAwayFromZero)
224+
225+
XCTAssertEqual(
226+
try decoder.decode(UInt16.self, from: 10.0),
227+
10
228+
)
229+
XCTAssertEqual(
230+
try decoder.decode(UInt16.self, from: 10.00001),
231+
10
232+
)
233+
XCTAssertEqual(
234+
try decoder.decode(UInt16.self, from: 10.1),
235+
10
236+
)
237+
XCTAssertEqual(
238+
try decoder.decode(UInt16.self, from: 10.5),
239+
11
240+
)
241+
XCTAssertEqual(
242+
try decoder.decode(UInt16.self, from: NSNumber(10.5)),
243+
11
244+
)
245+
XCTAssertEqual(
246+
try decoder.decode([UInt].self, from: [10.1, 20.9, 50.00001]),
247+
[10, 21, 50]
248+
)
249+
250+
XCTAssertThrowsError(
251+
try KeyValueDecoder.decode(UInt8.self, from: Double(Int16.max))
252+
)
253+
XCTAssertThrowsError(
254+
try KeyValueDecoder.decode(UInt8.self, from: NSNumber(value: Double(Int16.max)))
255+
)
256+
XCTAssertThrowsError(
257+
try KeyValueDecoder.decode(UInt16.self, from: Double(-1))
258+
)
259+
XCTAssertThrowsError(
260+
try KeyValueDecoder.decode(UInt16.self, from: Optional<Double>.none)
261+
)
262+
XCTAssertThrowsError(
263+
try KeyValueDecoder.decode(UInt16.self, from: NSNull())
264+
)
265+
}
266+
174267
func testDecodes_Float(){
175268
XCTAssertEqual(
176269
try KeyValueDecoder.decode(Float.self, from: 10),

0 commit comments

Comments
 (0)