66// Copyright © 2021 Mochi Development, Inc. All rights reserved.
77//
88
9+ import Foundation
10+
911/// `DynamicCodableDecoder` facilitates the decoding of [DynamicCodable](x-source-tag://DynamicCodable) representations into semantic `Decodable` types.
1012/// - Tag: DynamicCodableDecoder
1113open class DynamicCodableDecoder {
@@ -24,6 +26,35 @@ open class DynamicCodableDecoder {
2426 case exactMatch
2527 }
2628
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+
2758 /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
2859 /// - Tag: DynamicCodableDecoder.NonConformingFloatDecodingStrategy
2960 public enum NonConformingFloatDecodingStrategy {
@@ -40,6 +71,10 @@ open class DynamicCodableDecoder {
4071 /// - Tag: DynamicCodableDecoder.numberDecodingStrategy
4172 open var numberDecodingStrategy : NumberDecodingStrategy = . closestRepresentation
4273
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+
4378 /// The strategy to use in decoding non-conforming numbers. Defaults to [.throw](x-source-tag://DynamicCodableDecoder.NonConformingFloatDecodingStrategy.throw).
4479 /// - Tag: DynamicCodableDecoder.nonConformingFloatDecodingStrategy
4580 open var nonConformingFloatDecodingStrategy : NonConformingFloatDecodingStrategy = . throw
@@ -54,6 +89,9 @@ open class DynamicCodableDecoder {
5489 /// - Tag: DynamicCodableDecoder.Options.numberDecodingStrategy
5590 let numberDecodingStrategy : NumberDecodingStrategy
5691
92+ /// - Tag: DynamicCodableDecoder.Options.dateDecodingStrategy
93+ let dateDecodingStrategy : DateDecodingStrategy
94+
5795 /// - Tag: DynamicCodableDecoder.Options.nonConformingFloatDecodingStrategy
5896 let nonConformingFloatDecodingStrategy : NonConformingFloatDecodingStrategy
5997
@@ -66,6 +104,7 @@ open class DynamicCodableDecoder {
66104 fileprivate var options : Options {
67105 return Options (
68106 numberDecodingStrategy: numberDecodingStrategy,
107+ dateDecodingStrategy: dateDecodingStrategy,
69108 nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
70109 userInfo: userInfo
71110 )
@@ -169,6 +208,8 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder {
169208 is Primitive . Bool . Type ,
170209 is Primitive . String . Type ,
171210 is Primitive . Empty . Type : return try unwrapPrimitive ( )
211+ // Special Cases
212+ case is Date . Type : return unsafeBitCast ( try unwrapDate ( ) , to: T . self)
172213 // Decodable Types
173214 default : return try T ( from: self )
174215 }
@@ -284,6 +325,32 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder {
284325 }
285326 }
286327
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+
287354 private func createTypeMismatchError( type: Any . Type ) -> DecodingError {
288355 DecodingError . typeMismatch (
289356 type,
@@ -435,3 +502,11 @@ extension DynamicCodableDecoder.Decoder {
435502 func decode< T> ( _: T . Type ) throws -> T where T: Decodable { try decoder. unwrap ( ) }
436503 }
437504}
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