keelson_factory/lib.rs
1//! The test-data factory layer — the runtime the factory generator will emit
2//! against.
3//!
4//! bob took this layer from Ruby's FactoryBot; Rust has almost no equivalent,
5//! so it is designed the way Layer 2 was: **nothing is generated yet** — this
6//! crate is the machinery ([`Source`] per-column value sources, [`Parent`]/
7//! [`OptionalParent`] reference states, the [`Sequence`] and the [`Faker`]),
8//! and the hand-written users/posts/comments factory in `tests/` is the
9//! generator's specification, written once by hand and run against real
10//! engines. The call-site shape being served:
11//!
12//! ```ignore
13//! use factories as fac;
14//!
15//! // Mods are values, keelson's house style throughout.
16//! let u = fac::users::factory((fac::users::id(10), fac::users::name("Ada")))
17//! .create(&db)
18//! .await?;
19//!
20//! // The schema-aware win: a comment needs a post needs a user, and
21//! // create_many makes the whole chain exist.
22//! let cs = fac::comments::factory(()).create_many(&db, 10).await?;
23//! ```
24//!
25//! # Where this sits
26//!
27//! Layer 3's test-data half: factories create rows through the same
28//! [keelson-models](https://docs.rs/keelson-models) insert path production writes take, so
29//! hooks fire and the rows are ones the application could really have written.
30//! The factories themselves are emitted by [keelson-gen](https://docs.rs/keelson-gen), which
31//! reads the foreign keys that make parent auto-creation possible. Nothing here
32//! is test-only by construction — it is a normal dependency — but a factory has
33//! no reason to exist outside a test. The whole map is the
34//! [keelson](https://docs.rs/keelson) facade crate.
35//!
36//! # The decisions, recorded
37//!
38//! **Factories fire model hooks.** `create`/`create_many` insert through
39//! Layer 2's `ModelInsert` path — the same `table().insert(setter).one(db)`
40//! production writes take — so `before_insert` rewrites the setter and
41//! `after_insert` runs on the caller's executor, exactly as they would for any
42//! other write. FactoryBot fires callbacks for the same reason: a factory that
43//! bypassed hooks would manufacture rows the application could never have
44//! written, and tests against such rows test a database that does not exist.
45//! `build()` runs no hooks — it produces a plain setter with no executor in
46//! sight, and is the raw-data escape hatch.
47//!
48//! **Non-null FKs auto-create their parents.** A required parent reference is
49//! a [`Parent`] field on the template, defaulting to `Auto`: at create time
50//! the parent's own default template is created first (its parents
51//! recursively, so a comment chains a post chains a user) and the FK takes the
52//! created row's key. Each created row gets its **own** parent chain —
53//! FactoryBot's association semantics; to share a parent, create it once and
54//! pass it back in via the existing-row mod (`post(&p)`) or shape it with a
55//! template mod (`for_post(…)`). A *nullable* FK is an [`OptionalParent`]
56//! defaulting to `Absent` — the column stays NULL unless a mod opts in, so a
57//! factory never invents rows the schema does not require.
58//!
59//! **Uniqueness is sequence-based.** Primary-key and unique columns default to
60//! [`Sequence`] values: a process-unique, time-derived base plus an atomic
61//! counter (the same shape the Layer 2 spec's `key()` pinned), so
62//! `create_many(&db, 100)` cannot collide in-process and does not collide with
63//! earlier runs against a shared persistent server.
64//!
65//! **Random values: in-crate SplitMix64, no dependency.** The evaluation:
66//!
67//! - `fake` — rejected. Its realistic-looking data (names, addresses,
68//! locales) is cosmetic for schema-level test rows, its dependency tree is
69//! the largest of the three options, and its output for a given seed is not
70//! a stability contract, which fights the determinism switch below.
71//! - `rand` alone — nearly right, but `StdRng` documents that its algorithm
72//! may change between major versions, so "seeded runs reproduce" would be a
73//! promise held by a dependency's semver policy — and the actual need is a
74//! few dozen lines of uniform integers and short strings.
75//! - **Chosen: an in-crate SplitMix64** ([`Faker`]) — zero dependencies, and
76//! the exact output sequence is pinned by test *in this crate*, so
77//! reproducibility is keelson's own tested contract rather than an upstream
78//! accident. Reopening condition: if factories ever need realistic data,
79//! add `fake` behind an off-by-default feature; the [`Source::Gen`] seam is
80//! where it would plug in.
81//!
82//! **The determinism switch, and its honest scope.** Every random default
83//! draws from the [`Faker`] threaded through `build`/`create_with`;
84//! `Faker::seeded(n)` makes two runs draw identical values, and the spec pins
85//! that. [`Sequence`] values are deliberately **outside** the seed: sequences
86//! are uniqueness machinery, and reproducing a primary key against a shared
87//! server would reproduce a collision. Seeded runs therefore reproduce every
88//! random-sourced column while unique columns stay unique — which is the only
89//! version of "reproducible test data" that survives contact with a real
90//! database.
91//!
92//! **`build()` touches no database — by signature.** It takes no executor, so
93//! the guarantee is compile-time, not behavioural. Consequence, recorded: a
94//! required FK whose parent is `Auto` or a template cannot be filled without a
95//! database, so `build()` leaves it unset — the caller either provides the key
96//! (`user_id(k)` / `user(&u)`) or uses `create`, where the chain is made.
97
98#![warn(missing_docs)]
99
100mod faker;
101mod parent;
102mod sequence;
103mod source;
104
105pub use faker::Faker;
106pub use parent::{OptionalParent, Parent};
107pub use sequence::Sequence;
108pub use source::Source;