From 90c804f6e6bea0902ebaf7c82c9c78cc8876ca35 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 12 Oct 2026 18:13:14 +0200 Subject: [PATCH] Fail New without WithSeed, naming it, and seed the CLI from the system without --seed --- CHANGELOG.md | 12 +++++++----- README.md | 6 +++--- cli/cli.go | 10 ++++++---- fejkdata.go | 41 ++++++++++++++++++----------------------- todo.md | 9 ++------- 5 files changed, 36 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2e5d0..4a7fad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,17 +18,19 @@ and any release may change what a seed draws, so pin one exact version - The CLI, `fejkdata`, renders exactly the template on its stdin and adds nothing: `echo '{/sv_SE.person} hihi' | fejkdata` prints e.g. `Sara Eriksson hihi`. `--seed` - repeats a run's output while the version and data stay the same, `-n` renders the - template that many times, and `--list` prints the paths the data offers. + repeats a run's output while the version and data stay the same, and without it the + CLI seeds from the system; `-n` renders the template that many times, and `--list` + prints the paths the data offers. `--data-path` layers a directory of your own data over the shipped set, and `--no-shipped-data` loads that directory alone. `--format json|ndjson|csv|sql` writes a record's columns, one record per row. The CLI exits `0` on success, `1` on a runtime error such as data that fails to load, and `2` on misuse. - The library, `github.com/larvit/fejkdata`, for Go 1.22 or later on a 64-bit target. `New` loads the modules `WithDataFS` names and the folders `WithDataPath` names, and - fails, naming `WithDataFS`, where neither is given. `WithSeed` makes its draws - repeat while the version and data stay the same. `Fake` renders a path, and - `FakeTemplate` and `NewTemplate` render a template. `FakeRecord`, `FakeRecordTemplate` + fails, naming `WithDataFS`, where neither is given. `WithSeed` is required, and `New` + fails naming it where none is given; its draws repeat while the version and data stay + the same. `Fake` renders a path, and `FakeTemplate` and `NewTemplate` render a + template. `FakeRecord`, `FakeRecordTemplate` and `NewRecordTemplate` render a record, whose `JSON`, `CSVHeader`, `CSVLine` and `SQLInsert` methods write its columns as the CLI does. `FakeStruct` fills a struct's `fake:"…"` tagged fields. `List` gives the paths `Fake` renders, direct descents only, diff --git a/README.md b/README.md index 4ff6070..b8cd15a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ template holding one from a quoted heredoc or a file. The rest of stdin's rules |------|--| | `-d`, `--data-path D` | a directory to layer over the shipped data; repeatable, the last wins a name clash | | `--no-shipped-data` | load only the `--data-path` directories | -| `-s`, `--seed N` | same seed, version and data: identical output | +| `-s`, `--seed N` | same seed, version and data: identical output (default: a random seed) | | `-n`, `--repeat N` | render the template N times (0 to 1048576), each an independent draw, streamed; `0` prints nothing, `[]` under `--format json`, or the header under `--format csv` | | `--separator S` | between repeated renders (default empty); refused with a record `--format`, which has its own separators | | `--format F` | `text` (default), `json`, `ndjson`, `csv` or `sql` — a record's columns, one record per row (json frames them as an array) | @@ -360,7 +360,7 @@ ok, err := fejkdata.IsTemplate(arg) // an inline template by its shape, el | Option | | |--------|--| -| `WithSeed(n)` | same seed, version and data: identical sequence | +| `WithSeed(n)` | required; same seed, version and data: identical sequence. `WithSeed(rand.Uint64())`, with `math/rand/v2`, draws afresh each run | | `WithDataPath(dir)` | layer a directory; repeat to layer several, the last wins a clash | | `WithDataFS(modules...)` | layer modules in order, each an `fs.FS`: a shipped package's `Modules()...`, or your own `embed.FS`; `WithDataFS()` loads none | @@ -459,7 +459,7 @@ func main() { os.Exit(cli.Main(append(data.Modules(), mydata)...)) } The embed's folders are the paths: `//go:embed acme` serves `acme/customer.json` as `{/acme.customer}`. Add `*.tsv` to the embed where your data holds a table. The CLI loads its data only when it runs. To catch broken data at `go test`, call -`fejkdata.New(fejkdata.WithDataFS(...))` over the same modules, and render each path once: +`fejkdata.New(fejkdata.WithDataFS(...), fejkdata.WithSeed(1))` over the same modules, and render each path once: a module with an index checks a category only when a call first reaches it. The CLI takes `fejkdata`'s flags and exit codes, and names itself by the file it runs from in its help and errors. `--no-shipped-data` leaves out the modules passed to `Main`, and `--version` prints your module's version beside the fejkdata version it is built with. diff --git a/cli/cli.go b/cli/cli.go index beef533..bdaac3a 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "io/fs" + "math/rand/v2" "os" "path/filepath" "reflect" @@ -63,7 +64,7 @@ one INSERT. --no-shipped-data load only the --data-path directories -n, --repeat N render the template N times, 0..1048576 (default 1); 0 prints nothing, [] under --format json, or the header under --format csv - -s, --seed N same seed, version and data: identical output + -s, --seed N same seed, version and data: identical output (default: a random seed) --separator S string between repeated renders (default empty); refused with a record --format, which has its own separators --table T the INSERT target for --format sql, ignored under any other @@ -327,10 +328,11 @@ func (in invocation) options(modules []fs.FS) []fejkdata.Option { for _, dir := range in.dirs { opts = append(opts, fejkdata.WithDataPath(dir)) } - if in.seeded { - opts = append(opts, fejkdata.WithSeed(in.seed)) + seed := in.seed + if !in.seeded { + seed = rand.Uint64() } - return opts + return append(opts, fejkdata.WithSeed(seed)) } // write streams the input's renders to w, repeat of them joined by the separator; a write diff --git a/fejkdata.go b/fejkdata.go index 5e1e295..bc9e721 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -1,8 +1,6 @@ package fejkdata import ( - crand "crypto/rand" - "encoding/binary" "errors" "fmt" "io/fs" @@ -45,8 +43,9 @@ type config struct { // Option configures a [Generator]. type Option func(*config) -// WithSeed makes output reproducible: two generators with the same seed, version and -// data emit identical sequences. +// WithSeed seeds the generator, as [New] requires: two generators with the same seed, +// version and data emit identical sequences. WithSeed(rand.Uint64()), with math/rand/v2, +// draws afresh each run. func WithSeed(seed uint64) Option { return func(c *config) { c.seed, c.seeded = seed, true } } @@ -76,6 +75,8 @@ func WithDataFS(modules ...fs.FS) Option { } } +var errNoSeed = errors.New(`fejkdata: no seed: add fejkdata.WithSeed(n) to repeat a run's output, or WithSeed(rand.Uint64()) (import "math/rand/v2") to draw afresh each run`) + var errNoData = errors.New(`fejkdata: no data: add fejkdata.WithDataFS(data.Modules()...) (import "github.com/larvit/fejkdata/data") for all shipped data, WithDataFS/WithDataPath for your own, or WithDataFS() for none`) // ErrLoad marks data that fails to load: in New, or on the first call reaching a category @@ -89,8 +90,8 @@ func (e loadError) Unwrap() error { return e.error } func (loadError) Is(target error) bool { return target == ErrLoad } // New builds a generator from the options' sources, merged in order with the last -// winning a name clash; it fails where no [WithDataFS] or [WithDataPath] names the data -// to load. Each JSON file becomes a category named after the file (address.json -> +// winning a name clash; it fails where no [WithSeed] gives the seed, or no [WithDataFS] +// or [WithDataPath] names the data to load. Each JSON file becomes a category named after the file (address.json -> // "address") and each subdirectory a namespace segment. It errors on a missing // directory, invalid JSON or invalid data. Given only WithDataFS() with no modules, New // loads no data, and the generator renders templates that read none. A source whose .fejkdata.json carries an @@ -102,8 +103,8 @@ func New(opts ...Option) (*Generator, error) { for _, opt := range opts { opt(&c) } - if !c.given { - return nil, errNoData + if err := c.missing(); err != nil { + return nil, err } if c.nilModules != nil { return nil, c.nilModules @@ -112,11 +113,7 @@ func New(opts ...Option) (*Generator, error) { if err != nil { return nil, fmt.Errorf("fejkdata: %w", loadError{err}) } - seed, err := c.drawSeed() - if err != nil { - return nil, fmt.Errorf("fejkdata: %w", err) - } - return &Generator{drawState: drawstate.New(seed), root: root}, nil + return &Generator{drawState: drawstate.New(c.seed), root: root}, nil } // List returns the sorted dotted paths Fake renders: each category and every field, @@ -131,16 +128,14 @@ func (f *Generator) List() []string { return out } -var randomBytes = crand.Read - -// drawSeed is the seed WithSeed gave, or one read from the system's entropy. -func (c config) drawSeed() (uint64, error) { - if c.seeded { - return c.seed, nil +// missing names each option New requires that no option gave. +func (c config) missing() error { + var err error + if !c.given { + err = errNoData } - var b [8]byte - if _, err := randomBytes(b[:]); err != nil { - return 0, fmt.Errorf("seeding from crypto/rand: %w", err) + if !c.seeded { + err = errors.Join(err, errNoSeed) } - return binary.LittleEndian.Uint64(b[:]), nil + return err } diff --git a/todo.md b/todo.md index bbce014..8833b93 100644 --- a/todo.md +++ b/todo.md @@ -65,7 +65,6 @@ | ID | Release | Exempt | Item | R | S | A | G | Goals | Score | |---|---|---|---|---|---|---|---|---|---| -| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 | | 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 | | 243 | 0.2.0 | | **Add `en_US.ein`, drawing only the prefixes the IRS assigns.** | 2 | 2 | 7 | 8 | 2.1, 4 | 26.7 | | 230 | 0.2.0 | | **Add `sv_SE.organisationsnummer`, whose first digit is its legal form's group digit and whose last is a Luhn check digit, and `sv_SE.vat`, `SE` + the number + `01`.** | 2 | 3 | 7 | 8 | 2.1, 4 | 26.4 | @@ -215,10 +214,6 @@ ## Details -### 89. Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system. - -Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. It ships in 0.1.0, which already makes `New` refuse a call that names no data, so `New` breaks once, before anyone depends on it. Mark `WithSeed` required in the Library section's options table. `New`'s tests then stop swapping `randomBytes`. `fejkdata_test.go` swaps it today, and while that test runs, every `New` in the package fails. Rewrite the changelog's library line to say `New` fails naming `WithSeed` where none is given, and its CLI line to say the CLI seeds from the system without `--seed`. - ### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go. GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. The binaries carry the stamped tag, which `--version` prints. v0.1.0 ships first, so it can be tested before it is packaged. @@ -368,7 +363,7 @@ Staðfangaskrá, Iceland's address register, holds every street with its póstn ### 26. Spell `sv_SE.personnummer` with the `+` Skatteverket uses from the year the holder turns 100, judged by a date the caller gives. The format hard-codes `-`, and its earliest birthdate, 1930-01-01, makes the oldest draws invalid from 2030. A system holding Swedish personnummer often breaks on the `+`, so a fixture carrying one is a valuable test. Whether a number takes `+` depends on the date it is read, and the decision "No builtin reads the clock, so a date is bounded by days, never by an age" forbids a builtin from reading that date off the clock. This item revises it: once the caller gives the date, an age is a bound, and the CLI's default date makes output change with the day it runs unless the flag is given. Do the first step before the rest: -- Explore how a caller passes input data, such as the date a number is read on, to a render: in the template, as a plain option, or both. Make the library require the date, as item 89 makes it require the seed, and let the CLI default it to the system's when its flag is absent. Bench the spelling; `--now` reads like the clock. Shape the input so it can later narrow the draws to adults, children, 65 and over, or a mix, and so let a test ask for a centenarian; narrowing itself waits for its own item. +- Explore how a caller passes input data, such as the date a number is read on, to a render: in the template, as a plain option, or both. Make the library require the date, as it requires the seed, and let the CLI default it to the system's when its flag is absent. Bench the spelling; `--now` reads like the clock. Shape the input so it can later narrow the draws to adults, children, 65 and over, or a mix, and so let a test ask for a centenarian; narrowing itself waits for its own item. - Draw birthdates back past 100 years before the given date, so a `+` can appear. Read in 2026, the oldest shipped birthdate is 96 years old, so no draw carries a `+` until 2030. Skatteverket's test series reaches back to 1890, with birth numbers 980/981 before 1900. Draw no birthdate after the given date. - Weight the ages by the territory's population, so the share of `+` numbers follows its demography: [research-age-bands.md](docs/research/research-age-bands.md). - Let a data author's own category spell the separator too, through a builtin or a function a data module provides (item 186), and update the README's personnummer recipe and its "a day in 1930–2025" count. @@ -629,7 +624,7 @@ Needs item 178, which splits it into items before any code moves. The engine ret ### 186. Load a module carrying functions through an option of its own, its functions reaching the generator's randomness and date through an interface the root declares. -Carries out the decision "The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0". Needs item 89's required seed and item 26's caller-given date. `cli.Main`'s signature grows here, which a minor before 1.0 allows. +Carries out the decision "The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0". Needs item 26's caller-given date. `cli.Main`'s signature grows here, which a minor before 1.0 allows. ### 168. Test the shared steps of `data-import/` in the merge gate, starting with `geo.fence`.