Answer architecture round 2, stability round 1 and product-owner round 1: a format string on stdin keeps every byte while JSON or a lone reference drops one newline, an operand's error names stdin, a file or the pipe, {//x} is refused and "{/x}" read as its record, plain echo and a heredoc in the examples, goal 7 reads one command line, decision titles drop argument, and items 157 to 159 filed

This commit is contained in:
2026-10-07 17:04:00 +02:00
committed by Lillem4n
parent 83b4b0bdf0
commit 58a7b16a7f
7 changed files with 227 additions and 129 deletions
+7 -6
View File
@@ -11,8 +11,9 @@
wait for one.
- README goal 3 is gated at 7.0 on the `comprehension-panel` skill, and below it
nothing else merges — no feature, no category, no data — bar a security fix, a
dependency bump, a repair of behaviour the README documents, and the infrastructure
the round itself runs on. `todo.md` carries the round: a PR per item it plans, the nine-seat
dependency bump, a repair of behaviour the README documents, the infrastructure
the round itself runs on, and a chunk the comprehension-floor decision lists as
shipped under the floor. `todo.md` carries the round: a PR per item it plans, the nine-seat
findings at depth 1 or a restructure the maintainer approved, then the nine-seat run
again, until the score passes. At or above 7.0
every pull request scores with the four-seat run and answers it in one run. No merge
@@ -38,10 +39,10 @@ In [docs/decisions.md](docs/decisions.md):
- Options and fields share one namespace
- `{a|b}` stays beside nested choices
- Flags follow getopt_long
- An argument is a template by its shape, not by a flag
- The CLI renders the template on its stdin, less one newline ending it
- An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
- A padded JSON argument is rejected, not trimmed
- A struct tag is a template by its shape, not by a flag
- The CLI renders the template on its stdin, keeping a format string's every byte
- A template that does not compile is misuse (exit 2), including a reference that resolves to nothing, save the CLI's lone reference until item 129
- A padded JSON template is rejected, not trimmed
- `FakeTemplate` and `NewTemplate` both stay
- The shipped data is embedded, not discovered
- With only the shipped set, a category loads on the first call reaching it; beside a `--data-path`, every category loads in `New`
+5 -4
View File
@@ -27,10 +27,11 @@ replacement, and each removed path, column or flag.
operand: `{/misc.coordinate.lat as lat}{calc(lat * 60)}`. What `New` refuses is listed
under [Names](README.md#names).
- First release: the CLI, the library and the shipped data set.
- The CLI renders the template piped to its stdin, `echo -n '{/sv_SE.person} hihi' |
fejkdata`, and takes no path or template as an argument. One newline ending stdin,
`\n` or `\r\n`, is dropped. A template of one reference alone, `{/users}`, is the
record `users` under `--format`. An argument, or nothing piped in, exits 2.
- The CLI renders the template on its stdin, `echo '{/sv_SE.person} hihi' | fejkdata`,
and takes no operand. A format string keeps every byte, `echo`'s newline included; a
JSON template or one reference alone drops one newline ending stdin, `\n` or `\r\n`.
One reference alone, `{/users}`, is the record `users` under `--format`. An operand,
or nothing piped in, exits 2.
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
`key`, `name`, `weight` and `parent`; a path selects a row by key or name,
`misc.territory[SE]`, and descends to a linked table by name, one path drawing each
+43 -40
View File
@@ -6,41 +6,44 @@ reproducible.
```sh
go install github.com/larvit/fejkdata/cmd/fejkdata@latest
echo -n '{/sv_SE.person}' | fejkdata # Sara Eriksson
echo '{/sv_SE.person}' | fejkdata # Sara Eriksson
```
## CLI
```sh
echo -n '{/sv_SE.person}' | fejkdata # Sara Eriksson
echo -n '{/sv_SE.person.last}' | fejkdata # Eriksson
echo -n '{/sv_SE.person} hihi' | fejkdata # Sara Eriksson hihi
echo -n '{/sv_SE.address}' | fejkdata --seed 42 # the same address every run
echo -n '{/sv_SE.word}' | fejkdata -n 3 --separator ', ' # nät, barn, sol
echo '{/sv_SE.person}' | fejkdata # Sara Eriksson
echo '{/sv_SE.person.last}' | fejkdata # Eriksson
echo '{/sv_SE.person} hihi' | fejkdata # Sara Eriksson hihi, then echo's newline
echo '{/sv_SE.address}' | fejkdata --seed 42 # the same address every run
echo '{/sv_SE.word}' | fejkdata -n 3 --separator ', ' # nät, barn, sol
fejkdata --list # every path the data offers
echo -n '{/misc.territory[SE].capital}' | fejkdata # Stockholm — a table's row, selected by key or name
echo -n '{/geo.SE.locality[Lund].street}' | fejkdata # Fjelievägen — a linked table, drawn inside the row
echo -n '{/sv_SE.word}' | fejkdata --data-path ./mydata # layer a directory over the shipped data
echo '{/misc.territory[SE].capital}' | fejkdata # Stockholm — a table's row, selected by key or name
echo '{/geo.SE.locality[Lund].street}' | fejkdata # Fjelievägen — a linked table, drawn inside the row
echo '{/sv_SE.word}' | fejkdata --data-path ./mydata # layer a directory over the shipped data
fejkdata --no-shipped-data -d ./mydata --list # only your data
echo -n "{date(1990-01-01,2010-12-31,'2006-01-02')}" | fejkdata # 2003-11-27 — the layout in '…'
echo -n '{"format":"name: {x}","x":["bosse","lina"]}' | fejkdata # name: bosse or name: lina
echo '{"format":"name: {x}","x":["bosse","lina"]}' | fejkdata # name: bosse or name: lina
fejkdata < template.txt # a template kept in a file
fejkdata <<'EOF' # born 2003-11-27 — a quoted heredoc passes ', $ and \ as written
born {date(1990-01-01,2010-12-31,'2006-01-02')}
EOF
```
fejkdata renders the template on its stdin: a format string, or a JSON object, array or
string. Its `{…}` tokens reach the data by reference from the root,
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike available,
and everything outside them prints as written. A reference names a category, or a field
and everything outside them prints as written; stdin that is JSON is read as JSON, so
`42`, `true` or `null` alone is refused. A reference names a category, or a field
inside one: each dot segment descends one level — folders, then the category (a JSON
file), then fields — and `[SE]` after a [table](#table) selects its row. `--list` prints
these paths; a template reads one as `{/path}`.
One newline ending stdin, `\n` or `\r\n`, is dropped, so `echo` and `echo -n`, a file
and a heredoc render alike; end stdin with two newlines to print one. Some shells' `echo`
reads a backslash as an escape, so pipe a template holding one with `printf '%s'` or from
a file. One reference alone, `{/users}`, is the record `users` under `--format`; the
rest of stdin's rules are under
[Decisions](docs/decisions.md#the-cli-renders-the-template-on-its-stdin-less-one-newline-ending-it).
A format string keeps every byte of stdin, so `echo`'s newline renders too; `printf '%s'`
leaves it out. A JSON template or one reference alone drops one newline ending stdin,
`\n` or `\r\n`, which neither could print. One reference alone, `{/users}`, is the record
`users` under `--format`. Some shells' `echo` reads a backslash as an escape, so pipe a
template holding one from a quoted heredoc or a file. The rest of stdin's rules are under
[Decisions](docs/decisions.md#the-cli-renders-the-template-on-its-stdin-keeping-a-format-strings-every-byte).
| Flag | |
|------|--|
@@ -57,7 +60,7 @@ rest of stdin's rules are under
`--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](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
dir, a lone reference to nothing), `2` misuse — a bad flag, an argument, nothing on stdin, or a
dir, a lone reference to nothing), `2` misuse — a bad flag, an operand, nothing on stdin, or a
template that does not compile. From a checkout:
`go run ./cmd/fejkdata …`.
@@ -78,9 +81,9 @@ A category is a JSON file in a directory. Save this as `mydata/sql.json`:
```
```sh
echo -n '{/sql}' | fejkdata --seed 1 --data-path ./mydata
echo '{/sql}' | fejkdata --seed 1 --data-path ./mydata
# INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
echo -n '{/sql}' | fejkdata --repeat 100 --data-path ./mydata > seed.sql
echo '{/sql}' | fejkdata --repeat 100 --data-path ./mydata > seed.sql
```
That `format` string is the free-form spelling — you hand-write the whole row.
@@ -103,11 +106,11 @@ the whole. `--format json|ndjson|csv|sql` writes the records; the library's
```
```sh
echo -n '{/users}' | fejkdata --seed 1 -d ./mydata --format json # [{"first":"Bo","last":"Lovelace"}]
echo -n '{/users}' | fejkdata --seed 1 -d ./mydata --format ndjson # {"first":"Bo","last":"Lovelace"}
echo -n '{/users}' | fejkdata --seed 1 -d ./mydata --format csv # first,last → Bo,Lovelace
echo -n '{/users}' | fejkdata --seed 1 -d ./mydata --format sql # INSERT INTO "users" ("first", "last") VALUES ('Bo', 'Lovelace');
echo -n '{/users}' | fejkdata --seed 1 -d ./mydata --format sql --table people # INSERT into another table
echo '{/users}' | fejkdata --seed 1 -d ./mydata --format json # [{"first":"Bo","last":"Lovelace"}]
echo '{/users}' | fejkdata --seed 1 -d ./mydata --format ndjson # {"first":"Bo","last":"Lovelace"}
echo '{/users}' | fejkdata --seed 1 -d ./mydata --format csv # first,last → Bo,Lovelace
echo '{/users}' | fejkdata --seed 1 -d ./mydata --format sql # INSERT INTO "users" ("first", "last") VALUES ('Bo', 'Lovelace');
echo '{/users}' | fejkdata --seed 1 -d ./mydata --format sql --table people # INSERT into another table
```
A record is one reference alone, `{/users}`, or a template whose fields are its
@@ -508,11 +511,11 @@ SE Sweden 10500000
```
```sh
echo -n '{/country}' | fejkdata -d ./mydata # Sweden (SE), about half the time
echo -n '{/country.alpha2}' | fejkdata -d ./mydata # NO
echo -n '{/country[SE]}' | fejkdata -d ./mydata # Sweden (SE)
echo -n '{/country[Norway].alpha2}' | fejkdata -d ./mydata # NO
echo -n '{/country}' | fejkdata -d ./mydata --format csv # alpha2,name,population → SE,Sweden,10500000
echo '{/country}' | fejkdata -d ./mydata # Sweden (SE), about half the time
echo '{/country.alpha2}' | fejkdata -d ./mydata # NO
echo '{/country[SE]}' | fejkdata -d ./mydata # Sweden (SE)
echo '{/country[Norway].alpha2}' | fejkdata -d ./mydata # NO
echo '{/country}' | fejkdata -d ./mydata --format csv # alpha2,name,population → SE,Sweden,10500000
```
`rows` names the TSV beside the category file; `key` names the column a path
@@ -568,10 +571,10 @@ Stockholm SE 990000
```
```sh
echo -n '{/country[SE].city}' | fejkdata -d ./mydata # Stockholm or Göteborg
echo -n '{/country.city.name}' | fejkdata -d ./mydata # a country drawn, then a city inside it
echo -n '{/city[Oslo].country}' | fejkdata -d ./mydata # NO — the link column's cell
echo -n '{/city as c}{c.name}, {c..country.name}' | fejkdata -d ./mydata # Oslo, Norway — one city and its country
echo '{/country[SE].city}' | fejkdata -d ./mydata # Stockholm or Göteborg
echo '{/country.city.name}' | fejkdata -d ./mydata # a country drawn, then a city inside it
echo '{/city[Oslo].country}' | fejkdata -d ./mydata # NO — the link column's cell
echo '{/city as c}{c.name}, {c..country.name}' | fejkdata -d ./mydata # Oslo, Norway — one city and its country
```
A path descends from a row to a linked table by name, at any depth, and `--list`
@@ -593,10 +596,10 @@ chain of parents that closes, and a table named like a column of any table above
after the `..`:
```sh
echo -n '{/city[Oslo]..country.name}' | fejkdata -d ./mydata # Norway
echo -n '{/city..country}' | fejkdata -d ./mydata # a city drawn, then its country: Sweden (SE)
echo -n '{/city[Göteborg]..country.city}' | fejkdata -d ./mydata # Stockholm or Göteborg, drawn afresh
echo -n '{/city as c}{c}, {c..country.name}' | fejkdata -d ./mydata # Oslo, Norway
echo '{/city[Oslo]..country.name}' | fejkdata -d ./mydata # Norway
echo '{/city..country}' | fejkdata -d ./mydata # a city drawn, then its country: Sweden (SE)
echo '{/city[Göteborg]..country.city}' | fejkdata -d ./mydata # Stockholm or Göteborg, drawn afresh
echo '{/city as c}{c}, {c..country.name}' | fejkdata -d ./mydata # Oslo, Norway
```
It steps up from a selected row, a drawn row or a [name](#names) bound to a row, one
@@ -922,7 +925,7 @@ Where the project is heading; the sections before Audience document what ships t
fails to load instead.
2. Data that reads something nobody provided fails to load, and the error names the
module that provides it by default, or says that none does.
7. **One command gets you a value.**
7. **One command line gets you a value.**
1. Flags work like in other command-line tools, and can go anywhere on the line.
2. Your first template of your own needs no escaping and no options.
3. The CLI fills in what you leave out with a sane default: all the shipped data, the
+68 -26
View File
@@ -1,14 +1,15 @@
// Command fejkdata renders the template on its stdin with fake values from the shipped
// data and any directories layered over it.
//
// echo -n '{/sv_SE.person}' | fejkdata # a full person
// echo -n 'name: {/sv_SE.person.last}' | fejkdata # text around a surname
// echo -n '{/sv_SE.person}' | fejkdata --data-path ./mydata # layer custom data; the last dir wins
// echo -n '{/sv_SE.address}' | fejkdata --seed 42
// echo '{/sv_SE.person}' | fejkdata # a full person
// echo 'name: {/sv_SE.person.last}' | fejkdata # text around a surname
// echo '{/sv_SE.person}' | fejkdata --data-path ./mydata # layer custom data; the last dir wins
// echo '{/sv_SE.address}' | fejkdata --seed 42
package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
@@ -22,21 +23,29 @@ import (
)
const usage = `Usage: <template> | fejkdata [flags]
fejkdata --list | --help | --version
fejkdata reads one template from stdin and renders it:
echo -n 'name: {/sv_SE.person.last}' | fejkdata
echo -n '{"format":"{x}","x":["bosse","lina"]}' | fejkdata
echo 'name: {/sv_SE.person.last}' | fejkdata
echo '{"format":"{x}","x":["bosse","lina"]}' | fejkdata
fejkdata < template.txt
fejkdata <<'EOF'
born {date(1990-01-01,2010-12-31,'2006-01-02')}
EOF
A template is a format string, or a JSON object, array or string. Its {…} tokens
reach the data by reference from the root, {/sv_SE.person.last}, whether the data
is shipped or layered with --data-path; everything outside them prints as written.
is shipped or layered with --data-path; everything outside them prints as written,
and stdin that is JSON is read as JSON, so 42, true or null alone is refused.
A reference names a category or a dotted path into one ({/sv_SE.person.last}), a
table's row by key or name ({/misc.territory[SE]}), and .. steps up from a row to
the row it links to ({/geo.SE.locality..municipality.name}).
One newline ending stdin, \n or \r\n, is dropped, so echo and echo -n render
alike; end stdin with two to print one.
A format string keeps every byte of stdin, so echo's newline renders too. JSON and a
lone reference drop one newline ending stdin, \n or \r\n, which neither could print.
A quoted heredoc, <<'EOF', passes a template holding $, a backslash or a quote as
written.
With --format json, ndjson, csv or sql the template must name a record — a
template whose fields are its columns, or one reference alone, {/sv_SE.person} —
@@ -46,7 +55,7 @@ and the rows are written as one JSON array, one JSON object per line, one CSV ro
-d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash)
--format F output form: text (default), json, ndjson, csv or sql
-h, --help print this help, then exit
--list list the paths the data offers, each read from the root, then exit
--list list the paths the data offers, each one a reference's path, then exit
--no-shipped-data load only the --data-path directories
-n, --repeat N render the template N times, 1..1048576 (default 1)
-s, --seed N same seed, version and data: identical output
@@ -266,16 +275,28 @@ func (in invocation) checkFlags() error {
return nil
}
const pipeHint = "echo -n '{/sv_SE.person}' | fejkdata"
const pipeHint = "echo '{/sv_SE.person}' | fejkdata"
// check rejects a flag combination or an argument that cannot run, before any data is
// operandError refuses an operand, naming the spelling that reads what it most likely
// means: stdin for -, a file for a file's name, else the template piped in.
func operandError(arg string) error {
if arg == "-" {
return errors.New("fejkdata takes no operand, and reads stdin already; drop the -")
}
if fi, err := os.Stat(arg); err == nil && fi.Mode().IsRegular() {
return fmt.Errorf("fejkdata takes no operand; read the file from stdin: fejkdata < %s", arg)
}
return fmt.Errorf("fejkdata takes no operand; pipe the template to stdin: %s", pipeHint)
}
// check rejects a flag combination or an operand that cannot run, before any data is
// read.
func (in invocation) check() error {
if err := in.checkFlags(); err != nil {
return err
}
if len(in.args) > 0 {
return fmt.Errorf("the template comes from stdin, not from an argument: %s", pipeHint)
return operandError(in.args[0])
}
if in.list && (in.repeatSet || in.separatorSet || in.formatSet || in.tableSet) {
return errors.New("--list takes no --repeat, --separator, --format or --table")
@@ -343,7 +364,7 @@ func (in invocation) recordStream(f *fejkdata.Generator, src input) (func() (*fe
if src.path == "" {
t, err := f.NewRecordTemplate(src.template)
if err != nil {
return nil, "", inputError{err}
return nil, "", inputError{spacedReference(src.template, err)}
}
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
}
@@ -410,9 +431,9 @@ func defaultTable(path string) string {
return segments[len(segments)-1]
}
// inputError marks a failure that is the input's own fault — no template, or one that
// does not compile. run reports it as misuse (exit 2, with a pointer to --help), unlike
// an unknown path, which is a runtime error (exit 1).
// inputError marks a failure that is the input's own fault — no template, one that does
// not compile, or a stdin that cannot be read. run reports it as misuse (exit 2, with a
// pointer to --help), unlike a lone reference to nothing, which is a runtime error (exit 1).
type inputError struct{ error }
func (e inputError) Unwrap() error { return e.error }
@@ -423,37 +444,58 @@ type input struct {
template string
}
// parseInput reads stdin's text less one newline ending it.
// parseInput reads stdin's text. A format string keeps every byte; JSON or a lone
// reference sheds one newline ending it, which neither could print.
func parseInput(raw string) (input, error) {
text, found := strings.CutSuffix(raw, "\n")
if found {
text = strings.TrimSuffix(text, "\r")
}
if text == "" {
if raw == "" {
return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint)
}
if text, found := strings.CutSuffix(raw, "\n"); found {
text = strings.TrimSuffix(text, "\r")
if path, lone := loneReference(text); lone {
return input{path: path}, nil
}
if json.Valid([]byte(text)) {
return input{template: text}, nil
}
}
if path, lone := loneReference(raw); lone {
return input{path: path}, nil
}
return input{template: raw}, nil
}
// loneReference is the path that text, one reference from the root and nothing else,
// reads: the path IsTemplate advises writing instead.
// reads: the path IsTemplate advises writing instead. A JSON string holding one is one.
func loneReference(text string) (string, bool) {
var unquoted string
if json.Unmarshal([]byte(text), &unquoted) == nil {
text = unquoted
}
if !strings.HasPrefix(text, "{/") || !strings.HasSuffix(text, "}") {
return "", false
}
if inline, err := fejkdata.IsTemplate(text); inline || err == nil {
return "", false
}
path := strings.TrimLeft(text[1:len(text)-1], "/")
path := text[2 : len(text)-1]
if inline, err := fejkdata.IsTemplate(path); inline || err != nil {
return "", false
}
return path, true
}
// spacedReference adds to err, a refusal of template as a record, that the whitespace
// around a lone reference made it text, where it does.
func spacedReference(template string, err error) error {
if trimmed := strings.TrimSpace(template); trimmed != template {
if path, lone := loneReference(trimmed); lone {
return fmt.Errorf("%w; the whitespace around {/%s} makes it text, so remove it to write the record %s", err, path, path)
}
}
return err
}
func main() {
var stdin io.Reader = os.Stdin
if fi, err := os.Stdin.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
@@ -512,7 +554,7 @@ func readStdin(stdin io.Reader) (input, error) {
}
raw, err := io.ReadAll(stdin)
if err != nil {
return input{}, err
return input{}, inputError{fmt.Errorf("stdin cannot be read: %w", err)}
}
src, err := parseInput(string(raw))
if err != nil {
+55 -21
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/csv"
"encoding/json"
"errors"
"os"
"path/filepath"
"regexp"
@@ -224,7 +225,6 @@ func TestRunMisuse(t *testing.T) {
}{
{"", nil},
{"", []string{"--data-path", svSE}},
{"\n", []string{"--data-path", svSE}},
{"{/person}", []string{"-d", svSE, "word"}},
{"", []string{"-d", svSE, "--list", "person"}},
{"{/sv_SE.person}", []string{"--no-shipped-data"}},
@@ -242,14 +242,36 @@ func TestRunMisuse(t *testing.T) {
}
}
// TestRunTakesNoArgument pins the error for a path or a template given as an argument:
// it shows the template piped to stdin.
func TestRunTakesNoArgument(t *testing.T) {
for _, args := range [][]string{{"sv_SE.person"}, {"{/sv_SE.person}"}, {"name:", "{/sv_SE.person.last}"}} {
code, out, errb := runOut("", args...)
if code != 2 || out != "" || !strings.Contains(errb, "stdin") || !strings.Contains(errb, "| fejkdata") {
t.Errorf("run(%v) = %d, %q, %q; want misuse showing a template piped to fejkdata", args, code, out, errb)
func TestRunTakesNoOperand(t *testing.T) {
file := filepath.Join(t.TempDir(), "users.tmpl")
if err := os.WriteFile(file, []byte("{/sv_SE.person}"), 0o644); err != nil {
t.Fatal(err)
}
for _, c := range []struct {
args []string
want string
}{
{[]string{"sv_SE.person"}, "echo '{/sv_SE.person}' | fejkdata"},
{[]string{"{/sv_SE.person}"}, "echo '{/sv_SE.person}' | fejkdata"},
{[]string{"name:", "{/sv_SE.person.last}"}, "echo '{/sv_SE.person}' | fejkdata"},
{[]string{"-"}, "drop the -"},
{[]string{file}, "fejkdata < " + file},
} {
code, out, errb := runOut("", c.args...)
if code != 2 || out != "" || !strings.Contains(errb, "takes no operand") || !strings.Contains(errb, c.want) {
t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want)
}
}
}
type failingReader struct{}
func (failingReader) Read([]byte) (int, error) { return 0, errors.New("bad file descriptor") }
func TestRunUnreadableStdinIsMisuse(t *testing.T) {
var out, errb bytes.Buffer
if code := run(nil, failingReader{}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "cannot be read") {
t.Errorf("run with an unreadable stdin = %d, %q; want misuse naming it", code, errb.String())
}
}
@@ -265,26 +287,26 @@ func TestRunTerminalStdinIsMisuse(t *testing.T) {
}
}
func TestRunDropsOneTrailingNewline(t *testing.T) {
func TestRunNewlineEndingStdin(t *testing.T) {
for stdin, want := range map[string]string{
"hihi": "hihi\n",
"hihi\n": "hihi\n",
"hihi\r\n": "hihi\n",
"hihi\n\n": "hihi\n\n",
"hihi\r\n\r\n": "hihi\r\n\n",
"a\nb\n": "a\nb\n",
" hihi \n": " hihi \n",
"{{x}}\n": "{x}\n",
"hihi\n": "hihi\n\n",
"hihi\r\n": "hihi\r\n\n",
"\n": "\n\n",
"{{x}}\n": "{x}\n\n",
"\"a\"\n": "a\n",
"\"a\"\r\n": "a\n",
"{/misc.territory[SE].alpha2}\n": "SE\n",
"{/misc.territory[SE].alpha2}\r\n": "SE\n",
} {
code, out, errb := runOut(stdin, "--seed", "1")
if code != 0 || out != want {
t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want)
}
}
code, out, errb := runOut("x: {/sv_SE.person.last}\n\n", "--seed", "1")
code, out, errb := runOut("x: {/sv_SE.person.last}\n", "--seed", "1")
if code != 0 || !strings.HasPrefix(out, "x: ") || !strings.HasSuffix(out, "\n\n") || strings.Count(out, "\n") != 2 {
t.Errorf("a template ending in a newline = %d, %q, %q; want its own newline, then the line's", code, out, errb)
t.Errorf("echo's format string = %d, %q, %q; want echo's newline, then the line's", code, out, errb)
}
}
@@ -413,10 +435,10 @@ func TestUsageReferencesResolve(t *testing.T) {
}
}
func TestRunArgumentMisuseBeforeLoad(t *testing.T) {
func TestRunOperandMisuseBeforeLoad(t *testing.T) {
code, _, errb := runOut("", "--no-shipped-data", "x")
if code != 2 || !strings.Contains(errb, "stdin") || strings.Contains(errb, "--data-path") {
t.Fatalf("an argument with no data = %d, %q; want the argument refused before any load", code, errb)
if code != 2 || !strings.Contains(errb, "takes no operand") || strings.Contains(errb, "--data-path") {
t.Fatalf("an operand with no data = %d, %q; want the operand refused before any load", code, errb)
}
}
@@ -452,6 +474,7 @@ func TestRunTemplateMisuse(t *testing.T) {
" [\"a\",\"b\"]": "may not be padded",
"[\"a\",\"b\"]\n\n": "may not be padded",
"42": "number",
"{//sv_SE.person}": "write {/sv_SE.person}",
} {
code, out, errb := runOut(stdin, "--seed", "1")
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
@@ -605,6 +628,17 @@ func TestRunRecordDefaultTable(t *testing.T) {
}
}
func TestRunRecordOfAQuotedLoneReference(t *testing.T) {
code, out, errb := runOut(`"{/users}"`, "--seed", "1", "--format", "sql", "--data-path", recordDir(t))
if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) {
t.Fatalf("a JSON string holding {/users} = %d, %q, %q; want the record users", code, out, errb)
}
code, out, errb = runOut("{/users} ", "--format", "csv", "--data-path", recordDir(t))
if code != 2 || out != "" || !strings.Contains(errb, "whitespace around {/users}") {
t.Errorf("{/users} and a space = %d, %q, %q; want misuse naming the whitespace", code, out, errb)
}
}
func TestRunRecordInlineTemplate(t *testing.T) {
code, out, errb := runOut(`{"format":"{x}","x":["a","b"]}`, "--seed", "1", "--format", "json")
if code != 0 {
+24 -22
View File
@@ -55,41 +55,43 @@ single-dash long flag is rejected naming the double-dash spelling, and `-s=42` i
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 struct tag is a template by its shape, not by a flag
2026-09-03, Lilleman auf Larv.
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 struct tag of one reference alone, `{/users}`, is refused naming
the path `users`: both render the same text, and only the path names a record. A folder-relative `{.name}` or `{..name}` is refused naming `{/name}`, since an
inline template sits in no folder. `IsTemplate` exports the rule, so struct tags and any
other caller read one.
valid JSON so a stray copied bracket never swallows a tag — it names nothing, and says
so. No option 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 struct tag of one reference alone, `{/users}`, is refused naming the path
`users`: both render the same text, and only the path names a record. A folder-relative
`{.name}` or `{..name}` is refused naming `{/name}`, since an inline template sits in no
folder. `IsTemplate` exports the rule, so struct tags and any other caller read one.
## The CLI renders the template on its stdin, less one newline ending it
## The CLI renders the template on its stdin, keeping a format string's every byte
2026-10-07, Lilleman auf Larv. Serves goals 7 and 7.2.
2026-10-07, Lilleman auf Larv. Serves goals 7, 7.1 and 7.2.
The CLI takes no path or template as an argument: an argument is misuse, and so is a
terminal with nothing piped in, which would otherwise wait for typed input. Both errors
show `echo -n '{/sv_SE.person}' | fejkdata`. One newline ending stdin, `\n` or `\r\n`,
is dropped, so `echo`, a file and a heredoc render as `echo -n` does; only one, so a
template that should end in a newline ends stdin with two. The rest of stdin reaches
`NewTemplate` unchanged, so a padded JSON template is refused there, as goal 5.7 asks.
One reference from the root and nothing else, `{/users}`, reads the path it names: it
The template comes from stdin, from `echo`, a file or a heredoc, so one command line gets
a value and a quoted heredoc needs no escape. The CLI takes no operand: one is misuse
naming the spelling that reads what it likely means, `fejkdata < FILE` for a file and
nothing for `-`, and so is a terminal with nothing piped in, which would otherwise wait
for typed input. A format string keeps every byte, `echo`'s newline included, since
everything outside `{…}` prints as written. JSON and one reference alone drop one newline
ending stdin, `\n` or `\r\n`, which neither could print; the rest reaches `NewTemplate`
unchanged, so a padded JSON template is refused there, as goal 5.7 asks. One reference
from the root and nothing else, `{/users}` or `"{/users}"`, reads the path it names: it
renders the same text, and under `--format` it is the record `users`, with `users` the
default `--table`; one to nothing exits 1, as an unknown path does. That reading sits in
the CLI against goal 3.2 until `todo.md` item 129 moves it into the library. `--list`
still prints paths, each read as `{/path}`. Valid while the CLI reads one template per
run.
## An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
## A template that does not compile is misuse (exit 2), including a reference that resolves to nothing, save the CLI's lone reference until item 129
2026-09-24, Lilleman auf Larv.
@@ -97,9 +99,9 @@ The whole template is the spelling under test, and `NewTemplate` compiles, links
validates as one step. An unknown *path* stays a runtime error (exit 1): there the
template is well-formed and only the data is absent.
## A padded JSON argument is rejected, not trimmed
## A padded JSON template is rejected, not trimmed
2026-09-03, Lilleman auf Larv.
2026-09-03, Lilleman auf Larv. Serves goal 5.7.
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.
+18 -3
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 157`
`Next ID = 160`
| Goal | W |
|---|---|
@@ -74,6 +74,7 @@
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
| 96 | 0.1.0 | | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
| 153 | 0.1.0 | decision | **Run the nine-seat comprehension panel and the scoring run again once items 145 to 152 and item 10 have merged, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 157 | 0.1.0 | | **Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.** | 1 | 1 | 5 | 6 | 7.2 | 17.1 |
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
| 145 | 0.1.0 | | **Move `doc.go`'s definitions of a level, a pick key and an addressed key, with their worked example, to `pickKey` in `pick.go`, and gather `levelKeys`, `addressedKeys`, `renderEnv.keeps` and `readField`'s kept read beside `readUnder`, so a reader follows a named read in one file.** | 2 | 3 | 1 | 9 | 3.4, 3.2 | 15.4 |
| 127 | 0.1.0 | | **Give a Swedish street only postal codes whose delivery digit means street or rural delivery, dropping codes whose delivery digit is 9, or record why they stay.** | 3 | 3 | 4 | 6 | 4 | 14.4 |
@@ -116,8 +117,10 @@
| 42 | 0.2.0 | | **Refuse a struct tag's `datatype` naming the Go type that sets it before proving its values, as the README's Library section promises.** | 2 | 2 | 4 | 6 | 6 | 14.5 |
| 32 | 0.2.0 | | **Add `misc.browser` as the parent of `misc.useragent`, so `misc.browser[Chrome].useragent` resolves.** | 3 | 3 | 4 | 6 | 4.1 | 14.4 |
| 27 | 0.2.0 | | **Merge `en_US.ip` and `sv_SE.ip`, today byte-identical, into one `misc.ip`.** | 3 | 2 | 4 | 6 | 5.7 | 14.1 |
| 159 | 0.2.0 | | **Name the quoted spelling where a template that is a bare JSON number, boolean or null is refused: `42` names `"42"`.** | 1 | 2 | 3 | 6 | 6, 5.1 | 14.1 |
| 22 | 0.2.0 | | **Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.** | 3 | 4 | 7 | 8 | 16 | 14.0 |
| 46 | 0.2.0 | | **Report the same error every load for a table with two bad options, and for a folder with two unnamed rows files.** | 2 | 2 | 3 | 7 | 6, 10 | 14.0 |
| 158 | 0.2.0 | | **Drop a UTF-8 byte order mark that starts stdin, and say in the README how PowerShell 5.1 keeps `Göteborg` from reaching the CLI as `G?teborg`.** | 2 | 2 | 4 | 5 | 7.2, 5.1 | 13.5 |
| 79 | 0.2.0 | decision | **Beside a `--data-path`, load in `New` the user's categories, the shipped ones they override or read, and the shipped ones whose reads reach an overridden one; load the rest on first reach.** | 6 | 6 | 7 | 8 | 13 | 12.9 |
| 107 | 0.2.0 | decision | **Move the template engine into `internal/`, leaving the root the public API and its options.** | 6 | 9 | 3 | 8 | 3.2, 3.4 | 12.2 |
| 112 | 0.2.0 | | **Check a calc's field operands where its template resolves, beside its name operands, once item 36 reports every mistake at once.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 |
@@ -251,8 +254,8 @@ Goal 5.9. Today a path is a second spelling of the template that reads it; after
- Text that is not a JSON value and holds no `{…}` prints as written (goal 5.1), in a string field's struct tag and `Fake` alike, as stdin's text does in the CLI: `Fake("sv_SE.person")` returns `sv_SE.person`. `CHANGELOG.md` says that a path with no `{…}` prints as its text. A struct tag's `-` still skips its field.
- Where text with no `{…}` is refused, as a record or in a typed struct field, and it spells a path the data offers, the error names `{/that.path}`.
- An unknown reference exits 2, where an unknown path exits 1 today.
- The README's CLI section gives one line per shell family, PowerShell and cmd.exe included, showing how to quote a template.
- This revises the decisions "An argument is a template by its shape, not by a flag", "`FakeTemplate` and `NewTemplate` both stay", "`--list` stays a plain list of paths", and the exit-1 rule for an unknown path in "An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing". The README's examples and exit codes, the godoc examples in `doc.go`, the CLI's usage text and header comment, and `CHANGELOG.md`'s entries move to the template spelling. Delete items 40 and 110, which this closes.
- Decide whether a lone reference draws as its path draws today or as a template draws: under one seed, `{/sv_SE.person}` and `{/sv_SE.person} hihi` pick different people.
- This revises the decisions "A struct tag is a template by its shape, not by a flag", "`FakeTemplate` and `NewTemplate` both stay", "`--list` stays a plain list of paths", "The CLI renders the template on its stdin, keeping a format string's every byte", and the exit-1 rule for an unknown path in "A template that does not compile is misuse (exit 2), including a reference that resolves to nothing, save the CLI's lone reference until item 129". The README's examples and exit codes, the godoc examples in `doc.go`, the CLI's usage text and header comment, and `CHANGELOG.md`'s entries move to the template spelling. Delete items 40 and 110, which this closes.
Split it into items for the CLI, struct tags and the library before starting.
@@ -268,6 +271,10 @@ Asked of the maintainer by the product-owner pass of the chunk that made every `
The decision "comprehension floor: every dimension and the overall at 7.0 or above" holds the scores this run is measured against.
### 157. Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.
Split from item 129. cmd.exe's `echo` keeps its quotes and a trailing space, so `echo {/users} | fejkdata` sends `{/users} ` and stops naming the record.
### 89. Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.
Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. This item ships in 0.1.0 beside item 77, so `New` breaks once before anyone depends on it. Mark `WithSeed` required in the Library section's options table. Its tests then stop swapping `randomBytes`, as `fejkdata_test.go` does today, which fails every `New` in the package while that test runs.
@@ -431,6 +438,10 @@ Require the path step to reach a sibling category.
`datatype: boolean` over `{int(1,2)}` on an `int` field answers `prints an integer, not a boolean`.
### 159. Name the quoted spelling where a template that is a bare JSON number, boolean or null is refused: `42` names `"42"`.
On stdin `42` is refused as "a number" and `null` with advice to write `""`, which prints nothing; neither names the text the author meant. The goals audit of the stdin change found it.
### 22. Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.
17 of the 40 distinct ones shipped today sit on `.se`, `.nu`, `.io` and `.co`, which anyone may register, and only RFC 2606's `example.com`, `.net`, `.org`, `.test`, `.example`, `.invalid` and `.localhost` provably reach nothing.
@@ -439,6 +450,10 @@ Require the path step to reach a sibling category.
`readTableOptions` in `table.go` and `walkDir` in `internal/datafiles` return on the first in Go's map order.
### 158. Drop a UTF-8 byte order mark that starts stdin, and say in the README how PowerShell 5.1 keeps `Göteborg` from reaching the CLI as `G?teborg`.
A file a Windows editor saves with a BOM prints an invisible U+FEFF first, and a JSON template with one is read as a format string. PowerShell 5.1 encodes what it pipes to a program by `$OutputEncoding`, ASCII by default. The maintainer chose 0.2.0 on 2026-10-07.
### 79. Beside a `--data-path`, load in `New` the user's categories, the shipped ones they override or read, and the shipped ones whose reads reach an overridden one; load the rest on first reach.
Needs item 77, which makes an override fail unless it says it replaces.