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

This commit is contained in:
2026-10-12 18:17:45 +02:00
committed by Lillem4n
parent 7c0fa4075f
commit 8e8fa92e77
5 changed files with 31 additions and 34 deletions
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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.
+7 -7
View File
@@ -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)
}
+16 -19
View File
@@ -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)
}
+1 -1
View File
@@ -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