-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathJSValueDecoder.swift
More file actions
479 lines (404 loc) · 17.1 KB
/
JSValueDecoder.swift
File metadata and controls
479 lines (404 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
//
// JSValueDecoder.swift
// Capacitor
//
// Created by Steven Sherry on 12/8/23.
// Copyright © 2023 Drifty Co. All rights reserved.
//
import Foundation
import Combine
/// A decoder that can decode ``JSValue`` objects into `Decodable` types.
public final class JSValueDecoder: TopLevelDecoder {
/// The strategies available for formatting dates when decoding from a ``JSValue``
public typealias DateDecodingStrategy = JSONDecoder.DateDecodingStrategy
/// The strategies available for decoding raw data.
public typealias DataDecodingStrategy = JSONDecoder.DataDecodingStrategy
/// The strategies available for decoding NaN, Infinity, and -Infinity
public enum NonConformingFloatDecodingStrategy {
/// Decodes directly into the floating point type as .infinity, -.infinity, or .nan
case deferred
/// Throw an error when a non-conforming float is encountered
case `throw`
/// Converts from the provided strings into .infinity, -.infinity, or .nan
case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
}
fileprivate struct Options {
var dataStrategy: DataDecodingStrategy
var dateStrategy: DateDecodingStrategy
var nonConformingStrategy: NonConformingFloatDecodingStrategy
}
private var options: Options
/// Creates a new JSValueDecoder with the provided decoding and formatting strategies
/// - Parameters:
/// - dateDecodingStrategy: Defaults to `DateDecodingStrategy.deferredToDate`
/// - dataDecodingStrategy: Defaults to `DataDecodingStrategy.deferredToData`
/// - nonConformingFloatDecodingStrategy: Defaults to ``NonConformingFloatDecodingStrategy/deferred``
public init(
dateDecodingStrategy: DateDecodingStrategy = .deferredToDate,
dataDecodingStrategy: DataDecodingStrategy = .deferredToData,
nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .deferred
) {
self.options = .init(dataStrategy: dataDecodingStrategy, dateStrategy: dateDecodingStrategy, nonConformingStrategy: nonConformingFloatDecodingStrategy)
}
fileprivate init(options: Options) {
self.options = options
}
/// The strategy to use when decoding dates from a ``JSValue``
public var dateDecodingStrategy: DateDecodingStrategy {
get { options.dateStrategy }
set { options.dateStrategy = newValue }
}
/// The strategy to use when decoding raw data from a ``JSValue``
public var dataDecodingStrategy: DataDecodingStrategy {
get { options.dataStrategy }
set { options.dataStrategy = newValue }
}
/// The strategy used by a decoder when it encounters exceptional floating-point values
public var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy {
get { options.nonConformingStrategy }
set { options.nonConformingStrategy = newValue }
}
/// Decodes a ``JSValue`` into the provided `Decodable` type
/// - Parameters:
/// - type: The type of the value to decode from the provided ``JSValue`` object
/// - data: The ``JSValue`` to decode
/// - Returns: A value of the specified type.
///
/// An error will be thrown from this method for two possible reasons:
/// 1. A type mismatch was found.
/// 2. A key was not found in the `data` field that is required in the `type` provided.
public func decode<T>(_ type: T.Type, from data: JSValue) throws -> T where T: Decodable {
let decoder = _JSValueDecoder(data: data, options: options)
return try decoder.decodeData(as: T.self)
}
}
typealias CodingUserInfo = [CodingUserInfoKey: Any]
private typealias Options = JSValueDecoder.Options
private final class _JSValueDecoder {
var codingPath: [CodingKey] = []
var userInfo: CodingUserInfo = [:]
var options: Options
fileprivate var data: JSValue
init(data: JSValue, options: Options) {
self.data = data
self.options = options
}
}
extension _JSValueDecoder: Decoder {
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
guard let data = data as? JSObject else {
throw DecodingError.typeMismatch(JSObject.self, on: data, codingPath: codingPath)
}
return KeyedDecodingContainer(
KeyedContainer(
data: data,
codingPath: codingPath,
userInfo: userInfo,
options: options
)
)
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
guard let data = data as? JSArray else {
throw DecodingError.typeMismatch(JSArray.self, on: data, codingPath: codingPath)
}
return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options)
}
func singleValueContainer() throws -> SingleValueDecodingContainer {
SingleValueContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options)
}
// force casting is fine because we've already determined that T is the type in the case
// the swift standard library also force casts in their similar functions
// https://github.com/swiftlang/swift-foundation/blob/da80d51fa3e77f3e7ed57c4300a870689e755713/Sources/FoundationEssentials/JSON/JSONEncoder.swift#L1140
// swiftlint:disable force_cast
fileprivate func decodeData<T>(as type: T.Type) throws -> T where T: Decodable {
switch type {
case is Date.Type:
return try decodeDate() as! T
case is URL.Type:
return try decodeUrl() as! T
case is Data.Type:
return try decodeData() as! T
default:
return try T(from: self)
}
}
// swiftlint:enable force_cast
private func decodeDate() throws -> Date {
switch options.dateStrategy {
case .deferredToDate:
return try Date(from: self)
case .secondsSince1970:
guard let value = data as? NSNumber else { throw DecodingError.dataCorrupted(data, target: Double.self, codingPath: codingPath) }
return Date(timeIntervalSince1970: value.doubleValue)
case .millisecondsSince1970:
guard let value = data as? NSNumber else { throw DecodingError.dataCorrupted(data, target: Double.self, codingPath: codingPath) }
return Date(timeIntervalSince1970: value.doubleValue / Double(MSEC_PER_SEC))
case .iso8601:
guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) }
let formatter = ISO8601DateFormatter()
guard let date = formatter.date(from: value) else { throw DecodingError.dataCorrupted(value, target: Date.self, codingPath: codingPath) }
return date
case .formatted(let formatter):
guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) }
guard let date = formatter.date(from: value) else { throw DecodingError.dataCorrupted(value, target: Date.self, codingPath: codingPath) }
return date
case .custom(let decode):
return try decode(self)
@unknown default:
return try Date(from: self)
}
}
private func decodeUrl() throws -> URL {
guard let str = data as? String,
let url = URL(string: str)
else { throw DecodingError.dataCorrupted(data, target: URL.self, codingPath: codingPath) }
return url
}
private func decodeData() throws -> Data {
switch options.dataStrategy {
case .deferredToData:
return try Data(from: self)
case .base64:
guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) }
guard let data = Data(base64Encoded: value) else { throw DecodingError.dataCorrupted(value, target: Data.self, codingPath: codingPath) }
return data
case .custom(let decode):
return try decode(self)
@unknown default:
return try Data(from: self)
}
}
}
private final class KeyedContainer<Key> where Key: CodingKey {
var data: JSObject
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
var allKeys: [Key]
var options: Options
init(data: JSObject, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.data = data
self.codingPath = codingPath
self.userInfo = userInfo
self.allKeys = data.keys.compactMap(Key.init(stringValue:))
self.options = options
}
}
extension KeyedContainer: KeyedDecodingContainerProtocol {
func contains(_ key: Key) -> Bool {
allKeys.contains { $0.stringValue == key.stringValue }
}
func decodeNil(forKey key: Key) throws -> Bool {
data[key.stringValue] == nil || data[key.stringValue] is NSNull
}
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable {
guard let rawValue = data[key.stringValue] else {
throw DecodingError.keyNotFound(key, on: data, codingPath: codingPath)
}
var newPath = codingPath
newPath.append(key)
let decoder = _JSValueDecoder(data: rawValue, options: options)
return try decoder.decodeData(as: T.self)
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
var newPath = codingPath
newPath.append(key)
guard let data = data[key.stringValue] as? JSArray else {
throw DecodingError.typeMismatch(
JSArray.self,
on: data[key.stringValue] ?? "null value",
codingPath: newPath
)
}
return UnkeyedContainer(data: data, codingPath: newPath, userInfo: userInfo, options: options)
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
var newPath = codingPath
newPath.append(key)
guard let data = data[key.stringValue] as? JSObject else {
throw DecodingError.typeMismatch(
JSObject.self,
on: data[key.stringValue] ?? "null value",
codingPath: newPath
)
}
return KeyedDecodingContainer(KeyedContainer<NestedKey>(data: data, codingPath: newPath, userInfo: userInfo, options: options))
}
enum SuperKey: String, CodingKey { case `super` }
func superDecoder() throws -> Decoder {
var newPath = codingPath
newPath.append(SuperKey.super)
guard let data = data[SuperKey.super.stringValue] else {
throw DecodingError.keyNotFound(SuperKey.super, on: data, codingPath: newPath)
}
return _JSValueDecoder(data: data, options: options)
}
func superDecoder(forKey key: Key) throws -> Decoder {
var newPath = codingPath
newPath.append(key)
guard let data = data[key.stringValue] else {
throw DecodingError.keyNotFound(key, on: data, codingPath: newPath)
}
return _JSValueDecoder(data: data, options: options)
}
}
private final class UnkeyedContainer {
var data: JSArray
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
private(set) var currentIndex = 0
var options: Options
init(data: JSArray, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.data = data
self.codingPath = codingPath
self.userInfo = userInfo
self.options = options
}
}
extension UnkeyedContainer: UnkeyedDecodingContainer {
var count: Int? {
data.count
}
var isAtEnd: Bool {
currentIndex == data.endIndex
}
func decodeNil() throws -> Bool {
defer { currentIndex += 1 }
return data[currentIndex] is NSNull
}
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
defer { currentIndex += 1 }
let decoder = _JSValueDecoder(data: data[currentIndex], options: options)
return try decoder.decodeData(as: T.self)
}
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
defer { currentIndex += 1 }
guard let data = data[currentIndex] as? JSArray else {
throw DecodingError.typeMismatch(JSArray.self, on: data[currentIndex], codingPath: codingPath)
}
return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options)
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
defer { currentIndex += 1 }
guard let data = data[currentIndex] as? JSObject else {
throw DecodingError.typeMismatch(JSObject.self, on: data[currentIndex], codingPath: codingPath)
}
return KeyedDecodingContainer(KeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options))
}
func superDecoder() throws -> Decoder {
defer { currentIndex += 1 }
let data = data[currentIndex]
return _JSValueDecoder(data: data, options: options)
}
}
private final class SingleValueContainer {
var data: JSValue
var codingPath: [CodingKey]
var userInfo: CodingUserInfo
var options: Options
init(data: JSValue, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) {
self.data = data
self.codingPath = codingPath
self.userInfo = userInfo
self.options = options
}
}
extension SingleValueContainer: SingleValueDecodingContainer {
func decodeNil() -> Bool {
return data is NSNull
}
private func cast<T>(to type: T.Type) throws -> T {
guard let data = data as? T else {
throw DecodingError.typeMismatch(type, on: data, codingPath: codingPath)
}
return data
}
private func castFloat<N>(to type: N.Type) throws -> N where N: FloatingPoint {
if let data = data as? String,
case let .convertFromString(positiveInfinity: pos, negativeInfinity: neg, nan: nan) = options.nonConformingStrategy {
switch data {
case pos:
return N.infinity
case neg:
return -N.infinity
case nan:
return N.nan
default:
throw DecodingError.typeMismatch(type, on: data, codingPath: codingPath)
}
}
let data = try cast(to: N.self)
if !data.isFinite, case .throw = options.nonConformingStrategy {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "\(data) is a non-conforming floating point number"))
}
return data
}
func decode(_ type: Bool.Type) throws -> Bool {
try cast(to: type)
}
func decode(_ type: String.Type) throws -> String {
try cast(to: type)
}
func decode(_ type: Double.Type) throws -> Double {
try castFloat(to: type)
}
func decode(_ type: Float.Type) throws -> Float {
try castFloat(to: type)
}
func decode(_ type: Int.Type) throws -> Int {
try cast(to: type)
}
func decode(_ type: Int8.Type) throws -> Int8 {
try cast(to: type)
}
func decode(_ type: Int16.Type) throws -> Int16 {
try cast(to: type)
}
func decode(_ type: Int32.Type) throws -> Int32 {
try cast(to: type)
}
func decode(_ type: Int64.Type) throws -> Int64 {
try cast(to: type)
}
func decode(_ type: UInt.Type) throws -> UInt {
try cast(to: type)
}
func decode(_ type: UInt8.Type) throws -> UInt8 {
try cast(to: type)
}
func decode(_ type: UInt16.Type) throws -> UInt16 {
try cast(to: type)
}
func decode(_ type: UInt32.Type) throws -> UInt32 {
try cast(to: type)
}
func decode(_ type: UInt64.Type) throws -> UInt64 {
try cast(to: type)
}
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
let decoder = _JSValueDecoder(data: data, options: options)
return try decoder.decodeData(as: T.self)
}
}
extension DecodingError {
static func typeMismatch(_ type: Any.Type, on data: any JSValue, codingPath: [CodingKey]) -> DecodingError {
return .typeMismatch(
type,
.init(
codingPath: codingPath,
debugDescription: "\(data) was unable to be cast to \(type)."
)
)
}
static func keyNotFound(_ key: any CodingKey, on data: any JSValue, codingPath: [CodingKey]) -> DecodingError {
return .keyNotFound(
key,
.init(
codingPath: codingPath,
debugDescription: "Key \(key.stringValue) not found in \(data)")
)
}
static func dataCorrupted<T>(_ value: any JSValue, target type: T.Type, codingPath: [CodingKey]) -> DecodingError where T: Decodable {
return .dataCorrupted(.init(codingPath: codingPath, debugDescription: "\(value) was not in the format expected for \(T.self)"))
}
}