From 1b72caa90671cf9b9f3a33d276d76fddb5a2c798 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 12:53:12 +0200 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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/) · From aa81d85822831d1992d5d8e73a3bea4336b81e3f Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 16:02:25 +0200 Subject: [PATCH 06/12] Support Windows alongside Linux --- Sources/Enigmatic/Enigmatic.docc/Enigmatic.md | 7 ++++-- .../Primitives/EnigmaConvertibleObject.swift | 8 +++++-- .../Codec/Strategies/Codec+PathURL.swift | 22 ++++++++++++------- Tests/EnigmaticTests/CodableTests.swift | 10 ++++++--- .../SerializationContractTests.swift | 16 +++++++++----- .../StrategyContractTests.swift | 2 +- Tests/EnigmaticTests/Utility/ObjectiveC.swift | 2 +- 7 files changed, 44 insertions(+), 23 deletions(-) diff --git a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md index fd69413..11d0cee 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md +++ b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md @@ -10,7 +10,7 @@ model, then decode the result. ``Codec`` provides reusable coding strategies tha compose through a property wrapper. The package requires Swift 6.3. Availability annotations on individual declarations -specify Apple runtime requirements; Linux uses the toolchain runtime. +specify Apple runtime requirements; Linux and Windows use the toolchain runtime. ## Serialization limitations @@ -26,6 +26,9 @@ for date units, and a custom string strategy for non-finite numbers. See ## Compatibility +The package supports macOS, iOS, tvOS, watchOS, visionOS, Linux, and Windows. +Other platforms produce a compile-time error instead of using a fallback implementation. + The manifest leaves deployment targets at SwiftPM's defaults. Some APIs have higher runtime requirements: @@ -34,7 +37,7 @@ higher runtime requirements: - `Codec.UnixDate` and `Codec.UnixIntDate`: Apple OS 26. Use the named seconds/milliseconds strategies on older systems. -Linux uses the toolchain runtime without Apple OS availability limits. +Linux and Windows use the toolchain runtime without Apple OS availability limits. ## Topics diff --git a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift index 977efdf..1886e0e 100644 --- a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift +++ b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift @@ -43,10 +43,14 @@ extension NSValue: EnigmaConvertibleObject { } func extract(seed: consuming T) -> T { -#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) withUnsafeMutablePointer(to: &seed) { getValue($0, size: MemoryLayout.size) } -#else +#elseif os(Linux) + withUnsafeMutablePointer(to: &seed) { getValue($0) } +#elseif os(Windows) withUnsafeMutablePointer(to: &seed) { getValue($0) } +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif return consume seed } diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift index e9cd90d..afcc6b4 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift @@ -1,5 +1,5 @@ import Foundation -#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) import System #endif @@ -11,7 +11,7 @@ extension Codec { @inlinable public static func decode(decoder: some Decoder) throws -> BoxedValue { let value = try String(from: decoder) -#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { guard let url = URL(filePath: FilePath(value), directoryHint: .inferFromPath) else { throw DecodingError.dataCorrupted(DecodingError.Context( @@ -23,10 +23,12 @@ extension Codec { } else { return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/")) } +#elseif os(Linux) + 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("/")) +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif } @@ -35,16 +37,20 @@ extension Codec { /// - 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 os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) 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) } +#elseif os(Linux) + try value.path(percentEncoded: false).encode(to: encoder) +#elseif os(Windows) + var path = value.path + if value.hasDirectoryPath && !path.hasSuffix("/") { path += "/" } + try path.encode(to: encoder) +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif } diff --git a/Tests/EnigmaticTests/CodableTests.swift b/Tests/EnigmaticTests/CodableTests.swift index 3b587e5..0785c39 100644 --- a/Tests/EnigmaticTests/CodableTests.swift +++ b/Tests/EnigmaticTests/CodableTests.swift @@ -107,11 +107,15 @@ final class CodableTests: XCTestCase { func testAnyHashable() throws { let key1 = "café" let key2 = "cafe\u{301}" - #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) XCTAssertNotEqual(key1 as NSString, key2 as NSString) - #else +#elseif os(Linux) XCTAssertEqual(key1 as NSString, key2 as NSString) - #endif +#elseif os(Windows) + XCTAssertEqual(key1 as NSString, key2 as NSString) +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") +#endif XCTAssertEqual(key1, key2) let enigma1 = try Enigma(cast: [key1: 1]) let enigma2 = try Enigma(cast: [key2: 1]) diff --git a/Tests/EnigmaticTests/SerializationContractTests.swift b/Tests/EnigmaticTests/SerializationContractTests.swift index dac2ad2..e78e08e 100644 --- a/Tests/EnigmaticTests/SerializationContractTests.swift +++ b/Tests/EnigmaticTests/SerializationContractTests.swift @@ -386,12 +386,14 @@ final class SerializationContractTests: XCTestCase { func boxed(_ value: T, encoding: String) -> NSValue { encoding.withCString { type in withUnsafeBytes(of: value) { bytes in -#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) NSValue(bytes: bytes.baseAddress!, objCType: type) -#else - // Corelibs Foundation cannot construct NSValue with t/T encodings. - // Supply the same encoding and bytes through NSValue's primitive API. +#elseif os(Linux) + BitInt128Value(bytes: bytes.baseAddress!, objCType: type) +#elseif os(Windows) BitInt128Value(bytes: bytes.baseAddress!, objCType: type) +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif } } @@ -475,7 +477,7 @@ final class SerializationContractTests: XCTestCase { assertDecodingError("dataCorrupted", path: ["outer"]) { _ = try Enigma(cast: ["outer": unsupported]) } -#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) // NSString can distinguish keys that Swift String considers canonically equal. let colliding = NSDictionary(objects: [1, 2], forKeys: ["café" as NSString, "cafe\u{301}" as NSString]) XCTAssertEqual(colliding.count, 2) @@ -589,7 +591,9 @@ final class SerializationContractTests: XCTestCase { } } -#if !(os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)) +#if os(Linux) || os(Windows) +// Corelibs Foundation cannot construct NSValue with t/T encodings. +// Expose the encoding and bytes through NSValue's primitive API instead. private final class BitInt128Value: NSValue, @unchecked Sendable { private let storage: Data private let encoding: UnsafeMutablePointer diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index 915e680..59693ce 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -309,7 +309,7 @@ final class StrategyContractTests: XCTestCase { } #endif -#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) func testPathURLKeepsFilePathNormalization() throws { for (input, path) in [ diff --git a/Tests/EnigmaticTests/Utility/ObjectiveC.swift b/Tests/EnigmaticTests/Utility/ObjectiveC.swift index d4af22f..e99cc41 100644 --- a/Tests/EnigmaticTests/Utility/ObjectiveC.swift +++ b/Tests/EnigmaticTests/Utility/ObjectiveC.swift @@ -1,4 +1,4 @@ -#if !(os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)) +#if os(Linux) || os(Windows) @inline(__always) func autoreleasepool(_ body: () throws -> T) rethrows -> T { return try body() From 13453c05c772bc510ac1c5cc33fe7fc6f6b1ac53 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 16:15:53 +0200 Subject: [PATCH 07/12] Retain anyAppleOS in platform compilation guards --- .../Internal/Primitives/EnigmaConvertibleObject.swift | 2 +- .../Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift | 6 +++--- Tests/EnigmaticTests/CodableTests.swift | 2 +- Tests/EnigmaticTests/SerializationContractTests.swift | 4 ++-- Tests/EnigmaticTests/StrategyContractTests.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift index 1886e0e..4ae90ef 100644 --- a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift +++ b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift @@ -43,7 +43,7 @@ extension NSValue: EnigmaConvertibleObject { } func extract(seed: consuming T) -> T { -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) withUnsafeMutablePointer(to: &seed) { getValue($0, size: MemoryLayout.size) } #elseif os(Linux) withUnsafeMutablePointer(to: &seed) { getValue($0) } diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift index afcc6b4..0baf94e 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift @@ -1,5 +1,5 @@ import Foundation -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) import System #endif @@ -11,7 +11,7 @@ extension Codec { @inlinable public static func decode(decoder: some Decoder) throws -> BoxedValue { let value = try String(from: decoder) -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { guard let url = URL(filePath: FilePath(value), directoryHint: .inferFromPath) else { throw DecodingError.dataCorrupted(DecodingError.Context( @@ -37,7 +37,7 @@ extension Codec { /// - Complexity: O(n) in the URL or path string length. @inlinable public static func encode(value: BoxedValue, encoder: some Encoder) throws { -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { try value.path(percentEncoded: false).encode(to: encoder) } else { diff --git a/Tests/EnigmaticTests/CodableTests.swift b/Tests/EnigmaticTests/CodableTests.swift index 0785c39..a774496 100644 --- a/Tests/EnigmaticTests/CodableTests.swift +++ b/Tests/EnigmaticTests/CodableTests.swift @@ -107,7 +107,7 @@ final class CodableTests: XCTestCase { func testAnyHashable() throws { let key1 = "café" let key2 = "cafe\u{301}" -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) XCTAssertNotEqual(key1 as NSString, key2 as NSString) #elseif os(Linux) XCTAssertEqual(key1 as NSString, key2 as NSString) diff --git a/Tests/EnigmaticTests/SerializationContractTests.swift b/Tests/EnigmaticTests/SerializationContractTests.swift index e78e08e..0a17005 100644 --- a/Tests/EnigmaticTests/SerializationContractTests.swift +++ b/Tests/EnigmaticTests/SerializationContractTests.swift @@ -386,7 +386,7 @@ final class SerializationContractTests: XCTestCase { func boxed(_ value: T, encoding: String) -> NSValue { encoding.withCString { type in withUnsafeBytes(of: value) { bytes in -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) NSValue(bytes: bytes.baseAddress!, objCType: type) #elseif os(Linux) BitInt128Value(bytes: bytes.baseAddress!, objCType: type) @@ -477,7 +477,7 @@ final class SerializationContractTests: XCTestCase { assertDecodingError("dataCorrupted", path: ["outer"]) { _ = try Enigma(cast: ["outer": unsupported]) } -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) // NSString can distinguish keys that Swift String considers canonically equal. let colliding = NSDictionary(objects: [1, 2], forKeys: ["café" as NSString, "cafe\u{301}" as NSString]) XCTAssertEqual(colliding.count, 2) diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index 59693ce..915e680 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -309,7 +309,7 @@ final class StrategyContractTests: XCTestCase { } #endif -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#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 { for (input, path) in [ From 54d33e2d806538a0bd3860750fe4a22d0f4b9a0b Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 16:19:07 +0200 Subject: [PATCH 08/12] Lower the minimum Swift version to 6.2.3 --- .github/workflows/benchmarks.yml | 6 ++--- .github/workflows/checks.yml | 10 ++++---- AGENTS.md | 4 ++-- Package.swift | 21 +++-------------- README.md | 4 ++-- .../Enigmatic/Enigmatic.docc/Development.md | 23 ++++++++++++++++--- Sources/Enigmatic/Enigmatic.docc/Enigmatic.md | 2 +- .../Codec/Strategies/Codec+UnixDate.swift | 2 +- .../Codec/Strategies/Codec+UnixIntDate.swift | 2 +- .../Codec+UnixIntMillisecondsDate.swift | 6 ++++- .../Strategies/Codec+UnixIntSecondsDate.swift | 6 ++++- .../Codec+UnixMillisecondsDate.swift | 6 ++++- .../Strategies/Codec+UnixSecondsDate.swift | 6 ++++- Tests/EnigmaticTests/CodableTests.swift | 2 +- .../StrategyContractTests.swift | 2 +- 15 files changed, 60 insertions(+), 42 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 60aeb11..36e309b 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -14,13 +14,13 @@ jobs: benchmarks: runs-on: macos-26 env: - DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer steps: - uses: actions/checkout@v6 - - name: Verify Swift 6.3.3 + - name: Verify Swift 6.2.3 run: | swift --version - swift --version | grep -Eq 'Swift version 6\.3\.3([[:space:]]|$)' + swift --version | grep -Eq 'Swift version 6\.2\.3([[:space:]]|$)' - name: Release benchmarks without instrumentation shell: bash run: | diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index a7b9e86..c8ebb05 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -27,16 +27,16 @@ jobs: configuration: [debug, release] include: - os: ubuntu-24.04 - container: swift:6.3.3 + container: swift:6.2.3 steps: - uses: actions/checkout@v6 - - name: Select Xcode 26.6 + - name: Select Xcode 26.3 if: runner.os == 'macOS' - run: echo 'DEVELOPER_DIR=/Applications/Xcode_26.6.app/Contents/Developer' >> "$GITHUB_ENV" - - name: Verify Swift 6.3.3 + run: echo 'DEVELOPER_DIR=/Applications/Xcode_26.3.app/Contents/Developer' >> "$GITHUB_ENV" + - name: Verify Swift 6.2.3 run: | swift --version - swift --version | grep -Eq 'Swift version 6\.3\.3([[:space:]]|$)' + swift --version | grep -Eq 'Swift version 6\.2\.3([[:space:]]|$)' - name: Functional tests run: swift test -c ${{ matrix.configuration }} --skip PerformanceTests - name: Coverage gate tests diff --git a/AGENTS.md b/AGENTS.md index 77b9916..5d96e53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,8 @@ # Project guidance Enigmatic is a dependency-free SwiftPM library for Codable value trees and -composable coding strategies. It requires Swift 6.3; CI uses 6.3.3 on macOS 26 -and Ubuntu 24.04. +composable coding strategies. It requires Swift 6.2.3; Checks uses 6.2.3 on +macOS 26 and Ubuntu 24.04. Release uses 6.3.3 to build the previous API baseline. Follow the existing two-space Swift style. Preserve typed throws, runtime availability guards, and visibility required by `@inlinable` code. Update the diff --git a/Package.swift b/Package.swift index c994e44..018f69d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.3 +// swift-tools-version:6.2.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -9,25 +9,10 @@ let package = Package( .library(name: "Enigmatic", targets: ["Enigmatic"]), ], targets: [ - .target( - name: "Enigmatic", - swiftSettings: SwiftSetting.allSettings - ), + .target(name: "Enigmatic"), .testTarget( name: "EnigmaticTests", - dependencies: ["Enigmatic"], - swiftSettings: SwiftSetting.allSettings + dependencies: ["Enigmatic"] ), ] ) - -extension SwiftSetting { -#if compiler(<6.4) - static let anyAppleOSAvailability: [Self] = [.enableExperimentalFeature("AnyAppleOSAvailability")] -#else - static let anyAppleOSAvailability: [Self] = [] -#endif - static let allSettings: [Self] = [ - anyAppleOSAvailability, - ].flatMap(\.self) -} diff --git a/README.md b/README.md index bf39472..86f8523 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Checks](https://github.com/SwiftUtility/Enigmatic/actions/workflows/checks.yml/badge.svg?branch=main&event=push)](https://github.com/SwiftUtility/Enigmatic/actions/workflows/checks.yml) [![Release](https://img.shields.io/github/v/release/SwiftUtility/Enigmatic)](https://github.com/SwiftUtility/Enigmatic/releases/latest) -[![Swift](https://img.shields.io/badge/Swift-6.3%2B-orange?logo=swift)](Package.swift) +[![Swift](https://img.shields.io/badge/Swift-6.2.3%2B-orange?logo=swift)](Package.swift) [![SwiftPM](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen)](#install) [![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) @@ -34,7 +34,7 @@ Foundation's `JSONDecoder`. With that same decoder, Enigma and PotentCodables' ## Install -Requires Swift 6.3 or newer. Add +Requires Swift 6.2.3 or newer. Add [SwiftUtility/Enigmatic](https://github.com/SwiftUtility/Enigmatic) in Xcode and link the **Enigmatic** product to your target. For SwiftPM targets, use `.product(name: "Enigmatic", package: "Enigmatic")`. diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index 7b3c70f..2d3f606 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -7,6 +7,19 @@ swift test --skip PerformanceTests swift test -c release --skip PerformanceTests ``` +To check Linux compatibility with the minimum compiler version from macOS, run +the functional suite in the official Swift Docker image. The source mount is +read-only; build artifacts stay in the container: + +```sh +docker run --rm -v "$PWD:/package:ro" -w /package swift:6.2.3 \ + swift test --scratch-path /tmp/enigmatic-build --skip PerformanceTests +docker run --rm -v "$PWD:/package:ro" -w /package swift:6.2.3 \ + swift test --scratch-path /tmp/enigmatic-build -c release --skip PerformanceTests +``` + +Apple-specific branches still require validation on macOS with Swift 6.2.3. + 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. @@ -32,9 +45,9 @@ 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. -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 Checks workflow uses Swift 6.2.3 from Xcode 26.3 on macOS 26 and the +`swift:6.2.3` container on Ubuntu 24.04. Benchmarks uses the same Xcode version. +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 @@ -44,6 +57,10 @@ 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. +Release validation uses Swift 6.3.3 from Xcode 26.6 and the `swift:6.3.3` +container so the API gate can build previous releases requiring Swift 6.3. +Checks validates compatibility with the minimum supported compiler. + 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 diff --git a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md index 11d0cee..257b461 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md +++ b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md @@ -9,7 +9,7 @@ dates and data. Encode a model into a tree, modify selected paths, merge a secon model, then decode the result. ``Codec`` provides reusable coding strategies that compose through a property wrapper. -The package requires Swift 6.3. Availability annotations on individual declarations +The package requires Swift 6.2.3. Availability annotations on individual declarations specify Apple runtime requirements; Linux and Windows use the toolchain runtime. ## Serialization limitations diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift index 7178b0b..58f832a 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift @@ -4,7 +4,7 @@ extension Codec { /// Encodes seconds since 1970 multiplied by 10 to the given scale as Double. /// /// Rejects non-finite values, invalid scale factors and unrepresentable results. - @available(anyAppleOS 26, *) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) public enum UnixDate: Hashable, DecodeStrategy, EncodeStrategy { /// Decodes scaled seconds since 1970 into a `Date`. /// - Throws: `DecodingError.dataCorrupted` for non-finite input or an invalid scale. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift index 2f3510d..6834e41 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift @@ -4,7 +4,7 @@ extension Codec { /// Encodes seconds since 1970 multiplied by 10 to the given scale as Int, truncating toward zero. /// /// Rejects non-finite values, invalid scale factors and unrepresentable results. - @available(anyAppleOS 26, *) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) public enum UnixIntDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes an integer scaled timestamp into a date. /// - Throws: `DecodingError.dataCorrupted` when the scale is invalid or the result is non-finite. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift index 0ea4fc6..0297dbd 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift @@ -3,7 +3,11 @@ import Foundation extension Codec { /// Encodes milliseconds since 1970 as a truncated integer. /// Prefer `UnixIntDate<3>` when the deployment target supports it. - @available(anyAppleOS, deprecated: 26.0) + @available(macOS, deprecated: 26.0) + @available(iOS, deprecated: 26.0) + @available(tvOS, deprecated: 26.0) + @available(watchOS, deprecated: 26.0) + @available(visionOS, deprecated: 26.0) public enum UnixIntMillisecondsDate: Hashable, DecodeStrategy, EncodeStrategy { /// Decodes integer milliseconds since 1970 into a date. /// - Complexity: O(1); the operation performs a fixed number of numeric conversions and arithmetic operations. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift index 6983e20..6633388 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift @@ -3,7 +3,11 @@ import Foundation extension Codec { /// Encodes seconds since 1970 as a truncated integer. /// Prefer `UnixIntDate<0>` when the deployment target supports it. - @available(anyAppleOS, deprecated: 26.0) + @available(macOS, deprecated: 26.0) + @available(iOS, deprecated: 26.0) + @available(tvOS, deprecated: 26.0) + @available(watchOS, deprecated: 26.0) + @available(visionOS, deprecated: 26.0) public enum UnixIntSecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes integer seconds since 1970 into a date. /// - Complexity: O(1); the operation performs a fixed number of numeric conversions and arithmetic operations. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift index 351ce27..edd3d83 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift @@ -3,7 +3,11 @@ import Foundation extension Codec { /// Encodes seconds since 1970 multiplied by 1,000 as a floating-point value. /// Prefer `UnixDate<3>` when the deployment target supports it. - @available(anyAppleOS, deprecated: 26.0) + @available(macOS, deprecated: 26.0) + @available(iOS, deprecated: 26.0) + @available(tvOS, deprecated: 26.0) + @available(watchOS, deprecated: 26.0) + @available(visionOS, deprecated: 26.0) public enum UnixMillisecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes finite milliseconds since 1970 into a date. /// - Throws: `DecodingError.dataCorrupted` for non-finite input. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift index 228f2a4..448b9cd 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift @@ -3,7 +3,11 @@ import Foundation extension Codec { /// Encodes seconds since 1970 as a floating-point value. /// Prefer `UnixDate<0>` when the deployment target supports it. - @available(anyAppleOS, deprecated: 26.0) + @available(macOS, deprecated: 26.0) + @available(iOS, deprecated: 26.0) + @available(tvOS, deprecated: 26.0) + @available(watchOS, deprecated: 26.0) + @available(visionOS, deprecated: 26.0) public enum UnixSecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes finite seconds since 1970 into a date. /// - Throws: `DecodingError.dataCorrupted` for non-finite input. diff --git a/Tests/EnigmaticTests/CodableTests.swift b/Tests/EnigmaticTests/CodableTests.swift index a774496..87a7fe5 100644 --- a/Tests/EnigmaticTests/CodableTests.swift +++ b/Tests/EnigmaticTests/CodableTests.swift @@ -178,7 +178,7 @@ final class CodableTests: XCTestCase { XCTAssertEqual(direct, indirect) } - @available(anyAppleOS 26, *) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) func testUnixTime() throws { struct CodecDates: Codable, Equatable { @Codec.Box> diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index 915e680..111e3cc 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -741,7 +741,7 @@ final class StrategyContractTests: XCTestCase { } } - @available(anyAppleOS 26, *) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) func testScaledDatesAndTruncation() throws { let date = Date(timeIntervalSince1970: -123.25) try roundTrip(Codec.UnixDate<0>.self, date, expected: .double(-123.25)) From 95bb36964db39f9f73f77329cee41c2723687d3b Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 16:29:25 +0200 Subject: [PATCH 09/12] Update CI compiler versions --- .github/workflows/benchmarks.yml | 4 ++-- .github/workflows/checks.yml | 9 +++++++-- AGENTS.md | 5 +++-- Sources/Enigmatic/Enigmatic.docc/Development.md | 7 ++++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 36e309b..6f35395 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -17,10 +17,10 @@ jobs: DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer steps: - uses: actions/checkout@v6 - - name: Verify Swift 6.2.3 + - name: Verify Swift 6.2.4 run: | swift --version - swift --version | grep -Eq 'Swift version 6\.2\.3([[:space:]]|$)' + swift --version | grep -Eq 'Swift version 6\.2\.4([[:space:]]|$)' - name: Release benchmarks without instrumentation shell: bash run: | diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c8ebb05..5720d35 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -26,17 +26,22 @@ jobs: os: [macos-26, ubuntu-24.04] configuration: [debug, release] include: + - os: macos-26 + swift-version: 6.2.4 - os: ubuntu-24.04 container: swift:6.2.3 + swift-version: 6.2.3 steps: - uses: actions/checkout@v6 - name: Select Xcode 26.3 if: runner.os == 'macOS' run: echo 'DEVELOPER_DIR=/Applications/Xcode_26.3.app/Contents/Developer' >> "$GITHUB_ENV" - - name: Verify Swift 6.2.3 + - name: Verify Swift ${{ matrix.swift-version }} + env: + EXPECTED_SWIFT_VERSION: ${{ matrix.swift-version }} run: | swift --version - swift --version | grep -Eq 'Swift version 6\.2\.3([[:space:]]|$)' + swift --version | grep -F "Swift version $EXPECTED_SWIFT_VERSION " - name: Functional tests run: swift test -c ${{ matrix.configuration }} --skip PerformanceTests - name: Coverage gate tests diff --git a/AGENTS.md b/AGENTS.md index 5d96e53..5e87f39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,9 @@ # Project guidance Enigmatic is a dependency-free SwiftPM library for Codable value trees and -composable coding strategies. It requires Swift 6.2.3; Checks uses 6.2.3 on -macOS 26 and Ubuntu 24.04. Release uses 6.3.3 to build the previous API baseline. +composable coding strategies. It requires Swift 6.2.3; Checks uses 6.2.4 on +macOS 26 and 6.2.3 on Ubuntu 24.04. Release uses 6.3.3 to build the previous +API baseline. Follow the existing two-space Swift style. Preserve typed throws, runtime availability guards, and visibility required by `@inlinable` code. Update the diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index 2d3f606..b8043e0 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -45,8 +45,9 @@ 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. -The Checks workflow uses Swift 6.2.3 from Xcode 26.3 on macOS 26 and the -`swift:6.2.3` container on Ubuntu 24.04. Benchmarks uses the same Xcode version. +The Checks workflow uses Swift 6.2.4 from Xcode 26.3 on macOS 26 and the +`swift:6.2.3` container on Ubuntu 24.04 to test the minimum supported compiler. +Benchmarks uses the same Xcode version as Checks. 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 @@ -59,7 +60,7 @@ These jobs do not change the macOS/Linux checks or the Release workflow. Release validation uses Swift 6.3.3 from Xcode 26.6 and the `swift:6.3.3` container so the API gate can build previous releases requiring Swift 6.3. -Checks validates compatibility with the minimum supported compiler. +The Linux Checks jobs validate compatibility with the minimum supported compiler. 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 From b2697b5047ecb266a6cc1f645bd535be98aab0bf Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 17:01:11 +0200 Subject: [PATCH 10/12] Share identical Linux and Windows branches --- .../Internal/Primitives/EnigmaConvertibleObject.swift | 4 +--- Tests/EnigmaticTests/CodableTests.swift | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift index 4ae90ef..eeef83a 100644 --- a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift +++ b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift @@ -45,9 +45,7 @@ extension NSValue: EnigmaConvertibleObject { func extract(seed: consuming T) -> T { #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) withUnsafeMutablePointer(to: &seed) { getValue($0, size: MemoryLayout.size) } -#elseif os(Linux) - withUnsafeMutablePointer(to: &seed) { getValue($0) } -#elseif os(Windows) +#elseif os(Linux) || os(Windows) withUnsafeMutablePointer(to: &seed) { getValue($0) } #else #error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") diff --git a/Tests/EnigmaticTests/CodableTests.swift b/Tests/EnigmaticTests/CodableTests.swift index 87a7fe5..f45c3c0 100644 --- a/Tests/EnigmaticTests/CodableTests.swift +++ b/Tests/EnigmaticTests/CodableTests.swift @@ -109,9 +109,7 @@ final class CodableTests: XCTestCase { let key2 = "cafe\u{301}" #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) XCTAssertNotEqual(key1 as NSString, key2 as NSString) -#elseif os(Linux) - XCTAssertEqual(key1 as NSString, key2 as NSString) -#elseif os(Windows) +#elseif os(Linux) || os(Windows) XCTAssertEqual(key1 as NSString, key2 as NSString) #else #error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") From 35a897ff0fa90af4d321eb0e2f72fe4583da2e09 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 17:10:56 +0200 Subject: [PATCH 11/12] Gate anyAppleOS availability on Swift 6.4 --- .../Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift | 4 ++++ .../Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift index 58f832a..ffb5e35 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift @@ -4,7 +4,11 @@ extension Codec { /// Encodes seconds since 1970 multiplied by 10 to the given scale as Double. /// /// Rejects non-finite values, invalid scale factors and unrepresentable results. +#if compiler(<6.4) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else + @available(anyAppleOS 26, *) +#endif public enum UnixDate: Hashable, DecodeStrategy, EncodeStrategy { /// Decodes scaled seconds since 1970 into a `Date`. /// - Throws: `DecodingError.dataCorrupted` for non-finite input or an invalid scale. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift index 6834e41..b4e7ea1 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift @@ -4,7 +4,11 @@ extension Codec { /// Encodes seconds since 1970 multiplied by 10 to the given scale as Int, truncating toward zero. /// /// Rejects non-finite values, invalid scale factors and unrepresentable results. +#if compiler(<6.4) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else + @available(anyAppleOS 26, *) +#endif public enum UnixIntDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes an integer scaled timestamp into a date. /// - Throws: `DecodingError.dataCorrupted` when the scale is invalid or the result is non-finite. From d1327c4f5c5a83d03258865c1ee7668dfee8586d Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 17:32:52 +0200 Subject: [PATCH 12/12] Apply availability guards consistently across strategies and tests --- .../Codec/Strategies/Codec+UnixIntMillisecondsDate.swift | 4 ++++ .../Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift | 4 ++++ .../Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift | 4 ++++ .../Public/Codec/Strategies/Codec+UnixSecondsDate.swift | 4 ++++ Tests/EnigmaticTests/CodableTests.swift | 4 ++++ Tests/EnigmaticTests/SerializationContractTests.swift | 4 +--- Tests/EnigmaticTests/StrategyContractTests.swift | 4 ++++ 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift index 0297dbd..d4bd718 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift @@ -3,11 +3,15 @@ import Foundation extension Codec { /// Encodes milliseconds since 1970 as a truncated integer. /// Prefer `UnixIntDate<3>` when the deployment target supports it. +#if compiler(<6.4) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) +#else + @available(anyAppleOS, deprecated: 26.0) +#endif public enum UnixIntMillisecondsDate: Hashable, DecodeStrategy, EncodeStrategy { /// Decodes integer milliseconds since 1970 into a date. /// - Complexity: O(1); the operation performs a fixed number of numeric conversions and arithmetic operations. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift index 6633388..3ac1093 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift @@ -3,11 +3,15 @@ import Foundation extension Codec { /// Encodes seconds since 1970 as a truncated integer. /// Prefer `UnixIntDate<0>` when the deployment target supports it. +#if compiler(<6.4) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) +#else + @available(anyAppleOS, deprecated: 26.0) +#endif public enum UnixIntSecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes integer seconds since 1970 into a date. /// - Complexity: O(1); the operation performs a fixed number of numeric conversions and arithmetic operations. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift index edd3d83..ffae04e 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift @@ -3,11 +3,15 @@ import Foundation extension Codec { /// Encodes seconds since 1970 multiplied by 1,000 as a floating-point value. /// Prefer `UnixDate<3>` when the deployment target supports it. +#if compiler(<6.4) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) +#else + @available(anyAppleOS, deprecated: 26.0) +#endif public enum UnixMillisecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes finite milliseconds since 1970 into a date. /// - Throws: `DecodingError.dataCorrupted` for non-finite input. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift index 448b9cd..454c0a6 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift @@ -3,11 +3,15 @@ import Foundation extension Codec { /// Encodes seconds since 1970 as a floating-point value. /// Prefer `UnixDate<0>` when the deployment target supports it. +#if compiler(<6.4) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) +#else + @available(anyAppleOS, deprecated: 26.0) +#endif public enum UnixSecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes finite seconds since 1970 into a date. /// - Throws: `DecodingError.dataCorrupted` for non-finite input. diff --git a/Tests/EnigmaticTests/CodableTests.swift b/Tests/EnigmaticTests/CodableTests.swift index f45c3c0..89787ea 100644 --- a/Tests/EnigmaticTests/CodableTests.swift +++ b/Tests/EnigmaticTests/CodableTests.swift @@ -176,7 +176,11 @@ final class CodableTests: XCTestCase { XCTAssertEqual(direct, indirect) } +#if compiler(<6.4) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else + @available(anyAppleOS 26, *) +#endif func testUnixTime() throws { struct CodecDates: Codable, Equatable { @Codec.Box> diff --git a/Tests/EnigmaticTests/SerializationContractTests.swift b/Tests/EnigmaticTests/SerializationContractTests.swift index 0a17005..a91b1d5 100644 --- a/Tests/EnigmaticTests/SerializationContractTests.swift +++ b/Tests/EnigmaticTests/SerializationContractTests.swift @@ -388,9 +388,7 @@ final class SerializationContractTests: XCTestCase { withUnsafeBytes(of: value) { bytes in #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) NSValue(bytes: bytes.baseAddress!, objCType: type) -#elseif os(Linux) - BitInt128Value(bytes: bytes.baseAddress!, objCType: type) -#elseif os(Windows) +#elseif os(Linux) || os(Windows) BitInt128Value(bytes: bytes.baseAddress!, objCType: type) #else #error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index 111e3cc..a4fd833 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -741,7 +741,11 @@ final class StrategyContractTests: XCTestCase { } } +#if compiler(<6.4) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else + @available(anyAppleOS 26, *) +#endif func testScaledDatesAndTruncation() throws { let date = Date(timeIntervalSince1970: -123.25) try roundTrip(Codec.UnixDate<0>.self, date, expected: .double(-123.25))