From 1b72caa90671cf9b9f3a33d276d76fddb5a2c798 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 12:53:12 +0200 Subject: [PATCH 1/5] Support Foundation boolean bridging on Windows --- README.md | 2 +- .../Enigmatic/Enigmatic.docc/Development.md | 25 +++++++++++++++++++ .../Enigmatic/Enigmatic.docc/Serialization.md | 3 +++ .../Internal/Extensions/Enigma+Utility.swift | 1 - .../Primitives/EnigmaConvertibleObject.swift | 9 ++++++- Tests/EnigmaticTests/PerformanceTests.swift | 1 - .../SerializationContractTests.swift | 23 +++++++++++++++++ 7 files changed, 60 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4c68668..eb46e7c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![License](https://img.shields.io/github/license/SwiftUtility/Enigmatic)](LICENSE) [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftutility.github.io%2FEnigmatic%2Fcoverage-badge.json)](https://github.com/SwiftUtility/Enigmatic/actions/workflows/release.yml) [![Documentation](https://img.shields.io/badge/Documentation-DocC-blue)](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/) -[![Tested platforms](https://img.shields.io/badge/tested_on-macOS%20%7C%20Linux-blue)](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/development/) +[![Tested platforms](https://img.shields.io/badge/tested_on-macOS%20%7C%20Linux%20%7C%20Windows-blue)](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/development/) Turn `Codable` values into a tree you can inspect, edit, merge, and decode again. Use `@Codec.Box` to choose how individual properties are encoded. diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index 9538ca9..f1b92e2 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -7,6 +7,31 @@ swift test --skip PerformanceTests swift test -c release --skip PerformanceTests ``` +On Windows, use Developer PowerShell for Visual Studio with the C++ tools and +Windows SDK installed. Start a fresh shell after installing Swift so `PATH` and +`SDKROOT` include the toolchain and its SDK. + +The functional suite passed locally in Debug and Release on Windows x64 with +Swift 6.4.1 on 2026-10-16. Windows is not currently part of the CI matrix. + +Local Windows x64 validation with Swift 6.4.1 encountered two toolchain issues: +the default build system failed to find `stdlib.h`, and the compiler's debug-type +round-trip check asserted on negative integer generic arguments in the strategy +tests. To reproduce the successful validation, use: + +```powershell +$windowsSwiftFlags = @( + '--build-system', 'native', + '-Xswiftc', '-Xfrontend', '-Xswiftc', '-disable-round-trip-debug-types' +) +swift test @windowsSwiftFlags --skip PerformanceTests +swift test @windowsSwiftFlags -c release --skip PerformanceTests +``` + +These are toolchain workarounds, not package settings. The native build system is +deprecated; recheck these workarounds when updating Swift. Windows validation +does not replace the macOS and Linux CI jobs below. + CI uses Swift 6.3.3 from Xcode 26.6 on macOS 26 and the `swift:6.3.3` container on Ubuntu 24.04. Each job verifies the compiler version before testing. The macOS Debug job also checks the coverage script, enforces 95% library line diff --git a/Sources/Enigmatic/Enigmatic.docc/Serialization.md b/Sources/Enigmatic/Enigmatic.docc/Serialization.md index 6303357..ebc9f1b 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Serialization.md +++ b/Sources/Enigmatic/Enigmatic.docc/Serialization.md @@ -46,6 +46,9 @@ require keys that bridge to String. Keys normalize to strings; collisions and unsupported key types produce a decoding error. Arrays and sets convert to arrays, with set order unspecified. +Boxed Foundation booleans remain boolean nodes, distinct from numeric zero and +one, including on Windows where CoreFoundation is not exposed by the SDK. + On all platforms, `Enigma(cast:)` converts `NSDecimalNumber` values by parsing their `stringValue` with `Double.init(_:)`, rather than reading `doubleValue`. This avoids precision loss in Foundation's decimal-to-Double diff --git a/Sources/Enigmatic/Internal/Extensions/Enigma+Utility.swift b/Sources/Enigmatic/Internal/Extensions/Enigma+Utility.swift index 86f80be..34ab829 100644 --- a/Sources/Enigmatic/Internal/Extensions/Enigma+Utility.swift +++ b/Sources/Enigmatic/Internal/Extensions/Enigma+Utility.swift @@ -1,5 +1,4 @@ import Foundation -import CoreFoundation extension Enigma { func collectPins( diff --git a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift index 690e88d..977efdf 100644 --- a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift +++ b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift @@ -1,5 +1,7 @@ -import CoreFoundation import Foundation +#if canImport(CoreFoundation) +import CoreFoundation +#endif protocol EnigmaConvertibleObject: EnigmaConvertible, AnyObject {} @@ -52,7 +54,12 @@ extension NSValue: EnigmaConvertibleObject { extension NSNumber { var asEnigma: Enigma? { +#if canImport(CoreFoundation) guard CFGetTypeID(self) != CFBooleanGetTypeID() else { return .bool(boolValue) } +#else + // Foundation's boolean singletons distinguish Bool from numeric zero and one. + if self === NSNumber(value: true) || self === NSNumber(value: false) { return .bool(boolValue) } +#endif return switch objCType.pointee { case ObjCType.signedLong: .int(intValue) case ObjCType.unsignedLong: .uint(uintValue) diff --git a/Tests/EnigmaticTests/PerformanceTests.swift b/Tests/EnigmaticTests/PerformanceTests.swift index df8c834..c0f7745 100644 --- a/Tests/EnigmaticTests/PerformanceTests.swift +++ b/Tests/EnigmaticTests/PerformanceTests.swift @@ -1,6 +1,5 @@ @testable import Enigmatic import Foundation -import CoreFoundation import XCTest final class PerformanceTests: XCTestCase { diff --git a/Tests/EnigmaticTests/SerializationContractTests.swift b/Tests/EnigmaticTests/SerializationContractTests.swift index 5ef04b2..dac2ad2 100644 --- a/Tests/EnigmaticTests/SerializationContractTests.swift +++ b/Tests/EnigmaticTests/SerializationContractTests.swift @@ -161,6 +161,29 @@ final class SerializationContractTests: XCTestCase { XCTAssertEqual(try Enigma(cast: NSNull()), .null) } + func testFoundationBooleansRemainDistinctFromZeroAndOne() throws { + for value in [false, true] { + let tree = try Enigma(cast: NSNumber(value: value)) + XCTAssertEqual(tree.asBool, value) + XCTAssertNil(tree.asInt) + } + for value in [0, 1] { + for number in [NSNumber(value: value), NSNumber(value: Int8(value)), NSNumber(value: Double(value))] { + let tree = try Enigma(cast: number) + XCTAssertNil(tree.asBool) + XCTAssertEqual(tree.asInt, value) + } + } + let tree: Enigma = [false, true, 0, 1] + let data = try JSONSerialization.data(withJSONObject: tree.asJsonObject) + let restored = try Enigma(cast: JSONSerialization.jsonObject(with: data)) + XCTAssertEqual(restored[0]?.asBool, false) + XCTAssertEqual(restored[1]?.asBool, true) + XCTAssertNil(restored[2]?.asBool) + XCTAssertNil(restored[3]?.asBool) + XCTAssertEqual(restored, tree) + } + func testFloatFoundationSerializationRoundTrips() throws { let values: [Float] = [ 0.1, From d9920292022a94de618b9ec8821a258d847b59f4 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 12:58:11 +0200 Subject: [PATCH 2/5] Add Windows x64 debug and release CI checks --- .github/workflows/checks.yml | 39 +++++++++++++++++++ .../Enigmatic/Enigmatic.docc/Development.md | 11 ++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5c234b4..7bbdb02 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -80,3 +80,42 @@ jobs: path: Enigmatic.doccarchive.tar.gz compression-level: 0 if-no-files-found: error + + windows: + name: windows-2022 / ${{ matrix.configuration }} + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + configuration: [debug, release] + defaults: + run: + shell: pwsh + steps: + - uses: actions/checkout@v6 + - name: Install Swift 6.4.1 + uses: compnerd/gha-setup-swift@86d1ddeaf915c282137601aeb0654ea0e7b7f17e + with: + swift-version: swift-6.4.1-release + swift-build: 6.4.1-RELEASE + - name: Verify Swift 6.4.1 for Windows x64 + run: | + $version = (swift --version) -join "`n" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Output $version + if ($version -notmatch 'Swift version 6\.4\.1(\s|$)') { + throw 'Expected Swift 6.4.1' + } + if ($version -notmatch 'Target: x86_64-unknown-windows-msvc') { + throw 'Expected the Windows x64 toolchain' + } + - name: Functional tests + run: | + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $vsPath = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vsPath) { throw 'Visual Studio C++ tools not found' } + Import-Module (Join-Path $vsPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') + Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' + # Work around Swift 6.4.1's build-system and debug-type assertion issues. + swift test -c ${{ matrix.configuration }} --build-system native -Xswiftc -Xfrontend -Xswiftc -disable-round-trip-debug-types --skip PerformanceTests + exit $LASTEXITCODE diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index f1b92e2..0ea835c 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -12,7 +12,7 @@ Windows SDK installed. Start a fresh shell after installing Swift so `PATH` and `SDKROOT` include the toolchain and its SDK. The functional suite passed locally in Debug and Release on Windows x64 with -Swift 6.4.1 on 2026-10-16. Windows is not currently part of the CI matrix. +Swift 6.4.1 on 2026-10-16. Local Windows x64 validation with Swift 6.4.1 encountered two toolchain issues: the default build system failed to find `stdlib.h`, and the compiler's debug-type @@ -32,13 +32,18 @@ These are toolchain workarounds, not package settings. The native build system i deprecated; recheck these workarounds when updating Swift. Windows validation does not replace the macOS and Linux CI jobs below. -CI uses Swift 6.3.3 from Xcode 26.6 on macOS 26 and the `swift:6.3.3` -container on Ubuntu 24.04. Each job verifies the compiler version before testing. +The macOS and Linux CI jobs use Swift 6.3.3 from Xcode 26.6 on macOS 26 and the +`swift:6.3.3` container on Ubuntu 24.04. Each job verifies the compiler version +before testing. The macOS Debug job also checks the coverage script, enforces 95% library line coverage, and builds DocC. It uploads the coverage report as `coverage.json` and the documentation as `Enigmatic.doccarchive.tar.gz`, preserving DocC filenames inside the archive. +Checks also runs separate Windows x64 Debug and Release jobs on `windows-2022` +with Swift 6.4.1, Visual Studio's C++ environment, and the workarounds above. +These jobs do not change the macOS/Linux checks or the Release workflow. + The Release workflow publishes the DocC site to GitHub Pages after all release validation jobs pass and before creating the GitHub release. The site is at . Enable From 0be1ac2b8668c650032edac350349a6609067429 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 14:11:04 +0200 Subject: [PATCH 3/5] Run CI for pull requests on demand --- .github/workflows/checks.yml | 6 +++++- Sources/Enigmatic/Enigmatic.docc/Development.md | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7bbdb02..a7b9e86 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -3,17 +3,20 @@ name: Checks on: push: branches: [main] + pull_request: + types: [labeled] workflow_dispatch: permissions: contents: read concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} + group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event.label.name }} cancel-in-progress: true jobs: tests: + if: github.event_name != 'pull_request' || github.event.label.name == 'run-ci' name: ${{ matrix.os }} / ${{ matrix.configuration }} runs-on: ${{ matrix.os }} container: ${{ matrix.container }} @@ -82,6 +85,7 @@ jobs: if-no-files-found: error windows: + if: github.event_name != 'pull_request' || github.event.label.name == 'run-ci' name: windows-2022 / ${{ matrix.configuration }} runs-on: windows-2022 strategy: diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index 0ea835c..7b3c70f 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -56,9 +56,14 @@ It reports actual library line coverage from the last successful release documentation deployment. Documentation and coverage links become available after the first successful Pages deployment. -CI runs automatically for pushes to `main`. To check a pull request, start -**Actions → Checks → Run workflow** and select its source branch. Pull requests -and pushes to other branches do not start CI automatically. +CI runs automatically for pushes to `main`. To check a pull request, add the +`run-ci` label from the PR sidebar. Results appear in the PR's **Checks** tab. +Opening a PR or pushing new commits to it does not start CI automatically, +even while the label remains attached. Remove and reapply `run-ci` to check the +latest changes. Other labels do not run tests or cancel an active `run-ci` run. + +You can also start **Actions → Checks → Run workflow** and select a branch. +These manual branch runs do not appear as PR checks. ```sh python3 -m unittest discover -s Scripts/tests From 9724df8d51ccbdf1ad3a5039bcea48907032c752 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 15:06:28 +0200 Subject: [PATCH 4/5] Preserve Windows path semantics in PathURL --- .../Enigmatic.docc/CodingStrategies.md | 8 ++- .../Codec/Strategies/Codec+PathURL.swift | 9 +++ .../StrategyContractTests.swift | 65 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md b/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md index 3e1d858..ac59b8a 100644 --- a/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md +++ b/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md @@ -21,7 +21,13 @@ the present payload of an optional strategy. `Base64Data` encodes a Base64 string and rejects malformed input. `StringURL` uses Foundation's URL initializer, which permits relative URLs; it does not enforce an HTTP scheme. `PathURL` turns filesystem paths into file URLs and encodes their -unescaped paths. Encoding a non-file URL still writes only its path. +unescaped paths. Relative paths resolve against the process's working directory; +the original relative spelling and URL base are not preserved. Encoding a +non-file URL still writes only its path. +On Windows, `PathURL` accepts drive and network paths with either slash style. +It encodes forward slashes, keeps drive prefixes such as `C:/` without an extra +leading slash, and preserves trailing directory separators. Other platforms +retain their existing Foundation and FilePath normalization. Strategy arrays, dictionaries, optionals and sets transform their elements: `[Codec.Base64Data]`, `[String: Codec.Base64Data?]`, `Set>`. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift index 5b25a54..e9cd90d 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift @@ -23,20 +23,29 @@ extension Codec { } else { return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/")) } +#elseif os(Windows) + return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/") || value.hasSuffix("\\")) #else return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/")) #endif } /// Encodes the URL's unescaped filesystem path as a string. + /// Windows paths use forward slashes and preserve a trailing directory separator. /// - Complexity: O(n) in the URL or path string length. @inlinable public static func encode(value: BoxedValue, encoder: some Encoder) throws { +#if os(Windows) + var path = value.path + if value.hasDirectoryPath && !path.hasSuffix("/") { path += "/" } + try path.encode(to: encoder) +#else if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { try value.path(percentEncoded: false).encode(to: encoder) } else { try value.path.encode(to: encoder) } +#endif } /// The decoded and encoded value type. diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index 7c7fdde..915e680 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -244,6 +244,71 @@ final class StrategyContractTests: XCTestCase { } } + func testPathURLPreservesLiteralCharactersThroughSynthesizedCodable() throws { + struct Payload: Codable { + @Codec.Box var file: URL + } + let path = "/tmp/enigmatic/café with spaces%20#.txt" + let payload = Payload(file: URL(fileURLWithPath: path, isDirectory: false)) + let tree = try Enigma(encode: payload) + XCTAssertEqual(tree, ["file": .string(path)]) + let restored = try tree.decode(Payload.self) + XCTAssertEqual(restored.file, payload.file) + XCTAssertNil(restored.file.query) + XCTAssertNil(restored.file.fragment) + + let json = try JSONEncoder().encode(payload) + XCTAssertEqual(try JSONDecoder().decode([String: String].self, from: json), ["file": path]) + XCTAssertEqual(try JSONDecoder().decode(Payload.self, from: json).file, payload.file) + assertDecodingError("typeMismatch", path: ["file"]) { + _ = try Enigma.dictionary(["file": 42]).decode(Payload.self) + } + } + + func testPathURLResolvesRelativePathsAgainstTheWorkingDirectory() throws { + let path = "enigmatic relative/file.txt" + let expected = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) + .appendingPathComponent("enigmatic relative", isDirectory: true) + .appendingPathComponent("file.txt", isDirectory: false) + let box = try Enigma.string(path).decode(Codec.Box.self) + XCTAssertEqual(box.wrappedValue.absoluteURL, expected) + let tree = try Enigma(encode: box) + XCTAssertEqual(tree, .string(expected.path)) + XCTAssertEqual(try tree.decode(Codec.Box.self).wrappedValue.absoluteURL, expected) + } + + func testPathURLEncodesOnlyTheUnescapedPathOfANonFileURL() throws { + let url = URL(string: "https://example.com/path%20with%20spaces/%2520%23?query=value#fragment")! + let tree = try Enigma(encode: Codec.Box(wrappedValue: url)) + XCTAssertEqual(tree, "/path with spaces/%20#") + let restored = try tree.decode(Codec.Box.self).wrappedValue + XCTAssertTrue(restored.isFileURL) + XCTAssertNil(restored.query) + XCTAssertNil(restored.fragment) + } + +#if os(Windows) + func testPathURLHandlesWindowsDriveAndNetworkPaths() throws { + for (input, path, isDirectory) in [ + (#"C:\Enigmatic\café with spaces%20#.txt"#, "C:/Enigmatic/café with spaces%20#.txt", false), + ("C:/Enigmatic/file.txt", "C:/Enigmatic/file.txt", false), + (#"C:\Enigmatic\folder\"#, "C:/Enigmatic/folder/", true), + ("C:/Enigmatic/folder/", "C:/Enigmatic/folder/", true), + (#"C:\"#, "C:/", true), + ("C:/", "C:/", true), + (#"\\server\share\café with spaces%20#.txt"#, "//server/share/café with spaces%20#.txt", false), + (#"\\server\share\folder\"#, "//server/share/folder/", true), + ] { + let tree = Enigma.string(input) + let box = try tree.decode(Codec.Box.self) + XCTAssertTrue(box.wrappedValue.isFileURL, input) + XCTAssertEqual(box.wrappedValue.hasDirectoryPath, isDirectory, input) + XCTAssertEqual(try Enigma(encode: box), .string(path), input) + XCTAssertEqual(try JSONDecoder().decode(Codec.Box.self, from: JSONEncoder().encode(box)), box, input) + } + } +#endif + #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) func testPathURLKeepsFilePathNormalization() throws { From 51c47e16c36ccc20ee3f452a9dffa90d4d6b3454 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 15:34:30 +0200 Subject: [PATCH 5/5] Clarify library selection and serialization guidance --- README.md | 75 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index eb46e7c..bf39472 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,26 @@ Turn `Codable` values into a tree you can inspect, edit, merge, and decode again. Use `@Codec.Box` to choose how individual properties are encoded. +## Choosing a library + +Choose Enigmatic when you need to edit or merge a `Codable` model as a tree, then +decode it again without a JSON round trip. Its reusable property strategies also +work on their own. + +| Alternative | When it may fit better | +| --- | --- | +| Native `Codable` | You only read or write known models with `JSONEncoder` and `JSONDecoder`. Custom `Codable` code can handle property formats too, without another library or tree. | +| [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) | You want JSON getters with defaults, edits, and simple merges. Enigmatic adds more merge rules and direct model/tree conversion. | +| [AnyCodable](https://github.com/Flight-School/AnyCodable) | You only need mixed value types in `Codable` arrays or dictionaries. | +| [PotentCodables](https://github.com/outfoxx/PotentCodables) | You need dedicated parsers, YAML or CBOR support, or more native value types. Enigmatic has no parser of its own. | +| [CodableWrappers](https://github.com/GottaGetSwifty/CodableWrappers) | You want property wrappers or key-renaming macros. Enigmatic has no such macros. | +| [BetterCodable](https://github.com/marksands/BetterCodable) | You want ready-made defaults or collections that skip invalid values. | + +In our local plain JSON tests, native tools used less time and memory. +PotentCodables' own parser was much faster than decoding Enigma through +Foundation's `JSONDecoder`. With that same decoder, Enigma and PotentCodables' +`AnyValue` took similar time. Performance depends on what you do. + ## Install Requires Swift 6.3 or newer. Add @@ -19,7 +39,7 @@ Requires Swift 6.3 or newer. Add link the **Enigmatic** product to your target. For SwiftPM targets, use `.product(name: "Enigmatic", package: "Enigmatic")`. -Some APIs require newer Apple runtimes; see the +Some APIs require newer Apple OS versions; see the [compatibility notes](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/#Compatibility). ## Edit a model @@ -38,9 +58,9 @@ tree["scores", 1] = 20 let user = try tree.decode(User.self) // Grace, [10, 20] ``` -String keys and array indices form a path. Missing values return `nil`; -assigning `nil` deletes a child, while `.null` stores a null. Array writes append -at `count`; indices outside `0...count` leave the tree unchanged. +String keys and array indices form a path. Reading a missing value returns `nil`. +Assigning `nil` deletes a child; `.null` stores a null value. Writing at an array's +`count` appends a value. An index outside `0...count` leaves the tree unchanged. ## Merge a patch @@ -51,10 +71,11 @@ let merged = original.merging(patch, policy: .replace) // ["settings": ["enabled": true, "retries": 2]] ``` -Dictionaries merge recursively. `.replace` takes incoming values; `.skip` keeps -existing ones. A policy can also zip, append, or prepend arrays. +Nested dictionaries are merged too. `.replace` takes incoming values; `.skip` +keeps existing ones. For arrays, you can merge elements by index, append, or +prepend. -## Choose a representation +## Choose how a property is encoded ```swift import Foundation @@ -70,32 +91,34 @@ let tree = try Enigma(encode: payload) // ["bytes": "AQID"] let restored = try tree.decode(Payload.self) ``` -Strategies compose with arrays, dictionaries, optionals, and sets. Optional box -properties omit nil keys and decode missing or null keys as nil. `Codec.Either` -tries Right first; `Codec.Each` combines disjoint keyed models. +Strategies also work with arrays, dictionaries, optionals, and sets. An optional +`Codec.Box` leaves out the key when its value is `nil`. Missing or null keys +decode as `nil`. `Codec.Either` tries `Right` before `Left`. `Codec.Each` combines +models that encode as dictionaries with no overlapping keys. ## Serialization limitations -> **Warning:** Foundation's type-specific serialization strategies are not a -> portable representation contract for an Enigma tree. Choose representations -> with `Codec.Box` on the model before building the tree. +> **Warning:** When you encode an Enigma tree, Foundation's date and data settings +> do not choose how those values are written. Use `Codec.Box` on the model to +> choose the format before building the tree. -- `Data` and `Date` stay native in memory, but serializing a tree bypasses +- `Data` and `Date` keep their original types in memory. Encoding a tree bypasses `JSONEncoder.dataEncodingStrategy`, `JSONEncoder.dateEncodingStrategy`, and - PropertyListEncoder's native handling. Data becomes a byte array; Date becomes - seconds since 2001-01-01. Use `@Codec.Box` for Base64 and - `@Codec.Box` for explicit reference-date seconds, or - `Codec.UnixDate<0>` for Unix seconds on supported runtimes. -- `Float` and `Double` can retain `NaN` and positive/negative infinity in memory, - but these values are not JSON numbers. Default JSON encoding and - `asJsonObject` reject them. External JSONEncoder/JSONDecoder instances can use - matching `nonConformingFloatEncodingStrategy`/`nonConformingFloatDecodingStrategy` - settings. Enigma's own model/tree conversions expose no such settings; use - `Codec.Box` with a custom string strategy for an explicit representation. + the usual date/data handling in `PropertyListEncoder`. Data becomes a byte + array; Date becomes seconds since 2001-01-01. Use `@Codec.Box` + for Base64 and `@Codec.Box` for seconds since 2001-01-01, or + `Codec.UnixDate<0>` for Unix seconds on supported OS versions. +- `Float` and `Double` can store `NaN` and positive or negative infinity in memory. + These values are not valid JSON numbers. Default JSON encoding and + `asJsonObject` reject them. When encoding or decoding the tree with your own + `JSONEncoder` and `JSONDecoder`, you can use matching + `nonConformingFloatEncodingStrategy` and `nonConformingFloatDecodingStrategy` + settings. Enigma's model/tree conversions do not have these settings. Use a + custom `Codec.Box` strategy to store these values as strings. See [explicit serialization strategies](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/codingstrategies/#Explicit-portable-representations) -for a complete Data/Date/Float/Double example. `Codec.CanonicalFloat` changes -finite Float representation; it does not make `NaN` or infinity JSON-compatible. +for a complete Data/Date/Float/Double example. `Codec.CanonicalFloat` changes how +finite floats are stored; it does not make `NaN` or infinity valid JSON. [API reference](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/) · [Tree operations](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/treeoperations/) ·