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
39 changes: 25 additions & 14 deletions .github/workflows/swift-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ jobs:
matrix:
# 'system' uses the Xcode toolchain preinstalled on macos-latest,
# so breakage on current compilers surfaces even while the pinned
# version keeps validating the minimum supported toolchain.
# The pinned toolchain must run on an older image: Swift 6.0.2
# cannot compile against the newer SDKs of current Xcode.
# version keeps validating the minimum supported toolchain (6.1,
# the floor imposed by package traits). The pinned toolchain runs
# on macos-15, whose default Xcode SDK matches the Swift 6.1 era.
include:
- swift: '6.0.2'
runner: macos-14
- swift: '6.1'
runner: macos-15
- swift: 'system'
runner: macos-latest
steps:
Expand All @@ -40,13 +40,18 @@ jobs:
uses: swift-actions/setup-swift@v2
with:
swift-version: ${{ matrix.swift }}

- name: Build
run: swift build -v

- name: Run tests
run: swift test -v


# MacroTesting links swift-syntax as a library, which is incompatible
# with SwiftPM's prebuilt swift-syntax binaries — build from source.
- name: Run tests (Derive trait)
run: swift test -v --traits Derive --disable-experimental-prebuilts

- name: Run tests (release)
run: swift test -c release -v

Expand All @@ -55,17 +60,23 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# 'latest' tracks the newest stable Swift release image.
swift: ['6.0.2', 'latest']
# 'latest' tracks the newest stable Swift release image; 6.1 is the
# minimum supported toolchain (the floor imposed by package traits).
swift: ['6.1', 'latest']
container: swift:${{ matrix.swift }}
steps:
- uses: actions/checkout@v4

- name: Build
run: swift build -v

- name: Run tests
run: swift test --parallel


# MacroTesting links swift-syntax as a library, which is incompatible
# with SwiftPM's prebuilt swift-syntax binaries — build from source.
- name: Run tests (Derive trait)
run: swift test --parallel --traits Derive --disable-experimental-prebuilts

- name: Run tests (release)
run: swift test -c release --parallel
run: swift test -c release --parallel
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## [2.2.0] - Unreleased

### Added: `RandomGenerable` and the opt-in `@RandomGenerable` derivation macro

- New `RandomGenerable` protocol (always available, no trait required): a type exposes
a canonical `static var generator`. The standard types come pre-conformed with
simple, documented defaults — fixed-width integers (full range), `Double`/`Float`
(`0...1`), `Bool`, `String` (8 alphanumerics), `Character` (ASCII letter),
`Optional` (`nil` half the time), and `Array` (0–10 elements).
- New `Derive` package trait (off by default). Enabling it unlocks the
`@RandomGenerable` macro, which derives a `RandomGenerable` conformance for structs
(via the memberwise initializer) and enums (uniform case selection, associated
values included), plus the `@Gen(...)` attribute to override the generator for a
single stored property. Derived generators are ordinary generators: all combinators
and seeded RNGs apply.
- The macro's swift-syntax dependency is only built by consumers who enable the
trait; with the trait off (the default), SwiftRandomKit remains a
zero-build-cost dependency. swift-syntax is still *fetched* during dependency
resolution on current SwiftPM versions, but never compiled.

### Changed

- `swift-tools-version` bumped from 6.0 to 6.1, required for package traits.
Consumers need a Swift 6.1+ toolchain (Xcode 16.3+); older toolchains keep
resolving 2.1.0.
- CI now runs the test suite in both trait configurations, and the pinned
minimum-toolchain job moves from Swift 6.0.2 to 6.1.


## [2.1.0] - 2026-07-10

No library changes. The test suite (37 suites, 179 tests) is migrated from XCTest to
Expand Down
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 39 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// swift-tools-version: 6.0.0
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription

let package = Package(
Expand All @@ -10,13 +11,36 @@ let package = Package(
.library(name: "SwiftRandomKit", targets: ["SwiftRandomKit"]),
.library(name: "SwiftRandomKitGenerators", targets: ["SwiftRandomKitGenerators"])
],
traits: [
.trait(
name: "Derive",
description: """
Enables the @RandomGenerable derivation macro. Opting in adds a \
build-time dependency on swift-syntax.
"""
)
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.0")
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"700.0.0"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.6.0")
],
targets: [
.target(name: "SwiftRandomKit"),
.target(
name: "SwiftRandomKit",
dependencies: [
.target(name: "SwiftRandomKitMacros", condition: .when(traits: ["Derive"]))
]
),
.macro(
name: "SwiftRandomKitMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.target(name: "SwiftRandomKitGenerators", dependencies: ["SwiftRandomKit"]),

.executableTarget(name: "SwiftRandomKitExample", dependencies: ["SwiftRandomKit", "SwiftRandomKitGenerators"]),

.testTarget(
Expand All @@ -26,6 +50,17 @@ let package = Package(
"SwiftRandomKitGenerators"
],
exclude: ["SwiftRandomKit-Package.xctestplan"]
),

// Macro expansion tests live in their own target that only imports the
// macro implementation and MacroTesting; both dependencies exist only
// when the Derive trait is enabled, and the sources are gated on it.
.testTarget(
name: "SwiftRandomKitMacrosTests",
dependencies: [
.target(name: "SwiftRandomKitMacros", condition: .when(traits: ["Derive"])),
.product(name: "MacroTesting", package: "swift-macro-testing", condition: .when(traits: ["Derive"]))
]
)
]
)
103 changes: 101 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ SwiftRandomKit is a powerful Swift library that provides a composable, protocol-
- 🔄 Support for custom random number generators
- 📦 Rich set of built-in generators
- 🛠 Extensive collection of combinators
- 🧬 Opt-in `@RandomGenerable` macro that derives generators for your own types (via the `Derive` package trait)

## Concurrency Safety

Expand All @@ -34,11 +35,20 @@ ordinary values, confined to the isolation domain where you build and run them:

### Swift Package Manager

Add the following to your `Package.swift` file:
Add the following to your `Package.swift` file (Swift 6.1+ toolchain required):

```swift
dependencies: [
.package(url: "https://github.com/ibrahimkteish/SwiftRandomKit.git", from: "2.0.0")
.package(url: "https://github.com/ibrahimkteish/SwiftRandomKit.git", from: "2.2.0")
]
```

To also enable the `@RandomGenerable` derivation macro, opt into the `Derive` trait
(see [Deriving Generators](#deriving-generators-with-randomgenerable)):

```swift
dependencies: [
.package(url: "https://github.com/ibrahimkteish/SwiftRandomKit.git", from: "2.2.0", traits: ["Derive"])
]
```

Expand Down Expand Up @@ -89,6 +99,95 @@ let roll3 = diceGen(using: &myRNG)

This syntax provides a more concise and natural way to generate random values, making your code cleaner and more expressive.

## Deriving Generators with @RandomGenerable

SwiftRandomKit can derive a generator for your own structs and enums from a single
attribute — ideal for test fixtures, SwiftUI preview data, and seeded demo content:

```swift
import SwiftRandomKit

@RandomGenerable
struct User {
@Gen(IntGenerator(in: 18...99)) var age: Int
var name: String // String default: 8 alphanumeric characters
var isAdmin: Bool
var nickname: String? // Optional default: nil half the time
}

let user = User.generator.run() // User(age: 42, name: "k0bSoKdI", ...)
let team = User.generator.array(5).run() // five random users

// Derived generators are ordinary generators: combinators and seeded RNGs work.
var rng = LCRNG(seed: 42)
let admin = User.generator.filter(\.isAdmin).run(using: &rng)
```

Enums derive too, including associated values — a case is picked uniformly at random:

```swift
@RandomGenerable
enum Reward {
case coins(Int)
case badge(name: String)
case nothing
}
```

How derivation works:

- Structs are built through their memberwise initializer. Every stored property is
generated by its `@Gen(...)` override if it has one, otherwise by its type's default
generator (see below). Constants with a value (`let version = 1`), `static`, `lazy`,
and computed properties are skipped.
- Enums pick a case uniformly at random and generate associated values the same way as
struct properties.
- Classes, actors, and generic types are not supported.

### The Derive trait

The macro needs swift-syntax to compile, which is a heavy build-time dependency — so it
is opt-in behind the `Derive` package trait. With the trait disabled (the default),
swift-syntax is never built and SwiftRandomKit stays a zero-build-cost dependency.

- **Package.swift consumers** (Swift 6.1+): declare the dependency with
`traits: ["Derive"]` as shown in [Installation](#installation).
- **Xcode app projects**: Xcode 26.4+ supports enabling package traits on a dependency.
On older Xcode versions, wrap SwiftRandomKit in a local package that enables the
trait and re-exports it.

### Default generators (`RandomGenerable`)

The `RandomGenerable` protocol — available with or without the trait — is what powers
derivation: a type conforms by exposing a canonical `static var generator`. The
standard types come pre-conformed with deliberately simple defaults:

| Type | Default |
| --- | --- |
| `Int`, `UInt8`, ... (all fixed-width integers) | uniform over the type's full range |
| `Double`, `Float` | uniform over `0...1` |
| `Bool` | fair coin flip |
| `String` | 8 random alphanumeric characters |
| `Character` | random ASCII letter |
| `Optional<T>` | `T`'s default, `nil` half the time |
| `Array<T>` | 0–10 elements of `T`'s default |

When a default doesn't fit, override per property with `@Gen(...)`, or conform your own
leaf types manually — no macro needed:

```swift
struct Temperature { var celsius: Double }

extension Temperature: RandomGenerable {
static var generator: some RandomGenerator<Temperature> {
FloatGenerator(in: -40...50).map(Temperature.init)
}
}
```

Types that conform — manually or via the macro — compose into other derived types
automatically: a `@RandomGenerable` struct with a `Temperature` property just works.

## Built-in Generators

### Core Generators
Expand Down
56 changes: 56 additions & 0 deletions Sources/SwiftRandomKit/Macros.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#if Derive
/// Derives a `RandomGenerable` conformance for a struct or an enum.
///
/// Available when the package is imported with the `Derive` trait enabled.
///
/// For a struct, the derived `generator` builds values through the memberwise
/// initializer, generating every stored property from its type's default generator —
/// or from a `@Gen(...)` override:
///
/// ```swift
/// @RandomGenerable
/// struct User {
/// let name: String
/// @Gen(IntGenerator(in: 18...99)) let age: Int
/// let isAdmin: Bool
/// }
///
/// let user = User.generator.run()
/// let team = User.generator.array(5).run()
/// ```
///
/// For an enum, the derived `generator` picks a case uniformly at random and derives
/// any associated values the same way:
///
/// ```swift
/// @RandomGenerable
/// enum Reward {
/// case coins(Int)
/// case badge(name: String)
/// case nothing
/// }
/// ```
///
/// Every stored property (and associated value) type must either conform to
/// `RandomGenerable` or carry a `@Gen` override. Derivation is not supported for
/// classes, actors, or generic types.
@attached(extension, conformances: RandomGenerable, names: named(generator))
public macro RandomGenerable() = #externalMacro(module: "SwiftRandomKitMacros", type: "RandomGenerableMacro")

/// Overrides the generator used for one stored property in a `@RandomGenerable` type.
///
/// Available when the package is imported with the `Derive` trait enabled.
///
/// The expression is captured as written and re-evaluated inside the derived
/// generator, so it composes with every combinator in the library:
///
/// ```swift
/// @RandomGenerable
/// struct Player {
/// @Gen(["north", "south", "east", "west"].element().map { $0! }) let region: String
/// @Gen(IntGenerator(in: 0...9999)) let score: Int
/// }
/// ```
@attached(peer)
public macro Gen<G: RandomGenerator>(_ generator: G) = #externalMacro(module: "SwiftRandomKitMacros", type: "GenMacro")
#endif
Loading
Loading