Rename the exported type to Generator and retire the faker wording
Tests / vet + fmt + tests (pull_request) Successful in 25s

This commit is contained in:
2026-09-01 21:34:27 +02:00
parent d44c941a92
commit 04b7b6a432
14 changed files with 85 additions and 85 deletions
+13 -13
View File
@@ -13,15 +13,15 @@ lacked the locale coverage and format control we needed.
(`sv_SE`), but the engine treats folders as plain namespaces — name yours (`sv_SE`), but the engine treats folders as plain namespaces — name yours
anything. anything.
- **Data lives in JSON** — all source data is recursive JSON on disk, read when - **Data lives in JSON** — all source data is recursive JSON on disk, read when
you create a faker and then served from memory. Add or change data without you create a generator and then served from memory. Add or change data without
touching the library. Behavior belongs in data too: the engine grows a touching the library. Behavior belongs in data too: the engine grows a
built-in function only for what data can't express (a checksum, a time-based built-in function only for what data can't express (a checksum, a time-based
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 faker and it emits the same sequence every time, for a - **Reproducible** — seed a generator and it emits the same sequence every time, for a
given version of the data: changing how a value is composed shifts the stream for given version of the data: changing how a value is composed shifts the stream for
that value and for everything drawn after it in the same faker. 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.
@@ -133,7 +133,7 @@ phone 072-402 91 67
address.locality Linköping address.locality Linköping
``` ```
Seed a faker for reproducible output — same seed + locale yields an identical Seed a generator for reproducible output — same seed + locale yields an identical
sequence, handy for stable tests: sequence, handy for stable tests:
```go ```go
@@ -148,7 +148,7 @@ av == bv // true
dotted fields and folder segments (what the CLI's `-list` prints). Every path it dotted fields and folder segments (what the CLI's `-list` prints). Every path it
lists renders. lists renders.
A `*Fejkdata` is **not** safe for concurrent use — create one per goroutine. A `*Generator` is **not** safe for concurrent use — create one per goroutine.
## Data ## Data
@@ -219,7 +219,7 @@ within a choice:
``` ```
Only template (object) nodes carry `weight` — a bare string or nested array in a Only template (object) nodes carry `weight` — a bare string or nested array in a
choice always counts as `1`. Weights are checked when you create the faker: a choice always counts as `1`. Weights are checked when you create the generator: a
negative, non-numeric, or all-zero set is rejected at `New`, so a typo fails negative, non-numeric, or all-zero set is rejected at `New`, so a typo fails
fast instead of silently skewing output. fast instead of silently skewing output.
@@ -264,14 +264,14 @@ form prefixes the century outside the checksummed core:
"core": { "format": "00{mmdd}-000{luhn()}", "mmdd": [ ] } } "core": { "format": "00{mmdd}-000{luhn()}", "mmdd": [ ] } }
``` ```
A function must be deterministic (no wall-clock), so a seeded faker stays A function must be deterministic (no wall-clock), so a seeded generator stays
reproducible. A time-based id (UUID v7, ULID) therefore draws its timestamp from reproducible. A time-based id (UUID v7, ULID) therefore draws its timestamp from
the rng, not the clock — the result is a valid, reproducible value, not a real 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; **generators** read only the rng, so they stand alone; one
**session counter** (`seq`) advances state held on the faker; 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
length, count or decimal place beyond a sane maximum is rejected there too, so a length, count or decimal place beyond a sane maximum is rejected there too, so a
@@ -290,7 +290,7 @@ fat-fingered `hex(2000000000)` can't try to allocate gigabytes at render.
| `{int(min,max)}` | generator | uniform integer in `[min, max]` | | `{int(min,max)}` | generator | uniform integer in `[min, max]` |
| `{float(min,max,dp)}` | generator | number in `[min, max]` with `dp` decimals | | `{float(min,max,dp)}` | generator | 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)}` | generator | 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 faker'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
@@ -299,8 +299,8 @@ 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).
`{seq()}`'s counter lives on the faker, so it spans `Fake` calls (and `repeat`) `{seq()}`'s counter lives on the generator, so it spans `Fake` calls (and `repeat`)
and resets when you build a new faker — `seq` is reproducible by being ordered, and resets when you build a new generator — `seq` is reproducible by being ordered,
not random. It's the natural fit for a primary-key column in the SQL example above. not random. It's the natural fit for a primary-key column in the SQL example above.
**Computation.** `{calc(expr)}` evaluates an arithmetic expression — `+ - * /`, **Computation.** `{calc(expr)}` evaluates an arithmetic expression — `+ - * /`,
@@ -332,7 +332,7 @@ data dirs:
{ "format": "Hej, {..en_US.person}!" } { "format": "Hej, {..en_US.person}!" }
``` ```
renders e.g. `Hej, Pat Smith!`. References are bound when you create the faker, so renders e.g. `Hej, Pat Smith!`. References are bound when you create the generator, so
a path that is unknown, names a folder, or steps through a multi-variant choice a path that is unknown, names a folder, or steps through a multi-variant choice
fails at `New`. A reference that leads back to its own value (directly, mutually, fails at `New`. A reference that leads back to its own value (directly, mutually,
or through a chain) is a cycle that would never finish rendering, so it too is or through a chain) is a cycle that would never finish rendering, so it too is
@@ -516,7 +516,7 @@ docker compose run --rm test # latest
## Layout ## Layout
``` ```
fejkdata.go Fejkdata, New, List, options, seeding fejkdata.go Generator, New, List, options, seeding
node.go the node model and JSON -> node compilation node.go the node model and JSON -> node compilation
render.go Fake and the recursive renderer (choices, format strings, paths, bound draws) render.go Fake and the recursive renderer (choices, format strings, paths, bound draws)
template.go the {token} grammar: scanning, function and path tokens, validation template.go the {token} grammar: scanning, function and path tokens, validation
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func benchFaker(b *testing.B, dir string) *Fejkdata { func benchGenerator(b *testing.B, dir string) *Generator {
b.Helper() b.Helper()
f, err := New([]string{dir}, WithSeed(1)) f, err := New([]string{dir}, WithSeed(1))
if err != nil { if err != nil {
@@ -16,7 +16,7 @@ func benchFaker(b *testing.B, dir string) *Fejkdata {
} }
func benchPath(b *testing.B, dir, path string) { func benchPath(b *testing.B, dir, path string) {
f := benchFaker(b, dir) f := benchGenerator(b, dir)
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
+2 -2
View File
@@ -19,7 +19,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); generators read only the rng (uuid, ulid, ...). All
// must stay pure over (rng, emitted, args) so a seeded faker 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
// 24-hex ObjectID both ship as data, so the uuid builtin is v7. // 24-hex ObjectID both ship as data, so the uuid builtin is v7.
@@ -62,7 +62,7 @@ var builtins = map[string]builtin{
"calc": {arity: -1, check: checkCalc, prep: calcPrep}, "calc": {arity: -1, check: checkCalc, prep: calcPrep},
// seq is the one stateful builtin: a per-session counter from 1, advancing on // seq is the one stateful builtin: a per-session counter from 1, advancing on
// each call. An optional name selects an independent counter; no name uses the // each call. An optional name selects an independent counter; no name uses the
// default one. Deterministic by construction, so a seeded faker stays stable. // default one. Deterministic by construction, so seeded output stays stable.
"seq": {arity: -1, check: seqArg, prep: func(a []string) callFn { "seq": {arity: -1, check: seqArg, prep: func(a []string) callFn {
key := "" key := ""
if len(a) == 1 { if len(a) == 1 {
+2 -2
View File
@@ -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 faker // each call, named counters independent, and the whole thing scoped to one session
// (session) so a fresh faker restarts at 1. // 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++ {
+5 -5
View File
@@ -126,7 +126,7 @@ func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"inv": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`, "inv": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
}) })
f := newFejkdata(t, dir, WithSeed(3)) f := newGenerator(t, dir, WithSeed(3))
for i := 0; i < 300; i++ { for i := 0; i < 300; i++ {
got := fake(t, f, "inv") got := fake(t, f, "inv")
var net, qty, want float64 var net, qty, want float64
@@ -145,7 +145,7 @@ func TestCalcOperandSharesOneDraw(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"same": `{"format":"{w} {w} {calc(w)}","w":["1","2","3","4","5"]}`, "same": `{"format":"{w} {w} {calc(w)}","w":["1","2","3","4","5"]}`,
}) })
f := newFejkdata(t, dir, WithSeed(5)) f := newGenerator(t, dir, WithSeed(5))
for i := 0; i < 200; i++ { for i := 0; i < 200; i++ {
got := fake(t, f, "same") got := fake(t, f, "same")
if p := strings.Fields(got); len(p) != 3 || p[0] != p[1] || p[0] != p[2] { if p := strings.Fields(got); len(p) != 3 || p[0] != p[1] || p[0] != p[2] {
@@ -158,7 +158,7 @@ func TestCalcOperandSharesOneDraw(t *testing.T) {
// held, so an ordinary {w} {w} still draws twice. // held, so an ordinary {w} {w} still draws twice.
func TestFieldNoCalcReadsDrawsEachTime(t *testing.T) { func TestFieldNoCalcReadsDrawsEachTime(t *testing.T) {
dir := writeData(t, map[string]string{"two": `{"format":"{w} {w}","w":["1","2","3","4","5"]}`}) dir := writeData(t, map[string]string{"two": `{"format":"{w} {w}","w":["1","2","3","4","5"]}`})
f := newFejkdata(t, dir, WithSeed(5)) f := newGenerator(t, dir, WithSeed(5))
for i := 0; i < 200; i++ { for i := 0; i < 200; i++ {
if p := strings.Fields(fake(t, f, "two")); p[0] != p[1] { if p := strings.Fields(fake(t, f, "two")); p[0] != p[1] {
return return
@@ -173,7 +173,7 @@ func TestCalcHoldIsPerExpansion(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"rep": `{"format":"{n}={calc(n * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`, "rep": `{"format":"{n}={calc(n * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
}) })
f := newFejkdata(t, dir, WithSeed(11)) f := newGenerator(t, dir, WithSeed(11))
varied := false varied := false
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
got := fake(t, f, "rep") got := fake(t, f, "rep")
@@ -199,7 +199,7 @@ func TestCalcHoldIsPerTemplate(t *testing.T) {
"nest": `{"format":"{v}={calc(v * 1)} {inner}","v":["2","3","4","5","6","7","8","9"], "nest": `{"format":"{v}={calc(v * 1)} {inner}","v":["2","3","4","5","6","7","8","9"],
"inner":{"format":"{v}={calc(v * 1)}","v":["2","3","4","5","6","7","8","9"]}}`, "inner":{"format":"{v}={calc(v * 1)}","v":["2","3","4","5","6","7","8","9"]}}`,
}) })
f := newFejkdata(t, dir, WithSeed(13)) f := newGenerator(t, dir, WithSeed(13))
differed := false differed := false
for i := 0; i < 200; i++ { for i := 0; i < 200; i++ {
got := fake(t, f, "nest") got := fake(t, f, "nest")
+11 -11
View File
@@ -51,8 +51,8 @@ func TestShippedDataCategories(t *testing.T) {
regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)}, regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)},
} }
en := newFejkdata(t, "data/en_US", WithSeed(1)) en := newGenerator(t, "data/en_US", WithSeed(1))
sv := newFejkdata(t, "data/sv_SE", WithSeed(1)) sv := newGenerator(t, "data/sv_SE", WithSeed(1))
for _, c := range cases { for _, c := range cases {
for i := 0; i < 200; i++ { for i := 0; i < 200; i++ {
if v := fake(t, en, c.path); !c.en.MatchString(v) { if v := fake(t, en, c.path); !c.en.MatchString(v) {
@@ -69,7 +69,7 @@ func TestShippedDataCategories(t *testing.T) {
// v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers // v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers
// whose trailing {luhn()} check passes. // whose trailing {luhn()} check passes.
func TestShippedMiscCategories(t *testing.T) { func TestShippedMiscCategories(t *testing.T) {
f := newFejkdata(t, "data/misc", WithSeed(1)) f := newGenerator(t, "data/misc", WithSeed(1))
v4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`) v4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
mac := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`) mac := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
objectid := regexp.MustCompile(`^[0-9a-f]{24}$`) objectid := regexp.MustCompile(`^[0-9a-f]{24}$`)
@@ -93,7 +93,7 @@ func TestShippedMiscCategories(t *testing.T) {
// codes follow their standard shape, dotted sub-paths resolve, emoji is a real // codes follow their standard shape, dotted sub-paths resolve, emoji is a real
// glyph, and a coordinate parses to a point within geographic bounds. // glyph, and a coordinate parses to a point within geographic bounds.
func TestShippedMiscReferenceData(t *testing.T) { func TestShippedMiscReferenceData(t *testing.T) {
f := newFejkdata(t, "data/misc", WithSeed(1)) f := newGenerator(t, "data/misc", WithSeed(1))
re := map[string]*regexp.Regexp{ re := map[string]*regexp.Regexp{
"currency": regexp.MustCompile(`^[A-Z]{3}$`), "currency": regexp.MustCompile(`^[A-Z]{3}$`),
"currency.name": regexp.MustCompile(`\p{L}`), "currency.name": regexp.MustCompile(`\p{L}`),
@@ -134,7 +134,7 @@ func TestShippedMiscReferenceData(t *testing.T) {
// TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape // TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape
// regexes miss: no generated name repeats a character three times over. // regexes miss: no generated name repeats a character three times over.
func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) { func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
f := newFejkdata(t, "data/sv_SE", WithSeed(11)) f := newGenerator(t, "data/sv_SE", WithSeed(11))
for _, path := range []string{"person", "person.last"} { for _, path := range []string{"person", "person.last"} {
for i := 0; i < 20000; i++ { for i := 0; i < 20000; i++ {
name := fake(t, f, path) name := fake(t, f, path)
@@ -152,7 +152,7 @@ func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
// is a real calendar date (so month-length variants never emit e.g. Apr 31 or // is a real calendar date (so month-length variants never emit e.g. Apr 31 or
// Feb 30) and the trailing digit is a valid Luhn checksum over the other nine. // Feb 30) and the trailing digit is a valid Luhn checksum over the other nine.
func TestSwedishPersonnummer(t *testing.T) { func TestSwedishPersonnummer(t *testing.T) {
sv := newFejkdata(t, "data/sv_SE", WithSeed(1)) sv := newGenerator(t, "data/sv_SE", WithSeed(1))
sawLongMonthEnd := false sawLongMonthEnd := false
for i := 0; i < 2000; i++ { for i := 0; i < 2000; i++ {
v := fake(t, sv, "ssn") v := fake(t, sv, "ssn")
@@ -176,7 +176,7 @@ func TestSwedishPersonnummer(t *testing.T) {
} }
func TestShippedSwedishPhone(t *testing.T) { func TestShippedSwedishPhone(t *testing.T) {
f := newFejkdata(t, "data/sv_SE", WithSeed(11)) f := newGenerator(t, "data/sv_SE", WithSeed(11))
re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`) re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`)
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
if n := fake(t, f, "phone"); !re.MatchString(n) { if n := fake(t, f, "phone"); !re.MatchString(n) {
@@ -186,7 +186,7 @@ func TestShippedSwedishPhone(t *testing.T) {
} }
func TestShippedSwedishAddress(t *testing.T) { func TestShippedSwedishAddress(t *testing.T) {
f := newFejkdata(t, "data/sv_SE", WithSeed(3)) f := newGenerator(t, "data/sv_SE", WithSeed(3))
digit := regexp.MustCompile(`\d`) digit := regexp.MustCompile(`\d`)
for i := 0; i < 30; i++ { for i := 0; i < 30; i++ {
a := fake(t, f, "address") a := fake(t, f, "address")
@@ -205,7 +205,7 @@ func TestShippedSwedishAddress(t *testing.T) {
func TestShippedPersonHasParts(t *testing.T) { func TestShippedPersonHasParts(t *testing.T) {
for _, dir := range []string{"data/sv_SE", "data/en_US"} { for _, dir := range []string{"data/sv_SE", "data/en_US"} {
f := newFejkdata(t, dir, WithSeed(7)) f := newGenerator(t, dir, WithSeed(7))
for i := 0; i < 30; i++ { for i := 0; i < 30; i++ {
if name := fake(t, f, "person"); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) { if name := fake(t, f, "person"); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) {
t.Fatalf("%s person %q lacks first and last name", dir, name) t.Fatalf("%s person %q lacks first and last name", dir, name)
@@ -215,7 +215,7 @@ func TestShippedPersonHasParts(t *testing.T) {
} }
func TestShippedUSPhone(t *testing.T) { func TestShippedUSPhone(t *testing.T) {
f := newFejkdata(t, "data/en_US", WithSeed(11)) f := newGenerator(t, "data/en_US", WithSeed(11))
re := regexp.MustCompile(`^(\(\d{3}\) \d{3}-\d{4}|\d{3}-\d{3}-\d{4})$`) re := regexp.MustCompile(`^(\(\d{3}\) \d{3}-\d{4}|\d{3}-\d{3}-\d{4})$`)
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
if n := fake(t, f, "phone"); !re.MatchString(n) { if n := fake(t, f, "phone"); !re.MatchString(n) {
@@ -227,7 +227,7 @@ func TestShippedUSPhone(t *testing.T) {
// TestShippedNamespacedTree loads the whole data/ tree (not a single locale) and // TestShippedNamespacedTree loads the whole data/ tree (not a single locale) and
// reaches each locale through its folder segment: data/sv_SE/person -> sv_SE.person. // reaches each locale through its folder segment: data/sv_SE/person -> sv_SE.person.
func TestShippedNamespacedTree(t *testing.T) { func TestShippedNamespacedTree(t *testing.T) {
f := newFejkdata(t, "data", WithSeed(1)) f := newGenerator(t, "data", WithSeed(1))
for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} { for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} {
if got := fake(t, f, path); got == "" { if got := fake(t, f, path); got == "" {
t.Fatalf("Fake(%q) returned empty", path) t.Fatalf("Fake(%q) returned empty", path)
+8 -8
View File
@@ -240,7 +240,7 @@ func TestPathThroughChoice(t *testing.T) {
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`, "notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
"some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`, "some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`,
}) })
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
for i := 0; i < 200; i++ { for i := 0; i < 200; i++ {
if got := fake(t, f, "every.f"); got != "1" && got != "2" { if got := fake(t, f, "every.f"); got != "1" && got != "2" {
t.Fatalf("every.f = %q, want 1 or 2", got) t.Fatalf("every.f = %q, want 1 or 2", got)
@@ -277,7 +277,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) {
t.Fatalf("New = %v, want the dotted field name rejected", err) t.Fatalf("New = %v, want the dotted field name rejected", err)
} }
// The same data without the dotted key is fine, and the path resolves. // The same data without the dotted key is fine, and the path resolves.
f := newFejkdata(t, writeData(t, map[string]string{ f := newGenerator(t, writeData(t, map[string]string{
"cat": `{"format":"{a.b}","a":{"format":"{b}","b":["2"]}}`, "cat": `{"format":"{a.b}","a":{"format":"{b}","b":["2"]}}`,
}), WithSeed(1)) }), WithSeed(1))
if !slices.Contains(f.List(), "cat.a.b") { if !slices.Contains(f.List(), "cat.a.b") {
@@ -292,7 +292,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) {
// single-variant choice always picks the same item, so it needs no every-variant // single-variant choice always picks the same item, so it needs no every-variant
// guard and the error can name the field that is missing. // guard and the error can name the field that is missing.
func TestMissingFieldNamesItself(t *testing.T) { func TestMissingFieldNamesItself(t *testing.T) {
f := newFejkdata(t, "data/sv_SE", WithSeed(1)) f := newGenerator(t, "data/sv_SE", WithSeed(1))
_, err := f.Fake("person.typo") _, err := f.Fake("person.typo")
if err == nil || !strings.Contains(err.Error(), `no field "typo"`) { if err == nil || !strings.Contains(err.Error(), `no field "typo"`) {
t.Errorf("Fake(person.typo) = %v, want it to name the missing field", err) t.Errorf("Fake(person.typo) = %v, want it to name the missing field", err)
@@ -306,7 +306,7 @@ func TestCategoryRootShapes(t *testing.T) {
"obj": `{"format":"00"}`, // object root "obj": `{"format":"00"}`, // object root
"lit": `"hello"`, // bare-string root "lit": `"hello"`, // bare-string root
}) })
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "obj"); !regexp.MustCompile(`^\d\d$`).MatchString(got) { if got := fake(t, f, "obj"); !regexp.MustCompile(`^\d\d$`).MatchString(got) {
t.Errorf("object-root category = %q, want two digits", got) t.Errorf("object-root category = %q, want two digits", got)
} }
@@ -327,7 +327,7 @@ func TestLongStringList(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
f := newFejkdata(t, writeData(t, map[string]string{"name": string(list)}), WithSeed(1)) f := newGenerator(t, writeData(t, map[string]string{"name": string(list)}), WithSeed(1))
valid := map[string]bool{} valid := map[string]bool{}
for _, v := range names { for _, v := range names {
@@ -355,7 +355,7 @@ var swedishName = regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`)
func TestShippedStreetComposition(t *testing.T) { func TestShippedStreetComposition(t *testing.T) {
// street is a choice of composed {first}{last} templates and literal names. // street is a choice of composed {first}{last} templates and literal names.
f := newFejkdata(t, "data/sv_SE", WithSeed(5)) f := newGenerator(t, "data/sv_SE", WithSeed(5))
for i := 0; i < 300; i++ { for i := 0; i < 300; i++ {
if s := fake(t, f, "address.street"); !swedishName.MatchString(s) { if s := fake(t, f, "address.street"); !swedishName.MatchString(s) {
t.Fatalf("street %q is not a Swedish street name", s) t.Fatalf("street %q is not a Swedish street name", s)
@@ -366,7 +366,7 @@ func TestShippedStreetComposition(t *testing.T) {
func TestShippedLastNameComposition(t *testing.T) { func TestShippedLastNameComposition(t *testing.T) {
// last is a choice of patronymic {first}sson templates, compound // last is a choice of patronymic {first}sson templates, compound
// {first}{last} templates and literal surnames. // {first}{last} templates and literal surnames.
f := newFejkdata(t, "data/sv_SE", WithSeed(6)) f := newGenerator(t, "data/sv_SE", WithSeed(6))
for i := 0; i < 300; i++ { for i := 0; i < 300; i++ {
if s := fake(t, f, "person.last"); !swedishName.MatchString(s) { if s := fake(t, f, "person.last"); !swedishName.MatchString(s) {
t.Fatalf("last name %q is not a Swedish surname", s) t.Fatalf("last name %q is not a Swedish surname", s)
@@ -376,7 +376,7 @@ func TestShippedLastNameComposition(t *testing.T) {
func TestShippedStreetNumberFormats(t *testing.T) { func TestShippedStreetNumberFormats(t *testing.T) {
// Reachable via a hyphenated path; covers all five weighted number variants. // Reachable via a hyphenated path; covers all five weighted number variants.
f := newFejkdata(t, "data/sv_SE", WithSeed(8)) f := newGenerator(t, "data/sv_SE", WithSeed(8))
re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`) re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`)
for i := 0; i < 300; i++ { for i := 0; i < 300; i++ {
if n := fake(t, f, "address.street-number"); !re.MatchString(n) { if n := fake(t, f, "address.street-number"); !re.MatchString(n) {
+12 -12
View File
@@ -13,7 +13,7 @@
// the last one wins on a name clash, so you can layer custom data over the // the last one wins on a name clash, so you can layer custom data over the
// built-ins. The JSON template format is documented in the README. // built-ins. The JSON template format is documented in the README.
// //
// A [Fejkdata] is not safe for concurrent use; create one per goroutine. // A [Generator] is not safe for concurrent use; create one per goroutine.
package fejkdata package fejkdata
import ( import (
@@ -24,15 +24,15 @@ import (
"sort" "sort"
) )
// Fejkdata generates fake data from a loaded namespace tree. Create one with [New]. // Generator generates fake data from a loaded namespace tree. Create one with [New].
type Fejkdata struct { type Generator struct {
rand *session rand *session
categories map[string]node // root namespace: name -> compiled node tree categories map[string]node // root namespace: name -> compiled node tree
} }
// session is one faker's mutable render state: the seeded rng plus the {seq()} // session is one generator's mutable render state: the seeded rng plus the {seq()}
// counters. Scoping it to the Fejkdata means sequences (and randomness) belong to // counters. Scoping it to the Generator means sequences (and randomness) belong to
// that faker and reset when you create a new one. Embedding *rand.Rand makes a // that generator and reset when you create a new one. Embedding *rand.Rand makes a
// *session satisfy the rng interface the renderer draws from. // *session satisfy the rng interface the renderer draws from.
type session struct { type session struct {
*rand.Rand *rand.Rand
@@ -50,21 +50,21 @@ type config struct {
seeded bool seeded bool
} }
// Option configures a [Fejkdata]. // Option configures a [Generator].
type Option func(*config) type Option func(*config)
// WithSeed makes output reproducible: two fakers with the same seed and locale // WithSeed makes output reproducible: two generators with the same seed and locale
// emit identical sequences. // emit identical sequences.
func WithSeed(seed uint64) Option { func WithSeed(seed uint64) Option {
return func(c *config) { c.seed, c.seeded = seed, true } return func(c *config) { c.seed, c.seeded = seed, true }
} }
// New builds a faker from one or more data directories (e.g. "./data/sv_SE"). // New builds a generator from one or more data directories (e.g. "./data/sv_SE").
// Each JSON file becomes a category named after the file (address.json -> // Each JSON file becomes a category named after the file (address.json ->
// "address") and each subdirectory a namespace segment; directories are merged // "address") and each subdirectory a namespace segment; directories are merged
// in order, the last winning a name clash. It errors on a missing directory, // in order, the last winning a name clash. It errors on a missing directory,
// invalid JSON, or no data found. // invalid JSON, or no data found.
func New(paths []string, opts ...Option) (*Fejkdata, error) { func New(paths []string, opts ...Option) (*Generator, error) {
cats, err := loadData(paths) cats, err := loadData(paths)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
@@ -73,7 +73,7 @@ func New(paths []string, opts ...Option) (*Fejkdata, error) {
for _, opt := range opts { for _, opt := range opts {
opt(&c) opt(&c)
} }
return &Fejkdata{rand: newRand(c.seed, c.seeded), categories: cats}, nil return &Generator{rand: newRand(c.seed, c.seeded), categories: cats}, nil
} }
// List returns the sorted dotted paths Fake can render: every category, the dotted // List returns the sorted dotted paths Fake can render: every category, the dotted
@@ -81,7 +81,7 @@ func New(paths []string, opts ...Option) (*Fejkdata, error) {
// single-variant choices the way a reference does. A choice consumes no segment, so // single-variant choices the way a reference does. A choice consumes no segment, so
// a path continues through a multi-variant one only where every variant carries it, // a path continues through a multi-variant one only where every variant carries it,
// which is the rule Fake applies too: List is the set of paths Fake accepts. // which is the rule Fake applies too: List is the set of paths Fake accepts.
func (f *Fejkdata) List() []string { func (f *Generator) List() []string {
var out []string var out []string
for _, name := range sortedNames(f.categories) { for _, name := range sortedNames(f.categories) {
for _, p := range paths(f.categories[name]) { for _, p := range paths(f.categories[name]) {
+8 -8
View File
@@ -5,14 +5,14 @@ import (
"testing" "testing"
) )
// newFejkdata creates a faker over a single data directory, failing on error. Most // newGenerator creates a generator over a single data directory, failing on error. Most
// tests load one dir; newFejkdataN loads several (last-loaded wins on conflicts). // tests load one dir; newGeneratorN loads several (last-loaded wins on conflicts).
func newFejkdata(t *testing.T, dir string, opts ...Option) *Fejkdata { func newGenerator(t *testing.T, dir string, opts ...Option) *Generator {
t.Helper() t.Helper()
return newFejkdataN(t, []string{dir}, opts...) return newGeneratorN(t, []string{dir}, opts...)
} }
func newFejkdataN(t *testing.T, dirs []string, opts ...Option) *Fejkdata { func newGeneratorN(t *testing.T, dirs []string, opts ...Option) *Generator {
t.Helper() t.Helper()
f, err := New(dirs, opts...) f, err := New(dirs, opts...)
if err != nil { if err != nil {
@@ -22,7 +22,7 @@ func newFejkdataN(t *testing.T, dirs []string, opts ...Option) *Fejkdata {
} }
// fake generates a value, failing the test on error. // fake generates a value, failing the test on error.
func fake(t *testing.T, f *Fejkdata, path string) string { func fake(t *testing.T, f *Generator, path string) string {
t.Helper() t.Helper()
s, err := f.Fake(path) s, err := f.Fake(path)
if err != nil { if err != nil {
@@ -39,7 +39,7 @@ func TestNewMissingDirectory(t *testing.T) {
} }
func TestWithSeedIsDeterministic(t *testing.T) { func TestWithSeedIsDeterministic(t *testing.T) {
a, b := newFejkdata(t, "data/sv_SE", WithSeed(42)), newFejkdata(t, "data/sv_SE", WithSeed(42)) a, b := newGenerator(t, "data/sv_SE", WithSeed(42)), newGenerator(t, "data/sv_SE", WithSeed(42))
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
if x, y := fake(t, a, "person"), fake(t, b, "person"); x != y { if x, y := fake(t, a, "person"), fake(t, b, "person"); x != y {
t.Fatalf("same seed diverged at %d: %q != %q", i, x, y) t.Fatalf("same seed diverged at %d: %q != %q", i, x, y)
@@ -48,7 +48,7 @@ func TestWithSeedIsDeterministic(t *testing.T) {
} }
func TestDifferentSeedsDiffer(t *testing.T) { func TestDifferentSeedsDiffer(t *testing.T) {
a, b := newFejkdata(t, "data/en_US", WithSeed(1)), newFejkdata(t, "data/en_US", WithSeed(2)) a, b := newGenerator(t, "data/en_US", WithSeed(1)), newGenerator(t, "data/en_US", WithSeed(2))
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
if fake(t, a, "person") != fake(t, b, "person") { if fake(t, a, "person") != fake(t, b, "person") {
return return
+9 -9
View File
@@ -21,7 +21,7 @@ func TestList(t *testing.T) {
// Only the fields every variant carries are addressable, so "extra" is not. // Only the fields every variant carries are addressable, so "extra" is not.
"coin": `[{"format":"{code}","code":["A"],"name":["Aa"]},{"format":"{code}","code":["B"],"name":["Bb"],"extra":["x"]}]`, "coin": `[{"format":"{code}","code":["A"],"name":["Aa"]},{"format":"{code}","code":["B"],"name":["Bb"],"extra":["x"]}]`,
}) })
got := newFejkdata(t, dir, WithSeed(1)).List() got := newGenerator(t, dir, WithSeed(1)).List()
want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"} want := []string{"coin", "coin.code", "coin.name", "geo.city", "greeting", "greeting.own", "person", "person.first", "person.last", "word"}
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
t.Fatalf("List() = %v, want %v", got, want) t.Fatalf("List() = %v, want %v", got, want)
@@ -50,7 +50,7 @@ func writeData(t *testing.T, files map[string]string) string {
func TestNewLoadsAnyDirName(t *testing.T) { func TestNewLoadsAnyDirName(t *testing.T) {
// No locale tag required: a directory named anything loads fine. // No locale tag required: a directory named anything loads fine.
dir := writeData(t, map[string]string{"greeting": `["hej", "hallå"]`}) dir := writeData(t, map[string]string{"greeting": `["hej", "hallå"]`})
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "greeting"); got != "hej" && got != "hallå" { if got := fake(t, f, "greeting"); got != "hej" && got != "hallå" {
t.Fatalf("greeting = %q, want hej or hallå", got) t.Fatalf("greeting = %q, want hej or hallå", got)
} }
@@ -69,7 +69,7 @@ func TestFoldersBecomeDotPaths(t *testing.T) {
"sv_SE/greeting": `["hej"]`, "sv_SE/greeting": `["hej"]`,
"en_US/greeting": `["hi"]`, "en_US/greeting": `["hi"]`,
}) })
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "sv_SE.greeting"); got != "hej" { if got := fake(t, f, "sv_SE.greeting"); got != "hej" {
t.Fatalf("sv_SE.greeting = %q, want hej", got) t.Fatalf("sv_SE.greeting = %q, want hej", got)
} }
@@ -84,7 +84,7 @@ func TestNestedFoldersAndJSON(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":["leaf"]}}}`, "a/b/c/thing": `{"format":"{x}","x":{"format":"{y}","y":{"format":"{z}","z":["leaf"]}}}`,
}) })
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
// Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path. // Folders a.b.c, file thing, then JSON fields x.y.z — one continuous path.
if got := fake(t, f, "a.b.c.thing.x.y.z"); got != "leaf" { if got := fake(t, f, "a.b.c.thing.x.y.z"); got != "leaf" {
t.Fatalf("a.b.c.thing.x.y.z = %q, want leaf", got) t.Fatalf("a.b.c.thing.x.y.z = %q, want leaf", got)
@@ -97,7 +97,7 @@ func TestNestedFoldersAndJSON(t *testing.T) {
func TestRenderingAFolderErrors(t *testing.T) { func TestRenderingAFolderErrors(t *testing.T) {
dir := writeData(t, map[string]string{"sv_SE/greeting": `["hej"]`}) dir := writeData(t, map[string]string{"sv_SE/greeting": `["hej"]`})
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if _, err := f.Fake("sv_SE"); err == nil { if _, err := f.Fake("sv_SE"); err == nil {
t.Fatal("Fake(folder) = nil error, want a not-a-value error") t.Fatal("Fake(folder) = nil error, want a not-a-value error")
} }
@@ -108,7 +108,7 @@ func TestRenderingAFolderErrors(t *testing.T) {
func TestMultiPathLastWins(t *testing.T) { func TestMultiPathLastWins(t *testing.T) {
a := writeData(t, map[string]string{"greeting": `["from-a"]`, "only-a": `["a"]`}) a := writeData(t, map[string]string{"greeting": `["from-a"]`, "only-a": `["a"]`})
b := writeData(t, map[string]string{"greeting": `["from-b"]`, "only-b": `["b"]`}) b := writeData(t, map[string]string{"greeting": `["from-b"]`, "only-b": `["b"]`})
f := newFejkdataN(t, []string{a, b}, WithSeed(1)) f := newGeneratorN(t, []string{a, b}, WithSeed(1))
if got := fake(t, f, "greeting"); got != "from-b" { if got := fake(t, f, "greeting"); got != "from-b" {
t.Fatalf("greeting = %q, want from-b (last loaded wins)", got) t.Fatalf("greeting = %q, want from-b (last loaded wins)", got)
} }
@@ -126,7 +126,7 @@ func TestMultiPathLastWins(t *testing.T) {
func TestMultiPathMergesFolders(t *testing.T) { func TestMultiPathMergesFolders(t *testing.T) {
a := writeData(t, map[string]string{"sv_SE/person": `["from-a"]`, "sv_SE/shared": `["a"]`}) a := writeData(t, map[string]string{"sv_SE/person": `["from-a"]`, "sv_SE/shared": `["a"]`})
b := writeData(t, map[string]string{"sv_SE/company": `["from-b"]`, "sv_SE/shared": `["b"]`}) b := writeData(t, map[string]string{"sv_SE/company": `["from-b"]`, "sv_SE/shared": `["b"]`})
f := newFejkdataN(t, []string{a, b}, WithSeed(1)) f := newGeneratorN(t, []string{a, b}, WithSeed(1))
if got := fake(t, f, "sv_SE.person"); got != "from-a" { if got := fake(t, f, "sv_SE.person"); got != "from-a" {
t.Fatalf("sv_SE.person = %q, want from-a (folder merged, not replaced)", got) t.Fatalf("sv_SE.person = %q, want from-a (folder merged, not replaced)", got)
} }
@@ -142,7 +142,7 @@ func TestMultiPathMergesFolders(t *testing.T) {
// tree: everything List advertises renders, every time, and the sub-fields the // tree: everything List advertises renders, every time, and the sub-fields the
// README advertises are discoverable. // README advertises are discoverable.
func TestListedPathsAllRender(t *testing.T) { func TestListedPathsAllRender(t *testing.T) {
f := newFejkdata(t, "data", WithSeed(4)) f := newGenerator(t, "data", WithSeed(4))
paths := f.List() paths := f.List()
for _, p := range paths { for _, p := range paths {
for i := 0; i < 20; i++ { for i := 0; i < 20; i++ {
@@ -171,7 +171,7 @@ func TestHiddenEntriesAreSkipped(t *testing.T) {
if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil { if err := os.Rename(filepath.Join(dir, "empty.folder", "doc.json"), filepath.Join(dir, "empty.folder", "doc.txt")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "cat"); got != "V" { if got := fake(t, f, "cat"); got != "V" {
t.Fatalf("cat = %q, want V", got) t.Fatalf("cat = %q, want V", got)
} }
+7 -7
View File
@@ -9,7 +9,7 @@ func TestRootReferenceAcrossFolders(t *testing.T) {
"en_US/person": `["Pat Smith"]`, "en_US/person": `["Pat Smith"]`,
"sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`, "sv_SE/greeting": `{"format":"Hej, {..en_US.person}!"}`,
}) })
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" { if got := fake(t, f, "sv_SE.greeting"); got != "Hej, Pat Smith!" {
t.Fatalf("greeting = %q, want \"Hej, Pat Smith!\"", got) t.Fatalf("greeting = %q, want \"Hej, Pat Smith!\"", got)
} }
@@ -22,7 +22,7 @@ func TestReferenceIntoAField(t *testing.T) {
"who": `[{"format":"{first} {last}","first":["Ada"],"last":["Byron"]}]`, "who": `[{"format":"{first} {last}","first":["Ada"],"last":["Byron"]}]`,
"card": `{"format":"signed {..who.last}"}`, "card": `{"format":"signed {..who.last}"}`,
}) })
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "card"); got != "signed Byron" { if got := fake(t, f, "card"); got != "signed Byron" {
t.Fatalf("card = %q, want \"signed Byron\"", got) t.Fatalf("card = %q, want \"signed Byron\"", got)
} }
@@ -35,7 +35,7 @@ func TestReferenceInAlternation(t *testing.T) {
"far": `["X"]`, "far": `["X"]`,
"near": `{"format":"{here|..far}","here":["H"]}`, "near": `{"format":"{here|..far}","here":["H"]}`,
}) })
f := newFejkdata(t, dir, WithSeed(2)) f := newGenerator(t, dir, WithSeed(2))
seen := map[string]bool{} seen := map[string]bool{}
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
seen[fake(t, f, "near")] = true seen[fake(t, f, "near")] = true
@@ -50,7 +50,7 @@ func TestReferenceInAlternation(t *testing.T) {
func TestReferenceCombinesLoadedPaths(t *testing.T) { func TestReferenceCombinesLoadedPaths(t *testing.T) {
a := writeData(t, map[string]string{"en_US/word": `["river"]`}) a := writeData(t, map[string]string{"en_US/word": `["river"]`})
b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`}) b := writeData(t, map[string]string{"mine/slug": `{"format":"the-{..en_US.word}"}`})
f := newFejkdataN(t, []string{a, b}, WithSeed(1)) f := newGeneratorN(t, []string{a, b}, WithSeed(1))
if got := fake(t, f, "mine.slug"); got != "the-river" { if got := fake(t, f, "mine.slug"); got != "the-river" {
t.Fatalf("slug = %q, want the-river", got) t.Fatalf("slug = %q, want the-river", got)
} }
@@ -64,7 +64,7 @@ func TestReferenceChain(t *testing.T) {
"b": `{"format":"{..c}"}`, "b": `{"format":"{..c}"}`,
"c": `["deep"]`, "c": `["deep"]`,
}) })
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "a"); got != "deep" { if got := fake(t, f, "a"); got != "deep" {
t.Fatalf("a = %q, want deep", got) t.Fatalf("a = %q, want deep", got)
} }
@@ -118,7 +118,7 @@ func TestReferenceErrors(t *testing.T) {
// starting with the reference prefix is covered by that same rule, since ".." // starting with the reference prefix is covered by that same rule, since ".."
// starts with "." — it is skipped, not rejected. // starts with "." — it is skipped, not rejected.
func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) { func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
f := newFejkdata(t, writeData(t, map[string]string{ f := newGenerator(t, writeData(t, map[string]string{
"sv_SE/ok": `["fine"]`, "sv_SE/ok": `["fine"]`,
"sv_SE/..bad": `{"format":"{..nope}"}`, "sv_SE/..bad": `{"format":"{..nope}"}`,
"sv_SE/..y/ct": `{"format":"{..nope}"}`, "sv_SE/..y/ct": `{"format":"{..nope}"}`,
@@ -134,7 +134,7 @@ func TestDotPrefixedDataEntriesAreSkipped(t *testing.T) {
// category, which terminates, and stays renderable by path. // category, which terminates, and stays renderable by path.
func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) { func TestReferenceFromUnrenderedFieldTerminates(t *testing.T) {
dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`}) dir := writeData(t, map[string]string{"cat": `{"format":"hi","x":{"format":"see {..cat}"}}`})
f := newFejkdata(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
if got := fake(t, f, "cat"); got != "hi" { if got := fake(t, f, "cat"); got != "hi" {
t.Fatalf("cat = %q, want hi", got) t.Fatalf("cat = %q, want hi", got)
} }
+1 -1
View File
@@ -17,7 +17,7 @@ type rng interface {
// names and the category (JSON file) come first, then named fields within it, // names and the category (JSON file) come first, then named fields within it,
// e.g. "sv_SE.address" or "sv_SE.address.street". Choices along the way are // e.g. "sv_SE.address" or "sv_SE.address.street". Choices along the way are
// resolved at random. A path naming a folder (no value of its own) is an error. // resolved at random. A path naming a folder (no value of its own) is an error.
func (f *Fejkdata) Fake(path string) (string, error) { func (f *Generator) Fake(path string) (string, error) {
n, err := descend(f.rand, &group{children: f.categories}, strings.Split(path, ".")) n, err := descend(f.rand, &group{children: f.categories}, strings.Split(path, "."))
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %s: %w", path, err) return "", fmt.Errorf("fejkdata: %s: %w", path, err)
+2 -2
View File
@@ -2,7 +2,7 @@ package fejkdata
import "testing" import "testing"
// TestSeededOutputIsStable pins the exact bytes a seeded faker emits for each // TestSeededOutputIsStable pins the exact bytes a seeded generator emits for each
// piece of the token grammar, so a change to how a format is scanned or rendered // piece of the token grammar, so a change to how a format is scanned or rendered
// cannot quietly shift the rng stream that reproducibility depends on. // cannot quietly shift the rng stream that reproducibility depends on.
func TestSeededOutputIsStable(t *testing.T) { func TestSeededOutputIsStable(t *testing.T) {
@@ -31,7 +31,7 @@ func TestSeededOutputIsStable(t *testing.T) {
"weights": {"big", "big", "big", "big"}, "weights": {"big", "big", "big", "big"},
} }
for path := range want { for path := range want {
f := newFejkdata(t, dir, WithSeed(42)) f := newGenerator(t, dir, WithSeed(42))
for i, expect := range want[path] { for i, expect := range want[path] {
if got := fake(t, f, path); got != expect { if got := fake(t, f, path); got != expect {
t.Errorf("%s draw %d = %q, want %q", path, i, got, expect) t.Errorf("%s draw %d = %q, want %q", path, i, got, expect)
+3 -3
View File
@@ -7,8 +7,8 @@ import (
"testing" "testing"
) )
// engine builds a seeded faker with no loaded categories, for rendering tests. // engine builds a seeded generator with no loaded categories, for rendering tests.
func engine(seed uint64) *Fejkdata { return &Fejkdata{rand: newRand(seed, true)} } func engine(seed uint64) *Generator { return &Generator{rand: newRand(seed, true)} }
// parse unmarshals a JSON template fragment into its dynamic form. // parse unmarshals a JSON template fragment into its dynamic form.
func parse(t *testing.T, s string) any { func parse(t *testing.T, s string) any {
@@ -30,7 +30,7 @@ func compiled(t *testing.T, s string) node {
return n return n
} }
func mustRender(t *testing.T, f *Fejkdata, s string) string { func mustRender(t *testing.T, f *Generator, s string) string {
t.Helper() t.Helper()
return render(f.rand, compiled(t, s)) return render(f.rand, compiled(t, s))
} }