diff --git a/README.md b/README.md index 7cd0dbc..fb4a067 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,17 @@ # fejkdata -A Go library and CLI for generating locale-aware fake data from JSON templates. -Forked from [github.com/Timewave-AB/fakes](https://github.com/Timewave-AB/fakes). +Locale-aware fake data for tests and fixtures, generated from JSON templates. Use +it from Go or the CLI — no data on disk, no dependencies, and a seed makes output +reproducible. -## Goals - -1. **Valid by construction** — every value passes the check its real consumer - applies; facts that belong together come from one draw, within a value and - across categories. -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. -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. -5. **Data lives in JSON** — a builtin only for what data can't express. -6. **Reproducible** — seed in, same stream out; no builtin reads a clock. -7. **Zero dependencies** — standard library only. -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. +```sh +go install gitea.larvit.se/larvit/fejkdata/cmd/fejkdata@latest +fejkdata sv_SE.person # Sara Eriksson +``` ## CLI ```sh -go install gitea.larvit.se/larvit/fejkdata/cmd/fejkdata@latest - fejkdata sv_SE.person # Sara Eriksson fejkdata sv_SE.person.last # Eriksson fejkdata --seed 42 sv_SE.address # the same address every run @@ -34,17 +19,27 @@ fejkdata -n 3 --separator ', ' sv_SE.word # nät, barn, sol fejkdata --list # every path the data offers fejkdata --data-path ./mydata sv_SE.word # layer a directory over the shipped data fejkdata --no-shipped-data -d ./mydata --list # only your data +fejkdata 'name: {/sv_SE.person.last}' # name: — an inline template +fejkdata '{"format":"name: {x}","x":["bosse","lina"]}' # name: bosse or name: lina ``` 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. +level — folders, then the category (a JSON file), then fields. An argument that is +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 | | |------|--| | `-d`, `--data-path D` | a directory to layer over the shipped data; repeatable, the last wins a name clash | | `--no-shipped-data` | load only the `--data-path` directories | | `-s`, `--seed N` | reproducible output | -| `-n`, `--repeat N` | render the path N times (up to 1048576), each an independent draw, streamed | +| `-n`, `--repeat N` | render the value N times (up to 1048576), each an independent draw, streamed | | `--separator S` | between repeated values (default a newline) | | `--list` | print every path, then exit | | `--version`, `-h`, `--help` | print, then exit | @@ -52,7 +47,9 @@ level — folders, then the category (a JSON file), then fields. `--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 @@ -89,6 +86,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}") // 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 | | @@ -110,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`, @@ -341,6 +341,26 @@ then costs about what its output costs: an unweighted pick is O(1) whatever the list's length, a weighted one O(log n), and long formats, deep nesting and many tokens add cost in proportion to the output. +## Goals + +1. **Valid by construction** — every value passes the check its real consumer + applies; facts that belong together come from one draw, within a value and + across categories. +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 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. +5. **Data lives in JSON** — a builtin only for what data can't express. +6. **Reproducible** — seed in, same stream out; no builtin reads a clock. +7. **Zero dependencies** — standard library only. +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. + ## Decisions - **Options and fields share one namespace.** `format`, `weight`, `repeat` and @@ -356,6 +376,33 @@ 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, 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. +- **An inline template skips the cycle fence, and only that one.** `New` proves the + loaded tree acyclic, an inline node is a finite tree of its own, and nothing in + the tree can reference it, so no render of it reaches itself. Every other fence + runs over both, from one `checkScope`. +- **An inline template that does not compile is misuse (exit 2), including a + reference that resolves to nothing** — the whole argument is the spelling under + test, and `NewTemplate` compiles, links and validates as one step. An unknown + *path* stays a runtime error (exit 1): there the argument is well-formed and only + the data is absent. +- **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. @@ -444,6 +491,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 @@ -457,4 +505,4 @@ data/ shipped data (JSON), embedded at build: locale folders + a misc ## License -MIT — see [LICENSE](LICENSE). +MIT — see [LICENSE](LICENSE). Forked from [github.com/Timewave-AB/fakes](https://github.com/Timewave-AB/fakes). diff --git a/builtins.go b/builtins.go index d72ce2e..8b9fec6 100644 --- a/builtins.go +++ b/builtins.go @@ -133,7 +133,7 @@ func transformArg(fields map[string]node, a []string) error { _, _, err := refShape(leaf) return err } - return checkArm(leaf, fields) + return checkArm(leaf, fields, false) } func transformOperand(a []string) []string { diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index d6b2d05..bf69ed0 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -9,6 +9,7 @@ package main import ( "bufio" + "encoding/json" "errors" "fmt" "io" @@ -20,21 +21,30 @@ import ( "gitea.larvit.se/larvit/fejkdata" ) -const usage = `Usage: fejkdata [flags] +const usage = `Usage: fejkdata [flags] a category, or a dotted path into one (person, person.last) +