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
8 changed files with 48 additions and 53 deletions
Showing only changes of commit f036f859d6 - Show all commits
+6 -4
View File
@@ -435,7 +435,7 @@ number, and a choice whose every weight is `0`.
### Repeat ### 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 `""`): that many times — each an independent draw — joined by `separator` (default `""`):
```json ```json
@@ -632,7 +632,7 @@ namespace too.
A `{name(args)}` token calls a builtin. Arguments are checked at `New`: a bad 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 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 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 time-based id takes its timestamp from the rng, not the clock — so seeded output
stays reproducible. 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 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 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 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: 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 - a binding of anything but a reference, or a field of the template binding it or a path into
one; one;
- a read of a name inside the field bound to it: read it outside that field; - 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; - 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 - `{n}` beside `{n.w}` where `n`'s category reads `w` twice, as in `{w}-{w}` or
`{w.a} {w.b}`; `{w.a} {w.b}`;
+2 -2
View File
@@ -91,7 +91,7 @@ func constantValue(n grammar.CalcNode, operand func(string) []node) (float64, bo
return 0, false return 0, false
} }
t, ok := nodes[0].(*template) t, ok := nodes[0].(*template)
if !ok || t.repeat > 1 { if !ok || t.repeat != 1 {
return 0, false return 0, false
} }
lit, fixed := t.fixedText() lit, fixed := t.fixedText()
@@ -122,7 +122,7 @@ func neverNumeric(n node) (text string, never bool) {
return "", true return "", true
case *template: case *template:
lit, fixed := n.fixedText() lit, fixed := n.fixedText()
if !fixed || n.repeat > 1 { if !fixed || n.repeat != 1 {
return "", false return "", false
} }
if _, err := strconv.ParseFloat(strings.TrimSpace(lit), 64); err != nil { if _, err := strconv.ParseFloat(strings.TrimSpace(lit), 64); err != nil {
-3
View File
@@ -571,9 +571,6 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
} }
} }
f, err := fejkdata.New(in.options()...) 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 { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
return 1 return 1
-4
View File
@@ -1,7 +1,6 @@
package fejkdata package fejkdata
import ( import (
"fmt"
"strings" "strings"
"github.com/larvit/fejkdata/internal/datafiles" "github.com/larvit/fejkdata/internal/datafiles"
@@ -16,9 +15,6 @@ func loadData(sources []datafiles.Source) (map[string]node, error) {
} }
mergeChildren(root, g.children) 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 { if err := categoryPipeline(categorySites(&folder{children: root}), root).run(); err != nil {
return nil, err return nil, err
} }
+2 -9
View File
@@ -3,7 +3,6 @@ package fejkdata
import ( import (
crand "crypto/rand" crand "crypto/rand"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"io/fs" "io/fs"
"reflect" "reflect"
@@ -20,9 +19,6 @@ const MaxRepeat = 1 << 20
var _ [^uint(0)>>63 - 1]struct{} // docs/decisions.md#64-bit-targets-only 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]. // 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 seeded sequence is reproducible only when drawn
// from one goroutine. // from one goroutine.
@@ -73,8 +69,8 @@ func WithoutShippedData() Option {
// New builds a generator from the shipped data set and the options' sources, merged // 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 // 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 // named after the file (address.json -> "address") and each subdirectory a
// namespace segment. It errors on a missing directory, invalid JSON, invalid data, // namespace segment. It errors on a missing directory, invalid JSON or invalid data; a
// or no data at all. With only the shipped data, a category loads on the first call // 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. // reaching it; with WithDataPath or WithDataFS, every category loads here.
func New(opts ...Option) (*Generator, error) { func New(opts ...Option) (*Generator, error) {
c := config{shipped: true} c := config{shipped: true}
@@ -102,9 +98,6 @@ func (c config) load() (folder, error) {
sources = append(sources, shippedSource) sources = append(sources, shippedSource)
} }
sources = append(sources, c.sources...) sources = append(sources, c.sources...)
if len(sources) == 0 {
return folder{}, ErrNoData
}
cats, err := loadData(sources) cats, err := loadData(sources)
return folder{children: cats}, err return folder{children: cats}, err
} }
+9 -9
View File
@@ -33,14 +33,14 @@ var builtins = withTransforms(map[string]builtin{
"ean": {arity: 0, prep: derive(eanCheck)}, "ean": {arity: 0, prep: derive(eanCheck)},
"uuid": {arity: 0, prep: sample(uuidV7)}, "uuid": {arity: 0, prep: sample(uuidV7)},
"ulid": {arity: 0, prep: sample(ulid)}, "ulid": {arity: 0, prep: sample(ulid)},
"nanoid": {arity: 1, checkArgs: posIntArg, prep: chars(nanoidAlphabet)}, "nanoid": {arity: 1, checkArgs: countArg, prep: chars(nanoidAlphabet)},
"hex": {arity: 1, checkArgs: posIntArg, prep: chars(hexDigits)}, "hex": {arity: 1, checkArgs: countArg, 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 { "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)) 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")}, "upper": {arity: 1, checkArgs: countArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
"lower": {arity: 1, checkArgs: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")}, "lower": {arity: 1, checkArgs: countArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
"base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) Call { "base64": {arity: 1, checkArgs: countArg, prep: func(a []string) Call {
n := atoi(a[0]) n := atoi(a[0])
return func(s *drawstate.State, _ string, _ []string) string { return func(s *drawstate.State, _ string, _ []string) string {
return base64.StdEncoding.EncodeToString(randBytes(s, n)) return base64.StdEncoding.EncodeToString(randBytes(s, n))
@@ -158,7 +158,7 @@ func intArg(s string) (int, error) {
} }
return n, nil return n, nil
} }
func posIntArg(a []string) error { func countArg(a []string) error {
n, err := intArg(a[0]) n, err := intArg(a[0])
if errors.Is(err, strconv.ErrRange) { if errors.Is(err, strconv.ErrRange) {
return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen) return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen)
@@ -166,8 +166,8 @@ func posIntArg(a []string) error {
if err != nil { if err != nil {
return fmt.Errorf("count %w", err) return fmt.Errorf("count %w", err)
} }
if n < 1 { if n < 0 {
return fmt.Errorf("count %q must be positive", a[0]) return fmt.Errorf("count %q must not be negative", a[0])
} }
if n > maxLen { if n > maxLen {
return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen) return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen)
+26 -19
View File
@@ -9,12 +9,12 @@ import (
"github.com/larvit/fejkdata/internal/grammar" "github.com/larvit/fejkdata/internal/grammar"
) )
// nameScope is where a name is bound: a category, or one iteration of a repeat inside it. A // nameScope is where a name is bound: a category, one iteration of a repeat inside it, or one
// token sees its scope's names and every enclosing scope's, so one field may bind a name and // render of a choice's item. A token sees its scope's names and every enclosing scope's, so one
// another read it. // field may bind a name and another read it.
type nameScope struct { type nameScope struct {
up *nameScope 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 bindings map[string]*nameBinding
order []*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 // 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 // names live in, and refuses a name bound twice along one chain of scopes, a name a field spells
// choice's item, a name a field spells too, and a read no field or name answers. // too, and a read no field or name answers.
func bindNames(root node) error { func bindNames(root node) error {
var scopes []*nameScope var scopes []*nameScope
var gather func(n node, scope *nameScope, inChoice bool, where string) error var gather func(n node, scope *nameScope, item bool, where string) error
gather = func(n node, scope *nameScope, inChoice 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 { if t, isTemplate := n.(*template); n == root || isTemplate && (t.repeat > 1 || item) {
scope = &nameScope{up: scope, owner: n} scope = &nameScope{up: scope, owner: n}
scopes = append(scopes, scope) scopes = append(scopes, scope)
inChoice = false
} }
switch t := n.(type) { switch t := n.(type) {
case *template: case *template:
t.nameScope = scope t.nameScope = scope
if err := scope.bindAll(t, inChoice, where); err != nil { if err := scope.bindAll(t, where); err != nil {
return err return err
} }
case *choice: case *choice:
t.nameScope = scope t.nameScope = scope
} }
_, isChoice := n.(*choice) _, 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 { if err := gather(root, nil, false, ""); err != nil {
return err return err
@@ -85,21 +84,18 @@ func bindNames(root node) error {
for _, sc := range scopes[1:] { for _, sc := range scopes[1:] {
for _, b := range sc.order { for _, b := range sc.order {
if outer := sc.up.lookup(b.name); outer != nil { 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) 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 { for _, tok := range t.tokens {
if tok.Kind != grammar.NameBind { if tok.Kind != grammar.NameBind {
continue 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 { 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) 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. // spelled names the scope in an error.
func (sc *nameScope) spelled() string { 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 "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 // 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 == "" { if at == "" {
at = "an item of the root choice" 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) 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)
} }
} }
+3 -3
View File
@@ -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 // 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) { func repeatOf(m map[string]any) (int, error) {
rv, ok := m["repeat"] rv, ok := m["repeat"]
if !ok { if !ok {
@@ -311,8 +311,8 @@ func repeatOf(m map[string]any) (int, error) {
if !ok { if !ok {
return 0, fmt.Errorf("repeat must be a number, not %s", jsonKind(rv)) 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) { if math.IsNaN(r) || math.IsInf(r, 0) || r < 0 || r != math.Trunc(r) {
return 0, fmt.Errorf("repeat must be a positive integer, got %v", rv) 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 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) return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, MaxRepeat)