6
6
// Copyright © 2021 Mochi Development, Inc. All rights reserved.
7
7
//
8
8
9
+ import Foundation
10
+
9
11
/// `DynamicCodableDecoder` facilitates the decoding of [DynamicCodable](x-source-tag://DynamicCodable) representations into semantic `Decodable` types.
10
12
/// - Tag: DynamicCodableDecoder
11
13
open class DynamicCodableDecoder {
@@ -24,6 +26,35 @@ open class DynamicCodableDecoder {
24
26
case exactMatch
25
27
}
26
28
29
+ /// The strategy to use for decoding `Date` values.
30
+ /// - Tag: DynamicCodableDecoder.DateDecodingStrategy
31
+ public enum DateDecodingStrategy {
32
+ /// Defer to `Date` for decoding. This is the default strategy.
33
+ /// - Tag: DynamicCodableDecoder.DateDecodingStrategy.deferredToDate
34
+ case deferredToDate
35
+
36
+ /// Decode the `Date` as a UNIX timestamp from a JSON number.
37
+ /// - Tag: DynamicCodableDecoder.DateDecodingStrategy.secondsSince1970
38
+ case secondsSince1970
39
+
40
+ /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
41
+ /// - Tag: DynamicCodableDecoder.DateDecodingStrategy.millisecondsSince1970
42
+ case millisecondsSince1970
43
+
44
+ /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
45
+ /// - Tag: DynamicCodableDecoder.DateDecodingStrategy.iso8601
46
+ @available ( macOS 10 . 12 , iOS 10 . 0 , watchOS 3 . 0 , tvOS 10 . 0 , * )
47
+ case iso8601
48
+
49
+ /// Decode the `Date` as a string parsed by the given formatter.
50
+ /// - Tag: DynamicCodableDecoder.DateDecodingStrategy.formatted
51
+ case formatted( DateFormatter )
52
+
53
+ /// Decode the `Date` as a custom value decoded by the given closure.
54
+ /// - Tag: DynamicCodableDecoder.DateDecodingStrategy.custom
55
+ case custom( ( _ decoder: Swift . Decoder ) throws -> Date )
56
+ }
57
+
27
58
/// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
28
59
/// - Tag: DynamicCodableDecoder.NonConformingFloatDecodingStrategy
29
60
public enum NonConformingFloatDecodingStrategy {
@@ -40,6 +71,10 @@ open class DynamicCodableDecoder {
40
71
/// - Tag: DynamicCodableDecoder.numberDecodingStrategy
41
72
open var numberDecodingStrategy : NumberDecodingStrategy = . closestRepresentation
42
73
74
+ /// The strategy to use in decoding dates. Defaults to [.deferredToDate](x-source-tag://DynamicCodableDecoder.DateDecodingStrategy.deferredToDate).
75
+ /// - Tag: DynamicCodableDecoder.dateDecodingStrategy
76
+ open var dateDecodingStrategy : DateDecodingStrategy = . deferredToDate
77
+
43
78
/// The strategy to use in decoding non-conforming numbers. Defaults to [.throw](x-source-tag://DynamicCodableDecoder.NonConformingFloatDecodingStrategy.throw).
44
79
/// - Tag: DynamicCodableDecoder.nonConformingFloatDecodingStrategy
45
80
open var nonConformingFloatDecodingStrategy : NonConformingFloatDecodingStrategy = . throw
@@ -54,6 +89,9 @@ open class DynamicCodableDecoder {
54
89
/// - Tag: DynamicCodableDecoder.Options.numberDecodingStrategy
55
90
let numberDecodingStrategy : NumberDecodingStrategy
56
91
92
+ /// - Tag: DynamicCodableDecoder.Options.dateDecodingStrategy
93
+ let dateDecodingStrategy : DateDecodingStrategy
94
+
57
95
/// - Tag: DynamicCodableDecoder.Options.nonConformingFloatDecodingStrategy
58
96
let nonConformingFloatDecodingStrategy : NonConformingFloatDecodingStrategy
59
97
@@ -66,6 +104,7 @@ open class DynamicCodableDecoder {
66
104
fileprivate var options : Options {
67
105
return Options (
68
106
numberDecodingStrategy: numberDecodingStrategy,
107
+ dateDecodingStrategy: dateDecodingStrategy,
69
108
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
70
109
userInfo: userInfo
71
110
)
@@ -169,6 +208,8 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder {
169
208
is Primitive . Bool . Type ,
170
209
is Primitive . String . Type ,
171
210
is Primitive . Empty . Type : return try unwrapPrimitive ( )
211
+ // Special Cases
212
+ case is Date . Type : return unsafeBitCast ( try unwrapDate ( ) , to: T . self)
172
213
// Decodable Types
173
214
default : return try T ( from: self )
174
215
}
@@ -284,6 +325,32 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder {
284
325
}
285
326
}
286
327
328
+ @inline ( __always)
329
+ private func unwrapDate( ) throws -> Date {
330
+ switch options. dateDecodingStrategy {
331
+ case . deferredToDate: return try Date ( from: self )
332
+ case . secondsSince1970: return Date ( timeIntervalSince1970: try unwrapFloatingPoint ( ) )
333
+ case . millisecondsSince1970: return Date ( timeIntervalSince1970: try unwrapFloatingPoint ( ) / 1000.0 )
334
+ case . custom( let closure) : return try closure ( self )
335
+ case . iso8601:
336
+ guard #available( macOS 10 . 12 , iOS 10 . 0 , watchOS 3 . 0 , tvOS 10 . 0 , * ) else {
337
+ preconditionFailure ( " ISO8601DateFormatter is unavailable on this platform. " )
338
+ }
339
+
340
+ guard let date = _iso8601Formatter. date ( from: try unwrapPrimitive ( ) ) else {
341
+ throw dataCorruptedError ( " Expected date string to be ISO8601-formatted. " )
342
+ }
343
+
344
+ return date
345
+ case . formatted( let formatter) :
346
+ guard let date = formatter. date ( from: try unwrapPrimitive ( ) ) else {
347
+ throw dataCorruptedError ( " Date string does not match format expected by formatter. " )
348
+ }
349
+
350
+ return date
351
+ }
352
+ }
353
+
287
354
private func createTypeMismatchError( type: Any . Type ) -> DecodingError {
288
355
DecodingError . typeMismatch (
289
356
type,
@@ -435,3 +502,11 @@ extension DynamicCodableDecoder.Decoder {
435
502
func decode< T> ( _: T . Type ) throws -> T where T: Decodable { try decoder. unwrap ( ) }
436
503
}
437
504
}
505
+
506
+ // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS.
507
+ @available ( macOS 10 . 12 , iOS 10 . 0 , watchOS 3 . 0 , tvOS 10 . 0 , * )
508
+ private var _iso8601Formatter : ISO8601DateFormatter = {
509
+ let formatter = ISO8601DateFormatter ( )
510
+ formatter. formatOptions = . withInternetDateTime
511
+ return formatter
512
+ } ( )
0 commit comments