From 8e8fa92e774bc88fba1e0452d6863e9e50be415c Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 12 Oct 2026 18:17:45 +0200 Subject: [PATCH] Answer architecture round 1: Main draws the CLI's default seed and passes it to run, New names every option mistake at once, dataGiven says what it is, and the docs drop seeded where every sequence is --- CHANGELOG.md | 8 ++++---- README.md | 6 +++--- cli/cli.go | 14 +++++++------- fejkdata.go | 35 ++++++++++++++++------------------- inline.go | 2 +- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a7fad9..397cf3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,10 +26,10 @@ and any release may change what a seed draws, so pin one exact version 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` 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 + `New` loads the modules `WithDataFS` names and the folders `WithDataPath` names, drawing + from the seed `WithSeed` gives, and fails naming each of `WithSeed` and `WithDataFS` + that is missing. The same seed repeats the draws 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 diff --git a/README.md b/README.md index b8cd15a..4cf08bb 100644 --- a/README.md +++ b/README.md @@ -372,8 +372,8 @@ links all of them. Two locales, with `enus` from `github.com/larvit/fejkdata/dat are `WithDataFS(append(svse.Modules(), enus.Modules()...)...)`. To stand in your own places, pass the locale's `FS` alone beside a tree providing `geo.SE.address`, a record with a `format` and the columns `sv_SE.address` reads, `street`, `street-number`, -`postal-code` and `locality`: `WithDataFS(svse.FS, myPlaces)`. `New` fails where no `WithDataFS` or `WithDataPath` names the -data to load. +`postal-code` and `locality`: `WithDataFS(svse.FS, myPlaces)`. `New` fails where no `WithSeed` gives the seed, or no +`WithDataFS` or `WithDataPath` names the data to load. `New` refuses data that breaks the grammar, could mean two things or cannot render a valid value, and `NewTemplate` refuses such a template. A category of an indexed source is @@ -430,7 +430,7 @@ first call for a type compiles its tags and reports what they get wrong, with th error on every later call; a `datatype` in a tag other than the one its Go type sets is refused, naming that type. -A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only +A `*Generator` is safe for concurrent use; a sequence is reproducible only when drawn from one goroutine. Changing how a value is composed shifts the seeded stream for that value and everything drawn after it. diff --git a/cli/cli.go b/cli/cli.go index bdaac3a..e6a96f7 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -320,7 +320,7 @@ func (in invocation) check(name string) error { return nil } -func (in invocation) options(modules []fs.FS) []fejkdata.Option { +func (in invocation) options(modules []fs.FS, systemSeed uint64) []fejkdata.Option { if in.noShipped { modules = nil } @@ -330,7 +330,7 @@ func (in invocation) options(modules []fs.FS) []fejkdata.Option { } seed := in.seed if !in.seeded { - seed = rand.Uint64() + seed = systemSeed } return append(opts, fejkdata.WithSeed(seed)) } @@ -572,7 +572,7 @@ func Main(modules ...fs.FS) int { if fi, err := os.Stdin.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 { stdin = nil } - return run(commandName(os.Args[0]), modules, os.Args[1:], stdin, os.Stdout, os.Stderr) + return run(commandName(os.Args[0]), modules, os.Args[1:], rand.Uint64(), stdin, os.Stdout, os.Stderr) } // commandName is the name the command runs under: the file arg0 names, less a Windows @@ -588,9 +588,9 @@ func commandName(arg0 string) string { return name } -// run is Main over the given streams, run as the command name. A nil stdin is a terminal, -// which holds no template. -func run(name string, modules []fs.FS, args []string, stdin io.Reader, stdout, stderr io.Writer) int { +// run is Main over the given streams, run as the command name, seeded by systemSeed where +// no --seed is given. A nil stdin is a terminal, which holds no template. +func run(name string, modules []fs.FS, args []string, systemSeed uint64, stdin io.Reader, stdout, stderr io.Writer) int { in, err := parseArgs(args) if err != nil { return misuse(stderr, name, err) @@ -613,7 +613,7 @@ func run(name string, modules []fs.FS, args []string, stdin io.Reader, stdout, s return fail(stderr, name, err) } } - f, err := fejkdata.New(in.options(modules)...) + f, err := fejkdata.New(in.options(modules, systemSeed)...) if err != nil { return fail(stderr, name, err) } diff --git a/fejkdata.go b/fejkdata.go index bc9e721..93430d5 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -20,7 +20,7 @@ const MaxRepeat = 1 << 20 var _ [^uint(0)>>63 - 1]struct{} // Generator generates fake data from a namespace tree. Create one with [New]. -// It is safe for concurrent use; a seeded sequence is reproducible only when drawn +// It is safe for concurrent use; a sequence is reproducible only when drawn // from one goroutine. type Generator struct { // mu guards drawState, structs and root, which gains an indexed category on the @@ -36,8 +36,8 @@ type config struct { seeded bool sources []datafiles.Source nilModules error - // given is whether an option named the data to load, though it may name none. - given bool + // dataGiven is whether an option named the data to load, though it may name none. + dataGiven bool } // Option configures a [Generator]. @@ -54,7 +54,7 @@ func WithSeed(seed uint64) Option { // layer several; the last wins a name clash. func WithDataPath(dir string) Option { return func(c *config) { - c.sources, c.given = append(c.sources, datafiles.Dir(dir)), true + c.sources, c.dataGiven = append(c.sources, datafiles.Dir(dir)), true } } @@ -71,7 +71,7 @@ func WithDataFS(modules ...fs.FS) Option { } c.sources = append(c.sources, datafiles.FS(fsys)) } - c.given = true + c.dataGiven = true } } @@ -91,24 +91,21 @@ 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 [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 -// index, as each shipped module's does, loads each category on the first call reaching -// it. Data failing to load, in New or on that first call, gives an error matching +// 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 index, as each shipped +// module's does, loads each category on the first call reaching it. Data failing to load, in New or on that first call, gives an error matching // [ErrLoad]. func New(opts ...Option) (*Generator, error) { var c config for _, opt := range opts { opt(&c) } - if err := c.missing(); err != nil { + if err := c.misuse(); err != nil { return nil, err } - if c.nilModules != nil { - return nil, c.nilModules - } root, err := loadSources(c.sources) if err != nil { return nil, fmt.Errorf("fejkdata: %w", loadError{err}) @@ -128,14 +125,14 @@ func (f *Generator) List() []string { return out } -// missing names each option New requires that no option gave. -func (c config) missing() error { +// misuse names every mistake in the options, before any data loads. +func (c config) misuse() error { var err error - if !c.given { + if !c.dataGiven { err = errNoData } if !c.seeded { err = errors.Join(err, errNoSeed) } - return err + return errors.Join(err, c.nilModules) } diff --git a/inline.go b/inline.go index a9e15b3..c923ee8 100644 --- a/inline.go +++ b/inline.go @@ -11,7 +11,7 @@ import ( // Template is an inline template compiled, referenced and validated against a // generator's loaded data once, ready to render many times with [Template.Fake]. // It is safe for concurrent use: Fake serializes on its generator's lock, so a -// seeded sequence is reproducible only when a generator — and its templates — are +// sequence is reproducible only when a generator — and its templates — are // drawn from one goroutine. type Template struct { g *Generator