Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -435,7 +435,7 @@ number, and a choice whose every weight is `0`.
|
||||
|
||||
### Repeat
|
||||
|
||||
A template may carry `repeat` (a positive integer, default `1`) to render its format
|
||||
A template may carry `repeat` (an integer of 0 or more, default `1`; `0` renders nothing) to render its format
|
||||
that many times — each an independent draw — joined by `separator` (default `""`):
|
||||
|
||||
```json
|
||||
@@ -632,7 +632,7 @@ namespace too.
|
||||
|
||||
A `{name(args)}` token calls a builtin. Arguments are checked at `New`: a bad
|
||||
count, range, country or expression fails fast; an integer may carry a sign or leading
|
||||
zeros (`+5` and `05` are `5`); bounds are finite; and a length, count or decimal place beyond a sane maximum is rejected, so a
|
||||
zeros (`+5` and `05` are `5`); a count of `0` renders nothing; bounds are finite; and a length, count or decimal place beyond a sane maximum is rejected, so a
|
||||
fat-fingered `hex(2000000000)` never tries to allocate gigabytes. Every builtin draws only from the seed — a
|
||||
time-based id takes its timestamp from the rng, not the clock — so seeded output
|
||||
stays reproducible.
|
||||
@@ -764,7 +764,8 @@ and so are two names bound to one category.
|
||||
A name is drawn on its first read, and lives in the category binding it: any field of the
|
||||
category may read it, and a record's columns read one pick. Each render of the category picks
|
||||
anew, and so does each bare reference to it. A name a `repeat` binds picks again on every
|
||||
iteration, and one bound outside the repeat keeps its pick on every line.
|
||||
iteration, and one bound outside the repeat keeps its pick on every line. A name a choice's
|
||||
item binds lives in that item, and picks anew each time the item is drawn.
|
||||
|
||||
Refused at `New`, each error naming what to write instead:
|
||||
|
||||
@@ -774,7 +775,8 @@ Refused at `New`, each error naming what to write instead:
|
||||
- a binding of anything but a reference, or a field of the template binding it or a path into
|
||||
one;
|
||||
- a read of a name inside the field bound to it: read it outside that field;
|
||||
- a binding in a choice's item, which every other item would leave unbound;
|
||||
- a read outside a choice's item of a name bound inside it, which every other item would
|
||||
leave unbound;
|
||||
- a path through a name that selects a row;
|
||||
- `{n}` beside `{n.w}` where `n`'s category reads `w` twice, as in `{w}-{w}` or
|
||||
`{w.a} {w.b}`;
|
||||
|
||||
@@ -91,7 +91,7 @@ func constantValue(n grammar.CalcNode, operand func(string) []node) (float64, bo
|
||||
return 0, false
|
||||
}
|
||||
t, ok := nodes[0].(*template)
|
||||
if !ok || t.repeat > 1 {
|
||||
if !ok || t.repeat != 1 {
|
||||
return 0, false
|
||||
}
|
||||
lit, fixed := t.fixedText()
|
||||
@@ -122,7 +122,7 @@ func neverNumeric(n node) (text string, never bool) {
|
||||
return "", true
|
||||
case *template:
|
||||
lit, fixed := n.fixedText()
|
||||
if !fixed || n.repeat > 1 {
|
||||
if !fixed || n.repeat != 1 {
|
||||
return "", false
|
||||
}
|
||||
if _, err := strconv.ParseFloat(strings.TrimSpace(lit), 64); err != nil {
|
||||
|
||||
@@ -571,9 +571,6 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
}
|
||||
}
|
||||
f, err := fejkdata.New(in.options()...)
|
||||
if errors.Is(err, fejkdata.ErrNoData) {
|
||||
return misuse(stderr, errors.New("--no-shipped-data needs at least one --data-path"))
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 1
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/datafiles"
|
||||
@@ -16,9 +15,6 @@ func loadData(sources []datafiles.Source) (map[string]node, error) {
|
||||
}
|
||||
mergeChildren(root, g.children)
|
||||
}
|
||||
if len(root) == 0 {
|
||||
return nil, fmt.Errorf("no .json data found")
|
||||
}
|
||||
if err := categoryPipeline(categorySites(&folder{children: root}), root).run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-9
@@ -3,7 +3,6 @@ package fejkdata
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"reflect"
|
||||
@@ -20,9 +19,6 @@ const MaxRepeat = 1 << 20
|
||||
|
||||
var _ [^uint(0)>>63 - 1]struct{} // docs/decisions.md#64-bit-targets-only
|
||||
|
||||
// ErrNoData is returned by New when no source is loaded at all.
|
||||
var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS")
|
||||
|
||||
// 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
|
||||
// from one goroutine.
|
||||
@@ -73,8 +69,8 @@ func WithoutShippedData() Option {
|
||||
// New builds a generator from the shipped data set and the options' sources, merged
|
||||
// in order with the last winning a name clash. 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, invalid data,
|
||||
// or no data at all. With only the shipped data, a category loads on the first call
|
||||
// namespace segment. It errors on a missing directory, invalid JSON or invalid data; a
|
||||
// generator with no data at all renders templates that read none. With only the shipped data, a category loads on the first call
|
||||
// reaching it; with WithDataPath or WithDataFS, every category loads here.
|
||||
func New(opts ...Option) (*Generator, error) {
|
||||
c := config{shipped: true}
|
||||
@@ -102,9 +98,6 @@ func (c config) load() (folder, error) {
|
||||
sources = append(sources, shippedSource)
|
||||
}
|
||||
sources = append(sources, c.sources...)
|
||||
if len(sources) == 0 {
|
||||
return folder{}, ErrNoData
|
||||
}
|
||||
cats, err := loadData(sources)
|
||||
return folder{children: cats}, err
|
||||
}
|
||||
|
||||
@@ -33,14 +33,14 @@ var builtins = withTransforms(map[string]builtin{
|
||||
"ean": {arity: 0, prep: derive(eanCheck)},
|
||||
"uuid": {arity: 0, prep: sample(uuidV7)},
|
||||
"ulid": {arity: 0, prep: sample(ulid)},
|
||||
"nanoid": {arity: 1, checkArgs: posIntArg, prep: chars(nanoidAlphabet)},
|
||||
"hex": {arity: 1, checkArgs: posIntArg, prep: chars(hexDigits)},
|
||||
"digits": {arity: 1, checkArgs: posIntArg, prep: chars("0123456789"), prints: datatype.String, proveNumber: func(token string, prints datatype.DataType, a []string) proven.Facts {
|
||||
"nanoid": {arity: 1, checkArgs: countArg, prep: chars(nanoidAlphabet)},
|
||||
"hex": {arity: 1, checkArgs: countArg, prep: chars(hexDigits)},
|
||||
"digits": {arity: 1, checkArgs: countArg, prep: chars("0123456789"), prints: datatype.String, proveNumber: func(token string, prints datatype.DataType, a []string) proven.Facts {
|
||||
return proven.Printing(token, prints, proven.Bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true))
|
||||
}},
|
||||
"upper": {arity: 1, checkArgs: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
|
||||
"lower": {arity: 1, checkArgs: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
|
||||
"base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) Call {
|
||||
"upper": {arity: 1, checkArgs: countArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
|
||||
"lower": {arity: 1, checkArgs: countArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
|
||||
"base64": {arity: 1, checkArgs: countArg, prep: func(a []string) Call {
|
||||
n := atoi(a[0])
|
||||
return func(s *drawstate.State, _ string, _ []string) string {
|
||||
return base64.StdEncoding.EncodeToString(randBytes(s, n))
|
||||
@@ -158,7 +158,7 @@ func intArg(s string) (int, error) {
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
func posIntArg(a []string) error {
|
||||
func countArg(a []string) error {
|
||||
n, err := intArg(a[0])
|
||||
if errors.Is(err, strconv.ErrRange) {
|
||||
return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen)
|
||||
@@ -166,8 +166,8 @@ func posIntArg(a []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("count %w", err)
|
||||
}
|
||||
if n < 1 {
|
||||
return fmt.Errorf("count %q must be positive", a[0])
|
||||
if n < 0 {
|
||||
return fmt.Errorf("count %q must not be negative", a[0])
|
||||
}
|
||||
if n > maxLen {
|
||||
return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen)
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"github.com/larvit/fejkdata/internal/grammar"
|
||||
)
|
||||
|
||||
// nameScope is where a name is bound: a category, or one iteration of a repeat inside it. A
|
||||
// token sees its scope's names and every enclosing scope's, so one field may bind a name and
|
||||
// another read it.
|
||||
// nameScope is where a name is bound: a category, one iteration of a repeat inside it, or one
|
||||
// render of a choice's item. A token sees its scope's names and every enclosing scope's, so one
|
||||
// field may bind a name and another read it.
|
||||
type nameScope struct {
|
||||
up *nameScope
|
||||
owner node // the category's root, or the repeat
|
||||
owner node // the category's root, the repeat, or the choice's item
|
||||
bindings map[string]*nameBinding
|
||||
order []*nameBinding
|
||||
}
|
||||
@@ -56,28 +56,27 @@ func (sc *nameScope) lookup(name string) *nameBinding {
|
||||
}
|
||||
|
||||
// bindNames gives every template of a compiled category or inline template the scope its
|
||||
// names live in, and refuses a name bound twice along one chain of scopes, a binding in a
|
||||
// choice's item, a name a field spells too, and a read no field or name answers.
|
||||
// names live in, and refuses a name bound twice along one chain of scopes, a name a field spells
|
||||
// too, and a read no field or name answers.
|
||||
func bindNames(root node) error {
|
||||
var scopes []*nameScope
|
||||
var gather func(n node, scope *nameScope, inChoice bool, where string) error
|
||||
gather = func(n node, scope *nameScope, inChoice bool, where string) error {
|
||||
if t, isTemplate := n.(*template); n == root || isTemplate && t.repeat > 1 {
|
||||
var gather func(n node, scope *nameScope, item bool, where string) error
|
||||
gather = func(n node, scope *nameScope, item bool, where string) error {
|
||||
if t, isTemplate := n.(*template); n == root || isTemplate && (t.repeat > 1 || item) {
|
||||
scope = &nameScope{up: scope, owner: n}
|
||||
scopes = append(scopes, scope)
|
||||
inChoice = false
|
||||
}
|
||||
switch t := n.(type) {
|
||||
case *template:
|
||||
t.nameScope = scope
|
||||
if err := scope.bindAll(t, inChoice, where); err != nil {
|
||||
if err := scope.bindAll(t, where); err != nil {
|
||||
return err
|
||||
}
|
||||
case *choice:
|
||||
t.nameScope = scope
|
||||
}
|
||||
_, isChoice := n.(*choice)
|
||||
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, inChoice || isChoice, where) })
|
||||
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, isChoice, where) })
|
||||
}
|
||||
if err := gather(root, nil, false, ""); err != nil {
|
||||
return err
|
||||
@@ -85,21 +84,18 @@ func bindNames(root node) error {
|
||||
for _, sc := range scopes[1:] {
|
||||
for _, b := range sc.order {
|
||||
if outer := sc.up.lookup(b.name); outer != nil {
|
||||
return fmt.Errorf("%stoken {%s}: name %q is bound outside this repeat too, by {%s}; rename one", b.where, b.body, b.name, outer.body)
|
||||
return fmt.Errorf("%stoken {%s}: name %q is bound outside this %s too, by {%s}; rename one", b.where, b.body, b.name, sc.kind(), outer.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
return answerReads(root, scopes)
|
||||
}
|
||||
|
||||
func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
|
||||
func (sc *nameScope) bindAll(t *template, where string) error {
|
||||
for _, tok := range t.tokens {
|
||||
if tok.Kind != grammar.NameBind {
|
||||
continue
|
||||
}
|
||||
if inChoice {
|
||||
return fmt.Errorf("%stoken {%s}: a choice's item binds no name, since every other item would leave it unbound; bind it outside the choice, or move the item into a category of its own and reference that", where, tok.Body)
|
||||
}
|
||||
if b, twice := sc.bindings[tok.Bound]; twice {
|
||||
return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.Body, tok.Bound, sc.spelled(), b.body)
|
||||
}
|
||||
@@ -115,10 +111,18 @@ func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
|
||||
|
||||
// spelled names the scope in an error.
|
||||
func (sc *nameScope) spelled() string {
|
||||
if t, isTemplate := sc.owner.(*template); sc.up == nil && !(isTemplate && t.repeat > 1) {
|
||||
if sc.up == nil && sc.kind() != "repeat" {
|
||||
return "outside any repeat"
|
||||
}
|
||||
return "in one repeat"
|
||||
return "in one " + sc.kind()
|
||||
}
|
||||
|
||||
// kind names what a scope inside the category's is: a repeat, or a choice's item.
|
||||
func (sc *nameScope) kind() string {
|
||||
if t, isTemplate := sc.owner.(*template); isTemplate && t.repeat > 1 {
|
||||
return "repeat"
|
||||
}
|
||||
return "choice item"
|
||||
}
|
||||
|
||||
// ownScope is the scope t renders a frame of: a category's, once per render of its root, or a
|
||||
@@ -186,6 +190,9 @@ func unanswered(where string, u unboundRead, seen *nameScope, scopes []*nameScop
|
||||
if at == "" {
|
||||
at = "an item of the root choice"
|
||||
}
|
||||
if sc.kind() == "choice item" {
|
||||
return fmt.Errorf("%s%w; name %q is bound at %s, inside an item of a choice, which a read outside that item cannot see; bind it outside the choice", where, u.err, u.head, at)
|
||||
}
|
||||
return fmt.Errorf("%s%w; name %q is bound at %s, inside a repeat, which a read outside the repeat cannot see; bind it outside the repeat to read one pick on every line", where, u.err, u.head, at)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ func compileFields(m map[string]any, pos position) (map[string]node, error) {
|
||||
}
|
||||
|
||||
// repeatOf reads a template's "repeat" (default 1): how many times its format
|
||||
// is rendered and concatenated. A present one must be a positive integer.
|
||||
// is rendered and concatenated. A present one must be an integer of 0 or more.
|
||||
func repeatOf(m map[string]any) (int, error) {
|
||||
rv, ok := m["repeat"]
|
||||
if !ok {
|
||||
@@ -311,8 +311,8 @@ func repeatOf(m map[string]any) (int, error) {
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("repeat must be a number, not %s", jsonKind(rv))
|
||||
}
|
||||
if math.IsNaN(r) || math.IsInf(r, 0) || r < 1 || r != math.Trunc(r) {
|
||||
return 0, fmt.Errorf("repeat must be a positive integer, got %v", rv)
|
||||
if math.IsNaN(r) || math.IsInf(r, 0) || r < 0 || r != math.Trunc(r) {
|
||||
return 0, fmt.Errorf("repeat must be an integer of 0 or more, got %v", rv)
|
||||
}
|
||||
if r > MaxRepeat { // caps the renders one repeat asks for; repeatCheck bounds what nested ones multiply to
|
||||
return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, MaxRepeat)
|
||||
|
||||
Reference in New Issue
Block a user