diff --git a/README.md b/README.md index 340d1d0..a4cc68b 100644 --- a/README.md +++ b/README.md @@ -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 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 -template**: a format string or a JSON value compiled and rendered on the spot. Its -tokens reach the data by reference from the root — `{/sv_SE.person.last}` (or -`{.name}`, which means the same here), so shipped and `--data-path` categories are -available — and `{..name}` is rejected, an inline template having no folder to step -up from. A path never contains a brace, so the two cannot collide (see +a JSON object, array or string, or that carries a `{` token, is instead an +**inline template**: a format string or a JSON value compiled and rendered on the +spot. Its tokens reach the data by reference from the root — +`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike +available. An inline template sits in no folder, so the folder-relative `{.name}` +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)). | 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 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 -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 @@ -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 `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 -last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]` or `/`; dot-prefixed -entries are skipped, so a data directory can also be a checkout. +last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]`, `"` or `/`; +dot-prefixed entries are skipped, so a data directory can also be a checkout. Each locale carries `address`, `color`, `company`, `date`, `email`, `ip`, `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, random characters included (`{digits(3)}`). One spelling per result; the wrong one is a load error naming the right one. -3. **Every mistake is a load error** — `New` rejects; `Fake` on a loaded generator - fails only for an unknown path. +3. **Every mistake is a load error** — `New` rejects the data and `NewTemplate` + 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`: 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. @@ -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 spellings: `=` belongs to the long form, and reading `=42` as the value would make `-d=./x` a directory named `=./x`. -- **An argument is a template by its shape, not by a flag.** A JSON object or - array, or a string carrying a `{` token, is an inline template; anything else is - a path. A name may not contain a brace or a bracket, so a path can never collide - with either spelling, and the `[` of a JSON array is gated on valid JSON so a - stray copied bracket never swallows an argument. No `--template` flag is needed. - Reserving both brackets — though only a leading `[` could collide — keeps one - simple name rule instead of a leading-position special case. +- **An argument is a template by its shape, not by a flag.** A JSON object, array + or string, or a string carrying a `{` token, is an inline template; anything else + is a path. A name may not contain a brace, a bracket or a quote, so a path can + never collide with any of those spellings, and the leading `[` or `"` is gated on + valid JSON so a stray copied bracket never swallows an argument — it names + nothing, and says so. No `--template` flag is needed. Reserving the characters + 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 to have would make `--seed 42` machine-dependent. Data still lives in `data/` 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 path.go the dotted-path walk, and proving a path resolves 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 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 diff --git a/inline.go b/inline.go new file mode 100644 index 0000000..cc0a9ab --- /dev/null +++ b/inline.go @@ -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) + }) +} diff --git a/node.go b/node.go index 670671b..9d1520d 100644 --- a/node.go +++ b/node.go @@ -136,24 +136,6 @@ func (t *template) compileFormat() error { 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) { if len(items) == 0 { return nil, fmt.Errorf("empty choice") diff --git a/reference.go b/reference.go index 73bd4f2..7744474 100644 --- a/reference.go +++ b/reference.go @@ -110,28 +110,6 @@ func linkTemplateRefs(folder []string, path string, t *template, root map[string 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 // 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 { diff --git a/render.go b/render.go index b33ddbb..2d998fb 100644 --- a/render.go +++ b/render.go @@ -30,56 +30,6 @@ func (f *Generator) Fake(path string) (string, error) { 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 // 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