Add inline templates to the CLI and library #4
@@ -22,6 +22,16 @@ Forked from [github.com/Timewave-AB/fakes](https://github.com/Timewave-AB/fakes)
|
||||
8. **Docs index the grammar** — every syntax feature is a heading; every example
|
||||
runs under test and shows its output; a rule is stated once.
|
||||
|
||||
## Audience, scale and horizon
|
||||
|
||||
fejkdata is for developers generating test and fixture data, from Go or the CLI —
|
||||
in-memory and single-process, with no persistence or networking, so it reads no
|
||||
configuration beyond what the caller passes. A single value is bounded at 1 048 576
|
||||
renders (`MaxRepeat`); how large each render is stays what the data asked for. The
|
||||
data format becomes the frozen public API at the first tagged release, which is the
|
||||
promotion trigger for a compatibility review; until then there is no compatibility
|
||||
promise.
|
||||
|
||||
## CLI
|
||||
|
||||
```sh
|
||||
@@ -98,8 +108,9 @@ if err != nil {
|
||||
}
|
||||
v, err := f.Fake("sv_SE.address") // "Kungsvägen 68\n379 17 Stockholm"
|
||||
paths := f.List() // every path Fake accepts, sorted
|
||||
v, err = f.FakeTemplate("name: {/sv_SE.person.last}") // an inline template
|
||||
v, err = f.FakeTemplate(`{"format":"name: {x}","x":["bosse","lina"]}`)
|
||||
v, err = f.FakeTemplate("name: {/sv_SE.person.last}") // compile + render in one call
|
||||
t, err := f.NewTemplate(`{"format":"name: {x}","x":["bosse","lina"]}`) // compile once
|
||||
v = t.Fake() // render many times, no re-parse
|
||||
```
|
||||
|
||||
| Option | |
|
||||
@@ -121,8 +132,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`,
|
||||
@@ -369,8 +380,9 @@ tokens add cost in proportion to the output.
|
||||
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 path can never contain a brace — a name may not use one — so the two
|
||||
never collide, and no `--template` flag is needed to disambiguate them.
|
||||
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.
|
||||
- **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.
|
||||
|
||||
+16
-13
@@ -227,10 +227,20 @@ func (in invocation) options() []fejkdata.Option {
|
||||
// failure comes before anything is written; a write failure surfaces from Flush,
|
||||
// bufio keeping the first one.
|
||||
func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
||||
arg := in.paths[0]
|
||||
var draw func() (string, error)
|
||||
if isTemplate(arg) {
|
||||
t, err := f.NewTemplate(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
draw = func() (string, error) { return t.Fake(), nil }
|
||||
} else {
|
||||
draw = func() (string, error) { return f.Fake(arg) }
|
||||
}
|
||||
out := bufio.NewWriter(w)
|
||||
for i := 0; i < in.repeat; i++ {
|
||||
arg := in.paths[0]
|
||||
v, err := renderArg(f, arg)
|
||||
v, err := draw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -244,9 +254,10 @@ func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
||||
}
|
||||
|
||||
// isTemplate reports whether an argument is an inline template rather than a
|
||||
// path: a format string carrying a { token (a path can never contain a brace),
|
||||
// or a JSON object or array. A [ can begin a real category name, so a [
|
||||
// counts as a template only when the whole argument is valid JSON.
|
||||
// path: a format string carrying a { token, or a JSON object or array. A name may
|
||||
// not contain a brace or bracket, so both spellings collide with no path; the [
|
||||
// gate is valid-JSON so a [ alone never swallows an argument that merely began
|
||||
// with a copied bracket.
|
||||
func isTemplate(arg string) bool {
|
||||
if strings.ContainsRune(arg, '{') {
|
||||
return true
|
||||
@@ -254,14 +265,6 @@ func isTemplate(arg string) bool {
|
||||
return strings.HasPrefix(arg, "[") && json.Valid([]byte(arg))
|
||||
}
|
||||
|
||||
// renderArg renders one positional argument: an inline template, or a path.
|
||||
func renderArg(f *fejkdata.Generator, arg string) (string, error) {
|
||||
if isTemplate(arg) {
|
||||
return f.FakeTemplate(arg)
|
||||
}
|
||||
return f.Fake(arg)
|
||||
}
|
||||
|
||||
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
||||
|
||||
// run returns the exit code: 0 ok, 1 runtime error, 2 misuse.
|
||||
|
||||
@@ -335,23 +335,23 @@ func weightOf(raw any) (float64, error) {
|
||||
|
||||
// reservedInName is what a category, folder or field name may not contain: a dot
|
||||
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
||||
// call, braces delimit the token and '/' starts a reference. A name carrying one is
|
||||
// reachable by no format, so it is rejected where it is authored rather than at
|
||||
// the token that cannot reach it.
|
||||
const reservedInName = ".|({}/"
|
||||
// call, braces delimit the token, '/' starts a reference, and a bracket would be
|
||||
// misread by the command line as a JSON array. A name carrying one is rejected
|
||||
// where it is authored rather than where it would be unreachable.
|
||||
const reservedInName = ".|({}/[]"
|
||||
|
||||
// reservedList spells reservedInName for an error message, so the two cannot drift.
|
||||
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
||||
|
||||
// checkName rejects a name the dot path and {token} grammars cannot spell. Both a
|
||||
// category or folder and a field go through it, so there is one answer to what a
|
||||
// name may contain.
|
||||
// checkName rejects a name the dot path, {token} and CLI grammars cannot spell.
|
||||
// Both a category or folder and a field go through it, so there is one answer to
|
||||
// what a name may contain.
|
||||
func checkName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
||||
}
|
||||
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path and {token} grammars reserve",
|
||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and CLI grammars reserve",
|
||||
name, name[i:i+1], reservedList)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -30,30 +30,52 @@ func (f *Generator) Fake(path string) (string, error) {
|
||||
return render(f.rand, n), nil
|
||||
}
|
||||
|
||||
// FakeTemplate renders an inline template — a format string or a JSON value — the
|
||||
// way a category's data is compiled and rendered, references reaching the loaded
|
||||
// tree with {/path}. It shares [Fake]'s lock: an inline node is compiled and
|
||||
// referenced per draw, so a seeded sequence is reproducible when drawn from one
|
||||
// goroutine.
|
||||
func (f *Generator) FakeTemplate(input string) (string, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
// Template is an inline template compiled, referenced and validated against a
|
||||
// generator's loaded data once, ready to render many times with [Template.Fake].
|
||||
type Template struct {
|
||||
g *Generator
|
||||
n node
|
||||
}
|
||||
|
||||
// Fake renders the template with one draws.
|
||||
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.
|
||||
func (f *Generator) NewTemplate(input string) (*Template, error) {
|
||||
n, err := compileInput(input)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %w", err)
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
if err := linkNodeRefs(n, f.categories); err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %w", err)
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
// No cycle is possible: a reference binds only into the loaded tree, which has
|
||||
// no path into this node, so rendering it cannot reach itself.
|
||||
if err := checkNodeRepeatReach(n); err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %w", err)
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
if err := checkNodeBoundLevelsHeld(n); err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %w", err)
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
return render(f.rand, n), nil
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user