Skip to content

Repository files navigation

SwiftRandomKit

Swift Tests Swift 6.0.2 License: MIT

SwiftRandomKit is a powerful Swift library that provides a composable, protocol-based approach to random value generation. It offers a flexible and type-safe way to create and combine random generators for various data types.

Features

  • 🎲 Type-safe random generators
  • 🧩 Composable and chainable API
  • 🔄 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

SwiftRandomKit works with Swift's isolation model instead of against it. Generators are ordinary values, confined to the isolation domain where you build and run them:

  • The RandomGenerator protocol does not require Sendable. Your own generators can hold non-Sendable state and produce non-Sendable values, and transform closures like map { } are not @Sendable, so they can capture anything the surrounding context can.
  • Simple leaf generators (IntGenerator, FloatGenerator, BoolGenerator, Always, and the ready-made generators like Dice or UUIDGenerator) conform to Sendable and can be shared across isolation domains freely.
  • Composed pipelines (anything built with map, flatMap, filter, ...) are not Sendable. Moving one into a Task works when its region is disconnected — Swift's region isolation checks the transfer. To use a pipeline from several tasks, build one per task: generators are cheap, immutable descriptions.

Installation

Swift Package Manager

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

dependencies: [
    .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):

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

Basic Usage

import SwiftRandomKit

// Create a simple random number generator
let diceGen = IntGenerator(in: 1...6)
let roll = diceGen.run() // e.g., 4

// Generate random characters
let letterGen = RandomGenerators.letter
let letter = letterGen.run() // Random letter (a-z, A-Z)

let digitGen = RandomGenerators.number
let digit = digitGen.run() // Random digit (0-9)

// Generate arrays of random values
let fiveRolls = diceGen.array(5).run() // e.g., [3, 1, 6, 2, 5]

// Use a custom random number generator
var myRNG = MyCustomRandomNumberGenerator()
let customRoll = diceGen.run(using: &myRNG)

Function Call Syntax

SwiftRandomKit supports Swift's function call syntax, allowing you to call generators directly as functions:

import SwiftRandomKit

// Create a random number generator
let diceGen = IntGenerator(in: 1...6)

// Traditional way
let roll1 = diceGen.run() 

// Function call syntax - more concise!
let roll2 = diceGen()

// Works with custom RNGs too
var myRNG = MyCustomRandomNumberGenerator()
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:

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:

@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.
  • 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:

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

  • IntGenerator: Generate random integers within a range
  • FloatGenerator: Generate random floating-point numbers
  • BoolGenerator: Generate random boolean values
  • AnyRandomGenerator: Type-erased container for any random generator

Character and String

  • RandomGenerators.letter: Random letters (a-z, A-Z)
  • RandomGenerators.number: Random digits (0-9)
  • RandomGenerators.letterOrNumber: Random alphanumeric characters
  • RandomGenerators.uppercaseLetter: Random uppercase letters (A-Z)
  • RandomGenerators.lowercaseLetter: Random lowercase letters (a-z)
  • RandomGenerators.ascii: Random ASCII characters
  • RandomGenerators.latin1: Random Latin-1 characters
  • RandomGenerators.character(in:): Custom range of characters

Collections

  • Array: Generate arrays of random elements with fixed size
  • ArrayGenerator: Generate arrays with random size
  • Dictionary: Generate dictionaries with random key-value pairs
  • Element: Pick random elements from collections
  • Collection: Generate custom collections of random elements
  • Shuffled: Generate shuffled versions of collections

General Purpose Generators

  • Always: Always produce the same value
  • LCRNG: Linear Congruential Random Number Generator

Transformers and Combinators

  • Map: Transform the output of a generator
  • FlatMap: Create generators that depend on previous random values
  • Concat: Concatenate the output of multiple generators
  • Zip: Combine multiple generators into tuples
  • Collect: Collect results from multiple generators
  • Print: Debug generator outputs
  • RemoveDuplicates: Remove duplicate values from a generator
  • Filter: Generate only values that satisfy a predicate
  • Retry: Retry generation until a condition is met
  • AttemptBounded: Limit generation attempts with configurable fallback strategies
  • TryMap: Transform with operations that might fail
  • Frequency: Weight outputs by frequency
  • Optional: Generate optional values from a generator
  • Tuple: Create tuples from a generator's output

Special Generators

Additional generators available in the SwiftRandomKitGenerators product:

  • ColorGenerator: Generate random colors (supports UIKit and SwiftUI)
  • CreditCardGenerator: Generate valid credit card numbers for different card types
  • Dice: Generate random dice rolls of various types
  • IPAddressGenerator: Generate random IP addresses (IPv4 or IPv6)
  • LatLongGenerator: Generate random geographic coordinates (latitude and longitude)
  • SafariPasswordGenerator: Generate strong passwords following Safari's pattern
  • SudokuGenerator: Generate valid Sudoku puzzles with varying difficulty
  • UUIDGenerator: Generate random UUID strings in standard format
  • VersionNumberGenerator: Generate random semantic version numbers

Transformations and Combinations

SwiftRandomKit provides various transformations to modify and combine generators:

Mapping and Transforming

// Transform values with map
let diceGen = IntGenerator(in: 1...6)
let doubledDice = diceGen.map { $0 * 2 }
let result = doubledDice.run() // 2, 4, 6, 8, 10, or 12

// Use flatMap for dependent generators
let coinFlip = BoolGenerator()
let weightedDice = coinFlip.flatMap { isHeads in
    isHeads ? IntGenerator(in: 1...6) : IntGenerator(in: 4...9)
}

Collecting and Combining

// Fixed-size arrays
let fiveDice = IntGenerator(in: 1...6).array(5)

// Variable-size arrays
let countGen = IntGenerator(in: 3...7) 
let variableDice = IntGenerator(in: 1...6).arrayGenerator(countGen)

// Collect results from different generators
let smallNumberGen = IntGenerator(in: 1...10)
let mediumNumberGen = IntGenerator(in: 11...50)
let largeNumberGen = IntGenerator(in: 51...100)

let mixedNumbers = [smallNumberGen, mediumNumberGen, largeNumberGen].collect().run()
// e.g., [7, 23, 86]

Constant and Always Generators

// Always produce the same value
let alwaysSix = Always(6)

// Can be combined with other generators using flatMap
let loadedDice = BoolGenerator().flatMap { isLoaded in
    isLoaded ? alwaysSix.eraseToAnyRandomGenerator() : IntGenerator(in: 1...6).eraseToAnyRandomGenerator()
}

print(loadedDice())

Type Erasure

// Erase the concrete type for API simplicity
func createDiceGenerator() -> AnyRandomGenerator<Int> {
    return IntGenerator(in: 1...6).eraseToAnyRandomGenerator()
}

// Store different generator types in a collection
let generators: [AnyRandomGenerator<Int>] = [
    IntGenerator(in: 1...100).eraseToAnyRandomGenerator(),
    BoolGenerator().map { $0 == true ? 1 : 0 }.eraseToAnyRandomGenerator()
]

print(generators.collect().run())

Advanced Examples

Create a Password Generator

// Generate a secure password with specific requirements
let passwordGen = RandomGenerators.uppercaseLetter
    .array(2).flatMap { uppercase in
        RandomGenerators.lowercaseLetter.array(5).flatMap { lowercase in
            RandomGenerators.number.array(2).flatMap { digits in
                Always(Array("!@#$%^&*")).element().map { special in
                    let allChars = uppercase + lowercase + digits + (special != nil ? [special!] : [])
                    return String(allChars)
                }
            }
        }
    }

let securePassword = passwordGen.run() // e.g., "KTabnre45$"

A simpler approach using the zip operator:

// A simpler way to generate a random password
let simplePasswordGen = RandomGenerators.uppercaseLetter.array(2)
    .zip(RandomGenerators.lowercaseLetter.array(5))
    .zip(RandomGenerators.number.array(2)) 
    .zip(Always(Array("!@#$%^&*")).element())
    .map { (components, special) in
        let (upperAndLower, digits) = components
        let (upper, lower) = upperAndLower
        let chars = upper + lower + digits + (special != nil ? [special!] : [])
        return String(chars)
    }

let password = simplePasswordGen.run() // e.g., "ABcdefg12#"

Using Filter, Retry, and AttemptBounded Generators

// Generate only even numbers
let evenNumbers = IntGenerator(in: 1...100).filter { $0.isMultiple(of: 2) }
let evenNumber = evenNumbers.run() // Always an even number

// Retry until a condition is met
let diceGen = IntGenerator(in: 1...6)
let sixGenerator = diceGen.retry(maxAttempts: 10, until: { $0 == 6 })
let result = sixGenerator.run() // Will be 6 if found within 10 attempts

// Filter with fallback for maximum attempts
let primeGen = IntGenerator(in: 1...100).attemptBounded(
    maxAttempts: 20,
    condition: { number in
        // Check if number is prime (simplified)
        if number <= 1 { return false }
        if number <= 3 { return true }
        if number.isMultiple(of: 2) || number.isMultiple(of: 3) { return false }
        var i = 5
        while i * i <= number {
            if number.isMultiple(of: i) || number.isMultiple(of: (i + 2)) { return false }
            i += 6
        }
        return true
    },
    fallbackStrategy: .useDefault(17) // Default to 17 if no prime found in 20 attempts
)

let prime = primeGen.run() // A prime number or 17 if none found

// Using a different generator as fallback
let highRoll = diceGen.attemptBounded(
    maxAttempts: 3,
    condition: { $0 > 4 },
    fallbackStrategy: .useAnotherGenerator { Always(6).run() }
)
let highDiceRoll = highRoll.run() // 5 or 6, or always 6 if not found in 3 attempts

⚠️ Warning: Be careful when using the .keepTrying fallback strategy, as it can lead to infinite loops if the condition can never be satisfied. For example, Always(5).retry(fallbackStrategy: .keepTrying, until: { $0.isMultiple(of: 2) }) will hang indefinitely.

Create a Custom Generator

// Create a custom dice that can be loaded
struct LoadedDiceGenerator: RandomGenerator {
  let bias: Int
  let probability: Double

  init(bias: Int, probability: Double = 0.7) {
    self.bias = bias
    self.probability = probability
  }

  func run<RNG: RandomNumberGenerator>(using rng: inout RNG) -> Int {
    FloatGenerator<Double>(in: 0...1)
      .flatMap {
        $0 < probability ? Always(bias).eraseToAnyRandomGenerator()
        : IntGenerator(in: 1...6).eraseToAnyRandomGenerator()
      }
      .run(using: &rng)
  }
}

let loadedDice = LoadedDiceGenerator(bias: 6)
let roll = loadedDice.run() // 6 with 70% probability

License

This project is available under the MIT license. See the LICENSE file for more info.

About

A Swift library for random data generation.

Resources

Code of conduct

Contributing

Stars

17 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages