Give the inline pipeline its own file, and record the shape, padding and template-API decisions
Tests / vet + fmt + tests (pull_request) Successful in 55s
Tests / vet + fmt + tests (pull_request) Successful in 55s
This commit is contained in:
@@ -25,12 +25,13 @@ fejkdata '{"format":"name: {x}","x":["bosse","lina"]}' # name: bosse or name: l
|
|||||||
|
|
||||||
A path names a category, or a field inside one: each dot segment descends one
|
A path names a category, or a field inside one: each dot segment descends one
|
||||||
level — folders, then the category (a JSON file), then fields. An argument that is
|
level — folders, then the category (a JSON file), then fields. An argument that is
|
||||||
a JSON object or array, or that carries a `{` token, is instead an **inline
|
a JSON object, array or string, or that carries a `{` token, is instead an
|
||||||
template**: a format string or a JSON value compiled and rendered on the spot. Its
|
**inline template**: a format string or a JSON value compiled and rendered on the
|
||||||
tokens reach the data by reference from the root — `{/sv_SE.person.last}` (or
|
spot. Its tokens reach the data by reference from the root —
|
||||||
`{.name}`, which means the same here), so shipped and `--data-path` categories are
|
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike
|
||||||
available — and `{..name}` is rejected, an inline template having no folder to step
|
available. An inline template sits in no folder, so the folder-relative `{.name}`
|
||||||
up from. A path never contains a brace, so the two cannot collide (see
|
and `{..name}` are rejected naming the root spelling. A path never contains a
|
||||||
|
brace, a bracket or a quote, so the two cannot collide (see
|
||||||
[Decisions](#decisions)).
|
[Decisions](#decisions)).
|
||||||
|
|
||||||
| Flag | |
|
| Flag | |
|
||||||
@@ -46,7 +47,9 @@ up from. A path never contains a brace, so the two cannot collide (see
|
|||||||
`--name value` and `--name=value` both work, a short flag's value attaches or
|
`--name value` and `--name=value` both work, a short flag's value attaches or
|
||||||
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
|
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
|
||||||
[Decisions](#decisions); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
[Decisions](#decisions); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
||||||
dir, unknown path), `2` misuse. From a checkout: `go run ./cmd/fejkdata …`.
|
dir, unknown path), `2` misuse — a bad flag, an argument that names neither a
|
||||||
|
template nor a path, or an inline template that does not compile. From a checkout:
|
||||||
|
`go run ./cmd/fejkdata …`.
|
||||||
|
|
||||||
### Your own data
|
### Your own data
|
||||||
|
|
||||||
@@ -107,8 +110,8 @@ work with no data on disk. A directory is a namespace: each JSON file is a
|
|||||||
category named after the file, each subdirectory a dot-path segment, so
|
category named after the file, each subdirectory a dot-path segment, so
|
||||||
`mydata/sv_SE/person.json` is `sv_SE.person` and replaces the shipped one.
|
`mydata/sv_SE/person.json` is `sv_SE.person` and replaces the shipped one.
|
||||||
Sources merge in order; matching folders combine, any other clash is won by the
|
Sources merge in order; matching folders combine, any other clash is won by the
|
||||||
last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]` or `/`; dot-prefixed
|
last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]`, `"` or `/`;
|
||||||
entries are skipped, so a data directory can also be a checkout.
|
dot-prefixed entries are skipped, so a data directory can also be a checkout.
|
||||||
|
|
||||||
Each locale carries `address`, `color`, `company`, `date`, `email`, `ip`,
|
Each locale carries `address`, `color`, `company`, `date`, `email`, `ip`,
|
||||||
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
||||||
@@ -346,8 +349,9 @@ tokens add cost in proportion to the output.
|
|||||||
2. **Text means what it says** — a format renders as written; only `{…}` varies,
|
2. **Text means what it says** — a format renders as written; only `{…}` varies,
|
||||||
random characters included (`{digits(3)}`). One spelling per result; the wrong
|
random characters included (`{digits(3)}`). One spelling per result; the wrong
|
||||||
one is a load error naming the right one.
|
one is a load error naming the right one.
|
||||||
3. **Every mistake is a load error** — `New` rejects; `Fake` on a loaded generator
|
3. **Every mistake is a load error** — `New` rejects the data and `NewTemplate`
|
||||||
fails only for an unknown path.
|
the inline template; on a loaded generator `Fake` fails only for an unknown
|
||||||
|
path, and `Template.Fake` cannot fail at all.
|
||||||
4. **Zero to a value in one command** — `go install`, then `fejkdata sv_SE.person`:
|
4. **Zero to a value in one command** — `go install`, then `fejkdata sv_SE.person`:
|
||||||
no checkout, no flag. Flags are GNU-form (`--seed 42`, `-n 3`) in any position;
|
no checkout, no flag. Flags are GNU-form (`--seed 42`, `-n 3`) in any position;
|
||||||
the first custom template needs no escape and no option.
|
the first custom template needs no escape and no option.
|
||||||
@@ -372,13 +376,24 @@ tokens add cost in proportion to the output.
|
|||||||
naming the double-dash spelling, and `-s=42` is rejected naming both short
|
naming the double-dash spelling, and `-s=42` is rejected naming both short
|
||||||
spellings: `=` belongs to the long form, and reading `=42` as the value would
|
spellings: `=` belongs to the long form, and reading `=42` as the value would
|
||||||
make `-d=./x` a directory named `=./x`.
|
make `-d=./x` a directory named `=./x`.
|
||||||
- **An argument is a template by its shape, not by a flag.** A JSON object or
|
- **An argument is a template by its shape, not by a flag.** A JSON object, array
|
||||||
array, or a string carrying a `{` token, is an inline template; anything else is
|
or string, or a string carrying a `{` token, is an inline template; anything else
|
||||||
a path. A name may not contain a brace or a bracket, so a path can never collide
|
is a path. A name may not contain a brace, a bracket or a quote, so a path can
|
||||||
with either spelling, and the `[` of a JSON array is gated on valid JSON so a
|
never collide with any of those spellings, and the leading `[` or `"` is gated on
|
||||||
stray copied bracket never swallows an argument. No `--template` flag is needed.
|
valid JSON so a stray copied bracket never swallows an argument — it names
|
||||||
Reserving both brackets — though only a leading `[` could collide — keeps one
|
nothing, and says so. No `--template` flag is needed. Reserving the characters
|
||||||
simple name rule instead of a leading-position special case.
|
whole — though only a leading one could collide — keeps one simple name rule
|
||||||
|
instead of a leading-position special case. The JSON string is what makes the
|
||||||
|
library's own advice reachable: the error for an object holding only a format
|
||||||
|
names `"…"`, and that spelling has to work where it is printed.
|
||||||
|
- **A padded JSON argument is rejected, not trimmed.** Padding is the one place the
|
||||||
|
two readings disagree — a format string renders it, JSON drops it — so the
|
||||||
|
spelling that renders is named rather than silently chosen.
|
||||||
|
- **`FakeTemplate` and `NewTemplate` both stay.** They reach the same value but not
|
||||||
|
at the same cost: `NewTemplate` pays the compile and validation once and renders
|
||||||
|
many times, `FakeTemplate` is the one-shot call, and `--repeat` is exactly the
|
||||||
|
case that needs the first. The pair is `regexp.MustCompile` and `regexp.Match`,
|
||||||
|
not two spellings of one result.
|
||||||
- **The shipped data is embedded, not discovered.** A directory a machine happens
|
- **The shipped data is embedded, not discovered.** A directory a machine happens
|
||||||
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
||||||
as JSON; `--data-path` layers over it.
|
as JSON; `--data-path` layers over it.
|
||||||
@@ -467,6 +482,7 @@ fejkdata.go Generator, New, options, the embedded data set, List
|
|||||||
node.go the node model and JSON -> node compilation
|
node.go the node model and JSON -> node compilation
|
||||||
path.go the dotted-path walk, and proving a path resolves
|
path.go the dotted-path walk, and proving a path resolves
|
||||||
render.go Fake and the recursive renderer (choices, format strings, expansions)
|
render.go Fake and the recursive renderer (choices, format strings, expansions)
|
||||||
|
inline.go inline templates: Template, NewTemplate, FakeTemplate, and their compile and link
|
||||||
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
||||||
hold.go the hold: one draw per expansion for paths and operands, and its fences
|
hold.go the hold: one draw per expansion for paths and operands, and its fences
|
||||||
reference.go reference sigils, and binding references across the tree
|
reference.go reference sigils, and binding references across the tree
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Template is an inline template compiled, referenced and validated against a
|
||||||
|
// generator's loaded data once, ready to render many times with [Template.Fake].
|
||||||
|
// It is safe for concurrent use: Fake serializes on its generator's lock, so a
|
||||||
|
// seeded sequence is reproducible only when a generator — and its templates — are
|
||||||
|
// drawn from one goroutine.
|
||||||
|
type Template struct {
|
||||||
|
g *Generator
|
||||||
|
n node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fake renders the template with one draw.
|
||||||
|
func (t *Template) Fake() string {
|
||||||
|
t.g.mu.Lock()
|
||||||
|
defer t.g.mu.Unlock()
|
||||||
|
return render(t.g.rand, t.n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplate compiles an inline template — a format string or a JSON value — and
|
||||||
|
// binds its references against the loaded tree, so repeated renders pay the
|
||||||
|
// compile and validation once. It shares [New]'s guarantees: a bad template errors
|
||||||
|
// here, and rendering cannot fail.
|
||||||
|
//
|
||||||
|
// checkNoCycles is the one fence loadData runs that an inline node does not need:
|
||||||
|
// the loaded tree is proven acyclic at [New], the node is a finite tree, and no
|
||||||
|
// tree node can reference it, so nothing it renders can reach itself.
|
||||||
|
func (f *Generator) NewTemplate(input string) (*Template, error) {
|
||||||
|
n, err := compileInput(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
|
}
|
||||||
|
if err := linkNodeRefs(n, f.categories); err != nil {
|
||||||
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
|
}
|
||||||
|
if err := checkScope(inlineScope(n)); err != nil {
|
||||||
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
|
}
|
||||||
|
return &Template{g: f, n: n}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FakeTemplate compiles and renders an inline template in one call. It is
|
||||||
|
// [NewTemplate] then [Template.Fake]; to render the same template many times, hold
|
||||||
|
// the *Template and call its Fake.
|
||||||
|
func (f *Generator) FakeTemplate(input string) (string, error) {
|
||||||
|
t, err := f.NewTemplate(input)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return t.Fake(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileInput compiles an inline template: a JSON value, or a bare format string
|
||||||
|
// when the input is not JSON. A JSON string literal and a bare string compile
|
||||||
|
// alike (both are a template with no fields); the other JSON scalars — a number,
|
||||||
|
// bool or null — are no template, so they are rejected here, as a data file that
|
||||||
|
// was one would be at load. Padding is where the two readings would disagree — a
|
||||||
|
// format string renders it, JSON drops it — so a padded JSON value is rejected
|
||||||
|
// naming the one that renders.
|
||||||
|
func compileInput(input string) (node, error) {
|
||||||
|
var raw any
|
||||||
|
if err := json.Unmarshal([]byte(input), &raw); err != nil {
|
||||||
|
return compile(input)
|
||||||
|
}
|
||||||
|
if trimmed := strings.TrimSpace(input); trimmed != input {
|
||||||
|
return nil, fmt.Errorf("a JSON template may not be padded with spaces, which a format string would render; write %s", trimmed)
|
||||||
|
}
|
||||||
|
return compile(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// linkNodeRefs binds the references in an inline node's templates against the
|
||||||
|
// loaded tree. An inline template sits in no folder, so . and .. name nothing and
|
||||||
|
// are rejected for the root spelling they would otherwise silently mean.
|
||||||
|
func linkNodeRefs(n node, root map[string]node) error {
|
||||||
|
return eachNode(n, "template", func(path string, m node) error {
|
||||||
|
t, ok := m.(*template)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, name := range refTokens(t.format) {
|
||||||
|
sigil, rest, err := refShape(name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||||
|
}
|
||||||
|
if sigil != "/" {
|
||||||
|
return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return linkTemplateRefs(nil, path, t, root)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -136,24 +136,6 @@ func (t *template) compileFormat() error {
|
|||||||
return checkNoRepeatedRead(t.format, c, t.refs)
|
return checkNoRepeatedRead(t.format, c, t.refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileInput compiles an inline template: a JSON value, or a bare format string
|
|
||||||
// when the input is not JSON. A JSON string literal and a bare string compile
|
|
||||||
// alike (both are a template with no fields); the other JSON scalars — a number,
|
|
||||||
// bool or null — are no template, so they are rejected here, as a data file that
|
|
||||||
// was one would be at load. Padding is where the two readings would disagree — a
|
|
||||||
// format string renders it, JSON drops it — so a padded JSON value is rejected
|
|
||||||
// naming the one that renders.
|
|
||||||
func compileInput(input string) (node, error) {
|
|
||||||
var raw any
|
|
||||||
if err := json.Unmarshal([]byte(input), &raw); err != nil {
|
|
||||||
return compile(input)
|
|
||||||
}
|
|
||||||
if trimmed := strings.TrimSpace(input); trimmed != input {
|
|
||||||
return nil, fmt.Errorf("a JSON template may not be padded with spaces, which a format string would render; write %s", trimmed)
|
|
||||||
}
|
|
||||||
return compile(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func compileChoice(items []any) (node, error) {
|
func compileChoice(items []any) (node, error) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil, fmt.Errorf("empty choice")
|
return nil, fmt.Errorf("empty choice")
|
||||||
|
|||||||
@@ -110,28 +110,6 @@ func linkTemplateRefs(folder []string, path string, t *template, root map[string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkNodeRefs binds the references in an inline node's templates against the
|
|
||||||
// loaded tree. An inline template sits in no folder, so . and .. name nothing and
|
|
||||||
// are rejected for the root spelling they would otherwise silently mean.
|
|
||||||
func linkNodeRefs(n node, root map[string]node) error {
|
|
||||||
return eachNode(n, "template", func(path string, m node) error {
|
|
||||||
t, ok := m.(*template)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, name := range refTokens(t.format) {
|
|
||||||
sigil, rest, err := refShape(name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
|
||||||
}
|
|
||||||
if sigil != "/" {
|
|
||||||
return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return linkTemplateRefs(nil, path, t, root)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// eachTemplate calls fn once per template, with the folder its category sits in
|
// eachTemplate calls fn once per template, with the folder its category sits in
|
||||||
// and the dot path reaching it, folders and names in sorted order.
|
// and the dot path reaching it, folders and names in sorted order.
|
||||||
func eachTemplate(root map[string]node, fn func(folder []string, path string, t *template) error) error {
|
func eachTemplate(root map[string]node, fn func(folder []string, path string, t *template) error) error {
|
||||||
|
|||||||
@@ -30,56 +30,6 @@ func (f *Generator) Fake(path string) (string, error) {
|
|||||||
return render(f.rand, n), nil
|
return render(f.rand, n), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Template is an inline template compiled, referenced and validated against a
|
|
||||||
// generator's loaded data once, ready to render many times with [Template.Fake].
|
|
||||||
// It is safe for concurrent use: Fake serializes on its generator's lock, so a
|
|
||||||
// seeded sequence is reproducible only when a generator — and its templates — are
|
|
||||||
// drawn from one goroutine.
|
|
||||||
type Template struct {
|
|
||||||
g *Generator
|
|
||||||
n node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fake renders the template with one draw.
|
|
||||||
func (t *Template) Fake() string {
|
|
||||||
t.g.mu.Lock()
|
|
||||||
defer t.g.mu.Unlock()
|
|
||||||
return render(t.g.rand, t.n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTemplate compiles an inline template — a format string or a JSON value — and
|
|
||||||
// binds its references against the loaded tree, so repeated renders pay the
|
|
||||||
// compile and validation once. It shares [New]'s guarantees: a bad template errors
|
|
||||||
// here, and rendering cannot fail.
|
|
||||||
//
|
|
||||||
// checkNoCycles is the one fence loadData runs that an inline node does not need:
|
|
||||||
// the loaded tree is proven acyclic at [New], the node is a finite tree, and no
|
|
||||||
// tree node can reference it, so nothing it renders can reach itself.
|
|
||||||
func (f *Generator) NewTemplate(input string) (*Template, error) {
|
|
||||||
n, err := compileInput(input)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
|
||||||
}
|
|
||||||
if err := linkNodeRefs(n, f.categories); err != nil {
|
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
|
||||||
}
|
|
||||||
if err := checkScope(inlineScope(n)); err != nil {
|
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
|
||||||
}
|
|
||||||
return &Template{g: f, n: n}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FakeTemplate compiles and renders an inline template in one call. It is
|
|
||||||
// [NewTemplate] then [Template.Fake]; to render the same template many times, hold
|
|
||||||
// the *Template and call its Fake.
|
|
||||||
func (f *Generator) FakeTemplate(input string) (string, error) {
|
|
||||||
t, err := f.NewTemplate(input)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return t.Fake(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// descend walks named fields to the node a path names. It is the one render-side
|
// descend walks named fields to the node a path names. It is the one render-side
|
||||||
// step that can fail, because the path comes from the caller and may name a field
|
// step that can fail, because the path comes from the caller and may name a field
|
||||||
// that does not exist. A choice consumes no segment, so the rest of the path must
|
// that does not exist. A choice consumes no segment, so the rest of the path must
|
||||||
|
|||||||
Reference in New Issue
Block a user