Load a generator with no data, a repeat or count of 0, and a name a choice's item binds for that item's own reads
This commit is contained in:
@@ -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}`;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user