Rename the builtin kind from generator to sample
Tests / vet + fmt + tests (pull_request) Successful in 32s

This commit is contained in:
2026-09-01 21:41:35 +02:00
parent 04b7b6a432
commit 5d33f4c0f0
5 changed files with 30 additions and 29 deletions
+13 -13
View File
@@ -19,9 +19,9 @@ lacked the locale coverage and format control we needed.
id), never for what character classes and choices already do. id), never for what character classes and choices already do.
- **Composable** — templates nest without limit: weighted choices, character - **Composable** — templates nest without limit: weighted choices, character
classes and sub-templates combine to model any format. classes and sub-templates combine to model any format.
- **Reproducible** — seed a generator and it emits the same sequence every time, for a - **Reproducible** — seed a generator and it emits the same sequence every time,
given version of the data: changing how a value is composed shifts the stream for for a given version of the data: changing how a value is composed shifts the
that value and for everything drawn after it in the same generator. stream for that value and for everything drawn after it in the same generator.
Every built-in draws only from that seed — no wall-clock, no `crypto/rand` Every built-in draws only from that seed — no wall-clock, no `crypto/rand`
so determinism holds end to end. so determinism holds end to end.
- **Zero dependencies** — standard library only. - **Zero dependencies** — standard library only.
@@ -270,7 +270,7 @@ the rng, not the clock — the result is a valid, reproducible value, not a real
point in time. point in time.
There are four kinds. **Derivations** read the digits emitted so far, so put them There are four kinds. **Derivations** read the digits emitted so far, so put them
after their payload; **generators** read only the rng, so they stand alone; one after their payload; **samples** read only the rng, so they stand alone; one
**session counter** (`seq`) advances state held on the generator; and one **session counter** (`seq`) advances state held on the generator; and one
**computation** (`calc`) evaluates arithmetic over sibling fields. Arguments are **computation** (`calc`) evaluates arithmetic over sibling fields. Arguments are
validated at `New` (a bad count, range, country, or expression fails fast); a validated at `New` (a bad count, range, country, or expression fails fast); a
@@ -282,19 +282,19 @@ fat-fingered `hex(2000000000)` can't try to allocate gigabytes at render.
| `{luhn()}` | derivation | Luhn check digit (mod-10) over preceding digits | | `{luhn()}` | derivation | Luhn check digit (mod-10) over preceding digits |
| `{mod11()}` | derivation | weighted mod-11 check char (weights 27 from the right); `X` when it would be 10 | | `{mod11()}` | derivation | weighted mod-11 check char (weights 27 from the right); `X` when it would be 10 |
| `{ean()}` | derivation | EAN-13 / UPC-A / ISBN-13 / GTIN check digit | | `{ean()}` | derivation | EAN-13 / UPC-A / ISBN-13 / GTIN check digit |
| `{uuid()}` | generator | UUID v7 (v4 ships as data — see [Data](#data)) | | `{uuid()}` | sample | UUID v7 (v4 ships as data — see [Data](#data)) |
| `{ulid()}` | generator | ULID, 26-char Crockford base32 | | `{ulid()}` | sample | ULID, 26-char Crockford base32 |
| `{nanoid(n)}` | generator | URL-safe Nano ID, `n` chars | | `{nanoid(n)}` | sample | URL-safe Nano ID, `n` chars |
| `{hex(n)}` | generator | `n` lowercase hex digits | | `{hex(n)}` | sample | `n` lowercase hex digits |
| `{base64(n)}` | generator | `n` random bytes, base64 | | `{base64(n)}` | sample | `n` random bytes, base64 |
| `{int(min,max)}` | generator | uniform integer in `[min, max]` | | `{int(min,max)}` | sample | uniform integer in `[min, max]` |
| `{float(min,max,dp)}` | generator | number in `[min, max]` with `dp` decimals | | `{float(min,max,dp)}` | sample | number in `[min, max]` with `dp` decimals |
| `{iban(CC)}` | generator | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) | | `{iban(CC)}` | sample | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
| `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this generator's sequence; `name` selects an independent counter | | `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this generator's sequence; `name` selects an independent counter |
| `{calc(expr)}`, `{calc(expr,dp)}` | computation | value of an arithmetic expression over number literals and sibling fields; `dp` rounds | | `{calc(expr)}`, `{calc(expr,dp)}` | computation | value of an arithmetic expression over number literals and sibling fields; `dp` rounds |
`{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979 `{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979
prefix in data and call `{ean()}`). `{iban()}` is a generator, not a derivation: prefix in data and call `{ean()}`). `{iban()}` is a sample, not a derivation:
an IBAN's check digits sit *before* the account number, which a left-to-right an IBAN's check digits sit *before* the account number, which a left-to-right
reader can't reach, so it emits the whole value (a generic numeric BBAN — valid reader can't reach, so it emits the whole value (a generic numeric BBAN — valid
length and checksum, not real bank routing). length and checksum, not real bank routing).
+7 -7
View File
@@ -8,7 +8,7 @@ import (
"strings" "strings"
) )
// maxLen caps generator output lengths (hex, nanoid, base64) and maxDecimals caps // maxLen caps sample output lengths (hex, nanoid, base64) and maxDecimals caps
// float/calc decimal places, so a fat-fingered or overflowing argument fails at // float/calc decimal places, so a fat-fingered or overflowing argument fails at
// New instead of trying to allocate gigabytes — or panicking — at render. // New instead of trying to allocate gigabytes — or panicking — at render.
const ( const (
@@ -18,7 +18,7 @@ const (
// builtins is the registry of {name(args)} functions. Two kinds: derivations read // builtins is the registry of {name(args)} functions. Two kinds: derivations read
// the digits emitted so far in the current expansion (luhn, mod11, ean — place // the digits emitted so far in the current expansion (luhn, mod11, ean — place
// them after their payload); generators read only the rng (uuid, ulid, ...). All // them after their payload); samples read only the rng (uuid, ulid, ...). All
// must stay pure over (rng, emitted, args) so seeded output is reproducible — a // must stay pure over (rng, emitted, args) so seeded output is reproducible — a
// time-based id (uuid v7, ulid) draws its timestamp from the rng, not the wall // time-based id (uuid v7, ulid) draws its timestamp from the rng, not the wall
// clock. Add a builtin only for what data can't express: a random v4 UUID and a // clock. Add a builtin only for what data can't express: a random v4 UUID and a
@@ -27,8 +27,8 @@ var builtins = map[string]builtin{
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })}, "luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
"mod11": {arity: 0, prep: derive(mod11Check)}, "mod11": {arity: 0, prep: derive(mod11Check)},
"ean": {arity: 0, prep: derive(eanCheck)}, "ean": {arity: 0, prep: derive(eanCheck)},
"uuid": {arity: 0, prep: generate(uuidV7)}, "uuid": {arity: 0, prep: sample(uuidV7)},
"ulid": {arity: 0, prep: generate(ulid)}, "ulid": {arity: 0, prep: sample(ulid)},
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn { "nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
n := atoi(a[0]) n := atoi(a[0])
return func(s *session, _ string, _ []string) string { return nanoid(s, n) } return func(s *session, _ string, _ []string) string { return nanoid(s, n) }
@@ -74,8 +74,8 @@ var builtins = map[string]builtin{
}}, }},
} }
// derive and generate are the two argument-free builtin shapes the README names: a // derive and sample are the two argument-free builtin shapes the README names: a
// derivation reads the output emitted so far, a generator reads only the rng. Each // derivation reads the output emitted so far, a sample reads only the rng. Each
// lifts that one function into the prep every registry entry supplies. // lifts that one function into the prep every registry entry supplies.
func derive(f func(emitted string) string) func([]string) callFn { func derive(f func(emitted string) string) func([]string) callFn {
return func([]string) callFn { return func([]string) callFn {
@@ -83,7 +83,7 @@ func derive(f func(emitted string) string) func([]string) callFn {
} }
} }
func generate(f func(rng) string) func([]string) callFn { func sample(f func(rng) string) func([]string) callFn {
return func([]string) callFn { return func([]string) callFn {
return func(s *session, _ string, _ []string) string { return f(s) } return func(s *session, _ string, _ []string) string { return f(s) }
} }
+6 -6
View File
@@ -34,9 +34,9 @@ func TestBuiltinIDGenerators(t *testing.T) {
} }
} }
// TestBuiltinGeneratorsReproducible pins the determinism guardrail: every // TestBuiltinSamplesReproducible pins the determinism guardrail: every
// generator draws only from the seeded rng, so same seed -> same value. // sample draws only from the seeded rng, so same seed -> same value.
func TestBuiltinGeneratorsReproducible(t *testing.T) { func TestBuiltinSamplesReproducible(t *testing.T) {
for _, tmpl := range []string{ for _, tmpl := range []string{
`{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`, `{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`,
`{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`, `{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`,
@@ -96,8 +96,8 @@ func TestBuiltinChecksums(t *testing.T) {
} }
// TestBuiltinSeqPerSession pins seq's contract: a counter from 1, advancing on // TestBuiltinSeqPerSession pins seq's contract: a counter from 1, advancing on
// each call, named counters independent, and the whole thing scoped to one session // each call, named counters independent, and the whole thing scoped to one
// so a fresh Generator restarts at 1. // session so a fresh Generator restarts at 1.
func TestBuiltinSeqPerSession(t *testing.T) { func TestBuiltinSeqPerSession(t *testing.T) {
f := engine(1) f := engine(1)
for i := 1; i <= 5; i++ { for i := 1; i <= 5; i++ {
@@ -163,7 +163,7 @@ func TestBuiltinIBAN(t *testing.T) {
} }
} }
// ibanValid checks the IBAN mod-97 rule independently of the generator: move the // ibanValid checks the IBAN mod-97 rule independently of the builtin: move the
// first four chars to the end, map letters A-Z to 10-35, the number mod 97 == 1. // first four chars to the end, map letters A-Z to 10-35, the number mod 97 == 1.
func ibanValid(s string) bool { func ibanValid(s string) bool {
r := s[4:] + s[:4] r := s[4:] + s[:4]
+1 -1
View File
@@ -249,7 +249,7 @@ func digitsOnly(s string) string {
} }
// luhnValid verifies a full number (payload + trailing check digit). It doubles // luhnValid verifies a full number (payload + trailing check digit). It doubles
// from the second-from-right, independent of the generator's own Luhn code. // from the second-from-right, independent of the builtin's own Luhn code.
func luhnValid(s string) bool { func luhnValid(s string) bool {
sum, double := 0, false sum, double := 0, false
for i := len(s) - 1; i >= 0; i-- { for i := len(s) - 1; i >= 0; i-- {
+3 -2
View File
@@ -5,8 +5,9 @@ import (
"testing" "testing"
) )
// newGenerator creates a generator over a single data directory, failing on error. Most // newGenerator creates a generator over a single data directory, failing on
// tests load one dir; newGeneratorN loads several (last-loaded wins on conflicts). // error. Most tests load one dir; newGeneratorN loads several (last-loaded wins
// on conflicts).
func newGenerator(t *testing.T, dir string, opts ...Option) *Generator { func newGenerator(t *testing.T, dir string, opts ...Option) *Generator {
t.Helper() t.Helper()
return newGeneratorN(t, []string{dir}, opts...) return newGeneratorN(t, []string{dir}, opts...)