Skip to content

Guides

Forges

A Forge<T> maps generated values onto your own type. It is a value type: every method returns a new forge, so they compose and nothing mutates under you.

struct User {
var name = ""
var email = ""
var role = Role.member
var posts: [Post] = []
}
let users = Forge<User>("User") { User() }
.rule(\.name) { $0.person.fullName() }
.rule(\.email) { "user\($0.index)@example.com" }
.locale(DecoyLocaleEN.locale)
users.generate(3, seed: 1337)
// User(name: "James Grow", email: "user0@example.com", …)
// User(name: "Dr. Linda Vereen", email: "user1@example.com", …)
// User(name: "Phillis Madden", email: "user2@example.com", …)

The name — Forge<User>("User") — is given explicitly rather than derived. Nothing in the library reflects on a type, so there is nothing to read it from, and a mis-typed label is a worse failure than a redundant one.

rule takes a writable key path and a closure. Two shapes: one that sees only the faker, and one that also sees the row built so far.

.rule(\.name) { faker in faker.person.fullName() }
.rule(\.slug) { faker, user in user.name.lowercased().replacingOccurrences(of: " ", with: "-") }

faker.index is the zero-based row number. It is the cheapest way to get values that cannot collide, without paying for a uniqueness constraint:

.rule(\.email) { "user\($0.index)@example.com" }

rule(unique:) retries until it finds a value no earlier row in the same run used.

let users = Forge<User>("User") { User() }
.rule(unique: \.username, label: "username") { $0.internet.username() }

If the pool is too small it throws ForgeError.uniqueConstraintExhausted naming the property, the attempt count and any traits in play, rather than looping forever or quietly repeating. Use tryGenerate to catch it:

do {
let rows = try users.tryGenerate(50_000, seed: 1337)
} catch let error as ForgeError {
print(error) // names the property and how many attempts it made
}

Prefer index-derived values where you can. Uniqueness needs every row to see every other row, and that has consequences below.

let posts = Forge<Post>("Post") { Post() }
.rule(\.title) { $0.lorem.sentence() }
let users = Forge<User>("User") { User() }
.rule(\.name) { $0.person.fullName() }
.cycle(\.role, through: [.member, .editor, .admin])
.each(\.posts, 0...5, of: posts)

cycle assigns by row index, so a run of 300 gets exactly 100 of each — useful when you need coverage of every case rather than a random spread. each generates a nested forge with a seed derived from the parent’s stream, so children are reproducible too. Pass a plain number for a fixed fan-out: .each(\.posts, 3, of: posts).

The child inherits the parent’s locale, reference instant and novelNames setting, so you configure them once on the parent. A child that sets its own keeps it.

A trait is a named transformation, applied at generation time.

let admin = Trait<User>("admin") { $0.rule(\.role) { _ in .admin } }
let banned = Trait<User>("banned") { $0.rule(\.status) { _ in .banned } }
users.generate(10, seed: 1337, applying: admin, banned)

Traits appear in ForgeError messages, which is why they are named: a uniqueness failure that only happens under one combination of traits is otherwise very hard to reproduce.

Extending Trait with static members reads better at the call site:

extension Trait where T == User {
static var admin: Trait { Trait("admin") { $0.rule(\.role) { _ in .admin } } }
}
users.generate(10, seed: 1337, applying: .admin)

swift-testing exports a protocol also called Trait, so in a test file you need one selective import to say which you mean:

import Testing
import struct Decoy.Trait
import Decoy

Only the extension needs it — applying: .admin never names the type.

finish runs last and sees the whole row, for the invariants that need everything else decided first.

.finish { faker, user in
user.displayName = user.name.isEmpty ? user.email : user.name
}
Call Use
generate(_:seed:) the whole run, traps on error
tryGenerate(_:seed:) the same, throwing
one(seed:) a single row
generate(rows:seed:) a slice, for parallel or resumed runs
stream(seed:) a lazy sequence, for runs too large to hold

Rows are independent, so a slice is exactly what the same indices of the whole run would have been:

let whole = users.generate(500, seed: 1337)
let slice = users.generate(rows: 400..<403, seed: 1337)
whole[400].name == slice[0].name // true — "Mark Reddick IV"

That is what makes a million-row job splittable across tasks, or resumable after a failure at row 700,000.

generate(rows:seed:) refuses a forge with unique rules, and traps saying so. Separate chunks cannot see each other’s values, so uniqueness could not be honoured — and allowing it silently would produce duplicates that only surface as a constraint violation at insert time. Use generate(_:seed:) for the whole run instead.

users
.locale(DecoyLocaleDE.locale) // which language this forge speaks
.reference(myInstant) // the anchor `past()` and `future()` work from
.novelNames() // surnames no real person is recorded as having

novelNames() swaps the weighted register lists for character-level models trained on them. Off by default, because the registers carry real frequency data and turning that off silently would throw away the realism it was built for. See what was declined.