Skip to content

Commit 7b409b3

Browse files
Added support for date decoding strategies
1 parent ed1aebf commit 7b409b3

File tree

1 file changed

+75
-0
lines changed

1 file changed

+75
-0
lines changed

Sources/DynamicCodable/DynamicCodableDecoder.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
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
1113
open 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

Comments
 (0)