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
6 changes: 3 additions & 3 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.4
run: |
swift --version
swift --version | grep -Eq 'Swift version 6\.3\.3([[:space:]]|$)'
swift --version | grep -Eq 'Swift version 6\.2\.4([[:space:]]|$)'
- name: Release benchmarks without instrumentation
shell: bash
run: |
Expand Down
15 changes: 10 additions & 5 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.3.3
container: swift:6.2.3
swift-version: 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 ${{ matrix.swift-version }}
env:
EXPECTED_SWIFT_VERSION: ${{ matrix.swift-version }}
run: |
swift --version
swift --version | grep -Eq 'Swift version 6\.3\.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
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.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.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
Expand Down
21 changes: 3 additions & 18 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")`.
Expand Down
24 changes: 21 additions & 3 deletions Sources/Enigmatic/Enigmatic.docc/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -32,9 +45,10 @@ 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.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
the documentation as `Enigmatic.doccarchive.tar.gz`, preserving DocC filenames
Expand All @@ -44,6 +58,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.
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
<https://swiftutility.github.io/Enigmatic/documentation/enigmatic/>. Enable
Expand Down
9 changes: 6 additions & 3 deletions Sources/Enigmatic/Enigmatic.docc/Enigmatic.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ 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
specify Apple runtime requirements; Linux uses the toolchain runtime.
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

Expand All @@ -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:

Expand All @@ -34,7 +37,7 @@ higher runtime requirements:
- `Codec.UnixDate<scale>` and `Codec.UnixIntDate<scale>`: 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ extension NSValue: EnigmaConvertibleObject {
func extract<T: FixedWidthInteger>(seed: consuming T) -> T {
#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
withUnsafeMutablePointer(to: &seed) { getValue($0, size: MemoryLayout<T>.size) }
#else
#elseif os(Linux) || os(Windows)
withUnsafeMutablePointer(to: &seed) { getValue($0) }
#else
#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.")
#endif
return consume seed
}
Expand Down
18 changes: 12 additions & 6 deletions Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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(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 {
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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, *)
Comment thread
dmitriyborodko marked this conversation as resolved.
#else
@available(anyAppleOS 26, *)
#endif
public enum UnixDate<let scale: Int>: Hashable, DecodeStrategy, EncodeStrategy {
/// Decodes scaled seconds since 1970 into a `Date`.
/// - Throws: `DecodingError.dataCorrupted` for non-finite input or an invalid scale.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, *)
Comment thread
dmitriyborodko marked this conversation as resolved.
#else
@available(anyAppleOS 26, *)
#endif
public enum UnixIntDate<let scale: Int>: 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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.
Expand Down
12 changes: 9 additions & 3 deletions Tests/EnigmaticTests/CodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,13 @@ 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(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
XCTAssertNotEqual(key1 as NSString, key2 as NSString)
#else
#elseif os(Linux) || os(Windows)
XCTAssertEqual(key1 as NSString, key2 as NSString)
#endif
#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])
Expand Down Expand Up @@ -174,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, *)
Comment thread
dmitriyborodko marked this conversation as resolved.
#else
@available(anyAppleOS 26, *)
#endif
func testUnixTime() throws {
struct CodecDates: Codable, Equatable {
@Codec.Box<Codec.UnixDate<0>>
Expand Down
10 changes: 6 additions & 4 deletions Tests/EnigmaticTests/SerializationContractTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,10 @@ 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)
#else
// Corelibs Foundation cannot construct NSValue with t/T encodings.
// Supply the same encoding and bytes through NSValue's primitive API.
#elseif os(Linux) || os(Windows)
BitInt128Value(bytes: bytes.baseAddress!, objCType: type)
#else
#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.")
#endif
}
}
Expand Down Expand Up @@ -589,7 +589,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<CChar>
Expand Down
4 changes: 4 additions & 0 deletions Tests/EnigmaticTests/StrategyContractTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,11 @@ final class StrategyContractTests: XCTestCase {
}
}

#if compiler(<6.4)
@available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *)
Comment thread
dmitriyborodko marked this conversation as resolved.
#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))
Expand Down
2 changes: 1 addition & 1 deletion Tests/EnigmaticTests/Utility/ObjectiveC.swift
Original file line number Diff line number Diff line change
@@ -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<T>(_ body: () throws -> T) rethrows -> T {
return try body()
Expand Down
Loading