diff --git a/Sources/DynamicCodable/CoderInternals.swift b/Sources/DynamicCodable/CoderInternals.swift new file mode 100644 index 0000000..0fca8b0 --- /dev/null +++ b/Sources/DynamicCodable/CoderInternals.swift @@ -0,0 +1,98 @@ +// +// CoderInternals.swift +// DynamicCodable +// +// Created by Dimitri Bouniol on 4/18/21. +// 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 -> 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) } + 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? + } + + return try errorHandler() + } +} diff --git a/Sources/DynamicCodable/DynamicCodable.swift b/Sources/DynamicCodable/DynamicCodable.swift index 9e78281..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,18 +80,94 @@ 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 { /// 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) } } diff --git a/Sources/DynamicCodable/DynamicCodableDecoder.swift b/Sources/DynamicCodable/DynamicCodableDecoder.swift new file mode 100644 index 0000000..78684d7 --- /dev/null +++ b/Sources/DynamicCodable/DynamicCodableDecoder.swift @@ -0,0 +1,512 @@ +// +// DynamicCodableDecoder.swift +// DynamicCodable +// +// Created by Dimitri Bouniol on 4/17/21. +// 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 { + // 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 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 { + /// 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 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 + + /// 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.numberDecodingStrategy + let numberDecodingStrategy: NumberDecodingStrategy + + /// - Tag: DynamicCodableDecoder.Options.dateDecodingStrategy + let dateDecodingStrategy: DateDecodingStrategy + + /// - Tag: DynamicCodableDecoder.Options.nonConformingFloatDecodingStrategy + let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy + + /// - 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( + numberDecodingStrategy: numberDecodingStrategy, + dateDecodingStrategy: dateDecodingStrategy, + nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, + 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 { + typealias Primitive = DynamicCodable + + switch T.self { + // Return DynamicCodable as is if it is being decoded + 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) + 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.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) + } + } + + @inline(__always) + private func unwrapPrimitive() throws -> T { + try representation.unwrap { throw createTypeMismatchError(type: T.self) } + } + + @inline(__always) + private func unwrapFloatingPoint() throws -> T { + @inline(__always) + func validate(_ floatingPoint: T, originalValue: CustomStringConvertible) throws -> T { + guard floatingPoint.isFinite else { + throw dataCorruptedError("Represented number <\(floatingPoint)> does not fit in \(T.self).") + } + + return floatingPoint + } + + @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) + } + } + } + + @inline(__always) + private func unwrapFixedWidthInteger() throws -> T { + @inline(__always) + func validate(_ fixedWidthInteger: T?, originalValue: CustomStringConvertible) throws -> T { + guard let fixedWidthInteger = fixedWidthInteger else { + throw dataCorruptedError("Represented number <\(originalValue)> does not fit in \(T.self).") + } + + return fixedWidthInteger + } + + 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) + } + } + } + + @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, + .init( + codingPath: codingPath, + debugDescription: "Expected to decode \(type) but found \(representation.debugDataTypeDescription) instead." + ) + ) + } + + private func dataCorruptedError(_ debugDescription: String) -> DecodingError { + DecodingError.dataCorrupted( + .init( + codingPath: codingPath, + debugDescription: debugDescription + ) + ) + } +} + +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(_: 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(_: 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(_: 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 +}() 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)") + } + } }