Skip to content

Commit

Permalink
Added support for date decoding strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitribouniol committed Jun 4, 2021
1 parent ed1aebf commit 7b409b3
Showing 1 changed file with 75 additions and 0 deletions.
75 changes: 75 additions & 0 deletions Sources/DynamicCodable/DynamicCodableDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
// Copyright © 2021 Mochi Development, Inc. All rights reserved.
//

import Foundation

/// `DynamicCodableDecoder` facilitates the decoding of [DynamicCodable](x-source-tag://DynamicCodable) representations into semantic `Decodable` types.
/// - Tag: DynamicCodableDecoder
open class DynamicCodableDecoder {
Expand All @@ -24,6 +26,35 @@ open class DynamicCodableDecoder {
case exactMatch
}

/// The strategy to use for decoding `Date` values.
/// - Tag: DynamicCodableDecoder.DateDecodingStrategy
public enum DateDecodingStrategy {
/// Defer to `Date` for decoding. This is the default strategy.
/// - Tag: DynamicCodableDecoder.DateDecodingStrategy.deferredToDate
case deferredToDate

/// Decode the `Date` as a UNIX timestamp from a JSON number.
/// - Tag: DynamicCodableDecoder.DateDecodingStrategy.secondsSince1970
case secondsSince1970

/// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
/// - Tag: DynamicCodableDecoder.DateDecodingStrategy.millisecondsSince1970
case millisecondsSince1970

/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
/// - Tag: DynamicCodableDecoder.DateDecodingStrategy.iso8601
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
case iso8601

/// Decode the `Date` as a string parsed by the given formatter.
/// - Tag: DynamicCodableDecoder.DateDecodingStrategy.formatted
case formatted(DateFormatter)

/// Decode the `Date` as a custom value decoded by the given closure.
/// - Tag: DynamicCodableDecoder.DateDecodingStrategy.custom
case custom((_ decoder: Swift.Decoder) throws -> Date)
}

/// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
/// - Tag: DynamicCodableDecoder.NonConformingFloatDecodingStrategy
public enum NonConformingFloatDecodingStrategy {
Expand All @@ -40,6 +71,10 @@ open class DynamicCodableDecoder {
/// - Tag: DynamicCodableDecoder.numberDecodingStrategy
open var numberDecodingStrategy: NumberDecodingStrategy = .closestRepresentation

/// The strategy to use in decoding dates. Defaults to [.deferredToDate](x-source-tag://DynamicCodableDecoder.DateDecodingStrategy.deferredToDate).
/// - Tag: DynamicCodableDecoder.dateDecodingStrategy
open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate

/// The strategy to use in decoding non-conforming numbers. Defaults to [.throw](x-source-tag://DynamicCodableDecoder.NonConformingFloatDecodingStrategy.throw).
/// - Tag: DynamicCodableDecoder.nonConformingFloatDecodingStrategy
open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw
Expand All @@ -54,6 +89,9 @@ open class DynamicCodableDecoder {
/// - Tag: DynamicCodableDecoder.Options.numberDecodingStrategy
let numberDecodingStrategy: NumberDecodingStrategy

/// - Tag: DynamicCodableDecoder.Options.dateDecodingStrategy
let dateDecodingStrategy: DateDecodingStrategy

/// - Tag: DynamicCodableDecoder.Options.nonConformingFloatDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy

Expand All @@ -66,6 +104,7 @@ open class DynamicCodableDecoder {
fileprivate var options: Options {
return Options(
numberDecodingStrategy: numberDecodingStrategy,
dateDecodingStrategy: dateDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
userInfo: userInfo
)
Expand Down Expand Up @@ -169,6 +208,8 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder {
is Primitive.Bool.Type,
is Primitive.String.Type,
is Primitive.Empty.Type: return try unwrapPrimitive()
// Special Cases
case is Date.Type: return unsafeBitCast(try unwrapDate(), to: T.self)
// Decodable Types
default: return try T(from: self)
}
Expand Down Expand Up @@ -284,6 +325,32 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder {
}
}

@inline(__always)
private func unwrapDate() throws -> Date {
switch options.dateDecodingStrategy {
case .deferredToDate: return try Date(from: self)
case .secondsSince1970: return Date(timeIntervalSince1970: try unwrapFloatingPoint())
case .millisecondsSince1970: return Date(timeIntervalSince1970: try unwrapFloatingPoint() / 1000.0)
case .custom(let closure): return try closure(self)
case .iso8601:
guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else {
preconditionFailure("ISO8601DateFormatter is unavailable on this platform.")
}

guard let date = _iso8601Formatter.date(from: try unwrapPrimitive()) else {
throw dataCorruptedError("Expected date string to be ISO8601-formatted.")
}

return date
case .formatted(let formatter):
guard let date = formatter.date(from: try unwrapPrimitive()) else {
throw dataCorruptedError("Date string does not match format expected by formatter.")
}

return date
}
}

private func createTypeMismatchError(type: Any.Type) -> DecodingError {
DecodingError.typeMismatch(
type,
Expand Down Expand Up @@ -435,3 +502,11 @@ extension DynamicCodableDecoder.Decoder {
func decode<T>(_: T.Type) throws -> T where T: Decodable { try decoder.unwrap() }
}
}

// 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.
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
private var _iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}()

0 comments on commit 7b409b3

Please sign in to comment.