Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions DictionaryCoder.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

123 changes: 112 additions & 11 deletions Sources/Decoder/DictionaryComponentDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,86 @@ extension DictionaryComponentDecoder {
at codingPath: [CodingKey]
) throws -> T {
guard let value = component as? T else {
return try decodeConvertedNumber(from: component, at: codingPath)
}

return value
}

// Numbers of other types are converted the same way as `NSNumber`,
// so a dictionary decodes equally whether it holds Swift numbers or `NSNumber` instances.
// Unlike `NSNumber`, booleans are not converted to or from numbers here, as in `JSONDecoder`.
@inline(never)
private func decodeConvertedNumber<T: Decodable>(
from component: Any?,
at codingPath: [CodingKey]
) throws -> T {
let number = component as? NSNumber

guard let number, !isBoolean(number), !(T.self is Bool.Type), let value = number as? T else {
throw DecodingError.invalidComponent(component, of: T.self, at: codingPath)
}

return value
}

// Booleans are bridged to `NSNumber` too, so they are told apart by their Core Foundation type.
private func isBoolean(_ number: NSNumber) -> Bool {
CFGetTypeID(number) == CFBooleanGetTypeID()
}

private func decodeDecimal(from component: Any?, at codingPath: [CodingKey]) throws -> Decimal {
let strategy = options.decimalDecodingStrategy

if strategy.contains(.number) {
if let decimal = component as? Decimal {
return decimal
}

if let number = component as? NSNumber, !isBoolean(number) {
return number.decimalValue
}
}

guard strategy.contains(.deferredToDecimal) else {
throw DecodingError.invalidComponent(component, of: Decimal.self, at: codingPath)
}

return try decodeNonPrimitiveValue(from: component, at: codingPath)
}

#if compiler(>=6.0)
// `NSNumber` does not bridge 128-bit integers, so other integers are converted exactly.
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
private func decodeWideInteger<T: FixedWidthInteger & Decodable>(
of type: T.Type = T.self,
from component: Any?,
at codingPath: [CodingKey]
) throws -> T {
if let value = component as? T {
return value
}

let value: T?

switch component {
case let integer as any BinaryInteger:
value = T(exactly: integer)

case let number as NSNumber where !isBoolean(number):
value = (number as? Int64).flatMap(T.init(exactly:)) ?? (number as? UInt64).flatMap(T.init(exactly:))

default:
value = nil
}

guard let value else {
throw DecodingError.invalidComponent(component, of: T.self, at: codingPath)
}

return value
}
#endif

private func decodeNonPrimitiveValue<T: Decodable>(
of type: T.Type = T.self,
Expand Down Expand Up @@ -88,7 +163,19 @@ extension DictionaryComponentDecoder {
break
}

throw DecodingError.invalidComponent(component, of: T.self, at: codingPath)
// Numbers of other types are converted, which strings do not survive.
let number: T = try decodeConvertedNumber(from: component, at: codingPath)

guard number.isFinite else {
let errorContext = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Parsed dictionary number \(number) does not fit in \(T.self)."
)

throw DecodingError.dataCorrupted(errorContext)
}

return number
}

private func decodeDate(from component: Any?, at codingPath: [CodingKey]) throws -> Date {
Expand All @@ -102,14 +189,10 @@ extension DictionaryComponentDecoder {
case .millisecondsSince1970:
return Date(timeIntervalSince1970: try decodePrimitiveValue(from: component, at: codingPath) / 1000.0)

case .iso8601:
guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}

case let .iso8601(style):
let formattedDate = try decodePrimitiveValue(of: String.self, from: component, at: codingPath)

guard let date = ISO8601DateFormatter().date(from: formattedDate) else {
guard let date = style.date(from: formattedDate) else {
let errorContext = DecodingError.Context(
codingPath: codingPath,
debugDescription: "Expected date string to be ISO8601-formatted."
Expand Down Expand Up @@ -213,6 +296,13 @@ extension DictionaryComponentDecoder {
try decodePrimitiveValue(from: component, at: codingPath)
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int128 {
try decodeWideInteger(from: component, at: codingPath)
}
#endif

internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt {
try decodePrimitiveValue(from: component, at: codingPath)
}
Expand All @@ -233,6 +323,13 @@ extension DictionaryComponentDecoder {
try decodePrimitiveValue(from: component, at: codingPath)
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt128 {
try decodeWideInteger(from: component, at: codingPath)
}
#endif

internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Double {
try decodeFloatingPointValue(from: component, at: codingPath)
}
Expand All @@ -250,16 +347,20 @@ extension DictionaryComponentDecoder {
from component: Any?,
at codingPath: [CodingKey]
) throws -> T {
switch T.self {
case is Date.Type:
// The type is compared rather than cast, as a cast costs much more and is made for every value.
switch ObjectIdentifier(T.self) {
case ObjectIdentifier(Date.self):
return try decodeDate(from: component, at: codingPath) as! T

case is Data.Type:
case ObjectIdentifier(Data.self):
return try decodeData(from: component, at: codingPath) as! T

case is URL.Type:
case ObjectIdentifier(URL.self):
return try decodeURL(from: component, at: codingPath) as! T

case ObjectIdentifier(Decimal.self):
return try decodeDecimal(from: component, at: codingPath) as! T

default:
return try decodeNonPrimitiveValue(from: component, at: codingPath)
}
Expand Down
33 changes: 16 additions & 17 deletions Sources/Decoder/DictionaryDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public final class DictionaryDecoder: Sendable {
set { optionsMutex.withLock { $0.dataDecodingStrategy = newValue } }
}

public var decimalDecodingStrategy: DictionaryDecimalDecodingStrategy {
get { optionsMutex.withLock { $0.decimalDecodingStrategy } }
set { optionsMutex.withLock { $0.decimalDecodingStrategy = newValue } }
}

public var nonConformingFloatDecodingStrategy: DictionaryNonConformingFloatDecodingStrategy {
get { optionsMutex.withLock { $0.nonConformingFloatDecodingStrategy } }
set { optionsMutex.withLock { $0.nonConformingFloatDecodingStrategy = newValue } }
Expand All @@ -37,13 +42,15 @@ public final class DictionaryDecoder: Sendable {
public init(
dateDecodingStrategy: DictionaryDateDecodingStrategy = .deferredToDate,
dataDecodingStrategy: DictionaryDataDecodingStrategy = .base64,
decimalDecodingStrategy: DictionaryDecimalDecodingStrategy = .deferredToDecimal,
nonConformingFloatDecodingStrategy: DictionaryNonConformingFloatDecodingStrategy = .throw,
keyDecodingStrategy: DictionaryKeyDecodingStrategy = .useDefaultKeys,
userInfo: [CodingUserInfoKey: Sendable] = [:]
) {
let options = DictionaryDecodingOptions(
dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
decimalDecodingStrategy: decimalDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy
)
Expand All @@ -54,41 +61,33 @@ public final class DictionaryDecoder: Sendable {

// MARK: - Instance Methods

public func decode<T: Decodable>(
_ type: T.Type = T.self,
from dictionary: [String: Any]
) throws -> T {
private func rootDecoder(for dictionary: [String: Any]) -> DictionarySingleValueDecodingContainer {
let options = optionsMutex.withLock { $0 }

let decoder = DictionarySingleValueDecodingContainer(
return DictionarySingleValueDecodingContainer(
component: dictionary,
options: options,
userInfo: userInfo,
codingPath: []
)
}

return try T(from: decoder)
public func decode<T: Decodable>(
_ type: T.Type = T.self,
from dictionary: [String: Any]
) throws -> T {
try T(from: rootDecoder(for: dictionary))
}

public func decode<T: Decodable>(from dictionary: [String: Any]) throws -> T {
try decode(T.self, from: dictionary)
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public func decode<T: DecodableWithConfiguration>(
_ type: T.Type = T.self,
from dictionary: [String: Any],
configuration: T.DecodingConfiguration
) throws -> T {
let options = optionsMutex.withLock { $0 }

let decoder = DictionarySingleValueDecodingContainer(
component: dictionary,
options: options,
userInfo: userInfo,
codingPath: []
)

return try T(from: decoder, configuration: configuration)
try T(from: rootDecoder(for: dictionary), configuration: configuration)
}
}
26 changes: 19 additions & 7 deletions Sources/Decoder/DictionaryKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
KeyedDecodingContainerProtocol,
DictionaryComponentDecoder {
Expand Down Expand Up @@ -27,12 +25,12 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
case .useDefaultKeys:
self.components = components

case let.custom(closure):
let componentKeysAndValues = components.map { key, value in
(closure(codingPath.appending(AnyCodingKey(key))).stringValue, value)
}
case let .custom(closure):
let componentKeysAndValues = components
.sorted { $0.key < $1.key }
.map { key, value in (closure(codingPath.appending(AnyCodingKey(key))).stringValue, value) }

self.components = Dictionary(componentKeysAndValues) { $1 }
self.components = Dictionary(componentKeysAndValues) { first, _ in first }
}

self.options = options
Expand Down Expand Up @@ -100,6 +98,13 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: Int128.Type, forKey key: Key) throws -> Int128 {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
#endif

internal func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
Expand All @@ -120,6 +125,13 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: UInt128.Type, forKey key: Key) throws -> UInt128 {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
#endif

internal func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
Expand Down
16 changes: 14 additions & 2 deletions Sources/Decoder/DictionarySingleValueDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

internal final class DictionarySingleValueDecodingContainer:
Decoder,
SingleValueDecodingContainer,
Expand Down Expand Up @@ -56,6 +54,13 @@ internal final class DictionarySingleValueDecodingContainer:
try decodeComponentValue(from: component, at: codingPath)
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: Int128.Type) throws -> Int128 {
try decodeComponentValue(from: component, at: codingPath)
}
#endif

internal func decode(_ type: UInt.Type) throws -> UInt {
try decodeComponentValue(from: component, at: codingPath)
}
Expand All @@ -76,6 +81,13 @@ internal final class DictionarySingleValueDecodingContainer:
try decodeComponentValue(from: component, at: codingPath)
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: UInt128.Type) throws -> UInt128 {
try decodeComponentValue(from: component, at: codingPath)
}
#endif

internal func decode(_ type: Double.Type) throws -> Double {
try decodeComponentValue(from: component, at: codingPath)
}
Expand Down
Loading
Loading