From cdc23346148ae99d403033fa24a3ad06a8416e00 Mon Sep 17 00:00:00 2001
From: Leo Dion
Date: Tue, 23 Jun 2026 17:31:14 -0400
Subject: [PATCH 1/5] refactor(packages): extract DictionaryCoding and
SundialKitStreamSync
Give two self-contained, dependency-light units cleaner homes.
DictionaryCoding (Foundation-only, zero deps) moves out of AtLeastKit into
its own package at Packages/DictionaryCoding; AtLeastKit now depends on it
via ../DictionaryCoding. This keeps it independently reusable/testable
rather than entangling it with SundialKit.
ContextEngine and its revisioned-message protocols (RevisionedMessage,
ExpiringMessage, StaleWindow) move into a new SundialKitStreamSync target
+ product inside the SundialKitStream package, separating the snapshot-sync
reliability layer from the core observers. Consumers (ConnectivityService,
SessionIntent+Sync, TimerStateUpdate+Sync) import the new module; the sync
tests stay in SundialKitStreamTests (they share MockConnectivitySession)
with the new module added as a dependency.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
Package.swift | 38 +++
.../DecodingError+Dictionary.swift | 51 ++++
.../DictionaryCodingKey.swift | 36 +++
...ctionaryCodingKeyedDecodingContainer.swift | 230 +++++++++++++++
...ctionaryCodingKeyedEncodingContainer.swift | 180 ++++++++++++
...ictionaryDecoder.KeyDecodingStrategy.swift | 71 +++++
.../DictionaryCoding/DictionaryDecoder.swift | 240 ++++++++++++++++
.../DictionaryDecoderImpl+SingleValue.swift | 158 +++++++++++
.../DictionaryDecoderImpl+Unbox.swift | 99 +++++++
...DictionaryDecoderImpl+UnboxDecodable.swift | 183 ++++++++++++
.../DictionaryDecoderImpl+UnboxFloats.swift | 101 +++++++
.../DictionaryDecoderImpl+UnboxIntegers.swift | 161 +++++++++++
.../DictionaryDecoderImpl.swift | 103 +++++++
.../DictionaryDecoderOptions.swift | 23 ++
.../DictionaryDecodingStorage.swift | 50 ++++
.../DictionaryEncoder+Encode.swift | 108 ++++++++
.../DictionaryCoding/DictionaryEncoder.swift | 214 ++++++++++++++
.../DictionaryEncoderImpl+Box.swift | 262 ++++++++++++++++++
.../DictionaryEncoderImpl.swift | 203 ++++++++++++++
.../DictionaryEncoderOptions.swift | 21 ++
.../DictionaryEncodingStorage.swift | 54 ++++
.../DictionaryReferencingEncoder.swift | 105 +++++++
...onaryUnkeyedDecodingContainer+Nested.swift | 117 ++++++++
...naryUnkeyedDecodingContainer+Scalars.swift | 259 +++++++++++++++++
...onaryUnkeyedDecodingContainer+String.swift | 31 +++
.../DictionaryUnkeyedDecodingContainer.swift | 112 ++++++++
.../DictionaryUnkeyedEncodingContainer.swift | 160 +++++++++++
.../EncodingError+Dictionary.swift | 42 +++
Sources/DictionaryCoding/NSNumber+Bool.swift | 27 ++
.../DictionaryCodingArrayAndKeyTests.swift | 90 ++++++
.../DictionaryCodingArrayTests.swift | 65 +++++
.../DictionaryCodingDateDataTests.swift | 97 +++++++
.../DictionaryCodingErrorTests.swift | 125 +++++++++
.../DictionaryCodingFloatStrategyTests.swift | 132 +++++++++
.../DictionaryCodingNestedTests.swift | 98 +++++++
.../DictionaryCodingRoundTripTests.swift | 57 ++++
.../DictionaryCodingScalarTests.swift | 132 +++++++++
.../DictionaryCodingSpecialTypeTests.swift | 94 +++++++
.../DictionaryCodingStrategyErrorTests.swift | 79 ++++++
.../DictionaryCodingSuperDecoderTests.swift | 82 ++++++
.../DictionaryCodingTestKey.swift | 23 ++
.../DictionaryDecoderPlatformTests.swift | 89 ++++++
.../DictionaryDecoderTests.swift | 53 ++++
.../DictionaryEncoderTests.swift | 65 +++++
.../SuperDecoderBase.swift | 17 ++
.../SuperDecoderChild.swift | 41 +++
46 files changed, 4778 insertions(+)
create mode 100644 Package.swift
create mode 100644 Sources/DictionaryCoding/DecodingError+Dictionary.swift
create mode 100644 Sources/DictionaryCoding/DictionaryCodingKey.swift
create mode 100644 Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift
create mode 100644 Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoder.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoderImpl.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoderOptions.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecodingStorage.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoder+Encode.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoder.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoderImpl+Box.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoderImpl.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoderOptions.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncodingStorage.swift
create mode 100644 Sources/DictionaryCoding/DictionaryReferencingEncoder.swift
create mode 100644 Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift
create mode 100644 Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift
create mode 100644 Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift
create mode 100644 Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift
create mode 100644 Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift
create mode 100644 Sources/DictionaryCoding/EncodingError+Dictionary.swift
create mode 100644 Sources/DictionaryCoding/NSNumber+Bool.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryDecoderTests.swift
create mode 100644 Tests/DictionaryCodingTests/DictionaryEncoderTests.swift
create mode 100644 Tests/DictionaryCodingTests/SuperDecoderBase.swift
create mode 100644 Tests/DictionaryCodingTests/SuperDecoderChild.swift
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..8e4cbea
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,38 @@
+// swift-tools-version: 6.3
+//
+// Package.swift
+// DictionaryCoding
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import PackageDescription
+
+// swiftlint:disable:next explicit_acl explicit_top_level_acl
+let package = Package(
+ name: "DictionaryCoding",
+ platforms: [
+ .macOS(.v15),
+ .iOS(.v26),
+ .tvOS(.v26),
+ .watchOS(.v26),
+ .visionOS(.v26)
+ ],
+ products: [
+ .library(
+ name: "DictionaryCoding",
+ targets: ["DictionaryCoding"]
+ ),
+ ],
+ targets: [
+ .target(
+ name: "DictionaryCoding",
+ swiftSettings: [.swiftLanguageMode(.v6)]
+ ),
+ .testTarget(
+ name: "DictionaryCodingTests",
+ dependencies: ["DictionaryCoding"]
+ ),
+ ]
+)
diff --git a/Sources/DictionaryCoding/DecodingError+Dictionary.swift b/Sources/DictionaryCoding/DecodingError+Dictionary.swift
new file mode 100644
index 0000000..1bbda66
--- /dev/null
+++ b/Sources/DictionaryCoding/DecodingError+Dictionary.swift
@@ -0,0 +1,51 @@
+//
+// DecodingError+Dictionary.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+extension DecodingError {
+ /// Returns a `.typeMismatch` error describing the expected type.
+ ///
+ /// - parameter path: The path of `CodingKey`s taken to decode a value of this type.
+ /// - parameter expectation: The type expected to be encountered.
+ /// - parameter reality: The value that was encountered instead of the expected type.
+ /// - returns: A `DecodingError` with the appropriate path and debug description.
+ internal static func typeMismatch(
+ at path: [CodingKey],
+ expectation: Any.Type,
+ reality: Any
+ ) -> DecodingError {
+ let description =
+ "Expected to decode \(expectation) "
+ + "but found \(typeDescription(of: reality)) instead."
+ return .typeMismatch(
+ expectation,
+ Context(codingPath: path, debugDescription: description)
+ )
+ }
+
+ /// Returns a description of the type of `value` appropriate for an error message.
+ ///
+ /// - parameter value: The value whose type to describe.
+ /// - returns: A string describing `value`.
+ internal static func typeDescription(of value: Any) -> String {
+ if value is NSNull {
+ "a null value"
+ } else if value is NSNumber {
+ "a number"
+ } else if value is String {
+ "a string/data"
+ } else if value is [Any] {
+ "an array"
+ } else if value is [String: Any] {
+ "a dictionary"
+ } else {
+ "\(type(of: value))"
+ }
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryCodingKey.swift b/Sources/DictionaryCoding/DictionaryCodingKey.swift
new file mode 100644
index 0000000..d84fbf9
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryCodingKey.swift
@@ -0,0 +1,36 @@
+//
+// DictionaryCodingKey.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+internal struct DictionaryCodingKey: CodingKey {
+ internal static let `super` = DictionaryCodingKey(stringValue: "super", intValue: nil)
+
+ internal var stringValue: String
+ internal var intValue: Int?
+
+ internal init?(stringValue: String) {
+ self.stringValue = stringValue
+ intValue = nil
+ }
+
+ internal init?(intValue: Int) {
+ stringValue = "\(intValue)"
+ self.intValue = intValue
+ }
+
+ internal init(stringValue: String, intValue: Int?) {
+ self.stringValue = stringValue
+ self.intValue = intValue
+ }
+
+ internal init(index: Int) {
+ stringValue = "Index \(index)"
+ intValue = index
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift b/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift
new file mode 100644
index 0000000..81e121e
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift
@@ -0,0 +1,230 @@
+//
+// DictionaryCodingKeyedDecodingContainer.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Keyed Decoding Container
+internal struct DictionaryCodingKeyedDecodingContainer:
+ KeyedDecodingContainerProtocol
+{
+ internal typealias Key = K
+
+ // MARK: - Instance Properties
+
+ /// A reference to the decoder we're reading from.
+ internal let decoder: DictionaryDecoderImpl
+
+ /// A reference to the container we're reading from.
+ internal let container: [String: Any]
+
+ /// The path of coding keys taken to get to this point in decoding.
+ internal private(set) var codingPath: [CodingKey]
+
+ // MARK: - Computed Properties
+
+ internal var allKeys: [Key] {
+ self.container.keys.compactMap { Key(stringValue: $0) }
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` by referencing the given decoder and container.
+ internal init(
+ referencing decoder: DictionaryDecoderImpl,
+ wrapping container: [String: Any]
+ ) {
+ self.decoder = decoder
+ switch decoder.options.keyDecodingStrategy {
+ case .useDefaultKeys:
+ self.container = container
+ case .convertFromSnakeCase:
+ // Convert the snake case keys in the container to camel case.
+ // If we hit a duplicate key after conversion, then we'll use the first one
+ // we saw. Effectively an undefined behavior with Dictionary dictionaries.
+ self.container = Dictionary(
+ container.map { key, value in
+ (DictionaryDecoder.KeyDecodingStrategy.convertFromSnakeCase(key), value)
+ },
+ uniquingKeysWith: { first, _ in first }
+ )
+ case .custom(let converter):
+ self.container = Dictionary(
+ container.map { key, value in
+ (
+ converter(
+ decoder.codingPath
+ + [DictionaryCodingKey(stringValue: key, intValue: nil)]
+ ).stringValue,
+ value
+ )
+ },
+ uniquingKeysWith: { first, _ in first }
+ )
+ }
+ self.codingPath = decoder.codingPath
+ }
+
+ // MARK: - Instance Methods
+
+ internal func contains(_ key: Key) -> Bool {
+ self.container[key.stringValue] != nil
+ }
+
+ internal func notFoundError(key: Key) -> DecodingError {
+ DecodingError.keyNotFound(
+ key,
+ DecodingError.Context(
+ codingPath: self.decoder.codingPath,
+ debugDescription:
+ "No value associated with key \(errorDescription(of: key))."
+ )
+ )
+ }
+
+ internal func nullFoundError(type: T.Type) -> DecodingError {
+ DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: self.decoder.codingPath,
+ debugDescription: "Expected \(type) value but found null instead."
+ )
+ )
+ }
+
+ internal func errorDescription(of key: CodingKey) -> String {
+ switch decoder.options.keyDecodingStrategy {
+ case .convertFromSnakeCase:
+ let original = key.stringValue
+ let converted =
+ DictionaryEncoder.KeyEncodingStrategy.convertToSnakeCase(original)
+ if converted == original {
+ return "\(key) (\"\(original)\")"
+ } else {
+ return "\(key) (\"\(original)\"), converted to \(converted)"
+ }
+ default:
+ return "\(key) (\"\(key.stringValue)\")"
+ }
+ }
+
+ internal func decodeNil(forKey key: Key) throws -> Bool {
+ guard let entry = self.container[key.stringValue] else {
+ throw notFoundError(key: key)
+ }
+
+ return entry is NSNull
+ }
+
+ internal func decode(_ type: T.Type, forKey key: Key) throws -> T {
+ guard let entry = self.container[key.stringValue] else {
+ switch decoder.options.missingValueDecodingStrategy {
+ case .useDefault(let defaults):
+ let defaultKey = "\(type)"
+ if let def = defaults[defaultKey] as? T {
+ return def
+ }
+ default:
+ break
+ }
+
+ throw notFoundError(key: key)
+ }
+
+ self.decoder.codingPath.append(key)
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard let value = try self.decoder.unbox(entry, as: type) else {
+ throw nullFoundError(type: type)
+ }
+
+ return value
+ }
+}
+
+// MARK: - Nested containers and superDecoder
+extension DictionaryCodingKeyedDecodingContainer {
+ internal func nestedContainer(
+ keyedBy type: NestedKey.Type,
+ forKey key: Key
+ ) throws -> KeyedDecodingContainer {
+ self.decoder.codingPath.append(key)
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard let value = self.container[key.stringValue] else {
+ throw DecodingError.keyNotFound(
+ key,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get \(KeyedDecodingContainer.self)"
+ + " -- no value found for key \(errorDescription(of: key))"
+ )
+ )
+ }
+
+ guard let dictionary = value as? [String: Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: [String: Any].self, reality: value
+ )
+ }
+
+ let container = DictionaryCodingKeyedDecodingContainer(
+ referencing: self.decoder, wrapping: dictionary
+ )
+ return KeyedDecodingContainer(container)
+ }
+
+ internal func nestedUnkeyedContainer(
+ forKey key: Key
+ ) throws -> UnkeyedDecodingContainer {
+ self.decoder.codingPath.append(key)
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard let value = self.container[key.stringValue] else {
+ throw DecodingError.keyNotFound(
+ key,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get UnkeyedDecodingContainer"
+ + " -- no value found for key \(errorDescription(of: key))"
+ )
+ )
+ }
+
+ guard let array = value as? [Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: [Any].self, reality: value
+ )
+ }
+
+ return DictionaryUnkeyedDecodingContainer(
+ referencing: self.decoder, wrapping: array
+ )
+ }
+
+ internal func superDecoder() throws -> Decoder {
+ try makeSuperDecoder(forKey: DictionaryCodingKey.super)
+ }
+
+ internal func superDecoder(forKey key: Key) throws -> Decoder {
+ try makeSuperDecoder(forKey: key)
+ }
+
+ private func makeSuperDecoder(forKey key: CodingKey) throws -> Decoder {
+ self.decoder.codingPath.append(key)
+ defer { self.decoder.codingPath.removeLast() }
+
+ let value: Any = self.container[key.stringValue] ?? NSNull()
+ return DictionaryDecoderImpl(
+ referencing: value,
+ at: self.decoder.codingPath,
+ options: self.decoder.options
+ )
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift b/Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift
new file mode 100644
index 0000000..3d289d3
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift
@@ -0,0 +1,180 @@
+//
+// DictionaryCodingKeyedEncodingContainer.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Keyed Encoding Container
+internal struct DictionaryCodingKeyedEncodingContainer:
+ KeyedEncodingContainerProtocol
+{
+ internal typealias Key = K
+
+ // MARK: - Instance Properties
+
+ /// A reference to the encoder we're writing to.
+ private let encoder: DictionaryEncoderImpl
+
+ /// A reference to the container we're writing to.
+ private let container: NSMutableDictionary
+
+ /// The path of coding keys taken to get to this point in encoding.
+ internal private(set) var codingPath: [CodingKey]
+
+ // MARK: - Initializers
+
+ /// Initializes `self` with the given references.
+ internal init(
+ referencing encoder: DictionaryEncoderImpl,
+ codingPath: [CodingKey],
+ wrapping container: NSMutableDictionary
+ ) {
+ self.encoder = encoder
+ self.codingPath = codingPath
+ self.container = container
+ }
+
+ // MARK: - Instance Methods
+
+ private func converted(_ key: CodingKey) -> CodingKey {
+ switch encoder.options.keyEncodingStrategy {
+ case .useDefaultKeys:
+ return key
+ case .convertToSnakeCase:
+ let newKeyString =
+ DictionaryEncoder.KeyEncodingStrategy.convertToSnakeCase(key.stringValue)
+ return DictionaryCodingKey(stringValue: newKeyString, intValue: key.intValue)
+ case .custom(let converter):
+ return converter(codingPath + [key])
+ }
+ }
+
+ internal mutating func encodeNil(forKey key: Key) throws {
+ self.container[converted(key).stringValue] = NSNull()
+ }
+
+ internal mutating func encode(_ value: Bool, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: Int, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: Int8, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: Int16, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: Int32, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: Int64, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: UInt, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: UInt8, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: UInt16, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: UInt32, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: UInt64, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: String, forKey key: Key) throws {
+ self.container[converted(key).stringValue] = self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: Float, forKey key: Key) throws {
+ // Since the float may be invalid and throw, the coding path needs to
+ // contain this key.
+ self.encoder.codingPath.append(key)
+ defer { self.encoder.codingPath.removeLast() }
+ self.container[converted(key).stringValue] = try self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: Double, forKey key: Key) throws {
+ // Since the double may be invalid and throw, the coding path needs to
+ // contain this key.
+ self.encoder.codingPath.append(key)
+ defer { self.encoder.codingPath.removeLast() }
+ self.container[converted(key).stringValue] = try self.encoder.box(value)
+ }
+
+ internal mutating func encode(_ value: T, forKey key: Key) throws {
+ self.encoder.codingPath.append(key)
+ defer { self.encoder.codingPath.removeLast() }
+ self.container[converted(key).stringValue] = try self.encoder.box(value)
+ }
+
+ internal mutating func nestedContainer(
+ keyedBy keyType: NestedKey.Type,
+ forKey key: Key
+ ) -> KeyedEncodingContainer {
+ let dictionary = NSMutableDictionary()
+ self.container[converted(key).stringValue] = dictionary
+
+ self.codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+
+ let container = DictionaryCodingKeyedEncodingContainer(
+ referencing: self.encoder,
+ codingPath: self.codingPath,
+ wrapping: dictionary
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ internal mutating func nestedUnkeyedContainer(
+ forKey key: Key
+ ) -> UnkeyedEncodingContainer {
+ let array = NSMutableArray()
+ self.container[converted(key).stringValue] = array
+
+ self.codingPath.append(key)
+ defer { self.codingPath.removeLast() }
+ return DictionaryUnkeyedEncodingContainer(
+ referencing: self.encoder,
+ codingPath: self.codingPath,
+ wrapping: array
+ )
+ }
+
+ internal mutating func superEncoder() -> Encoder {
+ DictionaryReferencingEncoder(
+ referencing: self.encoder,
+ key: DictionaryCodingKey.super,
+ convertedKey: converted(DictionaryCodingKey.super),
+ wrapping: self.container
+ )
+ }
+
+ internal mutating func superEncoder(forKey key: Key) -> Encoder {
+ DictionaryReferencingEncoder(
+ referencing: self.encoder,
+ key: key,
+ convertedKey: converted(key),
+ wrapping: self.container
+ )
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift b/Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift
new file mode 100644
index 0000000..29a5e6a
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift
@@ -0,0 +1,71 @@
+//
+// DictionaryDecoder.KeyDecodingStrategy.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+extension DictionaryDecoder.KeyDecodingStrategy {
+ internal static func convertFromSnakeCase(_ stringKey: String) -> String {
+ guard !stringKey.isEmpty else {
+ return stringKey
+ }
+
+ // Find the first non-underscore character
+ guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else {
+ // Reached the end without finding an _
+ return stringKey
+ }
+
+ // Find the last non-underscore character
+ var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
+ while lastNonUnderscore > firstNonUnderscore
+ && stringKey[lastNonUnderscore] == "_"
+ {
+ stringKey.formIndex(before: &lastNonUnderscore)
+ }
+
+ let keyRange = firstNonUnderscore...lastNonUnderscore
+ let leadingUnderscoreRange = stringKey.startIndex..,
+ trailing: Range,
+ in stringKey: String
+ ) -> String {
+ if leading.isEmpty && trailing.isEmpty {
+ return joined
+ } else if !leading.isEmpty && !trailing.isEmpty {
+ return String(stringKey[leading]) + joined + String(stringKey[trailing])
+ } else if !leading.isEmpty {
+ return String(stringKey[leading]) + joined
+ } else {
+ return joined + String(stringKey[trailing])
+ }
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoder.swift b/Sources/DictionaryCoding/DictionaryDecoder.swift
new file mode 100644
index 0000000..7a41af3
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoder.swift
@@ -0,0 +1,240 @@
+//
+// DictionaryDecoder.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// ===----------------------------------------------------------------------===//
+// Dictionary Decoder
+// ===----------------------------------------------------------------------===//
+/// `DictionaryDecoder` facilitates the decoding of Dictionary into semantic
+/// `Decodable` types.
+open class DictionaryDecoder {
+ // MARK: - Subtypes
+
+ /// The strategy to use for decoding `Date` values.
+ public enum DateDecodingStrategy {
+ /// Defer to `Date` for decoding. This is the default strategy.
+ case deferredToDate
+
+ /// Decode the `Date` as a UNIX timestamp from a Dictionary number.
+ case secondsSince1970
+
+ /// Decode the `Date` as UNIX millisecond timestamp from a Dictionary number.
+ case millisecondsSince1970
+
+ /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
+ @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
+ case iso8601
+
+ /// Decode the `Date` as a string parsed by the given formatter.
+ case formatted(DateFormatter)
+
+ /// Decode the `Date` as a custom value decoded by the given closure.
+ case custom((_ decoder: Decoder) throws -> Date)
+ }
+
+ /// The strategy to use for decoding `Data` values.
+ public enum DataDecodingStrategy {
+ /// Defer to `Data` for decoding.
+ case deferredToData
+
+ /// Decode the `Data` from a Base64-encoded string. This is the default strategy.
+ case base64
+
+ /// Decode the `Data` as a custom value decoded by the given closure.
+ case custom((_ decoder: Decoder) throws -> Data)
+ }
+
+ /// The strategy to use for non-Dictionary-conforming floating-point values
+ /// (IEEE 754 infinity and NaN).
+ public enum NonConformingFloatDecodingStrategy {
+ /// Throw upon encountering non-conforming values. This is the default strategy.
+ case `throw`
+
+ /// Decode the values from the given representation strings.
+ case convertFromString(
+ positiveInfinity: String, negativeInfinity: String, nan: String
+ )
+ }
+
+ /// The strategy to use when decoding missing keys.
+ public enum MissingValueDecodingStrategy {
+ /// Throw upon encountering missing values.
+ case `throw`
+
+ /// Attempt to use a default value when encountering missing values for
+ /// standard types.
+ case useStandardDefault
+
+ /// Attempt to use a default value when encountering missing values.
+ /// The default value is read from the associated dictionary, keyed by the
+ /// name of the type.
+ case useDefault(defaults: [String: Any])
+ }
+
+ /// The strategy to use for automatically changing the value of keys before decoding.
+ public enum KeyDecodingStrategy {
+ /// Use the keys specified by each type. This is the default strategy.
+ case useDefaultKeys
+
+ /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to
+ /// match a key with the one specified by each type.
+ ///
+ /// Converting from snake case to camel case:
+ /// 1. Capitalizes the word starting after each `_`
+ /// 2. Removes all `_`
+ /// 3. Preserves starting and ending `_`.
+ /// For example, `one_two_three` becomes `oneTwoThree`.
+ ///
+ /// - Note: Using a key decoding strategy has a nominal performance cost.
+ case convertFromSnakeCase
+
+ /// Provide a custom conversion from the key in the encoded Dictionary to the
+ /// keys specified by the decoded types.
+ case custom((_ codingPath: [CodingKey]) -> CodingKey)
+ }
+
+ // MARK: - Instance Properties
+
+ /// The strategy to use when values are missing.
+ open var missingValueDecodingStrategy: MissingValueDecodingStrategy = .throw
+
+ /// The strategy to use in decoding dates.
+ ///
+ /// Defaults to `.deferredToDate`.
+ open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate
+
+ /// The strategy to use in decoding binary data.
+ ///
+ /// Defaults to `.base64`.
+ open var dataDecodingStrategy: DataDecodingStrategy = .base64
+
+ /// The strategy to use in decoding non-conforming numbers.
+ ///
+ /// Defaults to `.throw`.
+ open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw
+
+ /// The strategy to use for decoding keys.
+ ///
+ /// Defaults to `.useDefaultKeys`.
+ open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
+
+ /// Contextual user-provided information for use during decoding.
+ open var userInfo: [CodingUserInfoKey: Any] = [:]
+
+ /// The options set on the top-level decoder.
+ internal var options: DictionaryDecoderOptions {
+ DictionaryDecoderOptions(
+ missingValueDecodingStrategy: resolvedMissingValueStrategy,
+ dateDecodingStrategy: dateDecodingStrategy,
+ dataDecodingStrategy: dataDecodingStrategy,
+ nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
+ keyDecodingStrategy: keyDecodingStrategy,
+ userInfo: userInfo
+ )
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` with default strategies.
+ public init() {}
+
+ // MARK: - Instance Methods
+
+ /// Decodes a top-level value of the given type from the given
+ /// Dictionary representation.
+ ///
+ /// - parameter type: The type of the value to decode.
+ /// - parameter dictionary: The data to decode from.
+ /// - returns: A value of the requested type.
+ /// - throws: `DecodingError.dataCorrupted` if values requested from the payload
+ /// are corrupted, or if the given data is not valid Dictionary.
+ /// - throws: An error if any value throws an error during decoding.
+ open func decode(
+ _ type: T.Type,
+ from dictionary: NSDictionary
+ ) throws -> T {
+ let decoder = DictionaryDecoderImpl(referencing: dictionary, options: self.options)
+ guard let value = try decoder.unbox(dictionary, as: type) else {
+ throw DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: [],
+ debugDescription: "The given data did not contain a top-level value."
+ )
+ )
+ }
+
+ return value
+ }
+
+ /// Decodes a top-level value of the given type from the given
+ /// Dictionary representation.
+ ///
+ /// - parameter type: The type of the value to decode.
+ /// - parameter dictionary: The data to decode from.
+ /// - returns: A value of the requested type.
+ /// - throws: `DecodingError.dataCorrupted` if values requested from the payload
+ /// are corrupted, or if the given data is not valid Dictionary.
+ /// - throws: An error if any value throws an error during decoding.
+ open func decode(
+ _ type: T.Type,
+ from dictionary: [String: Any]
+ ) throws -> T {
+ let decoder = DictionaryDecoderImpl(referencing: dictionary, options: self.options)
+ guard let value = try decoder.unbox(dictionary, as: type) else {
+ throw DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: [],
+ debugDescription: "The given data did not contain a top-level value."
+ )
+ )
+ }
+
+ return value
+ }
+}
+
+extension DictionaryDecoder {
+ private static var standardDefaults: [String: Any] {
+ [
+ "Int": 0,
+ "Int8": Int8(0),
+ "Int16": Int16(0),
+ "Int32": Int32(0),
+ "Int64": Int64(0),
+ "UInt": UInt(0),
+ "UInt8": UInt8(0),
+ "UInt16": UInt16(0),
+ "UInt32": UInt32(0),
+ "UInt64": UInt64(0),
+ "Float": Float(0.0),
+ "Double": 0.0,
+ "String": "",
+ "Bool": false,
+ "Date": Date(timeIntervalSinceReferenceDate: 0),
+ "Data": Data(),
+ ]
+ }
+
+ private var resolvedMissingValueStrategy: MissingValueDecodingStrategy {
+ guard case .useStandardDefault = missingValueDecodingStrategy else {
+ return missingValueDecodingStrategy
+ }
+ return .useDefault(defaults: Self.standardDefaults)
+ }
+}
+
+#if canImport(Combine)
+ import Combine
+
+ extension DictionaryDecoder: TopLevelDecoder {
+ public typealias Input = [String: Any]
+ }
+#endif
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift
new file mode 100644
index 0000000..540e6c3
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift
@@ -0,0 +1,158 @@
+//
+// DictionaryDecoderImpl+SingleValue.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - SingleValueDecodingContainer
+extension DictionaryDecoderImpl: SingleValueDecodingContainer {
+ internal func expectNonNull(_ type: T.Type) throws {
+ guard !self.decodeNil() else {
+ throw DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription: "Expected \(type) but found null value instead."
+ )
+ )
+ }
+ }
+
+ internal func nullValueError(_ type: T.Type) -> DecodingError {
+ DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription: "Expected \(type) but found null value instead."
+ )
+ )
+ }
+
+ public func decodeNil() -> Bool {
+ self.storage.topContainer is NSNull
+ }
+
+ public func decode(_ type: Bool.Type) throws -> Bool {
+ try expectNonNull(Bool.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Bool.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: Int.Type) throws -> Int {
+ try expectNonNull(Int.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Int.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: Int8.Type) throws -> Int8 {
+ try expectNonNull(Int8.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Int8.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: Int16.Type) throws -> Int16 {
+ try expectNonNull(Int16.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Int16.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: Int32.Type) throws -> Int32 {
+ try expectNonNull(Int32.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Int32.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: Int64.Type) throws -> Int64 {
+ try expectNonNull(Int64.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Int64.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: UInt.Type) throws -> UInt {
+ try expectNonNull(UInt.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: UInt.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: UInt8.Type) throws -> UInt8 {
+ try expectNonNull(UInt8.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: UInt8.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: UInt16.Type) throws -> UInt16 {
+ try expectNonNull(UInt16.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: UInt16.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: UInt32.Type) throws -> UInt32 {
+ try expectNonNull(UInt32.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: UInt32.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: UInt64.Type) throws -> UInt64 {
+ try expectNonNull(UInt64.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: UInt64.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: Float.Type) throws -> Float {
+ try expectNonNull(Float.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Float.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: Double.Type) throws -> Double {
+ try expectNonNull(Double.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: Double.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: String.Type) throws -> String {
+ try expectNonNull(String.self)
+ guard let value = try self.unbox(self.storage.topContainer, as: String.self) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+
+ public func decode(_ type: T.Type) throws -> T {
+ try expectNonNull(type)
+ guard let value = try self.unbox(self.storage.topContainer, as: type) else {
+ throw nullValueError(type)
+ }
+ return value
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift
new file mode 100644
index 0000000..8641986
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift
@@ -0,0 +1,99 @@
+//
+// DictionaryDecoderImpl+Unbox.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Concrete Value Representations
+extension DictionaryDecoderImpl {
+ internal func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+
+ if let number = value as? NSNumber {
+ if number.isBool {
+ return number.boolValue
+ } else {
+ return number != 0
+ }
+ }
+
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: type, reality: value
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: String.Type) throws -> String? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+
+ if let url = value as? URL {
+ return url.absoluteString
+ }
+
+ if let uuid = value as? UUID {
+ return uuid.uuidString
+ }
+
+ guard let string = value as? String else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: type, reality: value
+ )
+ }
+
+ return string
+ }
+
+ internal func unbox(_ value: Any, as type: UUID.Type) throws -> UUID? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+
+ if let uuid = value as? UUID {
+ return uuid
+ }
+
+ if let string = value as? String {
+ return UUID(uuidString: string)
+ }
+
+ // NB this could be dangerous - we're assuming that it's ok to call
+ // CFGetTypeID with the value, which may not be true
+ #if canImport(Darwin)
+ let cfType = CFGetTypeID(value as CFTypeRef)
+ if cfType == CFUUIDGetTypeID() {
+ let cfValue = unsafeBitCast(value as CFTypeRef, to: CFUUID.self)
+ let string = CFUUIDCreateString(kCFAllocatorDefault, cfValue) as String
+ return UUID(uuidString: string)
+ }
+ #endif
+
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: type, reality: value
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: Decimal.Type) throws -> Decimal? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+
+ if let decimal = value as? Decimal {
+ return decimal
+ } else if let decimalNumber = value as? NSDecimalNumber {
+ // On Linux, NSDecimalNumber may not auto-bridge to Decimal.
+ return decimalNumber as Decimal
+ } else {
+ guard let doubleValue = try self.unbox(value, as: Double.self) else {
+ return nil
+ }
+ return Decimal(doubleValue)
+ }
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift
new file mode 100644
index 0000000..90eceff
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift
@@ -0,0 +1,183 @@
+//
+// DictionaryDecoderImpl+UnboxDecodable.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Date, Data, and Decodable unboxing
+extension DictionaryDecoderImpl {
+ internal func unbox(_ value: Any, as type: Date.Type) throws -> Date? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxDate(value)
+ }
+
+ private func unboxDate(_ value: Any) throws -> Date? {
+ switch self.options.dateDecodingStrategy {
+ case .deferredToDate:
+ return try unboxDateDeferred(value)
+ case .secondsSince1970:
+ return try unboxDateTimestamp(value, factor: 1.0)
+ case .millisecondsSince1970:
+ return try unboxDateTimestamp(value, factor: 1_000.0)
+ case .iso8601:
+ return try unboxDateISO8601(value)
+ case .formatted(let formatter):
+ return try unboxDateFormatted(value, formatter: formatter)
+ case .custom(let closure):
+ return try unboxDateCustom(value, closure: closure)
+ }
+ }
+
+ private func unboxDateDeferred(_ value: Any) throws -> Date? {
+ self.storage.push(container: value)
+ defer { self.storage.popContainer() }
+ return try Date(from: self)
+ }
+
+ private func unboxDateTimestamp(_ value: Any, factor: Double) throws -> Date? {
+ guard let double = try self.unbox(value, as: Double.self) else {
+ return nil
+ }
+ return Date(timeIntervalSince1970: double / factor)
+ }
+
+ private func unboxDateCustom(
+ _ value: Any,
+ closure: (Decoder) throws -> Date
+ ) throws -> Date? {
+ self.storage.push(container: value)
+ defer { self.storage.popContainer() }
+ return try closure(self)
+ }
+
+ private func unboxDateISO8601(_ value: Any) throws -> Date? {
+ guard let string = try self.unbox(value, as: String.self) else {
+ return nil
+ }
+ guard let date = try? Date(string, strategy: .iso8601) else {
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription: "Expected date string to be ISO8601-formatted."
+ )
+ )
+ }
+ return date
+ }
+
+ private func unboxDateFormatted(
+ _ value: Any,
+ formatter: DateFormatter
+ ) throws -> Date? {
+ guard let string = try self.unbox(value, as: String.self) else {
+ return nil
+ }
+ guard let date = formatter.date(from: string) else {
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Date string does not match format expected by formatter."
+ )
+ )
+ }
+ return date
+ }
+
+ internal func unbox(_ value: Any, as type: Data.Type) throws -> Data? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+
+ switch self.options.dataDecodingStrategy {
+ case .deferredToData:
+ self.storage.push(container: value)
+ defer { self.storage.popContainer() }
+ return try Data(from: self)
+
+ case .base64:
+ guard let string = value as? String else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: type, reality: value
+ )
+ }
+
+ guard let data = Data(base64Encoded: string) else {
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription: "Encountered Data is not valid Base64."
+ )
+ )
+ }
+
+ return data
+
+ case .custom(let closure):
+ self.storage.push(container: value)
+ defer { self.storage.popContainer() }
+ return try closure(self)
+ }
+ }
+
+ internal func unbox(_ value: Any, as type: T.Type) throws -> T? {
+ if let result = try unboxSpecialType(value, as: type) {
+ return result
+ }
+ self.storage.push(container: value)
+ defer { self.storage.popContainer() }
+ return try type.init(from: self)
+ }
+
+ private func unboxSpecialType(
+ _ value: Any,
+ as type: T.Type
+ ) throws -> T?? {
+ if type == Date.self || type == NSDate.self {
+ return try self.unbox(value, as: Date.self) as? T
+ } else if type == Data.self || type == NSData.self {
+ return try self.unbox(value, as: Data.self) as? T
+ } else if isUUIDCompatibleType(type) {
+ return try self.unbox(value, as: UUID.self) as? T
+ } else if type == URL.self || type == NSURL.self {
+ return try unboxURL(value)
+ } else if type == Decimal.self || type == NSDecimalNumber.self {
+ return try self.unbox(value, as: Decimal.self) as? T
+ }
+ return nil
+ }
+
+ private func isUUIDCompatibleType(_ type: T.Type) -> Bool {
+ #if canImport(Darwin)
+ return type == UUID.self || type == CFUUID.self
+ #else
+ return type == UUID.self
+ #endif
+ }
+
+ private func unboxURL(_ value: Any) throws -> T? {
+ guard let urlString = try self.unbox(value, as: String.self) else {
+ return nil
+ }
+
+ guard let url = URL(string: urlString) else {
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription: "Invalid URL string."
+ )
+ )
+ }
+
+ guard let result = url as? T else {
+ return nil
+ }
+ return result
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift
new file mode 100644
index 0000000..ea366de
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift
@@ -0,0 +1,101 @@
+//
+// DictionaryDecoderImpl+UnboxFloats.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Float and Double unboxing
+extension DictionaryDecoderImpl {
+ internal func unbox(_ value: Any, as type: Float.Type) throws -> Float? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+
+ if let number = value as? NSNumber,
+ !number.isBool
+ {
+ return try unboxFloatFromNumber(number, as: type)
+ } else if let float = unboxFloatFromString(value, as: type) {
+ return float
+ }
+
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: type, reality: value
+ )
+ }
+
+ private func unboxFloatFromNumber(
+ _ number: NSNumber,
+ as type: Float.Type
+ ) throws -> Float {
+ let double = number.doubleValue
+ guard abs(double) <= Double(Float.greatestFiniteMagnitude) else {
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Parsed Dictionary number \(number) does not fit in \(type)."
+ )
+ )
+ }
+ return Float(double)
+ }
+
+ private func unboxFloatFromString(_ value: Any, as type: Float.Type) -> Float? {
+ guard let string = value as? String,
+ case let .convertFromString(posInfString, negInfString, nanString) =
+ self.options.nonConformingFloatDecodingStrategy
+ else {
+ return nil
+ }
+
+ if string == posInfString {
+ return Float.infinity
+ } else if string == negInfString {
+ return -Float.infinity
+ } else if string == nanString {
+ return Float.nan
+ }
+ return nil
+ }
+
+ internal func unbox(_ value: Any, as type: Double.Type) throws -> Double? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+
+ if let number = value as? NSNumber,
+ !number.isBool
+ {
+ return number.doubleValue
+ } else if let double = unboxDoubleFromString(value) {
+ return double
+ }
+
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: type, reality: value
+ )
+ }
+
+ private func unboxDoubleFromString(_ value: Any) -> Double? {
+ guard let string = value as? String,
+ case let .convertFromString(posInfString, negInfString, nanString) =
+ self.options.nonConformingFloatDecodingStrategy
+ else {
+ return nil
+ }
+
+ if string == posInfString {
+ return Double.infinity
+ } else if string == negInfString {
+ return -Double.infinity
+ } else if string == nanString {
+ return Double.nan
+ }
+ return nil
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift
new file mode 100644
index 0000000..2a5164f
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift
@@ -0,0 +1,161 @@
+//
+// DictionaryDecoderImpl+UnboxIntegers.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Integer unboxing
+extension DictionaryDecoderImpl {
+ // On Linux, integer values in [String: Any] may not bridge to NSNumber.
+ // swiftlint:disable:next cyclomatic_complexity
+ private static func nsNumber(from value: Any) -> NSNumber? {
+ // Accept NSNumber directly (e.g. from DictionaryEncoder round-trips)
+ // but reject booleans masquerading as integers.
+ if let number = value as? NSNumber {
+ #if canImport(Darwin)
+ // On Darwin, CFBooleanGetTypeID() reliably identifies boolean NSNumbers.
+ if number.isBool {
+ return nil
+ }
+ #endif
+ // On Linux, Bool and Int8 share objCType "c" so isBool is unreliable;
+ // we accept NSNumber as-is and cannot reject boolean-sourced values.
+ return number
+ }
+ // On Linux, native Swift integers in [String: Any] may not bridge to
+ // NSNumber automatically — handle each concrete type.
+ switch value {
+ case let int as Int: return NSNumber(value: int)
+ case let int as Int8: return NSNumber(value: int)
+ case let int as Int16: return NSNumber(value: int)
+ case let int as Int32: return NSNumber(value: int)
+ case let int as Int64: return NSNumber(value: int)
+ case let uint as UInt: return NSNumber(value: uint)
+ case let uint as UInt8: return NSNumber(value: uint)
+ case let uint as UInt16: return NSNumber(value: uint)
+ case let uint as UInt32: return NSNumber(value: uint)
+ case let uint as UInt64: return NSNumber(value: uint)
+ default: return nil
+ }
+ }
+
+ internal func unboxInteger(
+ _ value: Any,
+ as type: T.Type,
+ extract: (NSNumber) -> T,
+ wrap: (T) -> NSNumber
+ ) throws -> T? {
+ guard let number = Self.nsNumber(from: value) else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: type, reality: value
+ )
+ }
+
+ let result = extract(number)
+ guard wrap(result) == number else {
+ throw DecodingError.dataCorrupted(
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Parsed Dictionary number <\(number)> does not fit in \(type)."
+ )
+ )
+ }
+
+ return result
+ }
+
+ internal func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.intValue }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.int8Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.int16Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.int32Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.int64Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.uintValue }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.uint8Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.uint16Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.uint32Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+
+ internal func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? {
+ guard !(value is NSNull) else {
+ return nil
+ }
+ return try unboxInteger(
+ value, as: type, extract: { $0.uint64Value }, wrap: { NSNumber(value: $0) }
+ )
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl.swift
new file mode 100644
index 0000000..9f5bb12
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl.swift
@@ -0,0 +1,103 @@
+//
+// DictionaryDecoderImpl.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - DictionaryDecoderImpl
+internal class DictionaryDecoderImpl: Decoder {
+ // MARK: - Instance Properties
+
+ /// The decoder's storage.
+ internal var storage: DictionaryDecodingStorage
+
+ /// Options set on the top-level decoder.
+ internal let options: DictionaryDecoderOptions
+
+ /// The path to the current point in encoding.
+ internal var codingPath: [CodingKey]
+
+ /// Contextual user-provided information for use during encoding.
+ internal var userInfo: [CodingUserInfoKey: Any] {
+ self.options.userInfo
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` with the given top-level container and options.
+ internal init(
+ referencing container: Any,
+ at codingPath: [CodingKey] = [],
+ options: DictionaryDecoderOptions
+ ) {
+ self.storage = DictionaryDecodingStorage()
+ self.storage.push(container: container)
+ self.codingPath = codingPath
+ self.options = options
+ }
+
+ // MARK: - Instance Methods
+
+ internal func container(
+ keyedBy type: Key.Type
+ ) throws -> KeyedDecodingContainer {
+ guard !(self.storage.topContainer is NSNull) else {
+ throw DecodingError.valueNotFound(
+ KeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get keyed decoding container -- found null value instead."
+ )
+ )
+ }
+
+ guard let topContainer = self.storage.topContainer as? [String: Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath,
+ expectation: [String: Any].self,
+ reality: self.storage.topContainer
+ )
+ }
+
+ let container = DictionaryCodingKeyedDecodingContainer(
+ referencing: self,
+ wrapping: topContainer
+ )
+ return KeyedDecodingContainer(container)
+ }
+
+ internal func unkeyedContainer() throws -> UnkeyedDecodingContainer {
+ guard !(self.storage.topContainer is NSNull) else {
+ throw DecodingError.valueNotFound(
+ UnkeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get unkeyed decoding container -- found null value instead."
+ )
+ )
+ }
+
+ guard let topContainer = self.storage.topContainer as? [Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath,
+ expectation: [Any].self,
+ reality: self.storage.topContainer
+ )
+ }
+
+ return DictionaryUnkeyedDecodingContainer(
+ referencing: self,
+ wrapping: topContainer
+ )
+ }
+
+ internal func singleValueContainer() throws -> SingleValueDecodingContainer {
+ self
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoderOptions.swift b/Sources/DictionaryCoding/DictionaryDecoderOptions.swift
new file mode 100644
index 0000000..fe521a8
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoderOptions.swift
@@ -0,0 +1,23 @@
+//
+// DictionaryDecoderOptions.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+/// Options set on the top-level decoder to pass down the decoding hierarchy.
+internal struct DictionaryDecoderOptions {
+ // MARK: - Instance Properties
+
+ // swiftlint:disable:next line_length
+ internal let missingValueDecodingStrategy: DictionaryDecoder.MissingValueDecodingStrategy
+ internal let dateDecodingStrategy: DictionaryDecoder.DateDecodingStrategy
+ internal let dataDecodingStrategy: DictionaryDecoder.DataDecodingStrategy
+ internal let nonConformingFloatDecodingStrategy:
+ DictionaryDecoder.NonConformingFloatDecodingStrategy
+ internal let keyDecodingStrategy: DictionaryDecoder.KeyDecodingStrategy
+ internal let userInfo: [CodingUserInfoKey: Any]
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecodingStorage.swift b/Sources/DictionaryCoding/DictionaryDecodingStorage.swift
new file mode 100644
index 0000000..a5b88fb
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecodingStorage.swift
@@ -0,0 +1,50 @@
+//
+// DictionaryDecodingStorage.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Decoding Storage
+internal struct DictionaryDecodingStorage {
+ // MARK: - Instance Properties
+
+ /// The container stack.
+ ///
+ /// Elements may be any one of the Dictionary types
+ /// (NSNull, NSNumber, String, Array, [String : Any]).
+ internal private(set) var containers: [Any] = []
+
+ // MARK: - Computed Properties
+
+ internal var count: Int {
+ self.containers.count
+ }
+
+ internal var topContainer: Any {
+ precondition(!self.containers.isEmpty, "Empty container stack.")
+ guard let last = self.containers.last else {
+ fatalError("Empty container stack.")
+ }
+ return last
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` with no containers.
+ internal init() {}
+
+ // MARK: - Instance Methods
+
+ internal mutating func push(container: Any) {
+ self.containers.append(container)
+ }
+
+ internal mutating func popContainer() {
+ precondition(!self.containers.isEmpty, "Empty container stack.")
+ self.containers.removeLast()
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryEncoder+Encode.swift b/Sources/DictionaryCoding/DictionaryEncoder+Encode.swift
new file mode 100644
index 0000000..5c7fe3c
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncoder+Encode.swift
@@ -0,0 +1,108 @@
+//
+// DictionaryEncoder+Encode.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+extension DictionaryEncoder {
+ /// Encodes the given top-level value and returns its Dictionary representation.
+ ///
+ /// - parameter value: The value to encode.
+ /// - returns: A new `NSDictionary` value containing the encoded Dictionary data.
+ /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value
+ /// is encountered during encoding, and the encoding strategy is `.throw`.
+ /// - throws: An error if any value throws an error during encoding.
+ public func encode(_ value: T) throws -> NSDictionary {
+ let topLevel = try encodeToTopLevel(value)
+
+ guard let dict = topLevel as? NSDictionary else {
+ throw EncodingError.invalidValue(
+ value,
+ EncodingError.Context(
+ codingPath: [],
+ debugDescription:
+ "Top-level \(T.self) did not encode as a dictionary."
+ )
+ )
+ }
+ return dict
+ }
+
+ /// Encodes the given top-level value and returns its Dictionary representation.
+ ///
+ /// - parameter value: The value to encode.
+ /// - returns: A new `[String: Any]` value containing the encoded Dictionary data.
+ /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value
+ /// is encountered during encoding, and the encoding strategy is `.throw`.
+ /// - throws: An error if any value throws an error during encoding.
+ public func encode(_ value: T) throws -> [String: Any] {
+ let topLevel = try encodeToTopLevel(value)
+
+ guard let dict = topLevel as? [String: Any] else {
+ throw EncodingError.invalidValue(
+ value,
+ EncodingError.Context(
+ codingPath: [],
+ debugDescription:
+ "Top-level \(T.self) did not encode as a dictionary."
+ )
+ )
+ }
+ return dict
+ }
+
+ private func encodeToTopLevel(_ value: T) throws -> Any {
+ let encoder = DictionaryEncoderImpl(options: self.options)
+
+ guard let topLevel = try encoder.boxEncodable(value) else {
+ throw EncodingError.invalidValue(
+ value,
+ EncodingError.Context(
+ codingPath: [],
+ debugDescription: "Top-level \(T.self) did not encode any values."
+ )
+ )
+ }
+
+ try validateTopLevelFragment(value, topLevel: topLevel)
+ return topLevel
+ }
+
+ private func validateTopLevelFragment(
+ _ value: T,
+ topLevel: Any
+ ) throws {
+ if topLevel is NSNull {
+ throw EncodingError.invalidValue(
+ value,
+ EncodingError.Context(
+ codingPath: [],
+ debugDescription:
+ "Top-level \(T.self) encoded as null Dictionary fragment."
+ )
+ )
+ } else if topLevel is NSNumber {
+ throw EncodingError.invalidValue(
+ value,
+ EncodingError.Context(
+ codingPath: [],
+ debugDescription:
+ "Top-level \(T.self) encoded as number Dictionary fragment."
+ )
+ )
+ } else if topLevel is NSString {
+ throw EncodingError.invalidValue(
+ value,
+ EncodingError.Context(
+ codingPath: [],
+ debugDescription:
+ "Top-level \(T.self) encoded as string Dictionary fragment."
+ )
+ )
+ }
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryEncoder.swift b/Sources/DictionaryCoding/DictionaryEncoder.swift
new file mode 100644
index 0000000..1dd3469
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncoder.swift
@@ -0,0 +1,214 @@
+//
+// DictionaryEncoder.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// ===----------------------------------------------------------------------===//
+// Dictionary Encoder
+// ===----------------------------------------------------------------------===//
+
+/// `DictionaryEncoder` facilitates the encoding of `Encodable` values into Dictionary.
+open class DictionaryEncoder {
+ // MARK: - Subtypes
+
+ /// The strategy to use for encoding `Date` values.
+ public enum DateEncodingStrategy {
+ /// Defer to `Date` for choosing an encoding. This is the default strategy.
+ case deferredToDate
+
+ /// Encode the `Date` as a UNIX timestamp (as a Dictionary number).
+ case secondsSince1970
+
+ /// Encode the `Date` as UNIX millisecond timestamp (as a Dictionary number).
+ case millisecondsSince1970
+
+ /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
+ @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
+ case iso8601
+
+ /// Encode the `Date` as a string formatted by the given formatter.
+ case formatted(DateFormatter)
+
+ /// Encode the `Date` as a custom value encoded by the given closure.
+ ///
+ /// If the closure fails to encode a value into the given encoder, the encoder
+ /// will encode an empty automatic container in its place.
+ case custom((Date, Encoder) throws -> Void)
+ }
+
+ /// The strategy to use for encoding `Data` values.
+ public enum DataEncodingStrategy {
+ /// Defer to `Data` for choosing an encoding.
+ case deferredToData
+
+ /// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
+ case base64
+
+ /// Encode the `Data` as a custom value encoded by the given closure.
+ ///
+ /// If the closure fails to encode a value into the given encoder, the encoder
+ /// will encode an empty automatic container in its place.
+ case custom((Data, Encoder) throws -> Void)
+ }
+
+ /// The strategy to use for non-Dictionary-conforming floating-point values
+ /// (IEEE 754 infinity and NaN).
+ public enum NonConformingFloatEncodingStrategy {
+ /// Throw upon encountering non-conforming values. This is the default strategy.
+ case `throw`
+
+ /// Encode the values using the given representation strings.
+ case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
+ }
+
+ /// The strategy to use for automatically changing the value of keys before encoding.
+ public enum KeyEncodingStrategy {
+ /// Use the keys specified by each type. This is the default strategy.
+ case useDefaultKeys
+
+ /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key
+ /// to Dictionary payload.
+ ///
+ /// Capital characters are determined by testing membership in
+ /// `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters`
+ /// (Unicode General Categories Lu and Lt).
+ /// The conversion to lower case uses `Locale.system`, also known as the ICU
+ /// "root" locale. This means the result is consistent regardless of the current
+ /// user's locale and language preferences.
+ ///
+ /// Converting from camel case to snake case:
+ /// 1. Splits words at the boundary of lower-case to upper-case
+ /// 2. Inserts `_` between words
+ /// 3. Lowercases the entire string
+ /// 4. Preserves starting and ending `_`.
+ ///
+ /// For example, `oneTwoThree` becomes `one_two_three`.
+ /// `_oneTwoThree_` becomes `_one_two_three_`.
+ ///
+ /// - Note: Using a key encoding strategy has a nominal performance cost,
+ /// as each string key has to be converted.
+ case convertToSnakeCase
+
+ /// Provide a custom conversion to the key in the encoded Dictionary from the keys
+ /// specified by the encoded types.
+ /// The full path to the current encoding position is provided for context
+ /// (in case you need to locate this key within the payload).
+ /// The returned key is used in place of the last component in the coding path
+ /// before encoding.
+ /// If the result of the conversion is a duplicate key, then only one value will
+ /// be present in the result.
+ case custom((_ codingPath: [CodingKey]) -> CodingKey)
+
+ internal static func convertToSnakeCase(_ stringKey: String) -> String {
+ guard !stringKey.isEmpty else {
+ return stringKey
+ }
+
+ var words: [Range] = []
+ var wordStart = stringKey.startIndex
+ var searchRange = stringKey.index(after: wordStart)..,
+ searchRange: inout Range,
+ wordStart: inout String.Index,
+ words: inout [Range]
+ ) {
+ guard
+ let lowerCaseRange = stringKey.rangeOfCharacter(
+ from: CharacterSet.lowercaseLetters,
+ options: [],
+ range: searchRange
+ )
+ else {
+ wordStart = searchRange.lowerBound
+ searchRange = searchRange.upperBound.. NSObject { NSNumber(value: value) }
+ internal func box(_ value: Int) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: Int8) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: Int16) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: Int32) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: Int64) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: UInt) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: UInt8) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: UInt16) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: UInt32) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: UInt64) -> NSObject { NSNumber(value: value) }
+ internal func box(_ value: String) -> NSObject { NSString(string: value) }
+
+ internal func box(_ float: Float) throws -> NSObject {
+ guard !float.isInfinite && !float.isNaN else {
+ return try boxNonConformingFloat(float)
+ }
+ return NSNumber(value: float)
+ }
+
+ private func boxNonConformingFloat(_ float: Float) throws -> NSObject {
+ guard
+ case let .convertToString(
+ positiveInfinity: posInfString,
+ negativeInfinity: negInfString,
+ nan: nanString
+ ) = self.options.nonConformingFloatEncodingStrategy
+ else {
+ throw EncodingError.invalidFloatingPointValue(float, at: codingPath)
+ }
+
+ if float == Float.infinity {
+ return NSString(string: posInfString)
+ } else if float == -Float.infinity {
+ return NSString(string: negInfString)
+ } else {
+ return NSString(string: nanString)
+ }
+ }
+
+ internal func box(_ double: Double) throws -> NSObject {
+ guard !double.isInfinite && !double.isNaN else {
+ return try boxNonConformingDouble(double)
+ }
+ return NSNumber(value: double)
+ }
+
+ private func boxNonConformingDouble(_ double: Double) throws -> NSObject {
+ guard
+ case let .convertToString(
+ positiveInfinity: posInfString,
+ negativeInfinity: negInfString,
+ nan: nanString
+ ) = self.options.nonConformingFloatEncodingStrategy
+ else {
+ throw EncodingError.invalidFloatingPointValue(double, at: codingPath)
+ }
+
+ if double == Double.infinity {
+ return NSString(string: posInfString)
+ } else if double == -Double.infinity {
+ return NSString(string: negInfString)
+ } else {
+ return NSString(string: nanString)
+ }
+ }
+
+ internal func box(_ date: Date) throws -> NSObject {
+ switch self.options.dateEncodingStrategy {
+ case .deferredToDate:
+ return try boxDateDeferred(date)
+ case .secondsSince1970:
+ return NSNumber(value: date.timeIntervalSince1970)
+ case .millisecondsSince1970:
+ return NSNumber(value: 1_000.0 * date.timeIntervalSince1970)
+ case .iso8601:
+ return boxDateISO8601(date)
+ case .formatted(let formatter):
+ return NSString(string: formatter.string(from: date))
+ case .custom(let closure):
+ return try boxDateCustom(date, closure: closure)
+ }
+ }
+
+ private func boxDateDeferred(_ date: Date) throws -> NSObject {
+ // Must be called with a surrounding with(pushedKey:) call.
+ // Dates encode as single-value objects; this can't both throw and push a
+ // container, so no need to catch the error.
+ try date.encode(to: self)
+ return self.storage.popContainer()
+ }
+
+ private func boxDateISO8601(_ date: Date) -> NSObject {
+ NSString(string: date.formatted(.iso8601))
+ }
+
+ private func boxDateCustom(
+ _ date: Date,
+ closure: (Date, Encoder) throws -> Void
+ ) throws -> NSObject {
+ let depth = self.storage.count
+ do {
+ try closure(date, self)
+ } catch {
+ if self.storage.count > depth {
+ _ = self.storage.popContainer()
+ }
+ throw error
+ }
+
+ guard self.storage.count > depth else {
+ return NSDictionary()
+ }
+
+ return self.storage.popContainer()
+ }
+
+ internal func box(_ data: Data) throws -> NSObject {
+ switch self.options.dataEncodingStrategy {
+ case .deferredToData:
+ return try boxDataDeferred(data)
+ case .base64:
+ return NSString(string: data.base64EncodedString())
+ case .custom(let closure):
+ return try boxDataCustom(data, closure: closure)
+ }
+ }
+
+ private func boxDataDeferred(_ data: Data) throws -> NSObject {
+ // Must be called with a surrounding with(pushedKey:) call.
+ let depth = self.storage.count
+ do {
+ try data.encode(to: self)
+ } catch {
+ if self.storage.count > depth {
+ _ = self.storage.popContainer()
+ }
+ throw error
+ }
+ return self.storage.popContainer()
+ }
+
+ private func boxDataCustom(
+ _ data: Data,
+ closure: (Data, Encoder) throws -> Void
+ ) throws -> NSObject {
+ let depth = self.storage.count
+ do {
+ try closure(data, self)
+ } catch {
+ if self.storage.count > depth {
+ _ = self.storage.popContainer()
+ }
+ throw error
+ }
+
+ guard self.storage.count > depth else {
+ return NSDictionary()
+ }
+
+ return self.storage.popContainer()
+ }
+
+ internal func box(_ value: T) throws -> NSObject {
+ try self.boxEncodable(value) ?? NSDictionary()
+ }
+
+ // This method is called "boxEncodable" instead of "box" to disambiguate it from the
+ // overloads. Because the return type here is different from all of the "box"
+ // overloads (and is more general), any "box" calls in here would call back
+ // into "box" recursively instead of calling the appropriate overload, which
+ // is not what we want.
+ internal func boxEncodable(_ value: T) throws -> NSObject? {
+ if let result = try boxSpecialType(value) {
+ return result
+ }
+
+ return try boxGenericEncodable(value)
+ }
+
+ private func boxSpecialType(_ value: T) throws -> NSObject? {
+ if T.self == Date.self || T.self == NSDate.self {
+ return try boxAsDate(value)
+ } else if T.self == Data.self || T.self == NSData.self {
+ return try boxAsData(value)
+ } else if T.self == URL.self || T.self == NSURL.self {
+ return boxAsURL(value)
+ } else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
+ return boxAsDecimal(value)
+ }
+ return nil
+ }
+
+ private func boxAsDate(_ value: T) throws -> NSObject? {
+ guard let date = value as? Date else {
+ return nil
+ }
+ return try self.box(date)
+ }
+
+ private func boxAsData(_ value: T) throws -> NSObject? {
+ guard let data = value as? Data else {
+ return nil
+ }
+ return try self.box(data)
+ }
+
+ private func boxAsURL(_ value: T) -> NSObject? {
+ guard let url = value as? URL else {
+ return nil
+ }
+ return self.box(url.absoluteString)
+ }
+
+ private func boxAsDecimal(_ value: T) -> NSObject? {
+ if let decimal = value as? NSDecimalNumber {
+ // DictionarySerialization can natively handle NSDecimalNumber.
+ return decimal
+ }
+ // On Linux, Swift Decimal doesn't auto-bridge to NSDecimalNumber.
+ if let decimal = value as? Decimal {
+ return NSDecimalNumber(decimal: decimal)
+ }
+ return nil
+ }
+
+ private func boxGenericEncodable(_ value: T) throws -> NSObject? {
+ // The value should request a container from the DictionaryEncoderImpl.
+ let depth = self.storage.count
+ do {
+ try value.encode(to: self)
+ } catch {
+ // If the value pushed a container before throwing, pop it back off to
+ // restore state.
+ if self.storage.count > depth {
+ _ = self.storage.popContainer()
+ }
+ throw error
+ }
+
+ // The top container should be a new container.
+ guard self.storage.count > depth else {
+ return nil
+ }
+
+ return self.storage.popContainer()
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryEncoderImpl.swift b/Sources/DictionaryCoding/DictionaryEncoderImpl.swift
new file mode 100644
index 0000000..51860ad
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncoderImpl.swift
@@ -0,0 +1,203 @@
+//
+// DictionaryEncoderImpl.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - DictionaryEncoderImpl
+internal class DictionaryEncoderImpl: Encoder {
+ // MARK: - Instance Properties
+
+ /// The encoder's storage.
+ internal var storage: DictionaryEncodingStorage
+
+ /// Options set on the top-level encoder.
+ internal let options: DictionaryEncoderOptions
+
+ /// The path to the current point in encoding.
+ internal var codingPath: [CodingKey]
+
+ /// Contextual user-provided information for use during encoding.
+ internal var userInfo: [CodingUserInfoKey: Any] {
+ self.options.userInfo
+ }
+
+ /// Returns whether a new element can be encoded at this coding path.
+ ///
+ /// `true` if an element has not yet been encoded at this coding path;
+ /// `false` otherwise.
+ internal var canEncodeNewValue: Bool {
+ // Every time a new value gets encoded, the key it's encoded for is pushed
+ // onto the coding path (even if it's a nil key from an unkeyed container).
+ // At the same time, every time a container is requested, a new value gets
+ // pushed onto the storage stack.
+ // If there are more values on the storage stack than on the coding path,
+ // it means the value is requesting more than one container, which violates
+ // the precondition.
+ //
+ // This means that anytime something that can request a new container goes
+ // onto the stack, we MUST push a key onto the coding path.
+ // Things which will not request containers do not need to have the coding
+ // path extended for them (but it doesn't matter if it is, because they
+ // will not reach here).
+ self.storage.count == self.codingPath.count
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` with the given top-level encoder options.
+ internal init(options: DictionaryEncoderOptions, codingPath: [CodingKey] = []) {
+ self.options = options
+ self.storage = DictionaryEncodingStorage()
+ self.codingPath = codingPath
+ }
+
+ // MARK: - Instance Methods
+
+ internal func container(keyedBy: Key.Type) -> KeyedEncodingContainer {
+ // If an existing keyed container was already requested, return that one.
+ let topContainer: NSMutableDictionary
+ if self.canEncodeNewValue {
+ // We haven't yet pushed a container at this level; do so here.
+ topContainer = self.storage.pushKeyedContainer()
+ } else {
+ guard let container = self.storage.containers.last as? NSMutableDictionary
+ else {
+ preconditionFailure(
+ "Attempt to push new keyed encoding container when already previously"
+ + " encoded at this path."
+ )
+ }
+ topContainer = container
+ }
+
+ let container = DictionaryCodingKeyedEncodingContainer(
+ referencing: self,
+ codingPath: self.codingPath,
+ wrapping: topContainer
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ internal func unkeyedContainer() -> UnkeyedEncodingContainer {
+ // If an existing unkeyed container was already requested, return that one.
+ let topContainer: NSMutableArray
+ if self.canEncodeNewValue {
+ // We haven't yet pushed a container at this level; do so here.
+ topContainer = self.storage.pushUnkeyedContainer()
+ } else {
+ guard let container = self.storage.containers.last as? NSMutableArray else {
+ preconditionFailure(
+ "Attempt to push new unkeyed encoding container when already previously"
+ + " encoded at this path."
+ )
+ }
+ topContainer = container
+ }
+
+ return DictionaryUnkeyedEncodingContainer(
+ referencing: self,
+ codingPath: self.codingPath,
+ wrapping: topContainer
+ )
+ }
+
+ internal func singleValueContainer() -> SingleValueEncodingContainer {
+ self
+ }
+}
+
+// MARK: - SingleValueEncodingContainer
+extension DictionaryEncoderImpl: SingleValueEncodingContainer {
+ internal func assertCanEncodeNewValue() {
+ precondition(
+ self.canEncodeNewValue,
+ "Attempt to encode value through single value container when previously"
+ + " value already encoded."
+ )
+ }
+
+ public func encodeNil() throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: NSNull())
+ }
+
+ public func encode(_ value: Bool) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: Int) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: Int8) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: Int16) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: Int32) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: Int64) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: UInt) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: UInt8) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: UInt16) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: UInt32) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: UInt64) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: String) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: Float) throws {
+ assertCanEncodeNewValue()
+ try self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: Double) throws {
+ assertCanEncodeNewValue()
+ try self.storage.push(container: self.box(value))
+ }
+
+ public func encode(_ value: T) throws {
+ assertCanEncodeNewValue()
+ try self.storage.push(container: self.box(value))
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryEncoderOptions.swift b/Sources/DictionaryCoding/DictionaryEncoderOptions.swift
new file mode 100644
index 0000000..d482c9c
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncoderOptions.swift
@@ -0,0 +1,21 @@
+//
+// DictionaryEncoderOptions.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+/// Options set on the top-level encoder to pass down the encoding hierarchy.
+internal struct DictionaryEncoderOptions {
+ // MARK: - Instance Properties
+
+ internal let dateEncodingStrategy: DictionaryEncoder.DateEncodingStrategy
+ internal let dataEncodingStrategy: DictionaryEncoder.DataEncodingStrategy
+ internal let nonConformingFloatEncodingStrategy:
+ DictionaryEncoder.NonConformingFloatEncodingStrategy
+ internal let keyEncodingStrategy: DictionaryEncoder.KeyEncodingStrategy
+ internal let userInfo: [CodingUserInfoKey: Any]
+}
diff --git a/Sources/DictionaryCoding/DictionaryEncodingStorage.swift b/Sources/DictionaryCoding/DictionaryEncodingStorage.swift
new file mode 100644
index 0000000..9a7bc1c
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncodingStorage.swift
@@ -0,0 +1,54 @@
+//
+// DictionaryEncodingStorage.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Encoding Storage and Containers
+internal struct DictionaryEncodingStorage {
+ // MARK: - Instance Properties
+
+ /// The container stack.
+ ///
+ /// Elements may be any one of the Dictionary types
+ /// (NSNull, NSNumber, NSString, NSArray, NSDictionary).
+ internal private(set) var containers: [NSObject] = []
+
+ // MARK: - Computed Properties
+
+ internal var count: Int {
+ self.containers.count
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` with no containers.
+ internal init() {}
+
+ // MARK: - Instance Methods
+
+ internal mutating func pushKeyedContainer() -> NSMutableDictionary {
+ let dictionary = NSMutableDictionary()
+ self.containers.append(dictionary)
+ return dictionary
+ }
+
+ internal mutating func pushUnkeyedContainer() -> NSMutableArray {
+ let array = NSMutableArray()
+ self.containers.append(array)
+ return array
+ }
+
+ internal mutating func push(container: NSObject) {
+ self.containers.append(container)
+ }
+
+ internal mutating func popContainer() -> NSObject {
+ precondition(!self.containers.isEmpty, "Empty container stack.")
+ return self.containers.removeLast()
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryReferencingEncoder.swift b/Sources/DictionaryCoding/DictionaryReferencingEncoder.swift
new file mode 100644
index 0000000..76d12aa
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryReferencingEncoder.swift
@@ -0,0 +1,105 @@
+//
+// DictionaryReferencingEncoder.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - DictionaryReferencingEncoder
+/// DictionaryReferencingEncoder is a special subclass of DictionaryEncoderImpl
+/// which has its own storage, but references the contents of a different encoder.
+///
+/// It's used in superEncoder(), which returns a new encoder for encoding a
+/// superclass -- the lifetime of the encoder should not escape the scope it's
+/// created in, but it doesn't necessarily know when it's done being used
+/// (to write to the original container).
+internal class DictionaryReferencingEncoder: DictionaryEncoderImpl {
+ // MARK: - Subtypes
+
+ /// The type of container we're referencing.
+ private enum Reference {
+ /// Referencing a specific index in an array container.
+ case array(NSMutableArray, Int)
+
+ /// Referencing a specific key in a dictionary container.
+ case dictionary(NSMutableDictionary, String)
+ }
+
+ // MARK: - Instance Properties
+
+ /// The encoder we're referencing.
+ internal let referencedEncoder: DictionaryEncoderImpl
+
+ /// The container reference itself.
+ private let reference: Reference
+
+ // MARK: - Computed Properties
+
+ override internal var canEncodeNewValue: Bool {
+ // With a regular encoder, the storage and coding path grow together.
+ // A referencing encoder, however, inherits its parents coding path,
+ // as well as the key it was created for.
+ // We have to take this into account.
+ self.storage.count == self.codingPath.count
+ - self.referencedEncoder.codingPath.count - 1
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` by referencing the given array container in the given encoder.
+ internal init(
+ referencing encoder: DictionaryEncoderImpl,
+ at index: Int,
+ wrapping array: NSMutableArray
+ ) {
+ self.referencedEncoder = encoder
+ self.reference = .array(array, index)
+ super.init(options: encoder.options, codingPath: encoder.codingPath)
+
+ self.codingPath.append(DictionaryCodingKey(index: index))
+ }
+
+ /// Initializes `self` by referencing the given dictionary container
+ /// in the given encoder.
+ internal init(
+ referencing encoder: DictionaryEncoderImpl,
+ key: CodingKey,
+ convertedKey: CodingKey,
+ wrapping dictionary: NSMutableDictionary
+ ) {
+ self.referencedEncoder = encoder
+ self.reference = .dictionary(dictionary, convertedKey.stringValue)
+ super.init(options: encoder.options, codingPath: encoder.codingPath)
+
+ self.codingPath.append(key)
+ }
+
+ // MARK: - Deinitialization
+
+ // Finalizes `self` by writing the contents of our storage to the referenced
+ // encoder's storage.
+ deinit {
+ let value: Any
+ switch self.storage.count {
+ case 0:
+ value = NSDictionary()
+ case 1:
+ value = self.storage.popContainer()
+ default:
+ fatalError(
+ "Referencing encoder deallocated with multiple containers on stack."
+ )
+ }
+
+ switch self.reference {
+ case let .array(array, index):
+ array.insert(value, at: index)
+
+ case let .dictionary(dictionary, key):
+ dictionary[NSString(string: key)] = value
+ }
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift
new file mode 100644
index 0000000..367ac8a
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift
@@ -0,0 +1,117 @@
+//
+// DictionaryUnkeyedDecodingContainer+Nested.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Nested container methods
+extension DictionaryUnkeyedDecodingContainer {
+ internal mutating func nestedContainer(
+ keyedBy type: NestedKey.Type
+ ) throws -> KeyedDecodingContainer {
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard !self.isAtEnd else {
+ throw DecodingError.valueNotFound(
+ KeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get nested keyed container -- unkeyed container is at end."
+ )
+ )
+ }
+
+ let value = self.container[self.currentIndex]
+ guard !(value is NSNull) else {
+ throw DecodingError.valueNotFound(
+ KeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get keyed decoding container -- found null value instead."
+ )
+ )
+ }
+
+ guard let dictionary = value as? [String: Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: [String: Any].self, reality: value
+ )
+ }
+
+ self.currentIndex += 1
+ let container = DictionaryCodingKeyedDecodingContainer(
+ referencing: self.decoder, wrapping: dictionary
+ )
+ return KeyedDecodingContainer(container)
+ }
+
+ internal mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard !self.isAtEnd else {
+ throw DecodingError.valueNotFound(
+ UnkeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get nested keyed container -- unkeyed container is at end."
+ )
+ )
+ }
+
+ let value = self.container[self.currentIndex]
+ guard !(value is NSNull) else {
+ throw DecodingError.valueNotFound(
+ UnkeyedDecodingContainer.self,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get keyed decoding container -- found null value instead."
+ )
+ )
+ }
+
+ guard let array = value as? [Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: [Any].self, reality: value
+ )
+ }
+
+ self.currentIndex += 1
+ return DictionaryUnkeyedDecodingContainer(
+ referencing: self.decoder, wrapping: array
+ )
+ }
+
+ internal mutating func superDecoder() throws -> Decoder {
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard !self.isAtEnd else {
+ throw DecodingError.valueNotFound(
+ Decoder.self,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get superDecoder() -- unkeyed container is at end."
+ )
+ )
+ }
+
+ let value = self.container[self.currentIndex]
+ self.currentIndex += 1
+ return DictionaryDecoderImpl(
+ referencing: value,
+ at: self.decoder.codingPath,
+ options: self.decoder.options
+ )
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift
new file mode 100644
index 0000000..e00de4d
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift
@@ -0,0 +1,259 @@
+//
+// DictionaryUnkeyedDecodingContainer+Scalars.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Scalar decode methods
+extension DictionaryUnkeyedDecodingContainer {
+ internal mutating func decode(_ type: Bool.Type) throws -> Bool {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Bool.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: Int.Type) throws -> Int {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Int.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: Int8.Type) throws -> Int8 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Int8.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: Int16.Type) throws -> Int16 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Int16.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: Int32.Type) throws -> Int32 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Int32.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: Int64.Type) throws -> Int64 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Int64.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt.Type) throws -> UInt {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt8.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt16.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt32.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt64.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: Float.Type) throws -> Float {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Float.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: Double.Type) throws -> Double {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: Double.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift
new file mode 100644
index 0000000..7fdc88e
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift
@@ -0,0 +1,31 @@
+//
+// DictionaryUnkeyedDecodingContainer+String.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - String decode
+extension DictionaryUnkeyedDecodingContainer {
+ internal mutating func decode(_ type: String.Type) throws -> String {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: String.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift
new file mode 100644
index 0000000..d5c9801
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift
@@ -0,0 +1,112 @@
+//
+// DictionaryUnkeyedDecodingContainer.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Unkeyed Decoding Container
+internal struct DictionaryUnkeyedDecodingContainer: UnkeyedDecodingContainer {
+ // MARK: - Instance Properties
+
+ /// A reference to the decoder we're reading from.
+ internal let decoder: DictionaryDecoderImpl
+
+ /// A reference to the container we're reading from.
+ internal let container: [Any]
+
+ /// The path of coding keys taken to get to this point in decoding.
+ internal private(set) var codingPath: [CodingKey]
+
+ /// The index of the element we're about to decode.
+ internal var currentIndex: Int
+
+ // MARK: - Computed Properties
+
+ internal var count: Int? {
+ self.container.count
+ }
+
+ internal var isAtEnd: Bool {
+ guard let count = self.count else {
+ return true
+ }
+ return self.currentIndex >= count
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` by referencing the given decoder and container.
+ internal init(referencing decoder: DictionaryDecoderImpl, wrapping container: [Any]) {
+ self.decoder = decoder
+ self.container = container
+ self.codingPath = decoder.codingPath
+ self.currentIndex = 0
+ }
+
+ // MARK: - Instance Methods
+
+ internal func atEndError(_ type: T.Type) -> DecodingError {
+ DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: self.decoder.codingPath
+ + [DictionaryCodingKey(index: self.currentIndex)],
+ debugDescription: "Unkeyed container is at end."
+ )
+ )
+ }
+
+ internal func nullFoundError(_ type: T.Type) -> DecodingError {
+ DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: self.decoder.codingPath
+ + [DictionaryCodingKey(index: self.currentIndex)],
+ debugDescription: "Expected \(type) but found null instead."
+ )
+ )
+ }
+
+ internal mutating func decodeNil() throws -> Bool {
+ guard !self.isAtEnd else {
+ throw DecodingError.valueNotFound(
+ Any?.self,
+ DecodingError.Context(
+ codingPath: self.decoder.codingPath
+ + [DictionaryCodingKey(index: self.currentIndex)],
+ debugDescription: "Unkeyed container is at end."
+ )
+ )
+ }
+
+ if self.container[self.currentIndex] is NSNull {
+ self.currentIndex += 1
+ return true
+ } else {
+ return false
+ }
+ }
+
+ internal mutating func decode(_ type: T.Type) throws -> T {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: type)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift b/Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift
new file mode 100644
index 0000000..35e20bc
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift
@@ -0,0 +1,160 @@
+//
+// DictionaryUnkeyedEncodingContainer.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+// MARK: - Unkeyed Encoding Container
+internal struct DictionaryUnkeyedEncodingContainer: UnkeyedEncodingContainer {
+ // MARK: - Instance Properties
+
+ /// A reference to the encoder we're writing to.
+ private let encoder: DictionaryEncoderImpl
+
+ /// A reference to the container we're writing to.
+ private let container: NSMutableArray
+
+ /// The path of coding keys taken to get to this point in encoding.
+ internal private(set) var codingPath: [CodingKey]
+
+ // MARK: - Computed Properties
+
+ /// The number of elements encoded into the container.
+ internal var count: Int {
+ self.container.count
+ }
+
+ // MARK: - Initializers
+
+ /// Initializes `self` with the given references.
+ internal init(
+ referencing encoder: DictionaryEncoderImpl,
+ codingPath: [CodingKey],
+ wrapping container: NSMutableArray
+ ) {
+ self.encoder = encoder
+ self.codingPath = codingPath
+ self.container = container
+ }
+
+ // MARK: - Instance Methods
+
+ internal mutating func encodeNil() throws {
+ self.container.add(NSNull())
+ }
+
+ internal mutating func encode(_ value: Float) throws {
+ // Since the float may be invalid and throw, the coding path needs to
+ // contain this key.
+ self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
+ defer { self.encoder.codingPath.removeLast() }
+ self.container.add(try self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: Double) throws {
+ // Since the double may be invalid and throw, the coding path needs to
+ // contain this key.
+ self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
+ defer { self.encoder.codingPath.removeLast() }
+ self.container.add(try self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: T) throws {
+ self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
+ defer { self.encoder.codingPath.removeLast() }
+ self.container.add(try self.encoder.box(value))
+ }
+
+ internal mutating func nestedContainer(
+ keyedBy keyType: NestedKey.Type
+ ) -> KeyedEncodingContainer {
+ self.codingPath.append(DictionaryCodingKey(index: self.count))
+ defer { self.codingPath.removeLast() }
+
+ let dictionary = NSMutableDictionary()
+ self.container.add(dictionary)
+
+ let container = DictionaryCodingKeyedEncodingContainer(
+ referencing: self.encoder,
+ codingPath: self.codingPath,
+ wrapping: dictionary
+ )
+ return KeyedEncodingContainer(container)
+ }
+
+ internal mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
+ self.codingPath.append(DictionaryCodingKey(index: self.count))
+ defer { self.codingPath.removeLast() }
+
+ let array = NSMutableArray()
+ self.container.add(array)
+ return DictionaryUnkeyedEncodingContainer(
+ referencing: self.encoder,
+ codingPath: self.codingPath,
+ wrapping: array
+ )
+ }
+
+ internal mutating func superEncoder() -> Encoder {
+ DictionaryReferencingEncoder(
+ referencing: self.encoder,
+ at: self.container.count,
+ wrapping: self.container
+ )
+ }
+}
+
+// MARK: - Scalar encode methods
+extension DictionaryUnkeyedEncodingContainer {
+ internal mutating func encode(_ value: Bool) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: Int) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: Int8) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: Int16) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: Int32) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: Int64) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: UInt) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: UInt8) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: UInt16) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: UInt32) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: UInt64) throws {
+ self.container.add(self.encoder.box(value))
+ }
+
+ internal mutating func encode(_ value: String) throws {
+ self.container.add(self.encoder.box(value))
+ }
+}
diff --git a/Sources/DictionaryCoding/EncodingError+Dictionary.swift b/Sources/DictionaryCoding/EncodingError+Dictionary.swift
new file mode 100644
index 0000000..eef1659
--- /dev/null
+++ b/Sources/DictionaryCoding/EncodingError+Dictionary.swift
@@ -0,0 +1,42 @@
+//
+// EncodingError+Dictionary.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+extension EncodingError {
+ /// Returns a `.invalidValue` error describing the given invalid floating-point value.
+ ///
+ /// - parameter value: The value that was invalid to encode.
+ /// - parameter path: The path of `CodingKey`s taken to encode this value.
+ /// - returns: An `EncodingError` with the appropriate path and debug description.
+ internal static func invalidFloatingPointValue(
+ _ value: T,
+ at codingPath: [CodingKey]
+ ) -> EncodingError {
+ let valueDescription: String
+ if value == T.infinity {
+ valueDescription = "\(T.self).infinity"
+ } else if value == -T.infinity {
+ valueDescription = "-\(T.self).infinity"
+ } else {
+ valueDescription = "\(T.self).nan"
+ }
+
+ let debugDescription =
+ "Unable to encode \(valueDescription) directly in Dictionary. "
+ + "Use DictionaryEncoder.NonConformingFloatEncodingStrategy"
+ + ".convertToString to specify how the value should be encoded."
+ return .invalidValue(
+ value,
+ EncodingError.Context(
+ codingPath: codingPath,
+ debugDescription: debugDescription
+ )
+ )
+ }
+}
diff --git a/Sources/DictionaryCoding/NSNumber+Bool.swift b/Sources/DictionaryCoding/NSNumber+Bool.swift
new file mode 100644
index 0000000..9d8e1c7
--- /dev/null
+++ b/Sources/DictionaryCoding/NSNumber+Bool.swift
@@ -0,0 +1,27 @@
+//
+// NSNumber+Bool.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+#if canImport(Darwin)
+ import CoreFoundation
+#endif
+
+extension NSNumber {
+ /// True if this NSNumber was created from a Swift/ObjC Bool, not an integer.
+ ///
+ /// Uses CFBooleanGetTypeID() on Darwin; objCType comparison on Linux.
+ internal var isBool: Bool {
+ #if canImport(Darwin)
+ return CFGetTypeID(self) == CFBooleanGetTypeID()
+ #else
+ // swiftlint:disable:next line_length
+ return String(cString: self.objCType) == String(cString: NSNumber(value: true).objCType)
+ #endif
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift
new file mode 100644
index 0000000..642ae53
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift
@@ -0,0 +1,90 @@
+//
+// DictionaryCodingArrayAndKeyTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding array and key strategies")
+internal struct DictionaryCodingArrayAndKeyTests {
+ private struct WithArray: Codable, Equatable {
+ let items: [String]
+ }
+
+ private struct WithIntArray: Codable, Equatable {
+ let values: [Int]
+ }
+
+ private struct CamelCaseModel: Codable, Equatable {
+ let firstName: String
+ let lastName: String
+ let itemCount: Int
+ }
+
+ @Test("encodes and decodes array of strings")
+ internal func roundTripsStringArray() throws {
+ let original = WithArray(items: ["alpha", "beta", "gamma"])
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(WithArray.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("encodes and decodes empty array")
+ internal func roundTripsEmptyArray() throws {
+ let original = WithArray(items: [])
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(WithArray.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("encodes and decodes array of ints")
+ internal func roundTripsIntArray() throws {
+ let original = WithIntArray(values: [1, 2, 3, -7])
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(WithIntArray.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("encoder convertToSnakeCase converts camelCase keys to snake_case")
+ internal func encoderConvertToSnakeCase() throws {
+ let value = CamelCaseModel(firstName: "Jane", lastName: "Doe", itemCount: 3)
+ let encoder = DictionaryEncoder()
+ encoder.keyEncodingStrategy = .convertToSnakeCase
+ let dict: [String: Any] = try encoder.encode(value)
+ #expect(dict["first_name"] as? String == "Jane")
+ #expect(dict["last_name"] as? String == "Doe")
+ #expect(dict["item_count"] as? Int == 3)
+ }
+
+ @Test("decoder convertFromSnakeCase converts snake_case keys to camelCase")
+ internal func decoderConvertFromSnakeCase() throws {
+ let dict: [String: Any] = [
+ "first_name": "Jane",
+ "last_name": "Doe",
+ "item_count": 3,
+ ]
+ let decoder = DictionaryDecoder()
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
+ let result = try decoder.decode(CamelCaseModel.self, from: dict)
+ #expect(result.firstName == "Jane")
+ #expect(result.lastName == "Doe")
+ #expect(result.itemCount == 3)
+ }
+
+ @Test("snake_case round-trip with both strategies")
+ internal func snakeCaseRoundTrip() throws {
+ let original = CamelCaseModel(firstName: "Alice", lastName: "Smith", itemCount: 10)
+ let encoder = DictionaryEncoder()
+ encoder.keyEncodingStrategy = .convertToSnakeCase
+ let decoder = DictionaryDecoder()
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
+ let dict: [String: Any] = try encoder.encode(original)
+ let decoded = try decoder.decode(CamelCaseModel.self, from: dict)
+ #expect(decoded == original)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift
new file mode 100644
index 0000000..d928a3d
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift
@@ -0,0 +1,65 @@
+//
+// DictionaryCodingArrayTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding array and optional round-trips")
+internal struct DictionaryCodingArrayTests {
+ private struct ScalarArrays: Codable, Equatable {
+ let int8s: [Int8]
+ let uint16s: [UInt16]
+ let doubles: [Double]
+ let bools: [Bool]
+ let strings: [String]
+ }
+
+ private struct OptionalIntArray: Codable, Equatable {
+ let values: [Int?]
+ }
+
+ @Test("round-trips arrays of all scalar types")
+ internal func scalarArrays() throws {
+ let original = ScalarArrays(
+ int8s: [Int8.min, 0, Int8.max],
+ uint16s: [0, 1_000, UInt16.max],
+ doubles: [-1.5, 0.0, 3.14],
+ bools: [true, false, true],
+ strings: ["hello", "", "world"]
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ ScalarArrays.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips empty scalar arrays")
+ internal func emptyScalarArrays() throws {
+ let original = ScalarArrays(
+ int8s: [], uint16s: [], doubles: [],
+ bools: [], strings: []
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ ScalarArrays.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips array with nil values")
+ internal func optionalIntArray() throws {
+ let original = OptionalIntArray(values: [1, nil, 3, nil, 5])
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ OptionalIntArray.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift
new file mode 100644
index 0000000..7028e15
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift
@@ -0,0 +1,97 @@
+//
+// DictionaryCodingDateDataTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding date, data and missing value strategies")
+internal struct DictionaryCodingDateDataTests {
+ private struct WithDate: Codable, Equatable {
+ let timestamp: Date
+ }
+
+ private struct WithData: Codable, Equatable {
+ let payload: Data
+ }
+
+ private struct WithAllScalars: Codable, Equatable {
+ let name: String
+ let number: Int
+ let ratio: Double
+ let flag: Bool
+ }
+
+ @Test("millisecondsSince1970 round-trip")
+ internal func millisecondsSince1970RoundTrip() throws {
+ let date = Date(timeIntervalSince1970: 1_700_000_000)
+ let original = WithDate(timestamp: date)
+ let encoder = DictionaryEncoder()
+ encoder.dateEncodingStrategy = .millisecondsSince1970
+ let decoder = DictionaryDecoder()
+ decoder.dateDecodingStrategy = .millisecondsSince1970
+ let dict: [String: Any] = try encoder.encode(original)
+ let decoded = try decoder.decode(WithDate.self, from: dict)
+ let expected = date.timeIntervalSince1970
+ let actual = decoded.timestamp.timeIntervalSince1970
+ #expect(abs(actual - expected) < 0.001)
+ }
+
+ @Test("iso8601 round-trip")
+ internal func iso8601RoundTrip() throws {
+ guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else {
+ return
+ }
+ let date = Date(timeIntervalSince1970: 1_700_000_000)
+ let original = WithDate(timestamp: date)
+ let encoder = DictionaryEncoder()
+ encoder.dateEncodingStrategy = .iso8601
+ let decoder = DictionaryDecoder()
+ decoder.dateDecodingStrategy = .iso8601
+ let dict: [String: Any] = try encoder.encode(original)
+ let decoded = try decoder.decode(WithDate.self, from: dict)
+ let expected = date.timeIntervalSince1970
+ let actual = decoded.timestamp.timeIntervalSince1970
+ #expect(abs(actual - expected) < 1.0)
+ }
+
+ @Test("base64 Data round-trip")
+ internal func base64DataRoundTrip() throws {
+ let bytes = Data([0x01, 0x02, 0xFF, 0xAB])
+ let original = WithData(payload: bytes)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(WithData.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("useStandardDefault fills missing keys with zero values")
+ internal func useStandardDefaultFillsMissingKeys() throws {
+ let dict: [String: Any] = [:]
+ let decoder = DictionaryDecoder()
+ decoder.missingValueDecodingStrategy = .useStandardDefault
+ let result = try decoder.decode(WithAllScalars.self, from: dict)
+ #expect(result.name.isEmpty)
+ #expect(result.number == 0)
+ #expect(result.ratio == 0.0)
+ #expect(result.flag == false)
+ }
+
+ @Test("useDefault fills missing keys from provided defaults")
+ internal func useDefaultFillsMissingKeysFromDefaults() throws {
+ let dict: [String: Any] = [:]
+ let decoder = DictionaryDecoder()
+ decoder.missingValueDecodingStrategy = .useDefault(
+ defaults: ["String": "fallback", "Int": 99, "Double": 3.14, "Bool": true]
+ )
+ let result = try decoder.decode(WithAllScalars.self, from: dict)
+ #expect(result.name == "fallback")
+ #expect(result.number == 99)
+ #expect(abs(result.ratio - 3.14) < 0.001)
+ #expect(result.flag == true)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift
new file mode 100644
index 0000000..84d177b
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift
@@ -0,0 +1,125 @@
+//
+// DictionaryCodingErrorTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding error paths")
+internal struct DictionaryCodingErrorTests {
+ // MARK: - Models
+
+ private struct IntField: Codable { let value: Int }
+ private struct StringField: Codable { let value: String }
+ private struct NonOptionalString: Codable { let name: String }
+ private struct WithArray: Codable { let items: [Int] }
+
+ private struct AllOptional: Codable, Equatable {
+ let name: String?
+ let count: Int?
+ let flag: Bool?
+ }
+
+ private struct Nested: Codable {
+ let inner: InnerModel
+ }
+
+ private struct InnerModel: Codable {
+ let count: Int
+ }
+
+ // MARK: - Type Mismatch
+
+ @Test("throws decoding string where int expected")
+ internal func typeMismatchStringForInt() {
+ let dict: [String: Any] = ["value": "not-an-int"]
+ #expect(throws: DecodingError.self) {
+ try DictionaryDecoder().decode(IntField.self, from: dict)
+ }
+ }
+
+ @Test("throws decoding number where string expected")
+ internal func typeMismatchNumberForString() {
+ let dict: [String: Any] = ["value": 42]
+ #expect(throws: DecodingError.self) {
+ try DictionaryDecoder().decode(StringField.self, from: dict)
+ }
+ }
+
+ // MARK: - Null and Missing
+
+ @Test("throws decoding NSNull for non-optional")
+ internal func nullForNonOptional() {
+ let dict: [String: Any] = ["name": NSNull()]
+ #expect(throws: DecodingError.self) {
+ try DictionaryDecoder().decode(
+ NonOptionalString.self, from: dict
+ )
+ }
+ }
+
+ @Test("throws on missing required key")
+ internal func missingRequiredKey() {
+ let dict: [String: Any] = ["unrelated": "data"]
+ #expect(throws: DecodingError.self) {
+ try DictionaryDecoder().decode(IntField.self, from: dict)
+ }
+ }
+
+ // MARK: - Nested Container Mismatches
+
+ @Test("throws when nested keyed finds string not dict")
+ internal func nestedKeyedTypeMismatch() {
+ let dict: [String: Any] = ["inner": "not-a-dictionary"]
+ #expect(throws: DecodingError.self) {
+ try DictionaryDecoder().decode(Nested.self, from: dict)
+ }
+ }
+
+ @Test("throws when nested unkeyed finds string not array")
+ internal func nestedUnkeyedTypeMismatch() {
+ let dict: [String: Any] = ["items": "not-an-array"]
+ #expect(throws: DecodingError.self) {
+ try DictionaryDecoder().decode(WithArray.self, from: dict)
+ }
+ }
+
+ // MARK: - Empty Dictionary
+
+ @Test("decodes empty dict as all-optional struct")
+ internal func emptyDictionaryAllOptional() throws {
+ let dict: [String: Any] = [:]
+ let result = try DictionaryDecoder().decode(
+ AllOptional.self, from: dict
+ )
+ #expect(
+ result
+ == AllOptional(
+ name: nil, count: nil, flag: nil
+ ))
+ }
+
+ // MARK: - NSDictionary Overload
+
+ @Test("decodes from NSDictionary overload")
+ internal func decodeFromNSDictionary() throws {
+ let nsDict: NSDictionary = ["value": 99]
+ let result = try DictionaryDecoder().decode(
+ IntField.self, from: nsDict
+ )
+ #expect(result.value == 99)
+ }
+
+ @Test("NSDictionary overload throws on type mismatch")
+ internal func nsDictionaryTypeMismatch() {
+ let nsDict: NSDictionary = ["value": "not-an-int"]
+ #expect(throws: DecodingError.self) {
+ try DictionaryDecoder().decode(IntField.self, from: nsDict)
+ }
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift
new file mode 100644
index 0000000..09ef533
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift
@@ -0,0 +1,132 @@
+//
+// DictionaryCodingFloatStrategyTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding non-conforming float strategies")
+internal struct DictionaryCodingFloatStrategyTests {
+ private struct FloatModel: Codable, Equatable {
+ let value: Float
+ }
+
+ private struct DoubleModel: Codable, Equatable {
+ let value: Double
+ }
+
+ // MARK: - convertToString strategy
+
+ @Test("Float.infinity round-trips with convertToString")
+ internal func floatInfinityConvertToString() throws {
+ let encoder = DictionaryEncoder()
+ encoder.nonConformingFloatEncodingStrategy = .convertToString(
+ positiveInfinity: "Inf", negativeInfinity: "-Inf", nan: "NaN"
+ )
+ let decoder = DictionaryDecoder()
+ decoder.nonConformingFloatDecodingStrategy = .convertFromString(
+ positiveInfinity: "Inf", negativeInfinity: "-Inf", nan: "NaN"
+ )
+
+ for value: Float in [.infinity, -.infinity] {
+ let dict: [String: Any] = try encoder.encode(
+ FloatModel(value: value)
+ )
+ let decoded = try decoder.decode(FloatModel.self, from: dict)
+ #expect(decoded.value == value)
+ }
+
+ let nanDict: [String: Any] = try encoder.encode(
+ FloatModel(value: .nan)
+ )
+ let nanDecoded = try decoder.decode(
+ FloatModel.self, from: nanDict
+ )
+ #expect(nanDecoded.value.isNaN)
+ }
+
+ @Test("Double.infinity round-trips with convertToString")
+ internal func doubleInfinityConvertToString() throws {
+ let encoder = DictionaryEncoder()
+ encoder.nonConformingFloatEncodingStrategy = .convertToString(
+ positiveInfinity: "+Inf", negativeInfinity: "-Inf", nan: "NaN"
+ )
+ let decoder = DictionaryDecoder()
+ decoder.nonConformingFloatDecodingStrategy = .convertFromString(
+ positiveInfinity: "+Inf", negativeInfinity: "-Inf", nan: "NaN"
+ )
+
+ for value: Double in [.infinity, -.infinity] {
+ let dict: [String: Any] = try encoder.encode(
+ DoubleModel(value: value)
+ )
+ let decoded = try decoder.decode(
+ DoubleModel.self, from: dict
+ )
+ #expect(decoded.value == value)
+ }
+
+ let nanDict: [String: Any] = try encoder.encode(
+ DoubleModel(value: .nan)
+ )
+ let nanDecoded = try decoder.decode(
+ DoubleModel.self, from: nanDict
+ )
+ #expect(nanDecoded.value.isNaN)
+ }
+
+ // MARK: - .throw strategy
+
+ @Test("throws encoding Float.infinity with .throw strategy")
+ internal func floatInfinityThrows() throws {
+ let encoder = DictionaryEncoder()
+ encoder.nonConformingFloatEncodingStrategy = .throw
+
+ #expect(throws: EncodingError.self) {
+ let _: [String: Any] = try encoder.encode(
+ FloatModel(value: .infinity)
+ )
+ }
+ }
+
+ @Test("throws encoding -Float.infinity with .throw strategy")
+ internal func negativeFloatInfinityThrows() throws {
+ let encoder = DictionaryEncoder()
+ encoder.nonConformingFloatEncodingStrategy = .throw
+
+ #expect(throws: EncodingError.self) {
+ let _: [String: Any] = try encoder.encode(
+ FloatModel(value: -.infinity)
+ )
+ }
+ }
+
+ @Test("throws encoding Float.nan with .throw strategy")
+ internal func floatNanThrows() throws {
+ let encoder = DictionaryEncoder()
+ encoder.nonConformingFloatEncodingStrategy = .throw
+
+ #expect(throws: EncodingError.self) {
+ let _: [String: Any] = try encoder.encode(
+ FloatModel(value: .nan)
+ )
+ }
+ }
+
+ @Test("throws encoding Double.infinity with .throw strategy")
+ internal func doubleInfinityThrows() throws {
+ let encoder = DictionaryEncoder()
+ encoder.nonConformingFloatEncodingStrategy = .throw
+
+ #expect(throws: EncodingError.self) {
+ let _: [String: Any] = try encoder.encode(
+ DoubleModel(value: .infinity)
+ )
+ }
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift
new file mode 100644
index 0000000..e888359
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift
@@ -0,0 +1,98 @@
+//
+// DictionaryCodingNestedTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding nested containers")
+internal struct DictionaryCodingNestedTests {
+ private struct NestedItem: Codable, Equatable {
+ let name: String
+ let value: Int
+ }
+
+ private struct ArrayOfObjects: Codable, Equatable {
+ let items: [NestedItem]
+ }
+
+ private struct NestedArrays: Codable, Equatable {
+ let grid: [[Int]]
+ }
+
+ private struct Level3: Codable, Equatable {
+ let label: String
+ }
+
+ private struct Level2: Codable, Equatable {
+ let child: Level3
+ }
+
+ private struct Level1: Codable, Equatable {
+ let nested: Level2
+ }
+
+ private struct KeyedArray: Codable, Equatable {
+ let tags: [String]
+ }
+
+ @Test("round-trips array of objects")
+ internal func arrayOfObjects() throws {
+ let original = ArrayOfObjects(items: [
+ NestedItem(name: "a", value: 1),
+ NestedItem(name: "b", value: 2),
+ ])
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ ArrayOfObjects.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips nested arrays")
+ internal func nestedArrays() throws {
+ let original = NestedArrays(grid: [[1, 2], [3, 4, 5], []])
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ NestedArrays.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips deeply nested structs")
+ internal func deeplyNestedStruct() throws {
+ let original = Level1(
+ nested: Level2(child: Level3(label: "deep"))
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ Level1.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("keyed container with array value")
+ internal func keyedArrayRoundTrip() throws {
+ let original = KeyedArray(tags: ["swift", "testing", "codable"])
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ KeyedArray.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("encode returns NSDictionary via overload")
+ internal func encodeToNSDictionary() throws {
+ let original = NestedItem(name: "test", value: 42)
+ let nsDict: NSDictionary = try DictionaryEncoder().encode(original)
+ let name = try #require(nsDict["name"] as? String)
+ let value = try #require(nsDict["value"] as? Int)
+ #expect(name == "test")
+ #expect(value == 42)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift
new file mode 100644
index 0000000..6cb8023
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift
@@ -0,0 +1,57 @@
+//
+// DictionaryCodingRoundTripTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding round-trip")
+internal struct DictionaryCodingRoundTripTests {
+ private struct AllTypes: Codable, Equatable {
+ let text: String
+ let number: Int
+ let decimal: Double
+ let flag: Bool
+ let optional: String?
+ }
+
+ private struct WithDate: Codable, Equatable {
+ let timestamp: Date
+ }
+
+ @Test("round-trips struct with all primitive types")
+ internal func roundTripsAllPrimitives() throws {
+ let original = AllTypes(
+ text: "abc", number: -5, decimal: 2.718, flag: false, optional: nil
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(AllTypes.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips struct with present optional")
+ internal func roundTripsPresentOptional() throws {
+ let original = AllTypes(text: "x", number: 0, decimal: 0, flag: true, optional: "set")
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(AllTypes.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips Date via secondsSince1970 strategy")
+ internal func roundTripsDate() throws {
+ let date = Date(timeIntervalSince1970: 1_700_000_000)
+ let original = WithDate(timestamp: date)
+ let encoder = DictionaryEncoder()
+ encoder.dateEncodingStrategy = .secondsSince1970
+ let decoder = DictionaryDecoder()
+ decoder.dateDecodingStrategy = .secondsSince1970
+ let dict: [String: Any] = try encoder.encode(original)
+ let decoded = try decoder.decode(WithDate.self, from: dict)
+ #expect(decoded == original)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift
new file mode 100644
index 0000000..87e8b4f
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift
@@ -0,0 +1,132 @@
+//
+// DictionaryCodingScalarTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding scalar round-trips")
+internal struct DictionaryCodingScalarTests {
+ private struct AllIntegers: Codable, Equatable {
+ let int: Int
+ let int8: Int8
+ let int16: Int16
+ let int32: Int32
+ let int64: Int64
+ let uint: UInt
+ let uint8: UInt8
+ let uint16: UInt16
+ let uint32: UInt32
+ let uint64: UInt64
+ }
+
+ private struct Floats: Codable, Equatable {
+ let float: Float
+ let double: Double
+ }
+
+ private struct BoolModel: Codable, Equatable {
+ let flag: Bool
+ }
+
+ private struct SmallInt: Codable, Equatable {
+ let value: Int8
+ }
+
+ @Test("round-trips all integer types")
+ internal func allIntegerTypes() throws {
+ let original = AllIntegers(
+ int: -42, int8: Int8.min, int16: Int16.max,
+ int32: -100_000, int64: Int64.max, uint: 99,
+ uint8: UInt8.max, uint16: 0,
+ uint32: UInt32.max, uint64: UInt64.max
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ AllIntegers.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips integer boundary values")
+ internal func integerBoundaries() throws {
+ let original = AllIntegers(
+ int: Int.min, int8: Int8.max, int16: Int16.min,
+ int32: Int32.max, int64: Int64.min, uint: UInt.max,
+ uint8: 0, uint16: UInt16.max, uint32: 0, uint64: 0
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ AllIntegers.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips float and double")
+ internal func floatAndDouble() throws {
+ let original = Floats(float: 3.14, double: 2.718281828459045)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(Floats.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips zero floats")
+ internal func zeroFloats() throws {
+ let original = Floats(float: 0.0, double: 0.0)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(Floats.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips very large float values")
+ internal func largeFloats() throws {
+ let original = Floats(
+ float: Float.greatestFiniteMagnitude,
+ double: Double.greatestFiniteMagnitude
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(Floats.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips negative floats")
+ internal func negativeFloats() throws {
+ let original = Floats(float: -1.5, double: -999_999.999)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(Floats.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips true bool")
+ internal func boolTrue() throws {
+ let original = BoolModel(flag: true)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ BoolModel.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips false bool")
+ internal func boolFalse() throws {
+ let original = BoolModel(flag: false)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ BoolModel.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("throws on integer overflow decoding as Int8")
+ internal func integerOverflow() throws {
+ let dict: [String: Any] = ["value": 200]
+ #expect(throws: DecodingError.self) {
+ _ = try DictionaryDecoder().decode(SmallInt.self, from: dict)
+ }
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift
new file mode 100644
index 0000000..583d02c
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift
@@ -0,0 +1,94 @@
+//
+// DictionaryCodingSpecialTypeTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding special types")
+internal struct DictionaryCodingSpecialTypeTests {
+ private struct WithURL: Codable, Equatable { let link: URL }
+ private struct WithDecimal: Codable, Equatable { let amount: Decimal }
+ private struct WithUUID: Codable, Equatable { let identifier: UUID }
+
+ private enum Compass: String, Codable, Equatable {
+ case north, south, east, west
+ }
+
+ private struct WithCompass: Codable, Equatable {
+ let direction: Compass
+ }
+
+ private enum Status: Codable, Equatable {
+ case idle
+ case running(progress: Double)
+ case finished(message: String)
+ }
+
+ private struct WithStatus: Codable, Equatable {
+ let status: Status
+ }
+
+ @Test("round-trips URL value")
+ internal func urlRoundTrip() throws {
+ let url = try #require(URL(string: "https://example.com/path"))
+ let original = WithURL(link: url)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ WithURL.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips Decimal value")
+ internal func decimalRoundTrip() throws {
+ let decimal = try #require(Decimal(string: "123.456"))
+ let original = WithDecimal(amount: decimal)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ WithDecimal.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips UUID value")
+ internal func uuidRoundTrip() throws {
+ let original = WithUUID(identifier: UUID())
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ WithUUID.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+
+ @Test("round-trips enum with associated values")
+ internal func enumWithAssociatedValues() throws {
+ let cases: [WithStatus] = [
+ WithStatus(status: .idle),
+ WithStatus(status: .running(progress: 0.75)),
+ WithStatus(status: .finished(message: "done")),
+ ]
+ for original in cases {
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ WithStatus.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+ }
+
+ @Test("raw-value enum round-trips")
+ internal func rawValueEnumRoundTrip() throws {
+ let original = WithCompass(direction: .east)
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ WithCompass.self, from: dict
+ )
+ #expect(decoded == original)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift
new file mode 100644
index 0000000..ec6a2ec
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift
@@ -0,0 +1,79 @@
+//
+// DictionaryCodingStrategyErrorTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding strategy error paths")
+internal struct DictionaryCodingStrategyErrorTests {
+ private struct WithDate: Codable { let timestamp: Date }
+
+ private struct WithData: Codable, Equatable {
+ let payload: Data
+ }
+
+ // MARK: - Formatted Date Failure
+
+ @Test("throws on malformed date with formatted strategy")
+ internal func formattedDateFailure() {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy-MM-dd"
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+
+ let dict: [String: Any] = ["timestamp": "not-a-date"]
+ let decoder = DictionaryDecoder()
+ decoder.dateDecodingStrategy = .formatted(formatter)
+ #expect(throws: DecodingError.self) {
+ try decoder.decode(WithDate.self, from: dict)
+ }
+ }
+
+ // MARK: - Invalid Base64
+
+ @Test("throws on invalid base64 with base64 strategy")
+ internal func invalidBase64() {
+ let dict: [String: Any] = ["payload": "!!!invalid!!!"]
+ let decoder = DictionaryDecoder()
+ decoder.dataDecodingStrategy = .base64
+ #expect(throws: DecodingError.self) {
+ try decoder.decode(WithData.self, from: dict)
+ }
+ }
+
+ // MARK: - Deferred Strategies
+
+ @Test("deferredToData round-trips")
+ internal func deferredToDataRoundTrip() throws {
+ let original = WithData(payload: Data([0xDE, 0xAD]))
+ let encoder = DictionaryEncoder()
+ encoder.dataEncodingStrategy = .deferredToData
+ let decoder = DictionaryDecoder()
+ decoder.dataDecodingStrategy = .deferredToData
+ let dict: [String: Any] = try encoder.encode(original)
+ let decoded = try decoder.decode(WithData.self, from: dict)
+ #expect(decoded == original)
+ }
+
+ @Test("deferredToDate round-trips")
+ internal func deferredToDateRoundTrip() throws {
+ let date = Date(timeIntervalSince1970: 1_700_000_000)
+ let original = WithDate(timestamp: date)
+ let encoder = DictionaryEncoder()
+ encoder.dateEncodingStrategy = .deferredToDate
+ let decoder = DictionaryDecoder()
+ decoder.dateDecodingStrategy = .deferredToDate
+ let dict: [String: Any] = try encoder.encode(original)
+ let decoded = try decoder.decode(WithDate.self, from: dict)
+ let diff = abs(
+ decoded.timestamp.timeIntervalSince1970
+ - date.timeIntervalSince1970
+ )
+ #expect(diff < 0.001)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift
new file mode 100644
index 0000000..47384d3
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift
@@ -0,0 +1,82 @@
+//
+// DictionaryCodingSuperDecoderTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryCoding superDecoder and key strategies")
+internal struct DictionaryCodingSuperDecoderTests {
+ private struct PrefixModel: Codable, Equatable {
+ let name: String
+ let age: Int
+ }
+
+ // MARK: - SuperDecoder Tests
+
+ @Test("round-trips class hierarchy using superDecoder")
+ internal func superDecoderRoundTrip() throws {
+ let original = SuperDecoderChild(
+ baseValue: 10, childValue: "hello"
+ )
+ let dict: [String: Any] = try DictionaryEncoder().encode(original)
+ let decoded = try DictionaryDecoder().decode(
+ SuperDecoderChild.self, from: dict
+ )
+ #expect(decoded.baseValue == 10)
+ #expect(decoded.childValue == "hello")
+ }
+
+ @Test("superDecoder from manual dictionary")
+ internal func superDecoderFromManualDict() throws {
+ let dict: [String: Any] = [
+ "childValue": "world",
+ "super": ["baseValue": 42] as [String: Any],
+ ]
+ let decoded = try DictionaryDecoder().decode(
+ SuperDecoderChild.self, from: dict
+ )
+ #expect(decoded.baseValue == 42)
+ #expect(decoded.childValue == "world")
+ }
+
+ // MARK: - Custom Key Strategies
+
+ @Test("custom key encoding applies prefix")
+ internal func customKeyEncoding() throws {
+ let encoder = DictionaryEncoder()
+ encoder.keyEncodingStrategy = .custom { path in
+ let key = path[path.count - 1].stringValue
+ return DictionaryCodingTestKey(
+ stringValue: "pfx_\(key)"
+ )
+ }
+ let value = PrefixModel(name: "Test", age: 25)
+ let dict: [String: Any] = try encoder.encode(value)
+ #expect(dict["pfx_name"] as? String == "Test")
+ #expect(dict["pfx_age"] as? Int == 25)
+ }
+
+ @Test("custom key decoding strips prefix")
+ internal func customKeyDecoding() throws {
+ let dict: [String: Any] = [
+ "pfx_name": "Test", "pfx_age": 25,
+ ]
+ let decoder = DictionaryDecoder()
+ decoder.keyDecodingStrategy = .custom { path in
+ let key = path[path.count - 1].stringValue
+ let stripped = key.replacingOccurrences(
+ of: "pfx_", with: ""
+ )
+ return DictionaryCodingTestKey(stringValue: stripped)
+ }
+ let result = try decoder.decode(PrefixModel.self, from: dict)
+ #expect(result.name == "Test")
+ #expect(result.age == 25)
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift b/Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift
new file mode 100644
index 0000000..02acab1
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift
@@ -0,0 +1,23 @@
+//
+// DictionaryCodingTestKey.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+/// A simple CodingKey used by custom key strategy tests.
+internal struct DictionaryCodingTestKey: CodingKey {
+ internal var stringValue: String
+ internal var intValue: Int?
+
+ internal init(stringValue: String) {
+ self.stringValue = stringValue
+ self.intValue = nil
+ }
+
+ internal init?(intValue: Int) {
+ self.stringValue = "\(intValue)"
+ self.intValue = intValue
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift b/Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift
new file mode 100644
index 0000000..70b15d1
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift
@@ -0,0 +1,89 @@
+//
+// DictionaryDecoderPlatformTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryDecoder platform behaviour")
+internal struct DictionaryDecoderPlatformTests {
+ #if canImport(Darwin)
+ internal static let isDarwin = true
+ #else
+ internal static let isDarwin = false
+ #endif
+
+ // NSNumber wrapping Bool must decode as Bool, not as Int.
+ @Test("NSNumber bool decoded as Bool")
+ internal func nsnumberBoolDecodedAsBool() throws {
+ struct BoolFixture: Codable {
+ let flag: Bool
+ }
+ let dict: [String: Any] = ["flag": NSNumber(value: true)]
+ let result = try DictionaryDecoder().decode(BoolFixture.self, from: dict)
+ #expect(result.flag == true)
+ }
+
+ // NSNumber wrapping Bool must be rejected when Int is expected.
+ // On Linux, Bool and Int8 share objCType "c" so isBool is unreliable;
+ // boolean rejection only works on Darwin (CFBooleanGetTypeID).
+ @Test("NSNumber bool rejected for Int")
+ internal func nsnumberBoolRejectedForInt() {
+ struct IntFixture: Codable {
+ let count: Int
+ }
+ let dict: [String: Any] = ["count": NSNumber(value: true)]
+ withKnownIssue(
+ "Bool/Int8 share objCType on Linux"
+ ) {
+ #expect(throws: (any Error).self) {
+ try DictionaryDecoder().decode(IntFixture.self, from: dict)
+ }
+ } when: {
+ !Self.isDarwin
+ }
+ }
+
+ // NSNumber wrapping Bool must be rejected when Double is expected.
+ @Test("NSNumber bool rejected for Double")
+ internal func nsnumberBoolRejectedForDouble() {
+ struct DoubleFixture: Codable {
+ let value: Double
+ }
+ let dict: [String: Any] = ["value": NSNumber(value: true)]
+ #expect(throws: (any Error).self) {
+ try DictionaryDecoder().decode(DoubleFixture.self, from: dict)
+ }
+ }
+
+ // Darwin-only: CFUUID values should decode as UUID.
+ // The trait disables the test on Linux; #if canImport(Darwin) in the body
+ // prevents CF types from being compiled on non-Darwin platforms.
+ @Test("CFUUID value decoded as UUID", .enabled(if: isDarwin))
+ internal func cfuuidDecodedAsUUID() throws {
+ #if canImport(Darwin)
+ struct UUIDFixture: Codable {
+ let id: UUID
+ }
+ guard let cfuuid = CFUUIDCreate(kCFAllocatorDefault) else {
+ Issue.record("CFUUIDCreate returned nil")
+ return
+ }
+ let dict: [String: Any] = ["id": cfuuid as AnyObject]
+ let result = try DictionaryDecoder().decode(UUIDFixture.self, from: dict)
+ let cfStringRef = CFUUIDCreateString(kCFAllocatorDefault, cfuuid)
+ guard let cfString = cfStringRef as String? else {
+ Issue.record("CFUUIDCreateString returned nil")
+ return
+ }
+ #expect(result.id == UUID(uuidString: cfString))
+ #else
+ Issue.record("CFUUID test must not run on non-Darwin platforms")
+ #endif
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryDecoderTests.swift b/Tests/DictionaryCodingTests/DictionaryDecoderTests.swift
new file mode 100644
index 0000000..76ceb1e
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryDecoderTests.swift
@@ -0,0 +1,53 @@
+//
+// DictionaryDecoderTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryDecoder")
+internal struct DictionaryDecoderTests {
+ private struct Simple: Codable, Equatable {
+ let name: String
+ let count: Int
+ }
+
+ private struct WithOptional: Codable, Equatable {
+ let value: String?
+ }
+
+ @Test("decodes string and int from dictionary")
+ internal func decodesSimpleFields() throws {
+ let dict: [String: Any] = ["name": "world", "count": 7]
+ let result = try DictionaryDecoder().decode(Simple.self, from: dict)
+ #expect(result.name == "world")
+ #expect(result.count == 7)
+ }
+
+ @Test("decodes present optional")
+ internal func decodesPresentOptional() throws {
+ let dict: [String: Any] = ["value": "here"]
+ let result = try DictionaryDecoder().decode(WithOptional.self, from: dict)
+ #expect(result.value == "here")
+ }
+
+ @Test("decodes missing key as nil optional")
+ internal func decodesMissingKeyAsNil() throws {
+ let dict: [String: Any] = [:]
+ let result = try DictionaryDecoder().decode(WithOptional.self, from: dict)
+ #expect(result.value == nil)
+ }
+
+ @Test("throws on missing required key")
+ internal func throwsOnMissingKey() {
+ let dict: [String: Any] = ["name": "only"]
+ #expect(throws: (any Error).self) {
+ try DictionaryDecoder().decode(Simple.self, from: dict)
+ }
+ }
+}
diff --git a/Tests/DictionaryCodingTests/DictionaryEncoderTests.swift b/Tests/DictionaryCodingTests/DictionaryEncoderTests.swift
new file mode 100644
index 0000000..dfe86fa
--- /dev/null
+++ b/Tests/DictionaryCodingTests/DictionaryEncoderTests.swift
@@ -0,0 +1,65 @@
+//
+// DictionaryEncoderTests.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+import Testing
+
+@Suite("DictionaryEncoder")
+internal struct DictionaryEncoderTests {
+ private struct Simple: Codable, Equatable {
+ let name: String
+ let count: Int
+ let ratio: Double
+ let flag: Bool
+ }
+
+ private struct WithOptional: Codable, Equatable {
+ let value: String?
+ }
+
+ private struct Nested: Codable, Equatable {
+ let label: String
+ let inner: Simple
+ }
+
+ @Test("encodes string, int, double, bool fields as top-level keys")
+ internal func encodesSimpleFields() throws {
+ let value = Simple(name: "hello", count: 42, ratio: 3.14, flag: true)
+ let dict: [String: Any] = try DictionaryEncoder().encode(value)
+ #expect(dict["name"] as? String == "hello")
+ #expect(dict["count"] as? Int == 42)
+ #expect(dict["flag"] as? Bool == true)
+ let ratio = try #require(dict["ratio"] as? Double)
+ #expect(abs(ratio - 3.14) < 0.001)
+ }
+
+ @Test("encodes present optional as value")
+ internal func encodesPresentOptional() throws {
+ let value = WithOptional(value: "present")
+ let dict: [String: Any] = try DictionaryEncoder().encode(value)
+ #expect(dict["value"] as? String == "present")
+ }
+
+ @Test("encodes nil optional as absent key")
+ internal func encodesNilOptional() throws {
+ let value = WithOptional(value: nil)
+ let dict: [String: Any] = try DictionaryEncoder().encode(value)
+ #expect(dict["value"] == nil)
+ }
+
+ @Test("encodes nested struct as sub-dictionary")
+ internal func encodesNestedStruct() throws {
+ let inner = Simple(name: "inner", count: 1, ratio: 0.5, flag: false)
+ let value = Nested(label: "outer", inner: inner)
+ let dict: [String: Any] = try DictionaryEncoder().encode(value)
+ #expect(dict["label"] as? String == "outer")
+ let innerDict = try #require(dict["inner"] as? [String: Any])
+ #expect(innerDict["name"] as? String == "inner")
+ }
+}
diff --git a/Tests/DictionaryCodingTests/SuperDecoderBase.swift b/Tests/DictionaryCodingTests/SuperDecoderBase.swift
new file mode 100644
index 0000000..560875c
--- /dev/null
+++ b/Tests/DictionaryCodingTests/SuperDecoderBase.swift
@@ -0,0 +1,17 @@
+//
+// SuperDecoderBase.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import Foundation
+
+internal class SuperDecoderBase: Codable {
+ internal let baseValue: Int
+
+ internal init(baseValue: Int) {
+ self.baseValue = baseValue
+ }
+}
diff --git a/Tests/DictionaryCodingTests/SuperDecoderChild.swift b/Tests/DictionaryCodingTests/SuperDecoderChild.swift
new file mode 100644
index 0000000..63cb2b3
--- /dev/null
+++ b/Tests/DictionaryCodingTests/SuperDecoderChild.swift
@@ -0,0 +1,41 @@
+//
+// SuperDecoderChild.swift
+// AtLeast
+//
+// Copyright (c) 2026 BrightDigit.
+// All rights reserved.
+//
+
+import DictionaryCoding
+import Foundation
+
+internal class SuperDecoderChild: SuperDecoderBase {
+ private enum CodingKeys: String, CodingKey {
+ case childValue
+ }
+
+ internal let childValue: String
+
+ internal init(baseValue: Int, childValue: String) {
+ self.childValue = childValue
+ super.init(baseValue: baseValue)
+ }
+
+ internal required init(from decoder: Decoder) throws {
+ let container = try decoder.container(
+ keyedBy: CodingKeys.self
+ )
+ self.childValue = try container.decode(
+ String.self, forKey: .childValue
+ )
+ try super.init(from: container.superDecoder())
+ }
+
+ override internal func encode(to encoder: Encoder) throws {
+ var container = encoder.container(
+ keyedBy: CodingKeys.self
+ )
+ try container.encode(childValue, forKey: .childValue)
+ try super.encode(to: container.superEncoder())
+ }
+}
From f222efd6f49bd2c8f14fadbdaab327747fa4578a Mon Sep 17 00:00:00 2001
From: Leo Dion
Date: Thu, 25 Jun 2026 16:32:04 -0400
Subject: [PATCH 2/5] chore(DictionaryCoding): add standalone package
scaffolding
Bring Packages/DictionaryCoding up to BrightDigit's standard package
scaffolding (modeled on SundialKit) ahead of extracting it into its own
repo: README, MIT LICENSE, .gitignore, swiftlint/swift-format,
.spi.yml, .mise.toml (no ruby), .periphery.yml, codecov.yml, Makefile,
project.yml, Scripts/, .devcontainer/, a DocC catalog, a package-scoped
CLAUDE.md, and GitHub workflows (CI, CodeQL, Claude) pinned to the
Swift 6.3 toolchain the manifest requires.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.devcontainer/devcontainer.json | 32 ++
.devcontainer/swift-6.1/devcontainer.json | 32 ++
.devcontainer/swift-6.2/devcontainer.json | 32 ++
.devcontainer/swift-6.3/devcontainer.json | 32 ++
.github/workflows/DictionaryCoding.yml | 173 +++++++++
.github/workflows/claude-code-review.yml | 57 +++
.github/workflows/claude.yml | 50 +++
.github/workflows/codeql.yml | 53 +++
.gitignore | 90 ++---
.mise.toml | 16 +
.periphery.yml | 1 +
.spi.yml | 5 +
.swift-format | 70 ++++
.swiftlint.yml | 134 +++++++
CLAUDE.md | 55 +++
Makefile | 55 +++
README.md | 91 ++++-
Scripts/header.sh | 104 +++++
Scripts/lint.sh | 102 +++++
Scripts/preview-docs.sh | 363 ++++++++++++++++++
.../DictionaryCoding.docc/DictionaryCoding.md | 35 ++
codecov.yml | 2 +
project.yml | 13 +
23 files changed, 1539 insertions(+), 58 deletions(-)
create mode 100644 .devcontainer/devcontainer.json
create mode 100644 .devcontainer/swift-6.1/devcontainer.json
create mode 100644 .devcontainer/swift-6.2/devcontainer.json
create mode 100644 .devcontainer/swift-6.3/devcontainer.json
create mode 100644 .github/workflows/DictionaryCoding.yml
create mode 100644 .github/workflows/claude-code-review.yml
create mode 100644 .github/workflows/claude.yml
create mode 100644 .github/workflows/codeql.yml
create mode 100644 .mise.toml
create mode 100644 .periphery.yml
create mode 100644 .spi.yml
create mode 100644 .swift-format
create mode 100644 .swiftlint.yml
create mode 100644 CLAUDE.md
create mode 100644 Makefile
create mode 100755 Scripts/header.sh
create mode 100755 Scripts/lint.sh
create mode 100755 Scripts/preview-docs.sh
create mode 100644 Sources/DictionaryCoding/DictionaryCoding.docc/DictionaryCoding.md
create mode 100644 codecov.yml
create mode 100644 project.yml
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..633ecfe
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,32 @@
+{
+ "name": "Swift 6.3",
+ "image": "swift:6.3",
+ "features": {
+ "ghcr.io/devcontainers/features/common-utils:2": {
+ "installZsh": "false",
+ "username": "vscode",
+ "upgradePackages": "false"
+ },
+ "ghcr.io/devcontainers/features/git:1": {
+ "version": "os-provided",
+ "ppa": "false"
+ }
+ },
+ "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
+ "runArgs": [
+ "--cap-add=SYS_PTRACE",
+ "--security-opt",
+ "seccomp=unconfined"
+ ],
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "lldb.library": "/usr/lib/liblldb.so"
+ },
+ "extensions": [
+ "swift-server.swift"
+ ]
+ }
+ },
+ "remoteUser": "root"
+}
\ No newline at end of file
diff --git a/.devcontainer/swift-6.1/devcontainer.json b/.devcontainer/swift-6.1/devcontainer.json
new file mode 100644
index 0000000..23821a4
--- /dev/null
+++ b/.devcontainer/swift-6.1/devcontainer.json
@@ -0,0 +1,32 @@
+{
+ "name": "Swift 6.1",
+ "image": "swift:6.1",
+ "features": {
+ "ghcr.io/devcontainers/features/common-utils:2": {
+ "installZsh": "false",
+ "username": "vscode",
+ "upgradePackages": "false"
+ },
+ "ghcr.io/devcontainers/features/git:1": {
+ "version": "os-provided",
+ "ppa": "false"
+ }
+ },
+ "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
+ "runArgs": [
+ "--cap-add=SYS_PTRACE",
+ "--security-opt",
+ "seccomp=unconfined"
+ ],
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "lldb.library": "/usr/lib/liblldb.so"
+ },
+ "extensions": [
+ "swift-server.swift"
+ ]
+ }
+ },
+ "remoteUser": "root"
+}
\ No newline at end of file
diff --git a/.devcontainer/swift-6.2/devcontainer.json b/.devcontainer/swift-6.2/devcontainer.json
new file mode 100644
index 0000000..f04399b
--- /dev/null
+++ b/.devcontainer/swift-6.2/devcontainer.json
@@ -0,0 +1,32 @@
+{
+ "name": "Swift 6.2",
+ "image": "swift:6.2",
+ "features": {
+ "ghcr.io/devcontainers/features/common-utils:2": {
+ "installZsh": "false",
+ "username": "vscode",
+ "upgradePackages": "false"
+ },
+ "ghcr.io/devcontainers/features/git:1": {
+ "version": "os-provided",
+ "ppa": "false"
+ }
+ },
+ "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
+ "runArgs": [
+ "--cap-add=SYS_PTRACE",
+ "--security-opt",
+ "seccomp=unconfined"
+ ],
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "lldb.library": "/usr/lib/liblldb.so"
+ },
+ "extensions": [
+ "swift-server.swift"
+ ]
+ }
+ },
+ "remoteUser": "root"
+}
\ No newline at end of file
diff --git a/.devcontainer/swift-6.3/devcontainer.json b/.devcontainer/swift-6.3/devcontainer.json
new file mode 100644
index 0000000..80941c7
--- /dev/null
+++ b/.devcontainer/swift-6.3/devcontainer.json
@@ -0,0 +1,32 @@
+{
+ "name": "Swift 6.3",
+ "image": "swift:6.3",
+ "features": {
+ "ghcr.io/devcontainers/features/common-utils:2": {
+ "installZsh": "false",
+ "username": "vscode",
+ "upgradePackages": "false"
+ },
+ "ghcr.io/devcontainers/features/git:1": {
+ "version": "os-provided",
+ "ppa": "false"
+ }
+ },
+ "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
+ "runArgs": [
+ "--cap-add=SYS_PTRACE",
+ "--security-opt",
+ "seccomp=unconfined"
+ ],
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "lldb.library": "/usr/lib/liblldb.so"
+ },
+ "extensions": [
+ "swift-server.swift"
+ ]
+ }
+ },
+ "remoteUser": "root"
+}
diff --git a/.github/workflows/DictionaryCoding.yml b/.github/workflows/DictionaryCoding.yml
new file mode 100644
index 0000000..9481cdc
--- /dev/null
+++ b/.github/workflows/DictionaryCoding.yml
@@ -0,0 +1,173 @@
+name: DictionaryCoding
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - 'v[0-9]*.[0-9]*.[0-9]*'
+ paths-ignore:
+ - '**.md'
+ - 'Docs/**'
+ - 'LICENSE'
+ - '.github/ISSUE_TEMPLATE/**'
+ pull_request:
+ branches:
+ - main
+ - 'v[0-9]*.[0-9]*.[0-9]*'
+ paths-ignore:
+ - '**.md'
+ - 'Docs/**'
+ - 'LICENSE'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
+ cancel-in-progress: true
+
+env:
+ PACKAGE_NAME: DictionaryCoding
+
+jobs:
+ configure:
+ name: Configure Build Matrix
+ runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'pull_request' || !contains(github.event.head_commit.message, 'ci skip') }}
+ outputs:
+ full-matrix: ${{ steps.set-matrix.outputs.full-matrix }}
+ ubuntu-os: ${{ steps.set-matrix.outputs.ubuntu-os }}
+ steps:
+ - name: Determine build matrix
+ id: set-matrix
+ run: |
+ # DictionaryCoding's Package.swift declares swift-tools-version 6.3, so every leg
+ # must use a Swift 6.3 toolchain — older toolchains cannot parse the manifest.
+ if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.event_name }}" == "pull_request" ]]; then
+ echo "full-matrix=true" >> "$GITHUB_OUTPUT"
+ echo 'ubuntu-os=["noble","jammy"]' >> "$GITHUB_OUTPUT"
+ else
+ echo "full-matrix=false" >> "$GITHUB_OUTPUT"
+ echo 'ubuntu-os=["noble"]' >> "$GITHUB_OUTPUT"
+ fi
+
+ build-ubuntu:
+ name: Build on Ubuntu
+ needs: [configure]
+ runs-on: ubuntu-latest
+ container: swift:6.3-${{ matrix.os }}
+ strategy:
+ matrix:
+ os: ${{ fromJSON(needs.configure.outputs.ubuntu-os) }}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: brightdigit/swift-build@v1
+ id: build
+ - name: Install curl
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ run: |
+ apt-get update -q
+ apt-get install -y curl
+ - uses: sersoft-gmbh/swift-coverage-action@v5
+ id: coverage-files
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ with:
+ fail-on-empty-output: true
+ - name: Upload coverage to Codecov
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ uses: codecov/codecov-action@v6
+ with:
+ fail_ci_if_error: true
+ flags: swift-6.3,ubuntu
+ verbose: true
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }}
+
+ build-macos:
+ name: Build on macOS
+ needs: [configure]
+ runs-on: macos-26
+ if: ${{ !cancelled() && needs.configure.result == 'success' }}
+ steps:
+ - uses: actions/checkout@v6
+ - name: Build and Test
+ id: build
+ uses: brightdigit/swift-build@v1
+ with:
+ scheme: ${{ env.PACKAGE_NAME }}-Package
+ xcode: "/Applications/Xcode_26.4.app"
+ - name: Process Coverage
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ uses: sersoft-gmbh/swift-coverage-action@v5
+ - name: Upload Coverage
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ uses: codecov/codecov-action@v6
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ flags: spm
+
+ build-macos-full:
+ name: Build on macOS (Full)
+ needs: [configure]
+ if: ${{ !cancelled() && needs.configure.result == 'success' && needs.configure.outputs.full-matrix == 'true' }}
+ runs-on: macos-26
+ strategy:
+ matrix:
+ include:
+ - type: macos
+ xcode: "/Applications/Xcode_26.4.app"
+ - type: ios
+ xcode: "/Applications/Xcode_26.4.app"
+ deviceName: "iPhone 17 Pro"
+ osVersion: "26.4"
+ download-platform: true
+ - type: watchos
+ xcode: "/Applications/Xcode_26.4.app"
+ deviceName: "Apple Watch Ultra 3 (49mm)"
+ osVersion: "26.4"
+ download-platform: true
+ - type: tvos
+ xcode: "/Applications/Xcode_26.4.app"
+ deviceName: "Apple TV"
+ osVersion: "26.4"
+ download-platform: true
+ - type: visionos
+ xcode: "/Applications/Xcode_26.4.app"
+ deviceName: "Apple Vision Pro"
+ osVersion: "26.4"
+ download-platform: true
+ steps:
+ - uses: actions/checkout@v6
+ - name: Build and Test
+ id: build
+ uses: brightdigit/swift-build@v1
+ with:
+ scheme: ${{ env.PACKAGE_NAME }}-Package
+ type: ${{ matrix.type }}
+ xcode: ${{ matrix.xcode }}
+ deviceName: ${{ matrix.deviceName }}
+ osVersion: ${{ matrix.osVersion }}
+ download-platform: ${{ matrix.download-platform }}
+ - name: Process Coverage
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ uses: sersoft-gmbh/swift-coverage-action@v5
+ - name: Upload Coverage
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ uses: codecov/codecov-action@v6
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }}
+
+ lint:
+ name: Linting
+ # build-macos-full is skipped on feature branches; !failure() lets this job proceed
+ # past skipped (not failed) dependencies.
+ if: ${{ !cancelled() && !failure() && (github.event_name == 'pull_request' || !contains(github.event.head_commit.message, 'ci skip')) }}
+ runs-on: ubuntu-latest
+ needs: [build-ubuntu, build-macos, build-macos-full]
+ steps:
+ - uses: actions/checkout@v6
+ - uses: jdx/mise-action@v4
+ with:
+ cache: true
+ - name: Lint
+ run: ./Scripts/lint.sh
+ env:
+ LINT_MODE: STRICT
diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml
new file mode 100644
index 0000000..205b0fe
--- /dev/null
+++ b/.github/workflows/claude-code-review.yml
@@ -0,0 +1,57 @@
+name: Claude Code Review
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ # Optional: Only run on specific file changes
+ # paths:
+ # - "src/**/*.ts"
+ # - "src/**/*.tsx"
+ # - "src/**/*.js"
+ # - "src/**/*.jsx"
+
+jobs:
+ claude-review:
+ # Optional: Filter by PR author
+ # if: |
+ # github.event.pull_request.user.login == 'external-contributor' ||
+ # github.event.pull_request.user.login == 'new-developer' ||
+ # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ issues: read
+ id-token: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code Review
+ id: claude-review
+ uses: anthropics/claude-code-action@v1
+ with:
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ prompt: |
+ REPO: ${{ github.repository }}
+ PR NUMBER: ${{ github.event.pull_request.number }}
+
+ Please review this pull request and provide feedback on:
+ - Code quality and best practices
+ - Potential bugs or issues
+ - Performance considerations
+ - Security concerns
+ - Test coverage
+
+ Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
+
+ Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
+
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
+ # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
+ claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
+
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
new file mode 100644
index 0000000..412cef9
--- /dev/null
+++ b/.github/workflows/claude.yml
@@ -0,0 +1,50 @@
+name: Claude Code
+
+on:
+ issue_comment:
+ types: [created]
+ pull_request_review_comment:
+ types: [created]
+ issues:
+ types: [opened, assigned]
+ pull_request_review:
+ types: [submitted]
+
+jobs:
+ claude:
+ if: |
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ issues: read
+ id-token: write
+ actions: read # Required for Claude to read CI results on PRs
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code
+ id: claude
+ uses: anthropics/claude-code-action@v1
+ with:
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+
+ # This is an optional setting that allows Claude to read CI results on PRs
+ additional_permissions: |
+ actions: read
+
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
+ # prompt: 'Update the pull request description to include a summary of changes.'
+
+ # Optional: Add claude_args to customize behavior and configuration
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
+ # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
+ # claude_args: '--allowed-tools Bash(gh pr:*)'
+
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..c62807b
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,53 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches-ignore:
+ - '*WIP'
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ "main", "v1.0.0-alpha.1" ]
+ schedule:
+ - cron: '20 11 * * 3'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-26') || 'ubuntu-latest' }}
+ timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'swift' ]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Xcode
+ # DictionaryCoding requires the Swift 6.3 toolchain (swift-tools-version: 6.3).
+ run: sudo xcode-select -s /Applications/Xcode_26.4.app/Contents/Developer
+
+ - name: Verify Swift Version
+ run: |
+ swift --version
+ swift package --version
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+
+ - run: |
+ echo "Run, Build Application using script"
+ swift build
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.gitignore b/.gitignore
index 52fe2f7..980b3b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,62 +1,38 @@
-# Xcode
-#
-# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-
-## User settings
-xcuserdata/
-
-## Obj-C/Swift specific
-*.hmap
-
-## App packaging
-*.ipa
-*.dSYM.zip
-*.dSYM
-
-## Playgrounds
-timeline.xctimeline
-playground.xcworkspace
+# macOS
+.DS_Store
# Swift Package Manager
-#
-# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
-# Packages/
-# Package.pins
-# Package.resolved
-# *.xcodeproj
-#
-# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
-# hence it is not needed unless you have added a package configuration file to your project
-# .swiftpm
-
.build/
+.swiftpm/
+DerivedData/
+.index-build/
+Package.resolved
-# CocoaPods
-#
-# We recommend against adding the Pods directory to your .gitignore. However
-# you should judge for yourself, the pros and cons are mentioned at:
-# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
-#
-# Pods/
-#
-# Add this line if you want to avoid checking in source code from the Xcode workspace
-# *.xcworkspace
-
-# Carthage
-#
-# Add this line if you want to avoid checking in source code from Carthage dependencies.
-# Carthage/Checkouts
-
-Carthage/Build/
-
-# fastlane
-#
-# It is recommended to not store the screenshots in the git repo.
-# Instead, use fastlane to re-generate the screenshots whenever they are needed.
-# For more information about the recommended setup visit:
-# https://docs.fastlane.tools/best-practices/source-control/#source-control
+# Xcode
+*.xcodeproj
+*.xcworkspace
+xcuserdata/
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots/**/*.png
-fastlane/test_output
+# IDE
+.vscode/
+.idea/
+
+# mise
+.mint/
+
+# DocC
+.docc-build/
+
+# Claude
+.claude/settings.local.json
+.mcp.json
+
+# Prevent accidental commits of private keys/certificates
+*.p8
+*.pem
+*.key
+*.cer
+*.crt
+*.der
+*.p12
+*.pfx
diff --git a/.mise.toml b/.mise.toml
new file mode 100644
index 0000000..5862095
--- /dev/null
+++ b/.mise.toml
@@ -0,0 +1,16 @@
+[tools]
+# Swift development tools managed via Swift Package Manager plugins
+"spm:swiftlang/swift-format" = "602.0.0"
+swiftlint = "0.61.0"
+"spm:peripheryapp/periphery" = "3.2.0"
+
+# Xcode project generation
+xcodegen = "2.43.0"
+
+[tasks]
+swift-format = "swift-format"
+swiftlint = "swiftlint"
+periphery = "periphery"
+
+[settings]
+experimental = true
diff --git a/.periphery.yml b/.periphery.yml
new file mode 100644
index 0000000..85b884a
--- /dev/null
+++ b/.periphery.yml
@@ -0,0 +1 @@
+retain_public: true
diff --git a/.spi.yml b/.spi.yml
new file mode 100644
index 0000000..062eb04
--- /dev/null
+++ b/.spi.yml
@@ -0,0 +1,5 @@
+version: 1
+builder:
+ configs:
+ - platform: ios
+ documentation_targets: [DictionaryCoding]
diff --git a/.swift-format b/.swift-format
new file mode 100644
index 0000000..257f555
--- /dev/null
+++ b/.swift-format
@@ -0,0 +1,70 @@
+{
+ "fileScopedDeclarationPrivacy" : {
+ "accessLevel" : "fileprivate"
+ },
+ "indentation" : {
+ "spaces" : 2
+ },
+ "indentConditionalCompilationBlocks" : true,
+ "indentSwitchCaseLabels" : false,
+ "lineBreakAroundMultilineExpressionChainComponents" : false,
+ "lineBreakBeforeControlFlowKeywords" : false,
+ "lineBreakBeforeEachArgument" : false,
+ "lineBreakBeforeEachGenericRequirement" : false,
+ "lineLength" : 100,
+ "maximumBlankLines" : 1,
+ "multiElementCollectionTrailingCommas" : true,
+ "noAssignmentInExpressions" : {
+ "allowedFunctions" : [
+ "XCTAssertNoThrow"
+ ]
+ },
+ "prioritizeKeepingFunctionOutputTogether" : false,
+ "respectsExistingLineBreaks" : true,
+ "rules" : {
+ "AllPublicDeclarationsHaveDocumentation" : true,
+ "AlwaysUseLiteralForEmptyCollectionInit" : false,
+ "AlwaysUseLowerCamelCase" : true,
+ "AmbiguousTrailingClosureOverload" : true,
+ "BeginDocumentationCommentWithOneLineSummary" : false,
+ "DoNotUseSemicolons" : true,
+ "DontRepeatTypeInStaticProperties" : true,
+ "FileScopedDeclarationPrivacy" : false,
+ "FullyIndirectEnum" : true,
+ "GroupNumericLiterals" : true,
+ "IdentifiersMustBeASCII" : true,
+ "NeverForceUnwrap" : true,
+ "NeverUseForceTry" : true,
+ "NeverUseImplicitlyUnwrappedOptionals" : true,
+ "NoAccessLevelOnExtensionDeclaration" : true,
+ "NoAssignmentInExpressions" : true,
+ "NoBlockComments" : true,
+ "NoCasesWithOnlyFallthrough" : true,
+ "NoEmptyTrailingClosureParentheses" : true,
+ "NoLabelsInCasePatterns" : true,
+ "NoLeadingUnderscores" : true,
+ "NoParensAroundConditions" : true,
+ "NoPlaygroundLiterals" : true,
+ "NoVoidReturnOnFunctionSignature" : true,
+ "OmitExplicitReturns" : false,
+ "OneCasePerLine" : true,
+ "OneVariableDeclarationPerLine" : true,
+ "OnlyOneTrailingClosureArgument" : true,
+ "OrderedImports" : true,
+ "ReplaceForEachWithForLoop" : true,
+ "ReturnVoidInsteadOfEmptyTuple" : true,
+ "TypeNamesShouldBeCapitalized" : true,
+ "UseEarlyExits" : false,
+ "UseExplicitNilCheckInConditions" : true,
+ "UseLetInEveryBoundCaseVariable" : true,
+ "UseShorthandTypeNames" : true,
+ "UseSingleLinePropertyGetter" : true,
+ "UseSynthesizedInitializer" : true,
+ "UseTripleSlashForDocumentationComments" : true,
+ "UseWhereClausesInForLoops" : true,
+ "ValidateDocumentationComments" : true
+ },
+ "spacesAroundRangeFormationOperators" : false,
+ "tabWidth" : 2,
+ "version" : 1
+}
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..cb9ec35
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,134 @@
+opt_in_rules:
+ - array_init
+ - closure_body_length
+ - closure_end_indentation
+ - closure_spacing
+ - collection_alignment
+ - conditional_returns_on_newline
+ - contains_over_filter_count
+ - contains_over_filter_is_empty
+ - contains_over_first_not_nil
+ - contains_over_range_nil_comparison
+ - convenience_type
+ - discouraged_object_literal
+ - empty_collection_literal
+ - empty_count
+ - empty_string
+ - empty_xctest_method
+ - enum_case_associated_values_count
+ - expiring_todo
+ - explicit_acl
+ - explicit_init
+ - explicit_top_level_acl
+ # - fallthrough
+ - fatal_error_message
+ - file_name
+ - file_name_no_space
+ - file_types_order
+ - first_where
+ - flatmap_over_map_reduce
+ - force_unwrapping
+# - function_default_parameter_at_end
+ - ibinspectable_in_extension
+ - identical_operands
+ - implicit_return
+ - implicitly_unwrapped_optional
+ - indentation_width
+ - joined_default_parameter
+ - last_where
+ - legacy_multiple
+ - legacy_random
+ - literal_expression_end_indentation
+ - lower_acl_than_parent
+ - missing_docs
+ - modifier_order
+ - multiline_arguments
+ - multiline_arguments_brackets
+ - multiline_function_chains
+ - multiline_literal_brackets
+ - multiline_parameters
+ - nimble_operator
+ - nslocalizedstring_key
+ - nslocalizedstring_require_bundle
+ - number_separator
+ - object_literal
+ - one_declaration_per_file
+ - operator_usage_whitespace
+ - optional_enum_case_matching
+ - overridden_super_call
+ - override_in_extension
+ - pattern_matching_keywords
+ - prefer_self_type_over_type_of_self
+ - prefer_zero_over_explicit_init
+ - private_action
+ - private_outlet
+ - prohibited_interface_builder
+ - prohibited_super_call
+ - quick_discouraged_call
+ - quick_discouraged_focused_test
+ - quick_discouraged_pending_test
+ - reduce_into
+ - redundant_nil_coalescing
+ - redundant_type_annotation
+ - required_enum_case
+ - single_test_class
+ - sorted_first_last
+ - sorted_imports
+ - static_operator
+ - strong_iboutlet
+ - toggle_bool
+# - trailing_closure
+ - type_contents_order
+ - unavailable_function
+ - unneeded_parentheses_in_closure_argument
+ - unowned_variable_capture
+ - untyped_error_in_catch
+ - vertical_parameter_alignment_on_call
+ - vertical_whitespace_closing_braces
+ - vertical_whitespace_opening_braces
+ - xct_specific_matcher
+ - yoda_condition
+analyzer_rules:
+ - unused_import
+ - unused_declaration
+cyclomatic_complexity:
+ - 6
+ - 12
+file_length:
+ warning: 225
+ error: 300
+function_body_length:
+ - 50
+ - 76
+function_parameter_count: 8
+line_length:
+ - 108
+ - 200
+closure_body_length:
+ - 50
+ - 60
+identifier_name:
+ excluded:
+ - id
+ - no
+excluded:
+ - DerivedData
+ - .build
+ - Mint
+ - Examples
+ - Packages
+indentation_width:
+ indentation_width: 2
+file_name:
+ severity: error
+fatal_error_message:
+ severity: error
+disabled_rules:
+ - nesting
+ - implicit_getter
+ - switch_case_alignment
+ - closure_parameter_position
+ - trailing_comma
+ - opening_brace
+ - optional_data_string_conversion
+ - pattern_matching_keywords
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..c345e8e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,55 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this
+repository.
+
+## Overview
+
+**DictionaryCoding** is a single-target Swift package providing `DictionaryEncoder` and
+`DictionaryDecoder` — a `Codable`-based `Encoder`/`Decoder` pair that converts `Codable` values to
+and from `[String: Any]` and `NSDictionary`, mirroring the `JSONEncoder` / `JSONDecoder` API. It has
+**no external dependencies**.
+
+The public entry points are ``DictionaryEncoder`` (`Sources/DictionaryCoding/DictionaryEncoder.swift`,
+`+Encode.swift`) and ``DictionaryDecoder`` (`DictionaryDecoder.swift`). Everything else
+(`*Impl*`, `*Container*`, `*Storage*`, `DictionaryCodingKey`, the `EncodingError`/`DecodingError`
+extensions, `NSNumber+Bool`) is internal plumbing that backs the two `open` classes.
+
+## Architecture
+
+- **One file per type.** SwiftLint's `one_declaration_per_file` and `file_name` rules are enabled;
+ keep filenames matching their primary type, and use the `Type+Feature.swift` convention for
+ extensions (e.g. `DictionaryDecoderImpl+UnboxIntegers.swift`).
+- **Strategies are nested enums** on the encoder/decoder (date, data, non-conforming float, key,
+ and `MissingValueDecodingStrategy`). Mirror `Foundation`'s `JSONEncoder`/`JSONDecoder` semantics
+ when adding or changing a strategy.
+- **`Combine` conformances are guarded** by `#if canImport(Combine)` (`TopLevelEncoder` /
+ `TopLevelDecoder`). Keep new Combine surface behind that check so non-Apple builds stay clean.
+
+## Build, Test & Lint
+
+```bash
+make build # swift build
+make test # swift test --enable-code-coverage
+make lint # strict swift-format + swiftlint + periphery (via mise)
+make format # format only, no linting
+make docs-build # build DocC for the DictionaryCoding target
+```
+
+Tooling is pinned in `.mise.toml` (swift-format, swiftlint, periphery, xcodegen); `make lint`
+shells out through `Scripts/lint.sh`, which bootstraps those via `mise`. `Scripts/header.sh`
+stamps the standard BrightDigit license header onto every file in `Sources/`.
+
+> **Toolchain:** `Package.swift` declares `swift-tools-version: 6.3` and platforms macOS 15 /
+> iOS·tvOS·watchOS·visionOS 26. Building requires the Swift 6.3 toolchain (Xcode 26+); older
+> toolchains cannot parse the manifest. The package builds in Swift 6 language mode
+> (`swiftLanguageMode(.v6)`).
+
+## Code Style
+
+- 2-space indentation, 100-column target (`.swift-format`); SwiftLint adds opt-in rules and tighter
+ limits (`file_length` warn 225 / error 300, `function_body_length` warn 50 / error 76).
+- Explicit access control is required (`explicit_acl` / `explicit_top_level_acl`); the package
+ manifest is the one place that opts out, via a `swiftlint:disable` comment.
+- All public declarations must carry documentation comments (`AllPublicDeclarationsHaveDocumentation`,
+ `missing_docs`).
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a027aa5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,55 @@
+.PHONY: help build test lint format clean docs-preview docs-build docs-clean
+
+# Default target
+help:
+ @echo "Available targets:"
+ @echo " build - Build the package"
+ @echo " test - Run tests with code coverage"
+ @echo " lint - Run linting and formatting checks (strict mode)"
+ @echo " format - Format code only (no linting)"
+ @echo " clean - Clean build artifacts"
+ @echo " docs-preview - Preview documentation with auto-rebuild"
+ @echo " docs-build - Build documentation without preview server"
+ @echo " docs-clean - Clean documentation build artifacts"
+ @echo " help - Show this help message"
+
+# Build the package
+build:
+ @echo "🔨 Building DictionaryCoding..."
+ @swift build
+
+# Run tests
+test:
+ @echo "🧪 Running tests with code coverage..."
+ @swift test --enable-code-coverage
+
+# Run linting in strict mode
+lint:
+ @echo "🔍 Running linting in strict mode..."
+ @LINT_MODE=STRICT ./Scripts/lint.sh
+
+# Format code only
+format:
+ @echo "✨ Formatting code..."
+ @FORMAT_ONLY=1 ./Scripts/lint.sh
+
+# Clean build artifacts
+clean:
+ @echo "🧹 Cleaning build artifacts..."
+ @swift package clean
+ @rm -rf .build
+
+# Preview documentation with auto-rebuild
+docs-preview:
+ @echo "📖 Starting documentation preview..."
+ @./Scripts/preview-docs.sh Sources/DictionaryCoding/DictionaryCoding.docc
+
+# Build documentation without preview server
+docs-build:
+ @echo "📚 Building documentation..."
+ @./Scripts/preview-docs.sh Sources/DictionaryCoding/DictionaryCoding.docc --no-server --no-watch
+
+# Clean documentation build artifacts
+docs-clean:
+ @echo "🧹 Cleaning documentation artifacts..."
+ @rm -rf .build/docs .build/docs-preview .build/symbol-graphs .build/docc
diff --git a/README.md b/README.md
index 1ed71bc..2bcc270 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,90 @@
-# DictionaryCoding
\ No newline at end of file
+
+
DictionaryCoding
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+A `Codable`-based `Encoder` and `Decoder` that convert `Codable` values to and from
+`[String: Any]` (and `NSDictionary`) — the dictionary equivalent of `JSONEncoder` /
+`JSONDecoder`. Use it when you need a plain in-memory dictionary instead of serialized data:
+`plist`-shaped payloads, WatchConnectivity / app-context dictionaries, `userInfo`, and any API
+that hands you a `[String: Any]`.
+
+## Features
+
+- `DictionaryEncoder` / `DictionaryDecoder` with a `JSONEncoder`-style API.
+- Configurable strategies for dates, data, non-conforming floats, and keys
+ (including `.convertToSnakeCase` / `.convertFromSnakeCase`).
+- A `MissingValueDecodingStrategy` for filling in defaults when keys are absent.
+- `Combine.TopLevelEncoder` / `TopLevelDecoder` conformance where Combine is available.
+
+## Usage
+
+```swift
+import DictionaryCoding
+
+struct Profile: Codable {
+ let firstName: String
+ let loginCount: Int
+}
+
+// Encode to a dictionary
+let profile = Profile(firstName: "Ada", loginCount: 42)
+let dictionary: [String: Any] = try DictionaryEncoder().encode(profile)
+// ["firstName": "Ada", "loginCount": 42]
+
+// Decode from a dictionary
+let decoded = try DictionaryDecoder().decode(Profile.self, from: dictionary)
+```
+
+### Strategies
+
+```swift
+let encoder = DictionaryEncoder()
+encoder.keyEncodingStrategy = .convertToSnakeCase
+encoder.dateEncodingStrategy = .secondsSince1970
+
+let decoder = DictionaryDecoder()
+decoder.keyDecodingStrategy = .convertFromSnakeCase
+decoder.missingValueDecodingStrategy = .useStandardDefault // fall back to type defaults
+```
+
+## Requirements
+
+- Swift 6.3+ / Xcode 26+
+- macOS 15+, iOS 26+, tvOS 26+, watchOS 26+, visionOS 26+
+
+## Installation
+
+Add DictionaryCoding to your `Package.swift` dependencies:
+
+```swift
+dependencies: [
+ .package(url: "https://github.com/brightdigit/DictionaryCoding.git", from: "1.0.0")
+]
+```
+
+Then add it to your target:
+
+```swift
+.target(
+ name: "YourTarget",
+ dependencies: [
+ .product(name: "DictionaryCoding", package: "DictionaryCoding")
+ ]
+)
+```
+
+## License
+
+DictionaryCoding is available under the MIT license. See the [LICENSE](LICENSE) file for more info.
diff --git a/Scripts/header.sh b/Scripts/header.sh
new file mode 100755
index 0000000..c571c18
--- /dev/null
+++ b/Scripts/header.sh
@@ -0,0 +1,104 @@
+#!/bin/bash
+
+# Function to print usage
+usage() {
+ echo "Usage: $0 -d directory -c creator -o company -p package [-y year]"
+ echo " -d directory Directory to read from (including subdirectories)"
+ echo " -c creator Name of the creator"
+ echo " -o company Name of the company with the copyright"
+ echo " -p package Package or library name"
+ echo " -y year Copyright year (optional, defaults to current year)"
+ exit 1
+}
+
+# Get the current year if not provided
+current_year=$(date +"%Y")
+
+# Default values
+year="$current_year"
+
+# Parse arguments
+while getopts ":d:c:o:p:y:" opt; do
+ case $opt in
+ d) directory="$OPTARG" ;;
+ c) creator="$OPTARG" ;;
+ o) company="$OPTARG" ;;
+ p) package="$OPTARG" ;;
+ y) year="$OPTARG" ;;
+ *) usage ;;
+ esac
+done
+
+# Check for mandatory arguments
+if [ -z "$directory" ] || [ -z "$creator" ] || [ -z "$company" ] || [ -z "$package" ]; then
+ usage
+fi
+
+# Define the header template
+header_template="//
+// %s
+// %s
+//
+// Created by %s.
+// Copyright © %s %s.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the \"Software\"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//"
+
+# Loop through each Swift file in the specified directory and subdirectories
+find "$directory" -type f -name "*.swift" | while read -r file; do
+ # Skip files in the Generated directory
+ if [[ "$file" == *"/Generated/"* ]]; then
+ echo "Skipping $file (generated file)"
+ continue
+ fi
+
+ # Check if the first line is the swift-format-ignore indicator
+ first_line=$(head -n 1 "$file")
+ if [[ "$first_line" == "// swift-format-ignore-file" ]]; then
+ echo "Skipping $file due to swift-format-ignore directive."
+ continue
+ fi
+
+ # Create the header with the current filename
+ filename=$(basename "$file")
+ header=$(printf "$header_template" "$filename" "$package" "$creator" "$year" "$company")
+
+ # Remove all consecutive lines at the beginning which start with "// ", contain only whitespace, or only "//"
+ awk '
+ BEGIN { skip = 1 }
+ {
+ if (skip && ($0 ~ /^\/\/ / || $0 ~ /^\/\/$/ || $0 ~ /^$/)) {
+ next
+ }
+ skip = 0
+ print
+ }' "$file" > temp_file
+
+ # Add the header to the cleaned file
+ (echo "$header"; echo; cat temp_file) > "$file"
+
+ # Remove the temporary file
+ rm temp_file
+done
+
+echo "Headers added or files skipped appropriately across all Swift files in the directory and subdirectories."
diff --git a/Scripts/lint.sh b/Scripts/lint.sh
new file mode 100755
index 0000000..85ac845
--- /dev/null
+++ b/Scripts/lint.sh
@@ -0,0 +1,102 @@
+#!/bin/bash
+
+# Remove set -e to allow script to continue running
+# set -e # Exit on any error
+
+ERRORS=0
+
+run_command() {
+ if [ "$LINT_MODE" = "STRICT" ]; then
+ "$@" || ERRORS=$((ERRORS + 1))
+ else
+ "$@"
+ fi
+}
+
+if [ "$LINT_MODE" = "INSTALL" ]; then
+ exit
+fi
+
+echo "LintMode: $LINT_MODE"
+
+# More portable way to get script directory
+if [ -z "$SRCROOT" ]; then
+ SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
+ PACKAGE_DIR="${SCRIPT_DIR}/.."
+else
+ PACKAGE_DIR="${SRCROOT}"
+fi
+
+# Detect if mise is available
+# Check common installation paths for mise
+MISE_PATHS=(
+ "/opt/homebrew/bin/mise"
+ "/usr/local/bin/mise"
+ "$HOME/.local/bin/mise"
+)
+
+MISE_BIN=""
+for mise_path in "${MISE_PATHS[@]}"; do
+ if [ -x "$mise_path" ]; then
+ MISE_BIN="$mise_path"
+ break
+ fi
+done
+
+# Fallback to PATH lookup
+if [ -z "$MISE_BIN" ] && command -v mise &> /dev/null; then
+ MISE_BIN="mise"
+fi
+
+if [ -n "$MISE_BIN" ]; then
+ TOOL_CMD="$MISE_BIN exec --"
+else
+ echo "Error: mise is not installed"
+ echo "Install mise: https://mise.jdx.dev/getting-started.html"
+ echo "Checked paths: ${MISE_PATHS[*]}"
+ exit 1
+fi
+
+if [ "$LINT_MODE" = "NONE" ]; then
+ exit
+elif [ "$LINT_MODE" = "STRICT" ]; then
+ SWIFTFORMAT_OPTIONS="--configuration .swift-format"
+ SWIFTLINT_OPTIONS="--strict"
+else
+ SWIFTFORMAT_OPTIONS="--configuration .swift-format"
+ SWIFTLINT_OPTIONS=""
+fi
+
+pushd $PACKAGE_DIR
+
+# Bootstrap tools (mise will install based on .mise.toml)
+run_command "$MISE_BIN" install
+
+if [ -z "$CI" ]; then
+ run_command $TOOL_CMD swift-format format $SWIFTFORMAT_OPTIONS --recursive --parallel --in-place Sources Tests
+ run_command $TOOL_CMD swiftlint --fix
+fi
+
+if [ -z "$FORMAT_ONLY" ]; then
+ run_command $TOOL_CMD swift-format lint --configuration .swift-format --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests
+ run_command $TOOL_CMD swiftlint lint $SWIFTLINT_OPTIONS
+ # Check for compilation errors
+ run_command swift build --build-tests
+fi
+
+$PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "DictionaryCoding"
+
+if [ -z "$CI" ]; then
+ run_command $TOOL_CMD periphery scan $PERIPHERY_OPTIONS --disable-update-check
+fi
+
+popd
+
+# Exit with error code if any errors occurred
+if [ $ERRORS -gt 0 ]; then
+ echo "Linting completed with $ERRORS error(s)"
+ exit 1
+else
+ echo "Linting completed successfully"
+ exit 0
+fi
diff --git a/Scripts/preview-docs.sh b/Scripts/preview-docs.sh
new file mode 100755
index 0000000..ae74f80
--- /dev/null
+++ b/Scripts/preview-docs.sh
@@ -0,0 +1,363 @@
+#!/bin/bash
+# preview-docs.sh
+# DocC documentation preview with auto-rebuild
+#
+# Copyright (c) 2025 BrightDigit, LLC
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Default configuration
+DEFAULT_PORT=8080
+CATALOG_PATHS=()
+PORT=$DEFAULT_PORT
+NO_WATCH=false
+NO_SERVER=false
+CLEAN=false
+WATCH_PID=""
+SERVER_PID=""
+
+# Build output directories
+SYMBOL_GRAPH_DIR=".build/symbol-graphs"
+
+# Usage information
+usage() {
+ cat < [...] [OPTIONS]
+
+DocC documentation preview with auto-rebuild on file changes.
+Supports multiple catalogs served from a single preview server.
+
+Arguments:
+ Path to .docc catalog directory (one or more)
+ Example: Sources/SundialKit/SundialKit.docc
+
+Options:
+ --port Preview server port (default: 8080)
+ --no-watch Build once, don't watch for changes
+ --no-server Build only, don't start preview server
+ --clean Clean build artifacts before building
+ --help Show this help message
+
+Examples:
+ $(basename "$0") Sources/SundialKit/SundialKit.docc
+ $(basename "$0") Sources/SundialKit/SundialKit.docc Sources/SundialKitCore/SundialKitCore.docc
+ $(basename "$0") Sources/SundialKit/SundialKit.docc --port 8081
+ $(basename "$0") Sources/SundialKit/SundialKit.docc --no-watch
+
+Note: This script requires fswatch for auto-rebuild functionality.
+Install with: brew install fswatch
+EOF
+}
+
+# Parse command-line arguments
+# Collect catalog paths and options
+if [[ $# -eq 0 ]] || [[ "$1" == "--help" ]]; then
+ usage
+ exit 0
+fi
+
+# Parse all arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --port)
+ PORT="$2"
+ shift 2
+ ;;
+ --no-watch)
+ NO_WATCH=true
+ shift
+ ;;
+ --no-server)
+ NO_SERVER=true
+ shift
+ ;;
+ --clean)
+ CLEAN=true
+ shift
+ ;;
+ --help)
+ usage
+ exit 0
+ ;;
+ --*)
+ echo -e "${RED}Error: Unknown option: $1${NC}"
+ usage
+ exit 1
+ ;;
+ *)
+ # Positional argument - catalog path
+ if [ ! -d "$1" ]; then
+ echo -e "${RED}Error: Catalog directory not found: $1${NC}"
+ exit 1
+ fi
+ CATALOG_PATHS+=("$1")
+ shift
+ ;;
+ esac
+done
+
+# Validate at least one catalog path provided
+if [ ${#CATALOG_PATHS[@]} -eq 0 ]; then
+ echo -e "${RED}Error: At least one catalog path is required${NC}"
+ usage
+ exit 1
+fi
+
+# Cleanup function
+cleanup() {
+ echo ""
+ echo -e "${YELLOW}Shutting down...${NC}"
+
+ if [ -n "$WATCH_PID" ]; then
+ kill "$WATCH_PID" 2>/dev/null || true
+ fi
+
+ if [ -n "$SERVER_PID" ]; then
+ kill "$SERVER_PID" 2>/dev/null || true
+ fi
+
+ # Kill any background jobs
+ jobs -p | xargs -r kill 2>/dev/null || true
+
+ echo -e "${GREEN}Cleanup complete${NC}"
+ exit 0
+}
+
+# Register cleanup on exit
+trap cleanup EXIT INT TERM
+
+# Clean build artifacts if requested
+if [ "$CLEAN" = true ]; then
+ echo -e "${BLUE}Cleaning build artifacts...${NC}"
+ rm -rf "$SYMBOL_GRAPH_DIR" .build/docc .build/docs
+fi
+
+# Build and extract symbol graphs
+build_symbols() {
+ echo ""
+ echo -e "${BLUE}========================================${NC}"
+ echo -e "${BLUE}Preparing documentation${NC}"
+ echo -e "${BLUE}========================================${NC}"
+
+ # Step 1: Build all targets
+ echo -e "${YELLOW}Building Swift targets...${NC}"
+ if ! swift build 2>&1 | grep -E "(Building|Build complete|error:|warning:)"; then
+ echo -e "${RED}Error: Swift build failed${NC}"
+ return 1
+ fi
+ echo -e "${GREEN}✓ Build complete${NC}"
+
+ # Step 2: Extract symbol graphs
+ echo -e "${YELLOW}Extracting symbol graphs...${NC}"
+
+ # Use swift package dump-symbol-graph (writes to .build//symbolgraph/)
+ if swift package dump-symbol-graph 2>&1 | grep -q "Emitting symbol graph"; then
+ # Find the symbolgraph directory (architecture-specific)
+ BUILT_SYMBOL_DIR=$(find .build -type d -name "symbolgraph" 2>/dev/null | head -1)
+
+ if [ -n "$BUILT_SYMBOL_DIR" ] && [ -d "$BUILT_SYMBOL_DIR" ]; then
+ # Use the built directory directly instead of copying
+ SYMBOL_GRAPH_DIR="$BUILT_SYMBOL_DIR"
+ echo -e "${GREEN}✓ Symbol graphs extracted to $SYMBOL_GRAPH_DIR${NC}"
+ else
+ echo -e "${YELLOW} Warning: No symbol graphs found. Documentation will only include catalog content.${NC}"
+ SYMBOL_GRAPH_DIR=""
+ fi
+ else
+ echo -e "${YELLOW} Warning: Symbol graph extraction failed. Documentation will only include catalog content.${NC}"
+ SYMBOL_GRAPH_DIR=""
+ fi
+
+ echo -e "${BLUE}========================================${NC}"
+ return 0
+}
+
+# Build a single catalog as DocC archive
+build_catalog() {
+ local catalog_path="$1"
+ local output_dir="$2"
+ local catalog_name=$(basename "$catalog_path" .docc)
+
+ # Ensure output directory exists
+ mkdir -p "$output_dir"
+
+ echo -e "${YELLOW}Building $catalog_name documentation...${NC}"
+
+ # Build docc convert command
+ local docc_cmd=(xcrun docc convert "$catalog_path"
+ --fallback-display-name "$catalog_name"
+ --fallback-bundle-identifier "com.brightdigit.$(echo "$catalog_name" | tr '[:upper:]' '[:lower:]')"
+ --fallback-bundle-version "2.0.0"
+ --transform-for-static-hosting
+ --hosting-base-path "/$catalog_name.doccarchive"
+ --output-path "$output_dir/$catalog_name.doccarchive")
+
+ # Add symbol graphs if available
+ if [ -n "$(ls -A "$SYMBOL_GRAPH_DIR" 2>/dev/null)" ]; then
+ docc_cmd+=(--additional-symbol-graph-dir "$SYMBOL_GRAPH_DIR")
+ fi
+
+ if ! "${docc_cmd[@]}" 2>&1 | grep -v "^$"; then
+ echo -e "${RED}Error: DocC conversion failed for $catalog_name${NC}"
+ return 1
+ fi
+
+ echo -e "${GREEN}✓ $catalog_name.doccarchive created${NC}"
+ return 0
+}
+
+# Build symbols initially
+if ! build_symbols; then
+ echo -e "${RED}Symbol graph generation failed${NC}"
+ exit 1
+fi
+
+# NO-SERVER MODE: Build all catalogs and exit
+if [ "$NO_SERVER" = true ]; then
+ echo ""
+ echo -e "${YELLOW}Converting to DocC archives...${NC}"
+
+ # Build each catalog
+ for catalog_path in "${CATALOG_PATHS[@]}"; do
+ if ! build_catalog "$catalog_path" ".build/docs"; then
+ echo -e "${RED}Failed to build $(basename "$catalog_path")${NC}"
+ exit 1
+ fi
+ done
+
+ echo ""
+ echo -e "${GREEN}✓ All DocC archives created in .build/docs/${NC}"
+ echo -e "${BLUE}========================================${NC}"
+ exit 0
+fi
+
+# PREVIEW MODE: Build all catalogs and serve with Python HTTP server
+echo ""
+echo -e "${YELLOW}Building DocC archives for preview...${NC}"
+
+# Create preview output directory
+PREVIEW_DIR=".build/docs-preview"
+mkdir -p "$PREVIEW_DIR"
+
+# Build each catalog
+for catalog_path in "${CATALOG_PATHS[@]}"; do
+ if ! build_catalog "$catalog_path" "$PREVIEW_DIR"; then
+ echo -e "${RED}Failed to build $(basename "$catalog_path")${NC}"
+ exit 1
+ fi
+done
+
+echo ""
+echo -e "${BLUE}Starting documentation preview server...${NC}"
+
+# Start Python HTTP server in preview directory
+cd "$PREVIEW_DIR"
+python3 -m http.server "$PORT" > /dev/null 2>&1 &
+SERVER_PID=$!
+cd - > /dev/null
+
+# Wait for server to start
+sleep 2
+
+# Check if server is still running
+if ! kill -0 "$SERVER_PID" 2>/dev/null; then
+ echo -e "${RED}Error: Preview server failed to start${NC}"
+ echo -e "${YELLOW}Check if port $PORT is already in use${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}✓ Preview server running${NC}"
+echo ""
+echo -e "${GREEN}========================================${NC}"
+echo -e "${GREEN}📚 Documentation available at:${NC}"
+for catalog_path in "${CATALOG_PATHS[@]}"; do
+ catalog_name=$(basename "$catalog_path" .docc)
+ catalog_name_lower=$(echo "$catalog_name" | tr '[:upper:]' '[:lower:]')
+ echo -e "${BLUE} http://localhost:$PORT/$catalog_name.doccarchive/documentation/$catalog_name_lower${NC}"
+done
+echo -e "${GREEN}========================================${NC}"
+
+# Watch mode for live updates
+if [ "$NO_WATCH" = false ]; then
+ # Check if fswatch is installed
+ if ! command -v fswatch &> /dev/null; then
+ echo ""
+ echo -e "${YELLOW}Note: fswatch not found. Source file watching disabled.${NC}"
+ echo -e "${YELLOW}Install with: brew install fswatch for auto-rebuild on source changes${NC}"
+ echo ""
+ echo -e "${BLUE}Press Ctrl+C to stop the preview server${NC}"
+ wait $SERVER_PID
+ else
+ echo ""
+ echo -e "${BLUE}Watching source files for changes...${NC}"
+ echo -e "${YELLOW}(Press Ctrl+C to stop)${NC}"
+ echo ""
+
+ # Watch Sources, Packages, and .docc directories
+ WATCH_PATHS=()
+
+ if [ -d "Sources" ]; then
+ WATCH_PATHS+=("Sources")
+ fi
+
+ if [ -d "Packages" ]; then
+ WATCH_PATHS+=("Packages")
+ fi
+
+ if [ ${#WATCH_PATHS[@]} -eq 0 ]; then
+ echo -e "${YELLOW}Warning: No Sources or Packages directories found to watch${NC}"
+ echo -e "${BLUE}Press Ctrl+C to stop the preview server${NC}"
+ wait $SERVER_PID
+ else
+ # Use fswatch to monitor Swift and markdown changes
+ fswatch -r \
+ -e ".*" \
+ -i "\\.swift$" \
+ -i "\\.md$" \
+ "${WATCH_PATHS[@]}" | while read -r changed_file; do
+
+ echo ""
+ echo -e "${YELLOW}File changed: $(basename "$changed_file")${NC}"
+ echo -e "${YELLOW}Rebuilding documentation...${NC}"
+
+ # Rebuild Swift and extract new symbols
+ if swift build 2>&1 | grep -E "(Building|Build complete|error:)" && \
+ swift package dump-symbol-graph 2>&1 | grep -q "Emitting symbol graph"; then
+
+ # Rebuild all catalogs
+ for catalog_path in "${CATALOG_PATHS[@]}"; do
+ catalog_name=$(basename "$catalog_path" .docc)
+ if build_catalog "$catalog_path" "$PREVIEW_DIR" > /dev/null 2>&1; then
+ echo -e "${GREEN}✓ $catalog_name updated${NC}"
+ else
+ echo -e "${RED}✗ $catalog_name update failed${NC}"
+ fi
+ done
+
+ echo -e "${BLUE} Refresh your browser to see changes${NC}"
+ else
+ echo -e "${RED}✗ Build failed${NC}"
+ echo -e "${YELLOW} Fix the errors and save to rebuild${NC}"
+ fi
+
+ echo ""
+ echo -e "${BLUE}Watching for changes...${NC}"
+ done &
+ WATCH_PID=$!
+
+ # Wait for server (cleanup trap will handle both processes)
+ wait $SERVER_PID
+ fi
+ fi
+else
+ echo ""
+ echo -e "${BLUE}Press Ctrl+C to stop the preview server${NC}"
+ wait $SERVER_PID
+fi
diff --git a/Sources/DictionaryCoding/DictionaryCoding.docc/DictionaryCoding.md b/Sources/DictionaryCoding/DictionaryCoding.docc/DictionaryCoding.md
new file mode 100644
index 0000000..ac00b36
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryCoding.docc/DictionaryCoding.md
@@ -0,0 +1,35 @@
+# ``DictionaryCoding``
+
+Convert `Codable` values to and from `[String: Any]` dictionaries.
+
+## Overview
+
+DictionaryCoding provides ``DictionaryEncoder`` and ``DictionaryDecoder``, a pair of types that
+mirror `JSONEncoder` / `JSONDecoder` but read and write plain in-memory dictionaries
+(`[String: Any]` and `NSDictionary`) instead of serialized `Data`. This is useful for `plist`-shaped
+payloads, WatchConnectivity / app-context dictionaries, `userInfo`, and any API that hands you a
+`[String: Any]`.
+
+```swift
+struct Profile: Codable {
+ let firstName: String
+ let loginCount: Int
+}
+
+let dictionary = try DictionaryEncoder().encode(Profile(firstName: "Ada", loginCount: 42))
+let profile = try DictionaryDecoder().decode(Profile.self, from: dictionary)
+```
+
+Both types expose configurable strategies for dates, binary data, non-conforming floating-point
+values, and key naming (including snake-case conversion), plus a
+``DictionaryDecoder/MissingValueDecodingStrategy`` for supplying defaults when keys are absent.
+
+## Topics
+
+### Encoding
+
+- ``DictionaryEncoder``
+
+### Decoding
+
+- ``DictionaryDecoder``
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..951b97b
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,2 @@
+ignore:
+ - "Tests"
diff --git a/project.yml b/project.yml
new file mode 100644
index 0000000..57f22fc
--- /dev/null
+++ b/project.yml
@@ -0,0 +1,13 @@
+name: DictionaryCoding
+settings:
+ LINT_MODE: ${LINT_MODE}
+packages:
+ DictionaryCoding:
+ path: .
+aggregateTargets:
+ Lint:
+ buildScripts:
+ - path: Scripts/lint.sh
+ name: Lint
+ basedOnDependencyAnalysis: false
+ schemes: {}
From 3be1380f9c1a0eab5538b93c9312057613984b47 Mon Sep 17 00:00:00 2001
From: leogdion
Date: Fri, 26 Jun 2026 09:00:09 -0400
Subject: [PATCH 3/5] Update Xcode version in workflow configuration
---
.github/workflows/DictionaryCoding.yml | 22 ++++++++++------------
1 file changed, 10 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/DictionaryCoding.yml b/.github/workflows/DictionaryCoding.yml
index 9481cdc..a0dbbc2 100644
--- a/.github/workflows/DictionaryCoding.yml
+++ b/.github/workflows/DictionaryCoding.yml
@@ -91,8 +91,7 @@ jobs:
id: build
uses: brightdigit/swift-build@v1
with:
- scheme: ${{ env.PACKAGE_NAME }}-Package
- xcode: "/Applications/Xcode_26.4.app"
+ xcode: "/Applications/Xcode_26.5.app"
- name: Process Coverage
if: steps.build.outputs.contains-code-coverage == 'true'
uses: sersoft-gmbh/swift-coverage-action@v5
@@ -112,26 +111,26 @@ jobs:
matrix:
include:
- type: macos
- xcode: "/Applications/Xcode_26.4.app"
+ xcode: "/Applications/Xcode_26.5.app"
- type: ios
- xcode: "/Applications/Xcode_26.4.app"
+ xcode: "/Applications/Xcode_26.5.app"
deviceName: "iPhone 17 Pro"
- osVersion: "26.4"
+ osVersion: "26.5"
download-platform: true
- type: watchos
- xcode: "/Applications/Xcode_26.4.app"
+ xcode: "/Applications/Xcode_26.5.app"
deviceName: "Apple Watch Ultra 3 (49mm)"
- osVersion: "26.4"
+ osVersion: "26.5"
download-platform: true
- type: tvos
- xcode: "/Applications/Xcode_26.4.app"
+ xcode: "/Applications/Xcode_26.5.app"
deviceName: "Apple TV"
- osVersion: "26.4"
+ osVersion: "26.5"
download-platform: true
- type: visionos
- xcode: "/Applications/Xcode_26.4.app"
+ xcode: "/Applications/Xcode_26.5.app"
deviceName: "Apple Vision Pro"
- osVersion: "26.4"
+ osVersion: "26.5"
download-platform: true
steps:
- uses: actions/checkout@v6
@@ -139,7 +138,6 @@ jobs:
id: build
uses: brightdigit/swift-build@v1
with:
- scheme: ${{ env.PACKAGE_NAME }}-Package
type: ${{ matrix.type }}
xcode: ${{ matrix.xcode }}
deviceName: ${{ matrix.deviceName }}
From 29d7e83d8e1f4c53c5d89482a874404b9007b381 Mon Sep 17 00:00:00 2001
From: Leo Dion
Date: Fri, 26 Jun 2026 11:14:05 -0400
Subject: [PATCH 4/5] ci(DictionaryCoding): expand build matrix and fix lint
Add Windows, Wasm (wasm/wasm-embedded), and Android build legs plus a
Swift 6.4 nightly Ubuntu leg; drop the jammy Ubuntu leg.
Resolve all swiftlint --strict and swift-format lint findings:
- split oversized files into Type+Feature extensions to satisfy file_length
- one-argument-per-line in test initializers (multiline_arguments)
- remove superfluous line_length disable comments
- move `let` inside case patterns; use plural Parameters: doc sections
- document public single-value encode/decode conformance members
Stamp the correct BrightDigit MIT header across Sources and Tests, and
update lint.sh to run header.sh for both directories.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.github/workflows/DictionaryCoding.yml | 112 ++++++++++++--
Scripts/lint.sh | 3 +-
.../DecodingError+Dictionary.swift | 34 ++++-
.../DictionaryCodingKey.swift | 27 +++-
...yCodingKeyedDecodingContainer+Nested.swift | 113 +++++++++++++++
...ctionaryCodingKeyedDecodingContainer.swift | 110 +++-----------
...ctionaryCodingKeyedEncodingContainer.swift | 27 +++-
.../DictionaryDecoder+Decode.swift | 88 +++++++++++
.../DictionaryDecoder+StandardDefaults.swift | 60 ++++++++
...ictionaryDecoder.KeyDecodingStrategy.swift | 27 +++-
.../DictionaryCoding/DictionaryDecoder.swift | 114 ++++-----------
.../DictionaryDecoderImpl+SingleValue.swift | 43 +++++-
.../DictionaryDecoderImpl+Unbox.swift | 27 +++-
...DictionaryDecoderImpl+UnboxDecodable.swift | 27 +++-
.../DictionaryDecoderImpl+UnboxFloats.swift | 31 +++-
.../DictionaryDecoderImpl+UnboxIntegers.swift | 27 +++-
.../DictionaryDecoderImpl.swift | 27 +++-
.../DictionaryDecoderOptions.swift | 28 +++-
.../DictionaryDecodingStorage.swift | 27 +++-
.../DictionaryEncoder+Encode.swift | 27 +++-
...ictionaryEncoder.KeyEncodingStrategy.swift | 90 ++++++++++++
.../DictionaryCoding/DictionaryEncoder.swift | 86 ++++-------
.../DictionaryEncoderImpl+Box.swift | 128 ++++------------
.../DictionaryEncoderImpl+BoxEncodable.swift | 118 +++++++++++++++
.../DictionaryEncoderImpl+SingleValue.swift | 137 ++++++++++++++++++
.../DictionaryEncoderImpl.swift | 118 +++------------
.../DictionaryEncoderOptions.swift | 27 +++-
.../DictionaryEncodingStorage.swift | 27 +++-
.../DictionaryReferencingEncoder.swift | 31 +++-
...onaryUnkeyedDecodingContainer+Nested.swift | 27 +++-
...naryUnkeyedDecodingContainer+Scalars.swift | 122 +++-------------
...onaryUnkeyedDecodingContainer+String.swift | 27 +++-
...yedDecodingContainer+UnsignedScalars.swift | 128 ++++++++++++++++
.../DictionaryUnkeyedDecodingContainer.swift | 27 +++-
.../DictionaryUnkeyedEncodingContainer.swift | 27 +++-
.../EncodingError+Dictionary.swift | 32 +++-
Sources/DictionaryCoding/NSNumber+Bool.swift | 28 +++-
.../DictionaryCodingArrayAndKeyTests.swift | 27 +++-
.../DictionaryCodingArrayTests.swift | 32 +++-
.../DictionaryCodingDateDataTests.swift | 27 +++-
.../DictionaryCodingErrorTests.swift | 27 +++-
.../DictionaryCodingFloatStrategyTests.swift | 27 +++-
.../DictionaryCodingNestedTests.swift | 27 +++-
.../DictionaryCodingRoundTripTests.swift | 27 +++-
.../DictionaryCodingScalarTests.swift | 54 +++++--
.../DictionaryCodingSpecialTypeTests.swift | 27 +++-
.../DictionaryCodingStrategyErrorTests.swift | 27 +++-
.../DictionaryCodingSuperDecoderTests.swift | 27 +++-
.../DictionaryCodingTestKey.swift | 27 +++-
.../DictionaryDecoderPlatformTests.swift | 27 +++-
.../DictionaryDecoderTests.swift | 27 +++-
.../DictionaryEncoderTests.swift | 27 +++-
.../SuperDecoderBase.swift | 27 +++-
.../SuperDecoderChild.swift | 27 +++-
54 files changed, 1977 insertions(+), 673 deletions(-)
create mode 100644 Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer+Nested.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoder+Decode.swift
create mode 100644 Sources/DictionaryCoding/DictionaryDecoder+StandardDefaults.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoder.KeyEncodingStrategy.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoderImpl+BoxEncodable.swift
create mode 100644 Sources/DictionaryCoding/DictionaryEncoderImpl+SingleValue.swift
create mode 100644 Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+UnsignedScalars.swift
diff --git a/.github/workflows/DictionaryCoding.yml b/.github/workflows/DictionaryCoding.yml
index a0dbbc2..1ddb916 100644
--- a/.github/workflows/DictionaryCoding.yml
+++ b/.github/workflows/DictionaryCoding.yml
@@ -34,32 +34,77 @@ jobs:
outputs:
full-matrix: ${{ steps.set-matrix.outputs.full-matrix }}
ubuntu-os: ${{ steps.set-matrix.outputs.ubuntu-os }}
+ ubuntu-swift: ${{ steps.set-matrix.outputs.ubuntu-swift }}
+ ubuntu-type: ${{ steps.set-matrix.outputs.ubuntu-type }}
steps:
- name: Determine build matrix
id: set-matrix
run: |
# DictionaryCoding's Package.swift declares swift-tools-version 6.3, so every leg
- # must use a Swift 6.3 toolchain — older toolchains cannot parse the manifest.
- if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.event_name }}" == "pull_request" ]]; then
- echo "full-matrix=true" >> "$GITHUB_OUTPUT"
- echo 'ubuntu-os=["noble","jammy"]' >> "$GITHUB_OUTPUT"
+ # must use a Swift 6.3-or-newer toolchain — older toolchains cannot parse the manifest.
+ FULL=false
+ REF="${{ github.ref }}"
+ EVENT="${{ github.event_name }}"
+ BASE_REF="${{ github.base_ref }}"
+
+ # Full matrix on main, on semver branches (v1.0.0, 1.2.3-alpha.1, etc.),
+ # and on PRs targeting either.
+ if [[ "$REF" == "refs/heads/main" ]]; then
+ FULL=true
+ elif [[ "$REF" =~ ^refs/heads/v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ FULL=true
+ elif [[ "$EVENT" == "pull_request" ]]; then
+ if [[ "$BASE_REF" == "main" || "$BASE_REF" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ FULL=true
+ fi
+ fi
+
+ if [[ "$FULL" == "true" ]]; then
+ {
+ echo "full-matrix=true"
+ echo 'ubuntu-os=["noble"]'
+ echo 'ubuntu-swift=[{"version":"6.3"},{"version":"6.4","nightly":true}]'
+ echo 'ubuntu-type=["","wasm","wasm-embedded"]'
+ } >> "$GITHUB_OUTPUT"
else
- echo "full-matrix=false" >> "$GITHUB_OUTPUT"
- echo 'ubuntu-os=["noble"]' >> "$GITHUB_OUTPUT"
+ {
+ echo "full-matrix=false"
+ echo 'ubuntu-os=["noble"]'
+ echo 'ubuntu-swift=[{"version":"6.3"}]'
+ echo 'ubuntu-type=[""]'
+ } >> "$GITHUB_OUTPUT"
fi
+ echo "Full matrix: $FULL (ref=$REF, event=$EVENT, base_ref=$BASE_REF)"
build-ubuntu:
name: Build on Ubuntu
needs: [configure]
runs-on: ubuntu-latest
- container: swift:6.3-${{ matrix.os }}
+ container: ${{ matrix.swift.nightly && format('swiftlang/swift:nightly-{0}-{1}', matrix.swift.version, matrix.os) || format('swift:{0}-{1}', matrix.swift.version, matrix.os) }}
strategy:
+ fail-fast: false
matrix:
os: ${{ fromJSON(needs.configure.outputs.ubuntu-os) }}
+ swift: ${{ fromJSON(needs.configure.outputs.ubuntu-swift) }}
+ type: ${{ fromJSON(needs.configure.outputs.ubuntu-type) }}
+ exclude:
+ # Nightly toolchains skip the wasm legs — nightly WASI SDKs may be unavailable.
+ - swift: { version: "6.4", nightly: true }
+ type: "wasm"
+ - swift: { version: "6.4", nightly: true }
+ type: "wasm-embedded"
steps:
- uses: actions/checkout@v6
- uses: brightdigit/swift-build@v1
id: build
+ with:
+ type: ${{ matrix.type }}
+ wasmtime-version: "40.0.2"
+ wasm-swift-flags: >-
+ -Xcc -D_WASI_EMULATED_SIGNAL
+ -Xcc -D_WASI_EMULATED_MMAN
+ -Xlinker -lwasi-emulated-signal
+ -Xlinker -lwasi-emulated-mman
- name: Install curl
if: steps.build.outputs.contains-code-coverage == 'true'
run: |
@@ -75,11 +120,60 @@ jobs:
uses: codecov/codecov-action@v6
with:
fail_ci_if_error: true
- flags: swift-6.3,ubuntu
+ flags: swift-${{ matrix.swift.version }}-${{ matrix.os }}${{ matrix.swift.nightly && '-nightly' || '' }}
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }}
+ build-windows:
+ name: Build on Windows
+ needs: [configure]
+ runs-on: windows-2025
+ if: ${{ needs.configure.outputs.full-matrix == 'true' && (github.event_name == 'pull_request' || !contains(github.event.head_commit.message, 'ci skip')) }}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: brightdigit/swift-build@v1
+ id: build
+ with:
+ windows-swift-version: swift-6.3-release
+ windows-swift-build: 6.3-RELEASE
+ - name: Upload coverage to Codecov
+ if: steps.build.outputs.contains-code-coverage == 'true'
+ uses: codecov/codecov-action@v6
+ with:
+ fail_ci_if_error: true
+ flags: swift-6.3,windows
+ verbose: true
+ token: ${{ secrets.CODECOV_TOKEN }}
+ os: windows
+ swift_project: DictionaryCoding
+
+ build-android:
+ name: Build on Android
+ needs: [configure]
+ runs-on: ubuntu-latest
+ if: ${{ needs.configure.outputs.full-matrix == 'true' && (github.event_name == 'pull_request' || !contains(github.event.head_commit.message, 'ci skip')) }}
+ steps:
+ - uses: actions/checkout@v6
+ - name: Free disk space
+ uses: jlumbroso/free-disk-space@v1.3.1
+ with:
+ tool-cache: false
+ android: false
+ dotnet: true
+ haskell: true
+ large-packages: true
+ docker-images: true
+ swap-storage: true
+ - uses: brightdigit/swift-build@v1
+ with:
+ type: android
+ android-swift-version: "6.3"
+ android-api-level: 34
+ android-run-tests: true
+ # Note: Code coverage is not supported on Android builds
+ # The Swift Android SDK does not include LLVM coverage tools (llvm-profdata, llvm-cov)
+
build-macos:
name: Build on macOS
needs: [configure]
@@ -159,7 +253,7 @@ jobs:
# past skipped (not failed) dependencies.
if: ${{ !cancelled() && !failure() && (github.event_name == 'pull_request' || !contains(github.event.head_commit.message, 'ci skip')) }}
runs-on: ubuntu-latest
- needs: [build-ubuntu, build-macos, build-macos-full]
+ needs: [build-ubuntu, build-macos, build-macos-full, build-windows, build-android]
steps:
- uses: actions/checkout@v6
- uses: jdx/mise-action@v4
diff --git a/Scripts/lint.sh b/Scripts/lint.sh
index 85ac845..4971af3 100755
--- a/Scripts/lint.sh
+++ b/Scripts/lint.sh
@@ -84,7 +84,8 @@ if [ -z "$FORMAT_ONLY" ]; then
run_command swift build --build-tests
fi
-$PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "DictionaryCoding"
+$PACKAGE_DIR/Scripts/header.sh -d "$PACKAGE_DIR/Sources" -c "Leo Dion" -o "BrightDigit" -p "DictionaryCoding"
+$PACKAGE_DIR/Scripts/header.sh -d "$PACKAGE_DIR/Tests" -c "Leo Dion" -o "BrightDigit" -p "DictionaryCoding"
if [ -z "$CI" ]; then
run_command $TOOL_CMD periphery scan $PERIPHERY_OPTIONS --disable-update-check
diff --git a/Sources/DictionaryCoding/DecodingError+Dictionary.swift b/Sources/DictionaryCoding/DecodingError+Dictionary.swift
index 1bbda66..f9aefc6 100644
--- a/Sources/DictionaryCoding/DecodingError+Dictionary.swift
+++ b/Sources/DictionaryCoding/DecodingError+Dictionary.swift
@@ -1,9 +1,30 @@
//
// DecodingError+Dictionary.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -11,9 +32,10 @@ import Foundation
extension DecodingError {
/// Returns a `.typeMismatch` error describing the expected type.
///
- /// - parameter path: The path of `CodingKey`s taken to decode a value of this type.
- /// - parameter expectation: The type expected to be encountered.
- /// - parameter reality: The value that was encountered instead of the expected type.
+ /// - Parameters:
+ /// - path: The path of `CodingKey`s taken to decode a value of this type.
+ /// - expectation: The type expected to be encountered.
+ /// - reality: The value that was encountered instead of the expected type.
/// - returns: A `DecodingError` with the appropriate path and debug description.
internal static func typeMismatch(
at path: [CodingKey],
diff --git a/Sources/DictionaryCoding/DictionaryCodingKey.swift b/Sources/DictionaryCoding/DictionaryCodingKey.swift
index d84fbf9..a563c2e 100644
--- a/Sources/DictionaryCoding/DictionaryCodingKey.swift
+++ b/Sources/DictionaryCoding/DictionaryCodingKey.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingKey.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer+Nested.swift b/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer+Nested.swift
new file mode 100644
index 0000000..2eafff9
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer+Nested.swift
@@ -0,0 +1,113 @@
+//
+// DictionaryCodingKeyedDecodingContainer+Nested.swift
+// DictionaryCoding
+//
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+// MARK: - Nested containers and superDecoder
+extension DictionaryCodingKeyedDecodingContainer {
+ internal func nestedContainer(
+ keyedBy type: NestedKey.Type,
+ forKey key: Key
+ ) throws -> KeyedDecodingContainer {
+ self.decoder.codingPath.append(key)
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard let value = self.container[key.stringValue] else {
+ throw DecodingError.keyNotFound(
+ key,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get \(KeyedDecodingContainer.self)"
+ + " -- no value found for key \(errorDescription(of: key))"
+ )
+ )
+ }
+
+ guard let dictionary = value as? [String: Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: [String: Any].self, reality: value
+ )
+ }
+
+ let container = DictionaryCodingKeyedDecodingContainer(
+ referencing: self.decoder, wrapping: dictionary
+ )
+ return KeyedDecodingContainer(container)
+ }
+
+ internal func nestedUnkeyedContainer(
+ forKey key: Key
+ ) throws -> UnkeyedDecodingContainer {
+ self.decoder.codingPath.append(key)
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard let value = self.container[key.stringValue] else {
+ throw DecodingError.keyNotFound(
+ key,
+ DecodingError.Context(
+ codingPath: self.codingPath,
+ debugDescription:
+ "Cannot get UnkeyedDecodingContainer"
+ + " -- no value found for key \(errorDescription(of: key))"
+ )
+ )
+ }
+
+ guard let array = value as? [Any] else {
+ throw DecodingError.typeMismatch(
+ at: self.codingPath, expectation: [Any].self, reality: value
+ )
+ }
+
+ return DictionaryUnkeyedDecodingContainer(
+ referencing: self.decoder, wrapping: array
+ )
+ }
+
+ internal func superDecoder() throws -> Decoder {
+ try makeSuperDecoder(forKey: DictionaryCodingKey.super)
+ }
+
+ internal func superDecoder(forKey key: Key) throws -> Decoder {
+ try makeSuperDecoder(forKey: key)
+ }
+
+ private func makeSuperDecoder(forKey key: CodingKey) throws -> Decoder {
+ self.decoder.codingPath.append(key)
+ defer { self.decoder.codingPath.removeLast() }
+
+ let value: Any = self.container[key.stringValue] ?? NSNull()
+ return DictionaryDecoderImpl(
+ referencing: value,
+ at: self.decoder.codingPath,
+ options: self.decoder.options
+ )
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift b/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift
index 81e121e..4f55261 100644
--- a/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift
+++ b/Sources/DictionaryCoding/DictionaryCodingKeyedDecodingContainer.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingKeyedDecodingContainer.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -145,86 +166,3 @@ internal struct DictionaryCodingKeyedDecodingContainer:
return value
}
}
-
-// MARK: - Nested containers and superDecoder
-extension DictionaryCodingKeyedDecodingContainer {
- internal func nestedContainer(
- keyedBy type: NestedKey.Type,
- forKey key: Key
- ) throws -> KeyedDecodingContainer {
- self.decoder.codingPath.append(key)
- defer { self.decoder.codingPath.removeLast() }
-
- guard let value = self.container[key.stringValue] else {
- throw DecodingError.keyNotFound(
- key,
- DecodingError.Context(
- codingPath: self.codingPath,
- debugDescription:
- "Cannot get \(KeyedDecodingContainer.self)"
- + " -- no value found for key \(errorDescription(of: key))"
- )
- )
- }
-
- guard let dictionary = value as? [String: Any] else {
- throw DecodingError.typeMismatch(
- at: self.codingPath, expectation: [String: Any].self, reality: value
- )
- }
-
- let container = DictionaryCodingKeyedDecodingContainer(
- referencing: self.decoder, wrapping: dictionary
- )
- return KeyedDecodingContainer(container)
- }
-
- internal func nestedUnkeyedContainer(
- forKey key: Key
- ) throws -> UnkeyedDecodingContainer {
- self.decoder.codingPath.append(key)
- defer { self.decoder.codingPath.removeLast() }
-
- guard let value = self.container[key.stringValue] else {
- throw DecodingError.keyNotFound(
- key,
- DecodingError.Context(
- codingPath: self.codingPath,
- debugDescription:
- "Cannot get UnkeyedDecodingContainer"
- + " -- no value found for key \(errorDescription(of: key))"
- )
- )
- }
-
- guard let array = value as? [Any] else {
- throw DecodingError.typeMismatch(
- at: self.codingPath, expectation: [Any].self, reality: value
- )
- }
-
- return DictionaryUnkeyedDecodingContainer(
- referencing: self.decoder, wrapping: array
- )
- }
-
- internal func superDecoder() throws -> Decoder {
- try makeSuperDecoder(forKey: DictionaryCodingKey.super)
- }
-
- internal func superDecoder(forKey key: Key) throws -> Decoder {
- try makeSuperDecoder(forKey: key)
- }
-
- private func makeSuperDecoder(forKey key: CodingKey) throws -> Decoder {
- self.decoder.codingPath.append(key)
- defer { self.decoder.codingPath.removeLast() }
-
- let value: Any = self.container[key.stringValue] ?? NSNull()
- return DictionaryDecoderImpl(
- referencing: value,
- at: self.decoder.codingPath,
- options: self.decoder.options
- )
- }
-}
diff --git a/Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift b/Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift
index 3d289d3..10de178 100644
--- a/Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift
+++ b/Sources/DictionaryCoding/DictionaryCodingKeyedEncodingContainer.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingKeyedEncodingContainer.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryDecoder+Decode.swift b/Sources/DictionaryCoding/DictionaryDecoder+Decode.swift
new file mode 100644
index 0000000..65ea921
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoder+Decode.swift
@@ -0,0 +1,88 @@
+//
+// DictionaryDecoder+Decode.swift
+// DictionaryCoding
+//
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+extension DictionaryDecoder {
+ /// Decodes a top-level value of the given type from the given
+ /// Dictionary representation.
+ ///
+ /// - Parameters:
+ /// - type: The type of the value to decode.
+ /// - dictionary: The data to decode from.
+ /// - returns: A value of the requested type.
+ /// - throws: `DecodingError.dataCorrupted` if values requested from the payload
+ /// are corrupted, or if the given data is not valid Dictionary.
+ /// - throws: An error if any value throws an error during decoding.
+ open func decode(
+ _ type: T.Type,
+ from dictionary: NSDictionary
+ ) throws -> T {
+ let decoder = DictionaryDecoderImpl(referencing: dictionary, options: self.options)
+ guard let value = try decoder.unbox(dictionary, as: type) else {
+ throw DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: [],
+ debugDescription: "The given data did not contain a top-level value."
+ )
+ )
+ }
+
+ return value
+ }
+
+ /// Decodes a top-level value of the given type from the given
+ /// Dictionary representation.
+ ///
+ /// - Parameters:
+ /// - type: The type of the value to decode.
+ /// - dictionary: The data to decode from.
+ /// - returns: A value of the requested type.
+ /// - throws: `DecodingError.dataCorrupted` if values requested from the payload
+ /// are corrupted, or if the given data is not valid Dictionary.
+ /// - throws: An error if any value throws an error during decoding.
+ open func decode(
+ _ type: T.Type,
+ from dictionary: [String: Any]
+ ) throws -> T {
+ let decoder = DictionaryDecoderImpl(referencing: dictionary, options: self.options)
+ guard let value = try decoder.unbox(dictionary, as: type) else {
+ throw DecodingError.valueNotFound(
+ type,
+ DecodingError.Context(
+ codingPath: [],
+ debugDescription: "The given data did not contain a top-level value."
+ )
+ )
+ }
+
+ return value
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoder+StandardDefaults.swift b/Sources/DictionaryCoding/DictionaryDecoder+StandardDefaults.swift
new file mode 100644
index 0000000..b1bf2f2
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryDecoder+StandardDefaults.swift
@@ -0,0 +1,60 @@
+//
+// DictionaryDecoder+StandardDefaults.swift
+// DictionaryCoding
+//
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+extension DictionaryDecoder {
+ private static var standardDefaults: [String: Any] {
+ [
+ "Int": 0,
+ "Int8": Int8(0),
+ "Int16": Int16(0),
+ "Int32": Int32(0),
+ "Int64": Int64(0),
+ "UInt": UInt(0),
+ "UInt8": UInt8(0),
+ "UInt16": UInt16(0),
+ "UInt32": UInt32(0),
+ "UInt64": UInt64(0),
+ "Float": Float(0.0),
+ "Double": 0.0,
+ "String": "",
+ "Bool": false,
+ "Date": Date(timeIntervalSinceReferenceDate: 0),
+ "Data": Data(),
+ ]
+ }
+
+ internal var resolvedMissingValueStrategy: MissingValueDecodingStrategy {
+ guard case .useStandardDefault = missingValueDecodingStrategy else {
+ return missingValueDecodingStrategy
+ }
+ return .useDefault(defaults: Self.standardDefaults)
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift b/Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift
index 29a5e6a..826ead0 100644
--- a/Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoder.KeyDecodingStrategy.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoder.KeyDecodingStrategy.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryDecoder.swift b/Sources/DictionaryCoding/DictionaryDecoder.swift
index 7a41af3..07315b1 100644
--- a/Sources/DictionaryCoding/DictionaryDecoder.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoder.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoder.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -143,98 +164,13 @@ open class DictionaryDecoder {
/// Initializes `self` with default strategies.
public init() {}
-
- // MARK: - Instance Methods
-
- /// Decodes a top-level value of the given type from the given
- /// Dictionary representation.
- ///
- /// - parameter type: The type of the value to decode.
- /// - parameter dictionary: The data to decode from.
- /// - returns: A value of the requested type.
- /// - throws: `DecodingError.dataCorrupted` if values requested from the payload
- /// are corrupted, or if the given data is not valid Dictionary.
- /// - throws: An error if any value throws an error during decoding.
- open func decode(
- _ type: T.Type,
- from dictionary: NSDictionary
- ) throws -> T {
- let decoder = DictionaryDecoderImpl(referencing: dictionary, options: self.options)
- guard let value = try decoder.unbox(dictionary, as: type) else {
- throw DecodingError.valueNotFound(
- type,
- DecodingError.Context(
- codingPath: [],
- debugDescription: "The given data did not contain a top-level value."
- )
- )
- }
-
- return value
- }
-
- /// Decodes a top-level value of the given type from the given
- /// Dictionary representation.
- ///
- /// - parameter type: The type of the value to decode.
- /// - parameter dictionary: The data to decode from.
- /// - returns: A value of the requested type.
- /// - throws: `DecodingError.dataCorrupted` if values requested from the payload
- /// are corrupted, or if the given data is not valid Dictionary.
- /// - throws: An error if any value throws an error during decoding.
- open func decode(
- _ type: T.Type,
- from dictionary: [String: Any]
- ) throws -> T {
- let decoder = DictionaryDecoderImpl(referencing: dictionary, options: self.options)
- guard let value = try decoder.unbox(dictionary, as: type) else {
- throw DecodingError.valueNotFound(
- type,
- DecodingError.Context(
- codingPath: [],
- debugDescription: "The given data did not contain a top-level value."
- )
- )
- }
-
- return value
- }
-}
-
-extension DictionaryDecoder {
- private static var standardDefaults: [String: Any] {
- [
- "Int": 0,
- "Int8": Int8(0),
- "Int16": Int16(0),
- "Int32": Int32(0),
- "Int64": Int64(0),
- "UInt": UInt(0),
- "UInt8": UInt8(0),
- "UInt16": UInt16(0),
- "UInt32": UInt32(0),
- "UInt64": UInt64(0),
- "Float": Float(0.0),
- "Double": 0.0,
- "String": "",
- "Bool": false,
- "Date": Date(timeIntervalSinceReferenceDate: 0),
- "Data": Data(),
- ]
- }
-
- private var resolvedMissingValueStrategy: MissingValueDecodingStrategy {
- guard case .useStandardDefault = missingValueDecodingStrategy else {
- return missingValueDecodingStrategy
- }
- return .useDefault(defaults: Self.standardDefaults)
- }
}
#if canImport(Combine)
import Combine
extension DictionaryDecoder: TopLevelDecoder {
+ /// The type this decoder accepts when decoding a value.
public typealias Input = [String: Any]
}
#endif
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift
index 540e6c3..4425479 100644
--- a/Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+SingleValue.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderImpl+SingleValue.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -32,10 +53,12 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
)
}
+ /// Returns whether the value stored in the container is null.
public func decodeNil() -> Bool {
self.storage.topContainer is NSNull
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Bool.Type) throws -> Bool {
try expectNonNull(Bool.self)
guard let value = try self.unbox(self.storage.topContainer, as: Bool.self) else {
@@ -44,6 +67,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Int.Type) throws -> Int {
try expectNonNull(Int.self)
guard let value = try self.unbox(self.storage.topContainer, as: Int.self) else {
@@ -52,6 +76,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Int8.Type) throws -> Int8 {
try expectNonNull(Int8.self)
guard let value = try self.unbox(self.storage.topContainer, as: Int8.self) else {
@@ -60,6 +85,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Int16.Type) throws -> Int16 {
try expectNonNull(Int16.self)
guard let value = try self.unbox(self.storage.topContainer, as: Int16.self) else {
@@ -68,6 +94,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Int32.Type) throws -> Int32 {
try expectNonNull(Int32.self)
guard let value = try self.unbox(self.storage.topContainer, as: Int32.self) else {
@@ -76,6 +103,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Int64.Type) throws -> Int64 {
try expectNonNull(Int64.self)
guard let value = try self.unbox(self.storage.topContainer, as: Int64.self) else {
@@ -84,6 +112,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: UInt.Type) throws -> UInt {
try expectNonNull(UInt.self)
guard let value = try self.unbox(self.storage.topContainer, as: UInt.self) else {
@@ -92,6 +121,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: UInt8.Type) throws -> UInt8 {
try expectNonNull(UInt8.self)
guard let value = try self.unbox(self.storage.topContainer, as: UInt8.self) else {
@@ -100,6 +130,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: UInt16.Type) throws -> UInt16 {
try expectNonNull(UInt16.self)
guard let value = try self.unbox(self.storage.topContainer, as: UInt16.self) else {
@@ -108,6 +139,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: UInt32.Type) throws -> UInt32 {
try expectNonNull(UInt32.self)
guard let value = try self.unbox(self.storage.topContainer, as: UInt32.self) else {
@@ -116,6 +148,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: UInt64.Type) throws -> UInt64 {
try expectNonNull(UInt64.self)
guard let value = try self.unbox(self.storage.topContainer, as: UInt64.self) else {
@@ -124,6 +157,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Float.Type) throws -> Float {
try expectNonNull(Float.self)
guard let value = try self.unbox(self.storage.topContainer, as: Float.self) else {
@@ -132,6 +166,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: Double.Type) throws -> Double {
try expectNonNull(Double.self)
guard let value = try self.unbox(self.storage.topContainer, as: Double.self) else {
@@ -140,6 +175,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: String.Type) throws -> String {
try expectNonNull(String.self)
guard let value = try self.unbox(self.storage.topContainer, as: String.self) else {
@@ -148,6 +184,7 @@ extension DictionaryDecoderImpl: SingleValueDecodingContainer {
return value
}
+ /// Decodes a single value of the given type.
public func decode(_ type: T.Type) throws -> T {
try expectNonNull(type)
guard let value = try self.unbox(self.storage.topContainer, as: type) else {
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift
index 8641986..e37dd32 100644
--- a/Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+Unbox.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderImpl+Unbox.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift
index 90eceff..86863aa 100644
--- a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxDecodable.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderImpl+UnboxDecodable.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift
index ea366de..c5e40a7 100644
--- a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxFloats.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderImpl+UnboxFloats.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -47,7 +68,7 @@ extension DictionaryDecoderImpl {
private func unboxFloatFromString(_ value: Any, as type: Float.Type) -> Float? {
guard let string = value as? String,
- case let .convertFromString(posInfString, negInfString, nanString) =
+ case .convertFromString(let posInfString, let negInfString, let nanString) =
self.options.nonConformingFloatDecodingStrategy
else {
return nil
@@ -83,7 +104,7 @@ extension DictionaryDecoderImpl {
private func unboxDoubleFromString(_ value: Any) -> Double? {
guard let string = value as? String,
- case let .convertFromString(posInfString, negInfString, nanString) =
+ case .convertFromString(let posInfString, let negInfString, let nanString) =
self.options.nonConformingFloatDecodingStrategy
else {
return nil
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift
index 2a5164f..6dae5df 100644
--- a/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl+UnboxIntegers.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderImpl+UnboxIntegers.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryDecoderImpl.swift b/Sources/DictionaryCoding/DictionaryDecoderImpl.swift
index 9f5bb12..d4d48ad 100644
--- a/Sources/DictionaryCoding/DictionaryDecoderImpl.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoderImpl.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderImpl.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryDecoderOptions.swift b/Sources/DictionaryCoding/DictionaryDecoderOptions.swift
index fe521a8..f9634fc 100644
--- a/Sources/DictionaryCoding/DictionaryDecoderOptions.swift
+++ b/Sources/DictionaryCoding/DictionaryDecoderOptions.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderOptions.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -12,7 +33,6 @@ import Foundation
internal struct DictionaryDecoderOptions {
// MARK: - Instance Properties
- // swiftlint:disable:next line_length
internal let missingValueDecodingStrategy: DictionaryDecoder.MissingValueDecodingStrategy
internal let dateDecodingStrategy: DictionaryDecoder.DateDecodingStrategy
internal let dataDecodingStrategy: DictionaryDecoder.DataDecodingStrategy
diff --git a/Sources/DictionaryCoding/DictionaryDecodingStorage.swift b/Sources/DictionaryCoding/DictionaryDecodingStorage.swift
index a5b88fb..e4f6313 100644
--- a/Sources/DictionaryCoding/DictionaryDecodingStorage.swift
+++ b/Sources/DictionaryCoding/DictionaryDecodingStorage.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecodingStorage.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryEncoder+Encode.swift b/Sources/DictionaryCoding/DictionaryEncoder+Encode.swift
index 5c7fe3c..0d1b920 100644
--- a/Sources/DictionaryCoding/DictionaryEncoder+Encode.swift
+++ b/Sources/DictionaryCoding/DictionaryEncoder+Encode.swift
@@ -1,9 +1,30 @@
//
// DictionaryEncoder+Encode.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryEncoder.KeyEncodingStrategy.swift b/Sources/DictionaryCoding/DictionaryEncoder.KeyEncodingStrategy.swift
new file mode 100644
index 0000000..cf76aef
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncoder.KeyEncodingStrategy.swift
@@ -0,0 +1,90 @@
+//
+// DictionaryEncoder.KeyEncodingStrategy.swift
+// DictionaryCoding
+//
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+extension DictionaryEncoder.KeyEncodingStrategy {
+ internal static func convertToSnakeCase(_ stringKey: String) -> String {
+ guard !stringKey.isEmpty else {
+ return stringKey
+ }
+
+ var words: [Range] = []
+ var wordStart = stringKey.startIndex
+ var searchRange = stringKey.index(after: wordStart)..,
+ searchRange: inout Range,
+ wordStart: inout String.Index,
+ words: inout [Range]
+ ) {
+ guard
+ let lowerCaseRange = stringKey.rangeOfCharacter(
+ from: CharacterSet.lowercaseLetters,
+ options: [],
+ range: searchRange
+ )
+ else {
+ wordStart = searchRange.lowerBound
+ searchRange = searchRange.upperBound.. CodingKey)
-
- internal static func convertToSnakeCase(_ stringKey: String) -> String {
- guard !stringKey.isEmpty else {
- return stringKey
- }
-
- var words: [Range] = []
- var wordStart = stringKey.startIndex
- var searchRange = stringKey.index(after: wordStart)..,
- searchRange: inout Range,
- wordStart: inout String.Index,
- words: inout [Range]
- ) {
- guard
- let lowerCaseRange = stringKey.rangeOfCharacter(
- from: CharacterSet.lowercaseLetters,
- options: [],
- range: searchRange
- )
- else {
- wordStart = searchRange.lowerBound
- searchRange = searchRange.upperBound.. NSObject {
guard
- case let .convertToString(
- positiveInfinity: posInfString,
- negativeInfinity: negInfString,
- nan: nanString
+ case .convertToString(
+ positiveInfinity: let posInfString,
+ negativeInfinity: let negInfString,
+ nan: let nanString
) = self.options.nonConformingFloatEncodingStrategy
else {
throw EncodingError.invalidFloatingPointValue(float, at: codingPath)
@@ -61,10 +82,10 @@ extension DictionaryEncoderImpl {
private func boxNonConformingDouble(_ double: Double) throws -> NSObject {
guard
- case let .convertToString(
- positiveInfinity: posInfString,
- negativeInfinity: negInfString,
- nan: nanString
+ case .convertToString(
+ positiveInfinity: let posInfString,
+ negativeInfinity: let negInfString,
+ nan: let nanString
) = self.options.nonConformingFloatEncodingStrategy
else {
throw EncodingError.invalidFloatingPointValue(double, at: codingPath)
@@ -174,89 +195,4 @@ extension DictionaryEncoderImpl {
return self.storage.popContainer()
}
-
- internal func box(_ value: T) throws -> NSObject {
- try self.boxEncodable(value) ?? NSDictionary()
- }
-
- // This method is called "boxEncodable" instead of "box" to disambiguate it from the
- // overloads. Because the return type here is different from all of the "box"
- // overloads (and is more general), any "box" calls in here would call back
- // into "box" recursively instead of calling the appropriate overload, which
- // is not what we want.
- internal func boxEncodable(_ value: T) throws -> NSObject? {
- if let result = try boxSpecialType(value) {
- return result
- }
-
- return try boxGenericEncodable(value)
- }
-
- private func boxSpecialType(_ value: T) throws -> NSObject? {
- if T.self == Date.self || T.self == NSDate.self {
- return try boxAsDate(value)
- } else if T.self == Data.self || T.self == NSData.self {
- return try boxAsData(value)
- } else if T.self == URL.self || T.self == NSURL.self {
- return boxAsURL(value)
- } else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
- return boxAsDecimal(value)
- }
- return nil
- }
-
- private func boxAsDate(_ value: T) throws -> NSObject? {
- guard let date = value as? Date else {
- return nil
- }
- return try self.box(date)
- }
-
- private func boxAsData(_ value: T) throws -> NSObject? {
- guard let data = value as? Data else {
- return nil
- }
- return try self.box(data)
- }
-
- private func boxAsURL(_ value: T) -> NSObject? {
- guard let url = value as? URL else {
- return nil
- }
- return self.box(url.absoluteString)
- }
-
- private func boxAsDecimal(_ value: T) -> NSObject? {
- if let decimal = value as? NSDecimalNumber {
- // DictionarySerialization can natively handle NSDecimalNumber.
- return decimal
- }
- // On Linux, Swift Decimal doesn't auto-bridge to NSDecimalNumber.
- if let decimal = value as? Decimal {
- return NSDecimalNumber(decimal: decimal)
- }
- return nil
- }
-
- private func boxGenericEncodable(_ value: T) throws -> NSObject? {
- // The value should request a container from the DictionaryEncoderImpl.
- let depth = self.storage.count
- do {
- try value.encode(to: self)
- } catch {
- // If the value pushed a container before throwing, pop it back off to
- // restore state.
- if self.storage.count > depth {
- _ = self.storage.popContainer()
- }
- throw error
- }
-
- // The top container should be a new container.
- guard self.storage.count > depth else {
- return nil
- }
-
- return self.storage.popContainer()
- }
}
diff --git a/Sources/DictionaryCoding/DictionaryEncoderImpl+BoxEncodable.swift b/Sources/DictionaryCoding/DictionaryEncoderImpl+BoxEncodable.swift
new file mode 100644
index 0000000..cab9f23
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncoderImpl+BoxEncodable.swift
@@ -0,0 +1,118 @@
+//
+// DictionaryEncoderImpl+BoxEncodable.swift
+// DictionaryCoding
+//
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+// MARK: - Encodable Value Representations
+extension DictionaryEncoderImpl {
+ internal func box(_ value: T) throws -> NSObject {
+ try self.boxEncodable(value) ?? NSDictionary()
+ }
+
+ // This method is called "boxEncodable" instead of "box" to disambiguate it from the
+ // overloads. Because the return type here is different from all of the "box"
+ // overloads (and is more general), any "box" calls in here would call back
+ // into "box" recursively instead of calling the appropriate overload, which
+ // is not what we want.
+ internal func boxEncodable(_ value: T) throws -> NSObject? {
+ if let result = try boxSpecialType(value) {
+ return result
+ }
+
+ return try boxGenericEncodable(value)
+ }
+
+ private func boxSpecialType(_ value: T) throws -> NSObject? {
+ if T.self == Date.self || T.self == NSDate.self {
+ return try boxAsDate(value)
+ } else if T.self == Data.self || T.self == NSData.self {
+ return try boxAsData(value)
+ } else if T.self == URL.self || T.self == NSURL.self {
+ return boxAsURL(value)
+ } else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
+ return boxAsDecimal(value)
+ }
+ return nil
+ }
+
+ private func boxAsDate(_ value: T) throws -> NSObject? {
+ guard let date = value as? Date else {
+ return nil
+ }
+ return try self.box(date)
+ }
+
+ private func boxAsData(_ value: T) throws -> NSObject? {
+ guard let data = value as? Data else {
+ return nil
+ }
+ return try self.box(data)
+ }
+
+ private func boxAsURL(_ value: T) -> NSObject? {
+ guard let url = value as? URL else {
+ return nil
+ }
+ return self.box(url.absoluteString)
+ }
+
+ private func boxAsDecimal(_ value: T) -> NSObject? {
+ if let decimal = value as? NSDecimalNumber {
+ // DictionarySerialization can natively handle NSDecimalNumber.
+ return decimal
+ }
+ // On Linux, Swift Decimal doesn't auto-bridge to NSDecimalNumber.
+ if let decimal = value as? Decimal {
+ return NSDecimalNumber(decimal: decimal)
+ }
+ return nil
+ }
+
+ private func boxGenericEncodable(_ value: T) throws -> NSObject? {
+ // The value should request a container from the DictionaryEncoderImpl.
+ let depth = self.storage.count
+ do {
+ try value.encode(to: self)
+ } catch {
+ // If the value pushed a container before throwing, pop it back off to
+ // restore state.
+ if self.storage.count > depth {
+ _ = self.storage.popContainer()
+ }
+ throw error
+ }
+
+ // The top container should be a new container.
+ guard self.storage.count > depth else {
+ return nil
+ }
+
+ return self.storage.popContainer()
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryEncoderImpl+SingleValue.swift b/Sources/DictionaryCoding/DictionaryEncoderImpl+SingleValue.swift
new file mode 100644
index 0000000..63c3e18
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryEncoderImpl+SingleValue.swift
@@ -0,0 +1,137 @@
+//
+// DictionaryEncoderImpl+SingleValue.swift
+// DictionaryCoding
+//
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+// MARK: - SingleValueEncodingContainer
+extension DictionaryEncoderImpl: SingleValueEncodingContainer {
+ internal func assertCanEncodeNewValue() {
+ precondition(
+ self.canEncodeNewValue,
+ "Attempt to encode value through single value container when previously"
+ + " value already encoded."
+ )
+ }
+
+ /// Encodes a null value.
+ public func encodeNil() throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: NSNull())
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Bool) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Int) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Int8) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Int16) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Int32) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Int64) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: UInt) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: UInt8) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: UInt16) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: UInt32) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: UInt64) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: String) throws {
+ assertCanEncodeNewValue()
+ self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Float) throws {
+ assertCanEncodeNewValue()
+ try self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: Double) throws {
+ assertCanEncodeNewValue()
+ try self.storage.push(container: self.box(value))
+ }
+
+ /// Encodes the given value into the single-value container.
+ public func encode(_ value: T) throws {
+ assertCanEncodeNewValue()
+ try self.storage.push(container: self.box(value))
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryEncoderImpl.swift b/Sources/DictionaryCoding/DictionaryEncoderImpl.swift
index 51860ad..ed49217 100644
--- a/Sources/DictionaryCoding/DictionaryEncoderImpl.swift
+++ b/Sources/DictionaryCoding/DictionaryEncoderImpl.swift
@@ -1,9 +1,30 @@
//
// DictionaryEncoderImpl.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -110,94 +131,3 @@ internal class DictionaryEncoderImpl: Encoder {
self
}
}
-
-// MARK: - SingleValueEncodingContainer
-extension DictionaryEncoderImpl: SingleValueEncodingContainer {
- internal func assertCanEncodeNewValue() {
- precondition(
- self.canEncodeNewValue,
- "Attempt to encode value through single value container when previously"
- + " value already encoded."
- )
- }
-
- public func encodeNil() throws {
- assertCanEncodeNewValue()
- self.storage.push(container: NSNull())
- }
-
- public func encode(_ value: Bool) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: Int) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: Int8) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: Int16) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: Int32) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: Int64) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: UInt) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: UInt8) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: UInt16) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: UInt32) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: UInt64) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: String) throws {
- assertCanEncodeNewValue()
- self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: Float) throws {
- assertCanEncodeNewValue()
- try self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: Double) throws {
- assertCanEncodeNewValue()
- try self.storage.push(container: self.box(value))
- }
-
- public func encode(_ value: T) throws {
- assertCanEncodeNewValue()
- try self.storage.push(container: self.box(value))
- }
-}
diff --git a/Sources/DictionaryCoding/DictionaryEncoderOptions.swift b/Sources/DictionaryCoding/DictionaryEncoderOptions.swift
index d482c9c..e6f03ba 100644
--- a/Sources/DictionaryCoding/DictionaryEncoderOptions.swift
+++ b/Sources/DictionaryCoding/DictionaryEncoderOptions.swift
@@ -1,9 +1,30 @@
//
// DictionaryEncoderOptions.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryEncodingStorage.swift b/Sources/DictionaryCoding/DictionaryEncodingStorage.swift
index 9a7bc1c..a7c4837 100644
--- a/Sources/DictionaryCoding/DictionaryEncodingStorage.swift
+++ b/Sources/DictionaryCoding/DictionaryEncodingStorage.swift
@@ -1,9 +1,30 @@
//
// DictionaryEncodingStorage.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryReferencingEncoder.swift b/Sources/DictionaryCoding/DictionaryReferencingEncoder.swift
index 76d12aa..1a32a7e 100644
--- a/Sources/DictionaryCoding/DictionaryReferencingEncoder.swift
+++ b/Sources/DictionaryCoding/DictionaryReferencingEncoder.swift
@@ -1,9 +1,30 @@
//
// DictionaryReferencingEncoder.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -95,10 +116,10 @@ internal class DictionaryReferencingEncoder: DictionaryEncoderImpl {
}
switch self.reference {
- case let .array(array, index):
+ case .array(let array, let index):
array.insert(value, at: index)
- case let .dictionary(dictionary, key):
+ case .dictionary(let dictionary, let key):
dictionary[NSString(string: key)] = value
}
}
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift
index 367ac8a..a2f7b8d 100644
--- a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Nested.swift
@@ -1,9 +1,30 @@
//
// DictionaryUnkeyedDecodingContainer+Nested.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift
index e00de4d..82d9511 100644
--- a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+Scalars.swift
@@ -1,9 +1,30 @@
//
// DictionaryUnkeyedDecodingContainer+Scalars.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -124,101 +145,6 @@ extension DictionaryUnkeyedDecodingContainer {
return decoded
}
- internal mutating func decode(_ type: UInt.Type) throws -> UInt {
- guard !self.isAtEnd else {
- throw atEndError(type)
- }
-
- self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
- defer { self.decoder.codingPath.removeLast() }
-
- guard
- let decoded =
- try self.decoder.unbox(self.container[self.currentIndex], as: UInt.self)
- else {
- throw nullFoundError(type)
- }
-
- self.currentIndex += 1
- return decoded
- }
-
- internal mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
- guard !self.isAtEnd else {
- throw atEndError(type)
- }
-
- self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
- defer { self.decoder.codingPath.removeLast() }
-
- guard
- let decoded =
- try self.decoder.unbox(self.container[self.currentIndex], as: UInt8.self)
- else {
- throw nullFoundError(type)
- }
-
- self.currentIndex += 1
- return decoded
- }
-
- internal mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
- guard !self.isAtEnd else {
- throw atEndError(type)
- }
-
- self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
- defer { self.decoder.codingPath.removeLast() }
-
- guard
- let decoded =
- try self.decoder.unbox(self.container[self.currentIndex], as: UInt16.self)
- else {
- throw nullFoundError(type)
- }
-
- self.currentIndex += 1
- return decoded
- }
-
- internal mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
- guard !self.isAtEnd else {
- throw atEndError(type)
- }
-
- self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
- defer { self.decoder.codingPath.removeLast() }
-
- guard
- let decoded =
- try self.decoder.unbox(self.container[self.currentIndex], as: UInt32.self)
- else {
- throw nullFoundError(type)
- }
-
- self.currentIndex += 1
- return decoded
- }
-
- internal mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
- guard !self.isAtEnd else {
- throw atEndError(type)
- }
-
- self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
- defer { self.decoder.codingPath.removeLast() }
-
- guard
- let decoded =
- try self.decoder.unbox(self.container[self.currentIndex], as: UInt64.self)
- else {
- throw nullFoundError(type)
- }
-
- self.currentIndex += 1
- return decoded
- }
-
internal mutating func decode(_ type: Float.Type) throws -> Float {
guard !self.isAtEnd else {
throw atEndError(type)
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift
index 7fdc88e..894cba8 100644
--- a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+String.swift
@@ -1,9 +1,30 @@
//
// DictionaryUnkeyedDecodingContainer+String.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+UnsignedScalars.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+UnsignedScalars.swift
new file mode 100644
index 0000000..1064bf8
--- /dev/null
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer+UnsignedScalars.swift
@@ -0,0 +1,128 @@
+//
+// DictionaryUnkeyedDecodingContainer+UnsignedScalars.swift
+// DictionaryCoding
+//
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+// MARK: - Unsigned integer scalar decode methods
+extension DictionaryUnkeyedDecodingContainer {
+ internal mutating func decode(_ type: UInt.Type) throws -> UInt {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt8.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt16.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt32.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+
+ internal mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
+ guard !self.isAtEnd else {
+ throw atEndError(type)
+ }
+
+ self.decoder.codingPath.append(DictionaryCodingKey(index: self.currentIndex))
+ defer { self.decoder.codingPath.removeLast() }
+
+ guard
+ let decoded =
+ try self.decoder.unbox(self.container[self.currentIndex], as: UInt64.self)
+ else {
+ throw nullFoundError(type)
+ }
+
+ self.currentIndex += 1
+ return decoded
+ }
+}
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift
index d5c9801..8fb41dc 100644
--- a/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedDecodingContainer.swift
@@ -1,9 +1,30 @@
//
// DictionaryUnkeyedDecodingContainer.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift b/Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift
index 35e20bc..ab79bbb 100644
--- a/Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift
+++ b/Sources/DictionaryCoding/DictionaryUnkeyedEncodingContainer.swift
@@ -1,9 +1,30 @@
//
// DictionaryUnkeyedEncodingContainer.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Sources/DictionaryCoding/EncodingError+Dictionary.swift b/Sources/DictionaryCoding/EncodingError+Dictionary.swift
index eef1659..c723a58 100644
--- a/Sources/DictionaryCoding/EncodingError+Dictionary.swift
+++ b/Sources/DictionaryCoding/EncodingError+Dictionary.swift
@@ -1,9 +1,30 @@
//
// EncodingError+Dictionary.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -11,8 +32,9 @@ import Foundation
extension EncodingError {
/// Returns a `.invalidValue` error describing the given invalid floating-point value.
///
- /// - parameter value: The value that was invalid to encode.
- /// - parameter path: The path of `CodingKey`s taken to encode this value.
+ /// - Parameters:
+ /// - value: The value that was invalid to encode.
+ /// - codingPath: The path of `CodingKey`s taken to encode this value.
/// - returns: An `EncodingError` with the appropriate path and debug description.
internal static func invalidFloatingPointValue(
_ value: T,
diff --git a/Sources/DictionaryCoding/NSNumber+Bool.swift b/Sources/DictionaryCoding/NSNumber+Bool.swift
index 9d8e1c7..f650b89 100644
--- a/Sources/DictionaryCoding/NSNumber+Bool.swift
+++ b/Sources/DictionaryCoding/NSNumber+Bool.swift
@@ -1,9 +1,30 @@
//
// NSNumber+Bool.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
@@ -20,7 +41,6 @@ extension NSNumber {
#if canImport(Darwin)
return CFGetTypeID(self) == CFBooleanGetTypeID()
#else
- // swiftlint:disable:next line_length
return String(cString: self.objCType) == String(cString: NSNumber(value: true).objCType)
#endif
}
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift
index 642ae53..7ea49e1 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingArrayAndKeyTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingArrayAndKeyTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift
index d928a3d..1328425 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingArrayTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingArrayTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
@@ -42,10 +63,7 @@ internal struct DictionaryCodingArrayTests {
@Test("round-trips empty scalar arrays")
internal func emptyScalarArrays() throws {
- let original = ScalarArrays(
- int8s: [], uint16s: [], doubles: [],
- bools: [], strings: []
- )
+ let original = ScalarArrays(int8s: [], uint16s: [], doubles: [], bools: [], strings: [])
let dict: [String: Any] = try DictionaryEncoder().encode(original)
let decoded = try DictionaryDecoder().decode(
ScalarArrays.self, from: dict
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift
index 7028e15..0875b2e 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingDateDataTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingDateDataTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift
index 84d177b..bac39ac 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingErrorTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingErrorTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift
index 09ef533..85b78ba 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingFloatStrategyTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingFloatStrategyTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift
index e888359..a84ce1c 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingNestedTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingNestedTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift
index 6cb8023..5dc9b01 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingRoundTripTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingRoundTripTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift
index 87e8b4f..fa42f1c 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingScalarTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingScalarTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
@@ -41,10 +62,16 @@ internal struct DictionaryCodingScalarTests {
@Test("round-trips all integer types")
internal func allIntegerTypes() throws {
let original = AllIntegers(
- int: -42, int8: Int8.min, int16: Int16.max,
- int32: -100_000, int64: Int64.max, uint: 99,
- uint8: UInt8.max, uint16: 0,
- uint32: UInt32.max, uint64: UInt64.max
+ int: -42,
+ int8: Int8.min,
+ int16: Int16.max,
+ int32: -100_000,
+ int64: Int64.max,
+ uint: 99,
+ uint8: UInt8.max,
+ uint16: 0,
+ uint32: UInt32.max,
+ uint64: UInt64.max
)
let dict: [String: Any] = try DictionaryEncoder().encode(original)
let decoded = try DictionaryDecoder().decode(
@@ -56,9 +83,16 @@ internal struct DictionaryCodingScalarTests {
@Test("round-trips integer boundary values")
internal func integerBoundaries() throws {
let original = AllIntegers(
- int: Int.min, int8: Int8.max, int16: Int16.min,
- int32: Int32.max, int64: Int64.min, uint: UInt.max,
- uint8: 0, uint16: UInt16.max, uint32: 0, uint64: 0
+ int: Int.min,
+ int8: Int8.max,
+ int16: Int16.min,
+ int32: Int32.max,
+ int64: Int64.min,
+ uint: UInt.max,
+ uint8: 0,
+ uint16: UInt16.max,
+ uint32: 0,
+ uint64: 0
)
let dict: [String: Any] = try DictionaryEncoder().encode(original)
let decoded = try DictionaryDecoder().decode(
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift
index 583d02c..8603d93 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingSpecialTypeTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingSpecialTypeTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift
index ec6a2ec..f14d935 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingStrategyErrorTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingStrategyErrorTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift b/Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift
index 47384d3..1681df0 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingSuperDecoderTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingSuperDecoderTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift b/Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift
index 02acab1..0ebdef6 100644
--- a/Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift
+++ b/Tests/DictionaryCodingTests/DictionaryCodingTestKey.swift
@@ -1,9 +1,30 @@
//
// DictionaryCodingTestKey.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
/// A simple CodingKey used by custom key strategy tests.
diff --git a/Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift b/Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift
index 70b15d1..57ca0f1 100644
--- a/Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryDecoderPlatformTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderPlatformTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryDecoderTests.swift b/Tests/DictionaryCodingTests/DictionaryDecoderTests.swift
index 76ceb1e..c86cdb0 100644
--- a/Tests/DictionaryCodingTests/DictionaryDecoderTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryDecoderTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryDecoderTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/DictionaryEncoderTests.swift b/Tests/DictionaryCodingTests/DictionaryEncoderTests.swift
index dfe86fa..246925d 100644
--- a/Tests/DictionaryCodingTests/DictionaryEncoderTests.swift
+++ b/Tests/DictionaryCodingTests/DictionaryEncoderTests.swift
@@ -1,9 +1,30 @@
//
// DictionaryEncoderTests.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
diff --git a/Tests/DictionaryCodingTests/SuperDecoderBase.swift b/Tests/DictionaryCodingTests/SuperDecoderBase.swift
index 560875c..57ebd42 100644
--- a/Tests/DictionaryCodingTests/SuperDecoderBase.swift
+++ b/Tests/DictionaryCodingTests/SuperDecoderBase.swift
@@ -1,9 +1,30 @@
//
// SuperDecoderBase.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
diff --git a/Tests/DictionaryCodingTests/SuperDecoderChild.swift b/Tests/DictionaryCodingTests/SuperDecoderChild.swift
index 63cb2b3..30e38d3 100644
--- a/Tests/DictionaryCodingTests/SuperDecoderChild.swift
+++ b/Tests/DictionaryCodingTests/SuperDecoderChild.swift
@@ -1,9 +1,30 @@
//
// SuperDecoderChild.swift
-// AtLeast
+// DictionaryCoding
//
-// Copyright (c) 2026 BrightDigit.
-// All rights reserved.
+// Created by Leo Dion.
+// Copyright © 2026 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
//
import DictionaryCoding
From 7d8bb004af6376976ff96fb933cf237d8a4e6ed6 Mon Sep 17 00:00:00 2001
From: leogdion
Date: Fri, 26 Jun 2026 11:16:25 -0400
Subject: [PATCH 5/5] Update Swift version in DictionaryCoding workflow
---
.github/workflows/DictionaryCoding.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/DictionaryCoding.yml b/.github/workflows/DictionaryCoding.yml
index 1ddb916..818d02f 100644
--- a/.github/workflows/DictionaryCoding.yml
+++ b/.github/workflows/DictionaryCoding.yml
@@ -63,7 +63,7 @@ jobs:
{
echo "full-matrix=true"
echo 'ubuntu-os=["noble"]'
- echo 'ubuntu-swift=[{"version":"6.3"},{"version":"6.4","nightly":true}]'
+ echo 'ubuntu-swift=[{"version":"6.3"},{"version":"6.4.x","nightly":true}]'
echo 'ubuntu-type=["","wasm","wasm-embedded"]'
} >> "$GITHUB_OUTPUT"
else
@@ -89,9 +89,9 @@ jobs:
type: ${{ fromJSON(needs.configure.outputs.ubuntu-type) }}
exclude:
# Nightly toolchains skip the wasm legs — nightly WASI SDKs may be unavailable.
- - swift: { version: "6.4", nightly: true }
+ - swift: { version: "6.4.x", nightly: true }
type: "wasm"
- - swift: { version: "6.4", nightly: true }
+ - swift: { version: "6.4.x", nightly: true }
type: "wasm-embedded"
steps:
- uses: actions/checkout@v6