Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
5 changed files with 31 additions and 34 deletions
Showing only changes of commit 8e8fa92e77 - Show all commits
+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 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. 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. - 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 `New` loads the modules `WithDataFS` names and the folders `WithDataPath` names, drawing
fails, naming `WithDataFS`, where neither is given. `WithSeed` is required, and `New` from the seed `WithSeed` gives, and fails naming each of `WithSeed` and `WithDataFS`
fails naming it where none is given; its draws repeat while the version and data stay that is missing. The same seed repeats the draws while the version and data stay the
the same. `Fake` renders a path, and `FakeTemplate` and `NewTemplate` render a same. `Fake` renders a path, and `FakeTemplate` and `NewTemplate` render a
template. `FakeRecord`, `FakeRecordTemplate` template. `FakeRecord`, `FakeRecordTemplate`
and `NewRecordTemplate` render a record, whose `JSON`, `CSVHeader`, `CSVLine` and and `NewRecordTemplate` render a record, whose `JSON`, `CSVHeader`, `CSVLine` and
`SQLInsert` methods write its columns as the CLI does. `FakeStruct` fills a struct's `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 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 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`, 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 `postal-code` and `locality`: `WithDataFS(svse.FS, myPlaces)`. `New` fails where no `WithSeed` gives the seed, or no
data to load. `WithDataFS` or `WithDataPath` names the data to load.
`New` refuses data that breaks the grammar, could mean two things or cannot render a valid `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 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 error on every later call; a `datatype` in a tag other than the one its Go type sets is
refused, naming that type. 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 when drawn from one goroutine. Changing how a value is composed shifts the seeded
stream for that value and everything drawn after it. 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 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 { if in.noShipped {
modules = nil modules = nil
} }
@@ -330,7 +330,7 @@ func (in invocation) options(modules []fs.FS) []fejkdata.Option {
} }
seed := in.seed seed := in.seed
if !in.seeded { if !in.seeded {
seed = rand.Uint64() seed = systemSeed
} }
return append(opts, fejkdata.WithSeed(seed)) 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 { if fi, err := os.Stdin.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
stdin = nil 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 // 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 return name
} }
// run is Main over the given streams, run as the command name. A nil stdin is a terminal, // run is Main over the given streams, run as the command name, seeded by systemSeed where
// which holds no template. // no --seed is given. 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 { func run(name string, modules []fs.FS, args []string, systemSeed uint64, stdin io.Reader, stdout, stderr io.Writer) int {
in, err := parseArgs(args) in, err := parseArgs(args)
if err != nil { if err != nil {
return misuse(stderr, name, err) 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) return fail(stderr, name, err)
} }
} }
f, err := fejkdata.New(in.options(modules)...) f, err := fejkdata.New(in.options(modules, systemSeed)...)
if err != nil { if err != nil {
return fail(stderr, name, err) return fail(stderr, name, err)
} }
+16 -19
View File
@@ -20,7 +20,7 @@ const MaxRepeat = 1 << 20
var _ [^uint(0)>>63 - 1]struct{} var _ [^uint(0)>>63 - 1]struct{}
// Generator generates fake data from a namespace tree. Create one with [New]. // 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. // from one goroutine.
type Generator struct { type Generator struct {
// mu guards drawState, structs and root, which gains an indexed category on the // mu guards drawState, structs and root, which gains an indexed category on the
@@ -36,8 +36,8 @@ type config struct {
seeded bool seeded bool
sources []datafiles.Source sources []datafiles.Source
nilModules error nilModules error
// given is whether an option named the data to load, though it may name none. // dataGiven is whether an option named the data to load, though it may name none.
given bool dataGiven bool
} }
// Option configures a [Generator]. // Option configures a [Generator].
@@ -54,7 +54,7 @@ func WithSeed(seed uint64) Option {
// layer several; the last wins a name clash. // layer several; the last wins a name clash.
func WithDataPath(dir string) Option { func WithDataPath(dir string) Option {
return func(c *config) { 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.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 // 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] // 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 -> // or [WithDataPath] names the data to load. Each JSON file becomes a category named
// "address") and each subdirectory a namespace segment. It errors on a missing // after the file (address.json -> "address") and each subdirectory a namespace segment.
// directory, invalid JSON or invalid data. Given only WithDataFS() with no modules, New // It errors on a missing directory, invalid JSON or invalid data. Given only
// loads no data, and the generator renders templates that read none. A source whose .fejkdata.json carries an // WithDataFS() with no modules, New loads no data, and the generator renders templates
// index, as each shipped module's does, loads each category on the first call reaching // that read none. A source whose .fejkdata.json carries an index, as each shipped
// it. Data failing to load, in New or on that first call, gives an error matching // 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]. // [ErrLoad].
func New(opts ...Option) (*Generator, error) { func New(opts ...Option) (*Generator, error) {
var c config var c config
for _, opt := range opts { for _, opt := range opts {
opt(&c) opt(&c)
} }
if err := c.missing(); err != nil { if err := c.misuse(); err != nil {
return nil, err return nil, err
} }
if c.nilModules != nil {
return nil, c.nilModules
}
root, err := loadSources(c.sources) root, err := loadSources(c.sources)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", loadError{err}) return nil, fmt.Errorf("fejkdata: %w", loadError{err})
@@ -128,14 +125,14 @@ func (f *Generator) List() []string {
return out return out
} }
// missing names each option New requires that no option gave. // misuse names every mistake in the options, before any data loads.
func (c config) missing() error { func (c config) misuse() error {
var err error var err error
if !c.given { if !c.dataGiven {
err = errNoData err = errNoData
} }
if !c.seeded { if !c.seeded {
err = errors.Join(err, errNoSeed) 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 // Template is an inline template compiled, referenced and validated against a
// generator's loaded data once, ready to render many times with [Template.Fake]. // 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 // 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. // drawn from one goroutine.
type Template struct { type Template struct {
g *Generator g *Generator