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