From 7f85fe36148e574657b4c53dc771d4ec6b9b9615 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sat, 17 Apr 2021 19:51:53 -0700 Subject: [PATCH 01/10] Added DynamicCodableDecoder.swift --- Sources/DynamicCodable/DynamicCodableDecoder.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Sources/DynamicCodable/DynamicCodableDecoder.swift diff --git a/Sources/DynamicCodable/DynamicCodableDecoder.swift b/Sources/DynamicCodable/DynamicCodableDecoder.swift new file mode 100644 index 0000000..15bbd0a --- /dev/null +++ b/Sources/DynamicCodable/DynamicCodableDecoder.swift @@ -0,0 +1,8 @@ +// +// DynamicCodableDecoder.swift +// DynamicCodable +// +// Created by Dimitri Bouniol on 4/17/21. +// Copyright © 2021 Mochi Development, Inc. All rights reserved. +// + From 67c9314206121c294c29476867f7bf38a0ff0017 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 18 Apr 2021 12:00:05 -0700 Subject: [PATCH 02/10] Added CoderInternals.swift --- Sources/DynamicCodable/CoderInternals.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Sources/DynamicCodable/CoderInternals.swift diff --git a/Sources/DynamicCodable/CoderInternals.swift b/Sources/DynamicCodable/CoderInternals.swift new file mode 100644 index 0000000..ba46957 --- /dev/null +++ b/Sources/DynamicCodable/CoderInternals.swift @@ -0,0 +1,8 @@ +// +// CoderInternals.swift +// DynamicCodable +// +// Created by Dimitri Bouniol on 4/18/21. +// Copyright © 2021 Mochi Development, Inc. All rights reserved. +// + From 802f7528e1ab38c1121365e81fbad0c342cfdb50 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 18 Apr 2021 12:00:30 -0700 Subject: [PATCH 03/10] Added missing Tags for convenience cases --- Sources/DynamicCodable/DynamicCodable.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/DynamicCodable/DynamicCodable.swift b/Sources/DynamicCodable/DynamicCodable.swift index 9e78281..7e75ada 100644 --- a/Sources/DynamicCodable/DynamicCodable.swift +++ b/Sources/DynamicCodable/DynamicCodable.swift @@ -86,12 +86,14 @@ extension DynamicCodable { /// A convenience case for creating a [float32 case](x-source-tag://DynamicCodable.float32). /// - Parameter float: The float to represent. /// - Returns: DynamicCodable.float32 + /// - Tag: DynamicCodable.float @inlinable public static func float(_ float: Float) -> Self { .float32(float) } /// A convenience case for creating a [float64 case](x-source-tag://DynamicCodable.float64). /// - Parameter float: The float to represent. /// - Returns: DynamicCodable.float64 + /// - Tag: DynamicCodable.double @inlinable public static func double(_ double: Double) -> Self { .float64(double) } } From 9f4a25f907f6ea26da95762a58f08fa063aff92b Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 18 Apr 2021 12:01:04 -0700 Subject: [PATCH 04/10] Added a canonical listing of types DynamicCodable uses --- Sources/DynamicCodable/DynamicCodable.swift | 78 ++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/Sources/DynamicCodable/DynamicCodable.swift b/Sources/DynamicCodable/DynamicCodable.swift index 7e75ada..8a2f434 100644 --- a/Sources/DynamicCodable/DynamicCodable.swift +++ b/Sources/DynamicCodable/DynamicCodable.swift @@ -11,11 +11,11 @@ public enum DynamicCodable: Equatable, Hashable { /// A value coded using a keyed container such as a dictionary. /// - Tag: DynamicCodable.keyed - case keyed([Key : Self]) + case keyed(Keyed) /// A value coded using a keyed container such as an array. /// - Tag: DynamicCodable.unkeyed - case unkeyed([Self]) + case unkeyed(Unkeyed) /// A value coding nil as a single value container. /// - Tag: DynamicCodable.nil @@ -80,6 +80,80 @@ public enum DynamicCodable: Equatable, Hashable { /// A (rare) value coding an empty single value container. Only certain decoders may even support this. /// - Tag: DynamicCodable.empty case empty + + // MARK: - DynamicCodableTypes + + /// The underlying type for [.keyed](x-source-tag://DynamicCodable.keyed) values. + /// - Tag: DynamicCodable.Keyed + public typealias Keyed = [DynamicCodable.Key : DynamicCodable] + + /// The underlying type for [.unkeyed](x-source-tag://DynamicCodable.unkeyed) values. + /// - Tag: DynamicCodable.Unkeyed + public typealias Unkeyed = [DynamicCodable] + + /// The underlying type for [.nil](x-source-tag://DynamicCodable.nil) values. + /// - Tag: DynamicCodable.Nil + public typealias Nil = Optional + + /// The underlying type for [.bool](x-source-tag://DynamicCodable.bool) values. + /// - Tag: DynamicCodable.Bool + public typealias Bool = Swift.Bool + + /// The underlying type for [.string](x-source-tag://DynamicCodable.string) values. + /// - Tag: DynamicCodable.String + public typealias String = Swift.String + + /// The underlying type for [.float64](x-source-tag://DynamicCodable.float64) values. + /// - Tag: DynamicCodable.Float64 + public typealias Float64 = Swift.Float64 + + /// The underlying type for [.float32](x-source-tag://DynamicCodable.float32) values. + /// - Tag: DynamicCodable.Float32 + public typealias Float32 = Swift.Float32 + + /// The underlying type for [.int](x-source-tag://DynamicCodable.int) values. + /// - Tag: DynamicCodable.Int + public typealias Int = Swift.Int + + /// The underlying type for [.int8](x-source-tag://DynamicCodable.int8) values. + /// - Tag: DynamicCodable.Int8 + public typealias Int8 = Swift.Int8 + + /// The underlying type for [.int16](x-source-tag://DynamicCodable.int16) values. + /// - Tag: DynamicCodable.Int16 + public typealias Int16 = Swift.Int16 + + /// The underlying type for [.int32](x-source-tag://DynamicCodable.int32) values. + /// - Tag: DynamicCodable.Int32 + public typealias Int32 = Swift.Int32 + + /// The underlying type for [.int64](x-source-tag://DynamicCodable.int64) values. + /// - Tag: DynamicCodable.Int64 + public typealias Int64 = Swift.Int64 + + /// The underlying type for [.uint](x-source-tag://DynamicCodable.uint) values. + /// - Tag: DynamicCodable.UInt + public typealias UInt = Swift.UInt + + /// The underlying type for [.uint8](x-source-tag://DynamicCodable.uint8) values. + /// - Tag: DynamicCodable.UInt8 + public typealias UInt8 = Swift.UInt8 + + /// The underlying type for [.uint16](x-source-tag://DynamicCodable.uint16) values. + /// - Tag: DynamicCodable.UInt16 + public typealias UInt16 = Swift.UInt16 + + /// The underlying type for [.uint32](x-source-tag://DynamicCodable.uint32) values. + /// - Tag: DynamicCodable.UInt32 + public typealias UInt32 = Swift.UInt32 + + /// The underlying type for [.uint64](x-source-tag://DynamicCodable.uint64) values. + /// - Tag: DynamicCodable.UInt64 + public typealias UInt64 = Swift.UInt64 + + /// The underlying type for [.empty](x-source-tag://DynamicCodable.empty) values. + /// - Tag: DynamicCodable.Empty + public typealias Empty = Swift.Void } extension DynamicCodable { From f4c7a38e758b041eed94842b4329fd2ad6912bc4 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 18 Apr 2021 12:03:38 -0700 Subject: [PATCH 05/10] Added a DynamicCodableDecoder --- Sources/DynamicCodable/CoderInternals.swift | 92 ++++ .../DynamicCodableDecoder.swift | 403 ++++++++++++++++++ .../DynamicCodableTests.swift | 46 ++ 3 files changed, 541 insertions(+) diff --git a/Sources/DynamicCodable/CoderInternals.swift b/Sources/DynamicCodable/CoderInternals.swift index ba46957..bf4859b 100644 --- a/Sources/DynamicCodable/CoderInternals.swift +++ b/Sources/DynamicCodable/CoderInternals.swift @@ -6,3 +6,95 @@ // Copyright © 2021 Mochi Development, Inc. All rights reserved. // +extension Dictionary where Key == DynamicCodable.Key, Value == DynamicCodable { + @inline(__always) + subscript(key: CodingKey) -> DynamicCodable? { + if let intKey = key.intValue, let value = self[intKey] { + return value + } else if let value = self[key.stringValue] { + return value + } + return nil + } +} + +struct DynamicCoderCodingKey: CodingKey { + public var stringValue: String + public var intValue: Int? + + public init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + public init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + init(stringValue: String, intValue: Int?) { + self.stringValue = stringValue + self.intValue = intValue + } + + init(index: Int) { + self.stringValue = "Index \(index)" + self.intValue = index + } + + static let `super` = DynamicCoderCodingKey(stringValue: "super", intValue: nil) +} + +extension DynamicCodable { + var debugDataTypeDescription: String { + switch self { + case .keyed(_): return "a keyed container" + case .unkeyed(_): return "an unkeyed container" + case .nil: return "nil" + case .bool(_): return "a boolean" + case .string(_): return "a string" + case .float64(_): return "a float64" + case .float32(_): return "a float32" + case .int(_): return "an int" + case .int8(_): return "an int8" + case .int16(_): return "an int16" + case .int32(_): return "an int32" + case .int64(_): return "an int64" + case .uint(_): return "a uint" + case .uint8(_): return "a uint8" + case .uint16(_): return "a uint16" + case .uint32(_): return "a uint32" + case .uint64(_): return "a uint64" + case .empty: return "an empty container" + } + } + + @inline(__always) + func unwrap(errorHandler: () throws -> Never) rethrows -> T { + let value: Any + + switch self { + case .keyed(let keyed): value = keyed + case .unkeyed(let unkeyed): value = unkeyed + case .nil: value = Nil.none as Any + case .bool(let bool): value = bool + case .string(let string): value = string + case .float64(let float64): value = float64 + case .float32(let float32): value = float32 + case .int(let int): value = int + case .int8(let int8): value = int8 + case .int16(let int16): value = int16 + case .int32(let int32): value = int32 + case .int64(let int64): value = int64 + case .uint(let uint): value = uint + case .uint8(let uint8): value = uint8 + case .uint16(let uint16): value = uint16 + case .uint32(let uint32): value = uint32 + case .uint64(let uint64): value = uint64 + case .empty: value = () + } + + guard let value = value as? T else { try errorHandler() } + return value + } +} diff --git a/Sources/DynamicCodable/DynamicCodableDecoder.swift b/Sources/DynamicCodable/DynamicCodableDecoder.swift index 15bbd0a..7424664 100644 --- a/Sources/DynamicCodable/DynamicCodableDecoder.swift +++ b/Sources/DynamicCodable/DynamicCodableDecoder.swift @@ -6,3 +6,406 @@ // Copyright © 2021 Mochi Development, Inc. All rights reserved. // +/// `DynamicCodableDecoder` facilitates the decoding of [DynamicCodable](x-source-tag://DynamicCodable) representations into semantic `Decodable` types. +/// - Tag: DynamicCodableDecoder +open class DynamicCodableDecoder { + // MARK: Options + + /// Contextual user-provided information for use during decoding. + /// - Tag: DynamicCodableDecoder.userInfo + open var userInfo: [CodingUserInfoKey: Any] = [:] + + /// Options set on the top-level encoder to pass down the decoding hierarchy. + /// - Tag: DynamicCodableDecoder.Options + fileprivate struct Options { + /// - Tag: DynamicCodableDecoder.Options.userInfo + let userInfo: [CodingUserInfoKey: Any] + } + + /// The options set on the top-level decoder. + /// - Tag: DynamicCodableDecoder.options + fileprivate var options: Options { + return Options( + userInfo: userInfo + ) + } + + // MARK: - Constructing a DynamicCodable Decoder + /// Initializes `self` with default strategies. + /// - Tag: DynamicCodableDecoder.init + public init() {} + + // MARK: - Decoding Values + /// Decodes a top-level value of the given type from the given [DynamicCodable](x-source-tag://DynamicCodable) representation. + /// + /// - parameter type: The type of the value to decode. + /// - parameter data: The data to decode from. + /// - returns: A value of the requested type. + /// - throws: An error if any value throws an error during decoding. + /// - Tag: DynamicCodableDecoder.decode + open func decode(_ type: T.Type, from representation: DynamicCodable) throws -> T { + try Decoder(from: representation, codingPath: [], options: options).unwrap() + } +} + +// MARK: - Decoder + +extension DynamicCodableDecoder { + fileprivate struct Decoder { + let codingPath: [CodingKey] + + let representation: DynamicCodable + let options: Options + + init(from representation: DynamicCodable, codingPath: [CodingKey], options: Options) { + self.codingPath = codingPath + self.representation = representation + self.options = options + } + + func appending(_ key: CodingKey, newValue: DynamicCodable) -> Self { + Self(from: newValue, codingPath: codingPath + [key], options: options) + } + } +} + +extension DynamicCodableDecoder.Decoder: Swift.Decoder { + var userInfo: [CodingUserInfoKey: Any] { options.userInfo } + + @usableFromInline + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + guard case .keyed(let keyedRepresentation) = representation else { + throw createTypeMismatchError(type: [DynamicCodable.Key : DynamicCodable].self) + } + + let container = KeyedContainer( + decoder: self, + representation: keyedRepresentation + ) + return KeyedDecodingContainer(container) + } + + @usableFromInline + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + guard case .unkeyed(let unkeyedRepresentation) = representation else { + throw createTypeMismatchError(type: [DynamicCodable].self) + } + + return UnkeyedContainer( + decoder: self, + representation: unkeyedRepresentation + ) + } + + @usableFromInline + func singleValueContainer() throws -> SingleValueDecodingContainer { + SingleValueContainter(decoder: self) + } + + @inline(__always) + func unwrap() throws -> T { + let value = representation + let error = createTypeMismatchError(type: T.self) + + typealias Primitive = DynamicCodable + + switch T.self { + // Return DynamicCodable as is if it is being decoded + case is DynamicCodable.Type: return unsafeBitCast(value, to: T.self) + // Primitive Types fast-path + case is Primitive.Keyed.Type, + is Primitive.Unkeyed.Type, + is Primitive.Nil.Type, + is Primitive.Bool.Type, + is Primitive.String.Type, + is Primitive.Float64.Type, + is Primitive.Float32.Type, + is Primitive.Int.Type, + is Primitive.Int8.Type, + is Primitive.Int16.Type, + is Primitive.Int32.Type, + is Primitive.Int64.Type, + is Primitive.UInt.Type, + is Primitive.UInt8.Type, + is Primitive.UInt16.Type, + is Primitive.UInt32.Type, + is Primitive.UInt64.Type, + is Primitive.Empty.Type: return try value.unwrap { throw error } + // Decodable Types + default: return try T(from: self) + } + } + + @inline(__always) + private func unwrapFloatingPoint() throws -> T { + @inline(__always) + func validate(_ floatingPoint: T, originalValue: CustomStringConvertible) throws -> T { + guard floatingPoint.isFinite else { + throw DecodingError.dataCorrupted( + .init( + codingPath: codingPath, + debugDescription: "Represented number <\(floatingPoint)> does not fit in \(T.self)." + ) + ) + } + + return floatingPoint + } + + switch representation { + case .float64(let number): return try validate(T(number), originalValue: number) + case .float32(let number): return try validate(T(number), originalValue: number) + case .int(let number): return try validate(T(number), originalValue: number) + case .int8(let number): return try validate(T(number), originalValue: number) + case .int16(let number): return try validate(T(number), originalValue: number) + case .int32(let number): return try validate(T(number), originalValue: number) + case .int64(let number): return try validate(T(number), originalValue: number) + case .uint(let number): return try validate(T(number), originalValue: number) + case .uint8(let number): return try validate(T(number), originalValue: number) + case .uint16(let number): return try validate(T(number), originalValue: number) + case .uint32(let number): return try validate(T(number), originalValue: number) + case .uint64(let number): return try validate(T(number), originalValue: number) + + case .string, + .bool, + .keyed, + .unkeyed, + .empty, + .nil: + throw self.createTypeMismatchError(type: T.self) + } + } + + @inline(__always) + private func unwrapFixedWidthInteger() throws -> T { + @inline(__always) + func validate(_ fixedWidthInteger: T?, originalValue: CustomStringConvertible) throws -> T { + guard let fixedWidthInteger = fixedWidthInteger else { + throw DecodingError.dataCorrupted( + .init( + codingPath: codingPath, + debugDescription: "Represented number <\(originalValue)> does not fit in \(T.self)." + ) + ) + } + + return fixedWidthInteger + } + + switch representation { + case .int(let number): return try validate(T(exactly: number), originalValue: number) + case .int8(let number): return try validate(T(exactly: number), originalValue: number) + case .int16(let number): return try validate(T(exactly: number), originalValue: number) + case .int32(let number): return try validate(T(exactly: number), originalValue: number) + case .int64(let number): return try validate(T(exactly: number), originalValue: number) + case .uint(let number): return try validate(T(exactly: number), originalValue: number) + case .uint8(let number): return try validate(T(exactly: number), originalValue: number) + case .uint16(let number): return try validate(T(exactly: number), originalValue: number) + case .uint32(let number): return try validate(T(exactly: number), originalValue: number) + case .uint64(let number): return try validate(T(exactly: number), originalValue: number) + case .float64(let number): return try validate(T(exactly: number), originalValue: number) + case .float32(let number): return try validate(T(exactly: number), originalValue: number) + case .string, + .bool, + .keyed, + .unkeyed, + .empty, + .nil: + throw self.createTypeMismatchError(type: T.self) + } + } + + private func createTypeMismatchError(type: Any.Type) -> DecodingError { + DecodingError.typeMismatch( + type, + .init( + codingPath: codingPath, + debugDescription: "Expected to decode \(type) but found \(representation.debugDataTypeDescription) instead." + ) + ) + } +} + +extension DynamicCodableDecoder.Decoder { + struct KeyedContainer: KeyedDecodingContainerProtocol { + let decoder: DynamicCodableDecoder.Decoder + let representation: [DynamicCodable.Key : DynamicCodable] + + var codingPath: [CodingKey] { decoder.codingPath } + + var allKeys: [Key] { + representation.keys.compactMap { dynamicKey in + switch dynamicKey { + case .int(let int): + return Key(intValue: int) + case .string(let string): + return Key(stringValue: string) + } + } + } + + func contains(_ key: Key) -> Bool { representation[key] != nil } + + @inline(__always) + private func getValue(forKey key: Key, transform: (_ decoder: DynamicCodableDecoder.Decoder) throws -> Result) throws -> Result { + guard let value = representation[key] else { + throw DecodingError.keyNotFound( + key, + .init( + codingPath: codingPath, + debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\")." + ) + ) + } + + do { + return try transform(decoder.appending(key, newValue: value)) + } catch { + throw error + } + } + + func decodeNil(forKey key: Key) throws -> Bool { try getValue(forKey: key) { $0.representation == .nil } } + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try getValue(forKey: key) { try $0.unwrap() } } + func decode(_ type: String.Type, forKey key: Key) throws -> String { try getValue(forKey: key) { try $0.unwrap() } } + + func decode(_: Double.Type, forKey key: Key) throws -> Double { try getValue(forKey: key) { try $0.unwrapFloatingPoint() } } + func decode(_: Float.Type, forKey key: Key) throws -> Float { try getValue(forKey: key) { try $0.unwrapFloatingPoint() } } + + func decode(_: Int.Type, forKey key: Key) throws -> Int { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: Int8.Type, forKey key: Key) throws -> Int8 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: Int16.Type, forKey key: Key) throws -> Int16 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: Int32.Type, forKey key: Key) throws -> Int32 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: Int64.Type, forKey key: Key) throws -> Int64 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: UInt.Type, forKey key: Key) throws -> UInt { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: UInt8.Type, forKey key: Key) throws -> UInt8 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: UInt16.Type, forKey key: Key) throws -> UInt16 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: UInt32.Type, forKey key: Key) throws -> UInt32 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + func decode(_: UInt64.Type, forKey key: Key) throws -> UInt64 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } + + func decode(_: T.Type, forKey key: Key) throws -> T where T: Decodable { try getValue(forKey: key) { try $0.unwrap() } } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + try getValue(forKey: key) { try $0.container(keyedBy: type) } + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + try getValue(forKey: key) { try $0.unkeyedContainer() } + } + + func superDecoder() throws -> Swift.Decoder { try getValue(forKey: DynamicCoderCodingKey.super) { $0 } } + func superDecoder(forKey key: Key) throws -> Swift.Decoder { try getValue(forKey: key) { $0 } } + } +} + +extension DynamicCodableDecoder.Decoder { + struct UnkeyedContainer: UnkeyedDecodingContainer { + let decoder: DynamicCodableDecoder.Decoder + let representation: [DynamicCodable] + + var codingPath: [CodingKey] { decoder.codingPath } + var count: Int? { representation.count } + var isAtEnd: Bool { currentIndex >= representation.count } + + var currentIndex = 0 + + struct DontIncrementButContinue: Error { + var value: T + } + + @inline(__always) + private mutating func getNextValue(transform: (_ decoder: DynamicCodableDecoder.Decoder) throws -> Result) throws -> Result { + guard !self.isAtEnd else { + var message = "Unkeyed container is at end." + if Result.self == UnkeyedContainer.self { + message = "Cannot get nested unkeyed container -- unkeyed container is at end." + } + if Result.self == Swift.Decoder.self { + message = "Cannot get superDecoder() -- unkeyed container is at end." + } + + throw DecodingError.valueNotFound( + Result.self, + .init( + codingPath: codingPath + [DynamicCoderCodingKey(index: currentIndex)], + debugDescription: message, + underlyingError: nil + ) + ) + } + + do { + let result = try transform(decoder.appending(DynamicCoderCodingKey(index: currentIndex), newValue: representation[currentIndex])) + currentIndex += 1 + return result + } catch let error as DontIncrementButContinue { + return error.value + } catch { + throw error + } + } + + mutating func decodeNil() throws -> Bool { + try getNextValue { decoder in + // The protocol states: If the value is not null, does not increment currentIndex. + if decoder.representation != .nil { throw DontIncrementButContinue(value: false) } + return true + } + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { try getNextValue { try $0.unwrap() } } + mutating func decode(_ type: String.Type) throws -> String { try getNextValue { try $0.unwrap() } } + + mutating func decode(_: Double.Type) throws -> Double { try getNextValue { try $0.unwrapFloatingPoint() } } + mutating func decode(_: Float.Type) throws -> Float { try getNextValue { try $0.unwrapFloatingPoint() } } + + mutating func decode(_: Int.Type) throws -> Int { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: Int8.Type) throws -> Int8 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: Int16.Type) throws -> Int16 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: Int32.Type) throws -> Int32 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: Int64.Type) throws -> Int64 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: UInt.Type) throws -> UInt { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: UInt8.Type) throws -> UInt8 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: UInt16.Type) throws -> UInt16 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: UInt32.Type) throws -> UInt32 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + mutating func decode(_: UInt64.Type) throws -> UInt64 { try getNextValue { try $0.unwrapFixedWidthInteger() } } + + mutating func decode(_: T.Type) throws -> T where T: Decodable { try getNextValue { try $0.unwrap() } } + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + try getNextValue { try $0.container(keyedBy: type) } + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { try getNextValue { try $0.unkeyedContainer() } } + + mutating func superDecoder() throws -> Swift.Decoder { try getNextValue { $0 } } + } +} + +extension DynamicCodableDecoder.Decoder { + struct SingleValueContainter: SingleValueDecodingContainer { + let decoder: DynamicCodableDecoder.Decoder + var codingPath: [CodingKey] { decoder.codingPath } + + func decodeNil() -> Bool { decoder.representation == .nil } + + func decode(_: Bool.Type) throws -> Bool { try decoder.unwrap() } + func decode(_: String.Type) throws -> String { try decoder.unwrap() } + + func decode(_: Double.Type) throws -> Double { try decoder.unwrapFloatingPoint() } + func decode(_: Float.Type) throws -> Float { try decoder.unwrapFloatingPoint() } + + func decode(_: Int.Type) throws -> Int { try decoder.unwrapFixedWidthInteger() } + func decode(_: Int8.Type) throws -> Int8 { try decoder.unwrapFixedWidthInteger() } + func decode(_: Int16.Type) throws -> Int16 { try decoder.unwrapFixedWidthInteger() } + func decode(_: Int32.Type) throws -> Int32 { try decoder.unwrapFixedWidthInteger() } + func decode(_: Int64.Type) throws -> Int64 { try decoder.unwrapFixedWidthInteger() } + func decode(_: UInt.Type) throws -> UInt { try decoder.unwrapFixedWidthInteger() } + func decode(_: UInt8.Type) throws -> UInt8 { try decoder.unwrapFixedWidthInteger() } + func decode(_: UInt16.Type) throws -> UInt16 { try decoder.unwrapFixedWidthInteger() } + func decode(_: UInt32.Type) throws -> UInt32 { try decoder.unwrapFixedWidthInteger() } + func decode(_: UInt64.Type) throws -> UInt64 { try decoder.unwrapFixedWidthInteger() } + + func decode(_: T.Type) throws -> T where T: Decodable { try decoder.unwrap() } + } +} diff --git a/Tests/DynamicCodableTests/DynamicCodableTests.swift b/Tests/DynamicCodableTests/DynamicCodableTests.swift index f243320..fcc2e8f 100644 --- a/Tests/DynamicCodableTests/DynamicCodableTests.swift +++ b/Tests/DynamicCodableTests/DynamicCodableTests.swift @@ -694,4 +694,50 @@ final class DynamicCodableTests: XCTestCase { XCTFail("Error occurred: \(error)") } } + + func testDynamicCodableDecoder() { + do { + struct Struct: Equatable, Codable { + let string: String + let int: Int + let int16: Int16 + let optional: String? + } + + let data: DynamicCodable = .keyed([ + "string": "A", + "int": 2, + "int16": .int16(2685), + "optional": nil + ]) + + let testRepresentation = Struct( + string: "A", + int: 2, + int16: 2685, + optional: nil + ) + + let decoder = DynamicCodableDecoder() + let representation = try decoder.decode(Struct.self, from: data) + XCTAssertEqual(representation, testRepresentation) + } catch { + XCTFail("Error occurred: \(error)") + } + + do { + let data: DynamicCodable = .keyed([ + "string": "A", + "int": 2, + "int16": .int16(2685), + "optional": nil + ]) + + let decoder = DynamicCodableDecoder() + let representation = try decoder.decode(DynamicCodable.self, from: data) + XCTAssertEqual(representation, data) + } catch { + XCTFail("Error occurred: \(error)") + } + } } From 9bdca3a2077c4444a3cf0e29d09587fcdccc01e7 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Sun, 18 Apr 2021 12:07:02 -0700 Subject: [PATCH 06/10] Updated primitive value unwrapping to enable static type optimizations during compilation This should be faster than the previous case, since the compiler should be able to optimize the switch away when it inlines the function for a specific generic type. --- Sources/DynamicCodable/CoderInternals.swift | 44 ++++++++++----------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Sources/DynamicCodable/CoderInternals.swift b/Sources/DynamicCodable/CoderInternals.swift index bf4859b..76d9cfb 100644 --- a/Sources/DynamicCodable/CoderInternals.swift +++ b/Sources/DynamicCodable/CoderInternals.swift @@ -71,30 +71,28 @@ extension DynamicCodable { @inline(__always) func unwrap(errorHandler: () throws -> Never) rethrows -> T { - let value: Any - - switch self { - case .keyed(let keyed): value = keyed - case .unkeyed(let unkeyed): value = unkeyed - case .nil: value = Nil.none as Any - case .bool(let bool): value = bool - case .string(let string): value = string - case .float64(let float64): value = float64 - case .float32(let float32): value = float32 - case .int(let int): value = int - case .int8(let int8): value = int8 - case .int16(let int16): value = int16 - case .int32(let int32): value = int32 - case .int64(let int64): value = int64 - case .uint(let uint): value = uint - case .uint8(let uint8): value = uint8 - case .uint16(let uint16): value = uint16 - case .uint32(let uint32): value = uint32 - case .uint64(let uint64): value = uint64 - case .empty: value = () + switch T.self { + case is Keyed.Type: if case .keyed(let keyed) = self { return unsafeBitCast(keyed, to: T.self) } + case is Unkeyed.Type: if case .unkeyed(let unkeyed) = self { return unsafeBitCast(unkeyed, to: T.self) } + case is Nil.Type: if case .nil = self { return unsafeBitCast(Nil.none, to: T.self) } + case is Bool.Type: if case .bool(let bool) = self { return unsafeBitCast(bool, to: T.self) } + case is String.Type: if case .string(let string) = self { return unsafeBitCast(string, to: T.self) } + case is Float64.Type: if case .float64(let float64) = self { return unsafeBitCast(float64, to: T.self) } + case is Float32.Type: if case .float64(let float32) = self { return unsafeBitCast(float32, to: T.self) } + case is Int.Type: if case .int(let int) = self { return unsafeBitCast(int, to: T.self) } + case is Int8.Type: if case .int8(let int8) = self { return unsafeBitCast(int8, to: T.self) } + case is Int16.Type: if case .int16(let int16) = self { return unsafeBitCast(int16, to: T.self) } + case is Int32.Type: if case .int32(let int32) = self { return unsafeBitCast(int32, to: T.self) } + case is Int64.Type: if case .int64(let int64) = self { return unsafeBitCast(int64, to: T.self) } + case is UInt.Type: if case .uint(let uint) = self { return unsafeBitCast(uint, to: T.self) } + case is UInt8.Type: if case .uint8(let uint8) = self { return unsafeBitCast(uint8, to: T.self) } + case is UInt16.Type: if case .uint16(let uint16) = self { return unsafeBitCast(uint16, to: T.self) } + case is UInt32.Type: if case .uint32(let uint32) = self { return unsafeBitCast(uint32, to: T.self) } + case is UInt64.Type: if case .uint64(let uint64) = self { return unsafeBitCast(uint64, to: T.self) } + case is Empty.Type: if case .empty = self { return unsafeBitCast((), to: T.self) } + default: break // TODO: We should do something different here, so we can ignore this case in the caller. Perhaps return a specialized error? } - guard let value = value as? T else { try errorHandler() } - return value + try errorHandler() } } From 8749cc675b0413870f9a74f39c1707e3c1adbf50 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Wed, 2 Jun 2021 22:52:32 -0700 Subject: [PATCH 07/10] Added support for number and float decoding strategies --- Sources/DynamicCodable/CoderInternals.swift | 4 +- .../DynamicCodableDecoder.swift | 244 ++++++++++-------- 2 files changed, 136 insertions(+), 112 deletions(-) diff --git a/Sources/DynamicCodable/CoderInternals.swift b/Sources/DynamicCodable/CoderInternals.swift index 76d9cfb..0fca8b0 100644 --- a/Sources/DynamicCodable/CoderInternals.swift +++ b/Sources/DynamicCodable/CoderInternals.swift @@ -70,7 +70,7 @@ extension DynamicCodable { } @inline(__always) - func unwrap(errorHandler: () throws -> Never) rethrows -> T { + func unwrap(errorHandler: () throws -> T) rethrows -> T { switch T.self { case is Keyed.Type: if case .keyed(let keyed) = self { return unsafeBitCast(keyed, to: T.self) } case is Unkeyed.Type: if case .unkeyed(let unkeyed) = self { return unsafeBitCast(unkeyed, to: T.self) } @@ -93,6 +93,6 @@ extension DynamicCodable { default: break // TODO: We should do something different here, so we can ignore this case in the caller. Perhaps return a specialized error? } - try errorHandler() + return try errorHandler() } } diff --git a/Sources/DynamicCodable/DynamicCodableDecoder.swift b/Sources/DynamicCodable/DynamicCodableDecoder.swift index 7424664..3d7d393 100644 --- a/Sources/DynamicCodable/DynamicCodableDecoder.swift +++ b/Sources/DynamicCodable/DynamicCodableDecoder.swift @@ -11,6 +11,39 @@ open class DynamicCodableDecoder { // MARK: Options + /// The strategy to use for decoding `Date` values. + /// - Tag: DynamicCodableDecoder.NumberDecodingStrategy + public enum NumberDecodingStrategy { + /// Decode numeric types using the closest representation that is encoded. For instance, if `Int` is requested, but [.int16](x-source-tag://DynamicCodable.int16) + /// is encoded, the value will be converted without issue, so long as it fits within the destination type. This is the default strategy. + /// - Tag: DynamicCodableDecoder.NumberDecodingStrategy.closestRepresentation + case closestRepresentation + + /// Decode numeric types exactly how they are represented. + /// - Tag: DynamicCodableDecoder.NumberDecodingStrategy.exactMatch + case exactMatch + } + + /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN). + /// - Tag: DynamicCodableDecoder.NonConformingFloatDecodingStrategy + public enum NonConformingFloatDecodingStrategy { + /// Throw upon encountering non-conforming values. This is the default strategy. + /// - Tag: DynamicCodableDecoder.NonConformingFloatDecodingStrategy.throw + case `throw` + + /// Decode the values from the given representation strings. + /// - Tag: DynamicCodableDecoder.NonConformingFloatDecodingStrategy.convertFromString + case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String) + } + + /// The strategy to use in decoding numeric types. Defaults to [.closestRepresentation](x-source-tag://DynamicCodableDecoder.NumberDecodingStrategy.closestRepresentation). + /// - Tag: DynamicCodableDecoder.numberDecodingStrategy + open var numberDecodingStrategy: NumberDecodingStrategy = .closestRepresentation + + /// 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 + /// Contextual user-provided information for use during decoding. /// - Tag: DynamicCodableDecoder.userInfo open var userInfo: [CodingUserInfoKey: Any] = [:] @@ -18,6 +51,12 @@ open class DynamicCodableDecoder { /// Options set on the top-level encoder to pass down the decoding hierarchy. /// - Tag: DynamicCodableDecoder.Options fileprivate struct Options { + /// - Tag: DynamicCodableDecoder.Options.numberDecodingStrategy + let numberDecodingStrategy: NumberDecodingStrategy + + /// - Tag: DynamicCodableDecoder.Options.nonConformingFloatDecodingStrategy + let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy + /// - Tag: DynamicCodableDecoder.Options.userInfo let userInfo: [CodingUserInfoKey: Any] } @@ -26,6 +65,8 @@ open class DynamicCodableDecoder { /// - Tag: DynamicCodableDecoder.options fileprivate var options: Options { return Options( + numberDecodingStrategy: numberDecodingStrategy, + nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, userInfo: userInfo ) } @@ -105,7 +146,6 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { @inline(__always) func unwrap() throws -> T { let value = representation - let error = createTypeMismatchError(type: T.self) typealias Primitive = DynamicCodable @@ -113,24 +153,24 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { // Return DynamicCodable as is if it is being decoded case is DynamicCodable.Type: return unsafeBitCast(value, to: T.self) // Primitive Types fast-path + case is Primitive.Float32.Type: return unsafeBitCast(try unwrapFloatingPoint() as Primitive.Float32, to: T.self) + case is Primitive.Float64.Type: return unsafeBitCast(try unwrapFloatingPoint() as Primitive.Float64, to: T.self) + case is Primitive.Int.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.Int, to: T.self) + case is Primitive.Int8.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.Int8, to: T.self) + case is Primitive.Int16.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.Int16, to: T.self) + case is Primitive.Int32.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.Int32, to: T.self) + case is Primitive.Int64.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.Int64, to: T.self) + case is Primitive.UInt.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.UInt, to: T.self) + case is Primitive.UInt8.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.UInt8, to: T.self) + case is Primitive.UInt16.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.UInt16, to: T.self) + case is Primitive.UInt32.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.UInt32, to: T.self) + case is Primitive.UInt64.Type: return unsafeBitCast(try unwrapFixedWidthInteger() as Primitive.UInt64, to: T.self) case is Primitive.Keyed.Type, is Primitive.Unkeyed.Type, is Primitive.Nil.Type, is Primitive.Bool.Type, is Primitive.String.Type, - is Primitive.Float64.Type, - is Primitive.Float32.Type, - is Primitive.Int.Type, - is Primitive.Int8.Type, - is Primitive.Int16.Type, - is Primitive.Int32.Type, - is Primitive.Int64.Type, - is Primitive.UInt.Type, - is Primitive.UInt8.Type, - is Primitive.UInt16.Type, - is Primitive.UInt32.Type, - is Primitive.UInt64.Type, - is Primitive.Empty.Type: return try value.unwrap { throw error } + is Primitive.Empty.Type: return try value.unwrap { throw createTypeMismatchError(type: T.self) } // Decodable Types default: return try T(from: self) } @@ -152,27 +192,63 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { return floatingPoint } - switch representation { - case .float64(let number): return try validate(T(number), originalValue: number) - case .float32(let number): return try validate(T(number), originalValue: number) - case .int(let number): return try validate(T(number), originalValue: number) - case .int8(let number): return try validate(T(number), originalValue: number) - case .int16(let number): return try validate(T(number), originalValue: number) - case .int32(let number): return try validate(T(number), originalValue: number) - case .int64(let number): return try validate(T(number), originalValue: number) - case .uint(let number): return try validate(T(number), originalValue: number) - case .uint8(let number): return try validate(T(number), originalValue: number) - case .uint16(let number): return try validate(T(number), originalValue: number) - case .uint32(let number): return try validate(T(number), originalValue: number) - case .uint64(let number): return try validate(T(number), originalValue: number) - - case .string, - .bool, - .keyed, - .unkeyed, - .empty, - .nil: - throw self.createTypeMismatchError(type: T.self) + @inline(__always) + func validate(_ string: String) throws -> T { + switch options.nonConformingFloatDecodingStrategy { + case .convertFromString(let posInfString, let negInfString, let nanString): + switch string { + case posInfString: return T.infinity + case negInfString: return -T.infinity + case nanString: return T.nan + default: throw createTypeMismatchError(type: T.self) + } + case .throw: throw createTypeMismatchError(type: T.self) + } + } + + if case .exactMatch = options.numberDecodingStrategy { + return try representation.unwrap { + if case .string(let string) = representation { + return try validate(string) + } + + throw createTypeMismatchError(type: T.self) + } + } + + switch options.numberDecodingStrategy { + case .exactMatch: + return try representation.unwrap { + if case .string(let string) = representation { + return try validate(string) + } + + throw createTypeMismatchError(type: T.self) + } + case .closestRepresentation: + switch representation { + case .float64(let number): return try validate(T(number), originalValue: number) + case .float32(let number): return try validate(T(number), originalValue: number) + case .int(let number): return try validate(T(number), originalValue: number) + case .int8(let number): return try validate(T(number), originalValue: number) + case .int16(let number): return try validate(T(number), originalValue: number) + case .int32(let number): return try validate(T(number), originalValue: number) + case .int64(let number): return try validate(T(number), originalValue: number) + case .uint(let number): return try validate(T(number), originalValue: number) + case .uint8(let number): return try validate(T(number), originalValue: number) + case .uint16(let number): return try validate(T(number), originalValue: number) + case .uint32(let number): return try validate(T(number), originalValue: number) + case .uint64(let number): return try validate(T(number), originalValue: number) + + case .string(let string): return try validate(string) + + case .bool, + .keyed, + .unkeyed, + .empty, + .nil: + throw createTypeMismatchError(type: T.self) + } } } @@ -192,26 +268,26 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { return fixedWidthInteger } - switch representation { - case .int(let number): return try validate(T(exactly: number), originalValue: number) - case .int8(let number): return try validate(T(exactly: number), originalValue: number) - case .int16(let number): return try validate(T(exactly: number), originalValue: number) - case .int32(let number): return try validate(T(exactly: number), originalValue: number) - case .int64(let number): return try validate(T(exactly: number), originalValue: number) - case .uint(let number): return try validate(T(exactly: number), originalValue: number) - case .uint8(let number): return try validate(T(exactly: number), originalValue: number) - case .uint16(let number): return try validate(T(exactly: number), originalValue: number) - case .uint32(let number): return try validate(T(exactly: number), originalValue: number) - case .uint64(let number): return try validate(T(exactly: number), originalValue: number) - case .float64(let number): return try validate(T(exactly: number), originalValue: number) - case .float32(let number): return try validate(T(exactly: number), originalValue: number) - case .string, - .bool, - .keyed, - .unkeyed, - .empty, - .nil: - throw self.createTypeMismatchError(type: T.self) + switch options.numberDecodingStrategy { + case .exactMatch: + return try representation.unwrap { throw createTypeMismatchError(type: T.self) } + case .closestRepresentation: + switch representation { + case .int(let number): return try validate(T(exactly: number), originalValue: number) + case .int8(let number): return try validate(T(exactly: number), originalValue: number) + case .int16(let number): return try validate(T(exactly: number), originalValue: number) + case .int32(let number): return try validate(T(exactly: number), originalValue: number) + case .int64(let number): return try validate(T(exactly: number), originalValue: number) + case .uint(let number): return try validate(T(exactly: number), originalValue: number) + case .uint8(let number): return try validate(T(exactly: number), originalValue: number) + case .uint16(let number): return try validate(T(exactly: number), originalValue: number) + case .uint32(let number): return try validate(T(exactly: number), originalValue: number) + case .uint64(let number): return try validate(T(exactly: number), originalValue: number) + case .float64(let number): return try validate(T(exactly: number), originalValue: number) + case .float32(let number): return try validate(T(exactly: number), originalValue: number) + case .string, .bool, .keyed, .unkeyed, .empty, .nil: + throw self.createTypeMismatchError(type: T.self) + } } } @@ -265,24 +341,7 @@ extension DynamicCodableDecoder.Decoder { } } - func decodeNil(forKey key: Key) throws -> Bool { try getValue(forKey: key) { $0.representation == .nil } } - func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try getValue(forKey: key) { try $0.unwrap() } } - func decode(_ type: String.Type, forKey key: Key) throws -> String { try getValue(forKey: key) { try $0.unwrap() } } - - func decode(_: Double.Type, forKey key: Key) throws -> Double { try getValue(forKey: key) { try $0.unwrapFloatingPoint() } } - func decode(_: Float.Type, forKey key: Key) throws -> Float { try getValue(forKey: key) { try $0.unwrapFloatingPoint() } } - - func decode(_: Int.Type, forKey key: Key) throws -> Int { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: Int8.Type, forKey key: Key) throws -> Int8 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: Int16.Type, forKey key: Key) throws -> Int16 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: Int32.Type, forKey key: Key) throws -> Int32 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: Int64.Type, forKey key: Key) throws -> Int64 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: UInt.Type, forKey key: Key) throws -> UInt { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: UInt8.Type, forKey key: Key) throws -> UInt8 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: UInt16.Type, forKey key: Key) throws -> UInt16 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: UInt32.Type, forKey key: Key) throws -> UInt32 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - func decode(_: UInt64.Type, forKey key: Key) throws -> UInt64 { try getValue(forKey: key) { try $0.unwrapFixedWidthInteger() } } - + func decodeNil(forKey key: Key) throws -> Bool { try getValue(forKey: key) { $0.representation == .nil } } func decode(_: T.Type, forKey key: Key) throws -> T where T: Decodable { try getValue(forKey: key) { try $0.unwrap() } } func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey: CodingKey { @@ -353,24 +412,7 @@ extension DynamicCodableDecoder.Decoder { } } - mutating func decode(_ type: Bool.Type) throws -> Bool { try getNextValue { try $0.unwrap() } } - mutating func decode(_ type: String.Type) throws -> String { try getNextValue { try $0.unwrap() } } - - mutating func decode(_: Double.Type) throws -> Double { try getNextValue { try $0.unwrapFloatingPoint() } } - mutating func decode(_: Float.Type) throws -> Float { try getNextValue { try $0.unwrapFloatingPoint() } } - - mutating func decode(_: Int.Type) throws -> Int { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: Int8.Type) throws -> Int8 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: Int16.Type) throws -> Int16 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: Int32.Type) throws -> Int32 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: Int64.Type) throws -> Int64 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: UInt.Type) throws -> UInt { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: UInt8.Type) throws -> UInt8 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: UInt16.Type) throws -> UInt16 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: UInt32.Type) throws -> UInt32 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - mutating func decode(_: UInt64.Type) throws -> UInt64 { try getNextValue { try $0.unwrapFixedWidthInteger() } } - - mutating func decode(_: T.Type) throws -> T where T: Decodable { try getNextValue { try $0.unwrap() } } + mutating func decode(_: T.Type) throws -> T where T: Decodable { try getNextValue { try $0.unwrap() } } mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey: CodingKey { try getNextValue { try $0.container(keyedBy: type) } @@ -388,24 +430,6 @@ extension DynamicCodableDecoder.Decoder { var codingPath: [CodingKey] { decoder.codingPath } func decodeNil() -> Bool { decoder.representation == .nil } - - func decode(_: Bool.Type) throws -> Bool { try decoder.unwrap() } - func decode(_: String.Type) throws -> String { try decoder.unwrap() } - - func decode(_: Double.Type) throws -> Double { try decoder.unwrapFloatingPoint() } - func decode(_: Float.Type) throws -> Float { try decoder.unwrapFloatingPoint() } - - func decode(_: Int.Type) throws -> Int { try decoder.unwrapFixedWidthInteger() } - func decode(_: Int8.Type) throws -> Int8 { try decoder.unwrapFixedWidthInteger() } - func decode(_: Int16.Type) throws -> Int16 { try decoder.unwrapFixedWidthInteger() } - func decode(_: Int32.Type) throws -> Int32 { try decoder.unwrapFixedWidthInteger() } - func decode(_: Int64.Type) throws -> Int64 { try decoder.unwrapFixedWidthInteger() } - func decode(_: UInt.Type) throws -> UInt { try decoder.unwrapFixedWidthInteger() } - func decode(_: UInt8.Type) throws -> UInt8 { try decoder.unwrapFixedWidthInteger() } - func decode(_: UInt16.Type) throws -> UInt16 { try decoder.unwrapFixedWidthInteger() } - func decode(_: UInt32.Type) throws -> UInt32 { try decoder.unwrapFixedWidthInteger() } - func decode(_: UInt64.Type) throws -> UInt64 { try decoder.unwrapFixedWidthInteger() } - - func decode(_: T.Type) throws -> T where T: Decodable { try decoder.unwrap() } + func decode(_: T.Type) throws -> T where T: Decodable { try decoder.unwrap() } } } From 5eba8e1d562a5176f90e6197e52e5ef01e548b14 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 3 Jun 2021 20:57:42 -0700 Subject: [PATCH 08/10] Moved primitive unwrapping to its own function --- Sources/DynamicCodable/DynamicCodableDecoder.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/DynamicCodable/DynamicCodableDecoder.swift b/Sources/DynamicCodable/DynamicCodableDecoder.swift index 3d7d393..714d50e 100644 --- a/Sources/DynamicCodable/DynamicCodableDecoder.swift +++ b/Sources/DynamicCodable/DynamicCodableDecoder.swift @@ -145,13 +145,11 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { @inline(__always) func unwrap() throws -> T { - let value = representation - typealias Primitive = DynamicCodable switch T.self { // Return DynamicCodable as is if it is being decoded - case is DynamicCodable.Type: return unsafeBitCast(value, to: T.self) + case is DynamicCodable.Type: return unsafeBitCast(representation, to: T.self) // Primitive Types fast-path case is Primitive.Float32.Type: return unsafeBitCast(try unwrapFloatingPoint() as Primitive.Float32, to: T.self) case is Primitive.Float64.Type: return unsafeBitCast(try unwrapFloatingPoint() as Primitive.Float64, to: T.self) @@ -170,12 +168,17 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { is Primitive.Nil.Type, is Primitive.Bool.Type, is Primitive.String.Type, - is Primitive.Empty.Type: return try value.unwrap { throw createTypeMismatchError(type: T.self) } + is Primitive.Empty.Type: return try unwrapPrimitive() // Decodable Types default: return try T(from: self) } } + @inline(__always) + private func unwrapPrimitive() throws -> T { + try representation.unwrap { throw createTypeMismatchError(type: T.self) } + } + @inline(__always) private func unwrapFloatingPoint() throws -> T { @inline(__always) From ed1aebf797c3c49a248844f87860b49529ae866e Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 3 Jun 2021 21:00:10 -0700 Subject: [PATCH 09/10] Refactored DecodingError.dataCorrupted calls into a helper function --- .../DynamicCodableDecoder.swift | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Sources/DynamicCodable/DynamicCodableDecoder.swift b/Sources/DynamicCodable/DynamicCodableDecoder.swift index 714d50e..c745acf 100644 --- a/Sources/DynamicCodable/DynamicCodableDecoder.swift +++ b/Sources/DynamicCodable/DynamicCodableDecoder.swift @@ -184,12 +184,7 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { @inline(__always) func validate(_ floatingPoint: T, originalValue: CustomStringConvertible) throws -> T { guard floatingPoint.isFinite else { - throw DecodingError.dataCorrupted( - .init( - codingPath: codingPath, - debugDescription: "Represented number <\(floatingPoint)> does not fit in \(T.self)." - ) - ) + throw dataCorruptedError("Represented number <\(floatingPoint)> does not fit in \(T.self).") } return floatingPoint @@ -260,12 +255,7 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { @inline(__always) func validate(_ fixedWidthInteger: T?, originalValue: CustomStringConvertible) throws -> T { guard let fixedWidthInteger = fixedWidthInteger else { - throw DecodingError.dataCorrupted( - .init( - codingPath: codingPath, - debugDescription: "Represented number <\(originalValue)> does not fit in \(T.self)." - ) - ) + throw dataCorruptedError("Represented number <\(originalValue)> does not fit in \(T.self).") } return fixedWidthInteger @@ -303,6 +293,15 @@ extension DynamicCodableDecoder.Decoder: Swift.Decoder { ) ) } + + private func dataCorruptedError(_ debugDescription: String) -> DecodingError { + DecodingError.dataCorrupted( + .init( + codingPath: codingPath, + debugDescription: debugDescription + ) + ) + } } extension DynamicCodableDecoder.Decoder { From 7b409b3540f2cd39c65db10a42f292ccfe13844c Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 3 Jun 2021 21:13:51 -0700 Subject: [PATCH 10/10] Added support for date decoding strategies --- .../DynamicCodableDecoder.swift | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/Sources/DynamicCodable/DynamicCodableDecoder.swift b/Sources/DynamicCodable/DynamicCodableDecoder.swift index c745acf..78684d7 100644 --- a/Sources/DynamicCodable/DynamicCodableDecoder.swift +++ b/Sources/DynamicCodable/DynamicCodableDecoder.swift @@ -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 { @@ -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 { @@ -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 @@ -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 @@ -66,6 +104,7 @@ open class DynamicCodableDecoder { fileprivate var options: Options { return Options( numberDecodingStrategy: numberDecodingStrategy, + dateDecodingStrategy: dateDecodingStrategy, nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, userInfo: userInfo ) @@ -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) } @@ -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, @@ -435,3 +502,11 @@ extension DynamicCodableDecoder.Decoder { func decode(_: 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 +}()