Files
fejkdata/fakes.go
T

87 lines
2.8 KiB
Go

// Package fakes generates fake data from recursive JSON templates.
//
// Data lives in JSON on disk, not in Go. Point [New] at one or more data
// directories; folders and files become a dot-path namespace, then generate
// values by path:
//
// f, _ := fakes.New([]string{"./data/sv_SE"}, fakes.WithSeed(42))
// f.Fake("address") // "Storgatan 12\n234 56 Göteborg"
// f.Fake("address.locality") // "Göteborg"
//
// A subdirectory is a namespace segment, so pointing at "./data" instead reaches
// a category as "sv_SE.address". Several directories are merged left to right;
// the last one wins on a name clash, so you can layer custom data over the
// built-ins. The JSON template format is documented in the README.
//
// A [Fakes] is not safe for concurrent use; create one per goroutine.
package fakes
import (
crand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand/v2"
)
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
type Fakes struct {
rand *session
categories map[string]node // root namespace: name -> compiled node tree
}
// session is one faker's mutable render state: the seeded rng plus the {seq()}
// counters. Scoping it to the Fakes means sequences (and randomness) belong to
// that faker and reset when you create a new one. Embedding *rand.Rand makes a
// *session satisfy the rng interface the renderer draws from.
type session struct {
*rand.Rand
counters map[string]uint64
}
// next returns the next value (counting from 1) of the named {seq()} counter.
func (s *session) next(key string) uint64 {
s.counters[key]++
return s.counters[key]
}
type config struct {
seed uint64
seeded bool
}
// Option configures a [Fakes].
type Option func(*config)
// WithSeed makes output reproducible: two fakers with the same seed and locale
// emit identical sequences.
func WithSeed(seed uint64) Option {
return func(c *config) { c.seed, c.seeded = seed, true }
}
// New builds a faker from one or more data directories (e.g. "./data/sv_SE").
// Each JSON file becomes a category named after the file (address.json ->
// "address") and each subdirectory a namespace segment; directories are merged
// in order, the last winning a name clash. It errors on a missing directory,
// invalid JSON, or no data found.
func New(paths []string, opts ...Option) (*Fakes, error) {
cats, err := loadData(paths)
if err != nil {
return nil, fmt.Errorf("fakes: %w", err)
}
var c config
for _, opt := range opts {
opt(&c)
}
return &Fakes{rand: newRand(c.seed, c.seeded), categories: cats}, nil
}
func newRand(seed uint64, seeded bool) *session {
r := rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15))
if !seeded {
var b [16]byte
_, _ = crand.Read(b[:])
r = rand.New(rand.NewPCG(binary.LittleEndian.Uint64(b[:8]), binary.LittleEndian.Uint64(b[8:])))
}
return &session{Rand: r, counters: map[string]uint64{}}
}