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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -80,3 +83,43 @@ jobs:
path: Enigmatic.doccarchive.tar.gz
compression-level: 0
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:
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 40 additions & 5 deletions Sources/Enigmatic/Enigmatic.docc/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,43 @@ swift test --skip PerformanceTests
swift test -c release --skip PerformanceTests
```

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.
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.

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.

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
<https://swiftutility.github.io/Enigmatic/documentation/enigmatic/>. Enable
Expand All @@ -26,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
Expand Down
3 changes: 3 additions & 0 deletions Sources/Enigmatic/Enigmatic.docc/Serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion Sources/Enigmatic/Internal/Extensions/Enigma+Utility.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import CoreFoundation

extension Enigma {
func collectPins(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import CoreFoundation
import Foundation
#if canImport(CoreFoundation)
import CoreFoundation
#endif

protocol EnigmaConvertibleObject: EnigmaConvertible, AnyObject {}

Expand Down Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion Tests/EnigmaticTests/PerformanceTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@testable import Enigmatic
import Foundation
import CoreFoundation
import XCTest

final class PerformanceTests: XCTestCase {
Expand Down
23 changes: 23 additions & 0 deletions Tests/EnigmaticTests/SerializationContractTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading