139 lines
4.9 KiB
Go
139 lines
4.9 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"reflect"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/larvit/fejkdata/internal/datafiles"
|
|
"github.com/larvit/fejkdata/internal/drawstate"
|
|
)
|
|
|
|
// MaxRepeat caps a repeat, and caps the renders nested repeats multiply to along
|
|
// any one path; the CLI's --repeat shares it.
|
|
const MaxRepeat = 1 << 20
|
|
|
|
// Fails to compile where uint is narrower than 64 bits: docs/decisions.md#64-bit-targets-only
|
|
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 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
|
|
// first call reaching it.
|
|
mu sync.Mutex
|
|
drawState *drawstate.State
|
|
root folder // the categories as the node a path walks from, owned here so a walk allocates none
|
|
structs map[reflect.Type]structResult
|
|
}
|
|
|
|
type config struct {
|
|
seed uint64
|
|
seeded bool
|
|
sources []datafiles.Source
|
|
nilModules error
|
|
// dataGiven is whether an option named the data to load, though it may name none.
|
|
dataGiven bool
|
|
}
|
|
|
|
// Option configures a [Generator].
|
|
type Option func(*config)
|
|
|
|
// 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,
|
|
// takes a new seed each run.
|
|
func WithSeed(seed uint64) Option {
|
|
return func(c *config) { c.seed, c.seeded = seed, true }
|
|
}
|
|
|
|
// WithDataPath layers a data directory over what is loaded before it. Repeat it to
|
|
// layer several; the last wins a name clash.
|
|
func WithDataPath(dir string) Option {
|
|
return func(c *config) {
|
|
c.sources, c.dataGiven = append(c.sources, datafiles.Dir(dir)), true
|
|
}
|
|
}
|
|
|
|
// WithDataFS layers modules, each a data tree held in an [fs.FS], over what is loaded
|
|
// before them, in order. A module may be a data package's FS, such as one of
|
|
// github.com/larvit/fejkdata/data's Modules, or an embed.FS of your own. WithDataFS()
|
|
// loads no data.
|
|
func WithDataFS(modules ...fs.FS) Option {
|
|
return func(c *config) {
|
|
for i, fsys := range modules {
|
|
if fsys == nil {
|
|
c.nilModules = errors.Join(c.nilModules, fmt.Errorf("fejkdata: WithDataFS module %d is nil", i+1))
|
|
continue
|
|
}
|
|
c.sources = append(c.sources, datafiles.FS(fsys))
|
|
}
|
|
c.dataGiven = true
|
|
}
|
|
}
|
|
|
|
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") for a new seed 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
|
|
// of an indexed source.
|
|
var ErrLoad = errors.New("data fails to load")
|
|
|
|
// loadError is a load's error, unchanged in its text, matching ErrLoad.
|
|
type loadError struct{ error }
|
|
|
|
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 [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 [ErrLoad].
|
|
func New(opts ...Option) (*Generator, error) {
|
|
var c config
|
|
for _, opt := range opts {
|
|
opt(&c)
|
|
}
|
|
if err := c.misuse(); err != nil {
|
|
return nil, err
|
|
}
|
|
root, err := loadSources(c.sources)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fejkdata: %w", loadError{err})
|
|
}
|
|
return &Generator{drawState: drawstate.New(c.seed), root: root}, nil
|
|
}
|
|
|
|
// List returns the sorted dotted paths Fake renders: each category and every field,
|
|
// column and linked table below it, a direct descent at a time, listing a level
|
|
// carrying a repeat but nothing below it. A choice consumes no segment, so a path
|
|
// continues through one only where every variant carries it.
|
|
func (f *Generator) List() []string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
out := paths(&f.root, false)
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// misuse names every mistake in the options, before any data loads.
|
|
func (c config) misuse() error {
|
|
var err error
|
|
if !c.dataGiven {
|
|
err = errNoData
|
|
}
|
|
if !c.seeded {
|
|
err = errors.Join(err, errNoSeed)
|
|
}
|
|
return errors.Join(err, c.nilModules)
|
|
}
|