diff --git a/README.md b/README.md index 7dc3d17..ac52840 100644 --- a/README.md +++ b/README.md @@ -24,27 +24,25 @@ lacked the locale coverage and format control we needed. ## CLI -Install the `fakes` command, then give it one or more `-path` flags and one or -more data directories — it prints one value per path to stdout. Each dot segment -descends one level: folders, then the category (a JSON file), then fields inside it. +Install the `fakes` command, then give it one or more `-data-path` directories +and a path — it prints one value to stdout. Each dot segment descends one level: +folders, then the category (a JSON file), then fields inside it. ```sh go install github.com/Timewave-AB/fakes/cmd/fakes@latest -fakes -path person ./data/sv_SE # Sara Eriksson -fakes -path person.last ./data/sv_SE # Eriksson (dotted path into a category) -fakes -path sv_SE.person ./data # point at the tree; the folder is a segment -fakes -path person -path address ./data/sv_SE # several paths, one value each -fakes -path word ./data/sv_SE ./mydata # layer dirs; the last wins a name clash -fakes -seed 42 -path address ./data/sv_SE -fakes -repeat 3 -path person ./data/sv_SE # three values, one per line -fakes -repeat 3 -separator ', ' -path word ./data/sv_SE # nät, barn, sol +fakes -data-path ./data/sv_SE person # Sara Eriksson +fakes -data-path ./data/sv_SE person.last # Eriksson (dotted path into a category) +fakes -data-path ./data sv_SE.person # point at the tree; the folder is a segment +fakes -data-path ./data/sv_SE -data-path ./mydata word # layer dirs; the last wins a name clash +fakes -seed 42 -data-path ./data/sv_SE address +fakes -repeat 3 -data-path ./data/sv_SE person # three values, one per line +fakes -repeat 3 -separator ', ' -data-path ./data/sv_SE word # nät, barn, sol ``` -`-path` is repeatable, and flags come before the data directories. `-repeat N` -renders each path N times — every render an independent draw — and all emitted -values (`repeat` × paths) are joined by `-separator` (default a newline, so -values land one per line). +`-data-path` is repeatable (last wins a name clash) and the path comes last, after +the flags. `-repeat N` renders the path N times — each an independent draw — joined +by `-separator` (default a newline, so values land one per line). Without installing, run it from a checkout with `go run ./cmd/fakes …`. Exit codes: `0` success, `1` runtime error (missing dir, unknown path), `2` misuse. @@ -72,7 +70,7 @@ the `),(` separator; the outer `V#ALUES(…)` wraps that into one valid row list letter token — see [Data format](#data-format).) ```sh -fakes -seed 1 -path sql ./data/sv_SE +fakes -seed 1 -data-path ./data/sv_SE sql # INSERT INTO users VALUES('zoom'),('wahoo'),('blip'); ``` @@ -80,7 +78,7 @@ Raise the template's `repeat` for more rows per statement; use the CLI's `-repeat` for more statements — together they build a whole seed file: ```sh -fakes -repeat 100 -path sql ./data/sv_SE > seed.sql +fakes -repeat 100 -data-path ./data/sv_SE sql > seed.sql ``` ## Library diff --git a/cmd/fakes/main.go b/cmd/fakes/main.go index 8bf3b80..d8608ee 100644 --- a/cmd/fakes/main.go +++ b/cmd/fakes/main.go @@ -1,11 +1,10 @@ -// Command fakes prints fake values from one or more data directories. +// Command fakes prints one fake value from one or more data directories. // -// fakes -path person ./data/sv_SE # a full person -// fakes -path person.last ./data/sv_SE # just the surname (dotted path) -// fakes -path sv_SE.person ./data # point at the tree, address by folder -// fakes -path person -path address ./data/sv_SE # several paths, one per line -// fakes -path person ./data/sv_SE ./mydata # layer custom data; last dir wins -// fakes -seed 42 -path address ./data/sv_SE +// fakes -data-path ./data/sv_SE person # a full person +// fakes -data-path ./data/sv_SE person.last # just the surname (dotted path) +// fakes -data-path ./data sv_SE.person # point at the tree, address by folder +// fakes -data-path ./data/sv_SE -data-path ./mydata person # layer custom data; last dir wins +// fakes -seed 42 -data-path ./data/sv_SE address // // It is a thin CLI over the fakes library: New(dirs) then Fake(path). package main @@ -20,13 +19,13 @@ import ( "github.com/Timewave-AB/fakes" ) -const usage = `Usage: fakes -path P [-path P]... [-seed N] [-repeat N] [-separator S] ... +const usage = `Usage: fakes -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] - -path P a category, or a dotted path into one (person, person.last); repeatable - one or more data directories, e.g. ./data/sv_SE (last wins on clash) + -data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash) + a category, or a dotted path into one (person, person.last) -seed N seed for reproducible output - -repeat N render each path N times (default 1) - -separator S string between emitted values (default newline)` + -repeat N render the path N times (default 1) + -separator S string between repeated values (default newline)` // stringList collects a repeatable string flag, preserving order. type stringList []string @@ -44,14 +43,14 @@ func run(args []string, stdout, stderr io.Writer) int { fs.SetOutput(stderr) fs.Usage = func() { fmt.Fprintln(stderr, usage) } seed := fs.Uint64("seed", 0, "seed for reproducible output") - repeat := fs.Int("repeat", 1, "render each path this many times") - sep := fs.String("separator", "\n", "string between emitted values") - var paths stringList - fs.Var(&paths, "path", "a category or dotted path to render (repeatable)") + repeat := fs.Int("repeat", 1, "render the path this many times") + sep := fs.String("separator", "\n", "string between repeated values") + var dirs stringList + fs.Var(&dirs, "data-path", "a data directory to load (repeatable)") if err := fs.Parse(args); err != nil { return 2 } - if len(paths) == 0 || fs.NArg() < 1 { + if len(dirs) == 0 || fs.NArg() != 1 { fs.Usage() return 2 } @@ -67,20 +66,17 @@ func run(args []string, stdout, stderr io.Writer) int { } }) - f, err := fakes.New(fs.Args(), opts...) + path := fs.Arg(0) + f, err := fakes.New(dirs, opts...) if err != nil { fmt.Fprintln(stderr, err) return 1 } - vals := make([]string, 0, *repeat*len(paths)) - for range *repeat { - for _, p := range paths { - v, err := f.Fake(p) - if err != nil { - fmt.Fprintln(stderr, err) - return 1 - } - vals = append(vals, v) + vals := make([]string, *repeat) + for i := range vals { + if vals[i], err = f.Fake(path); err != nil { + fmt.Fprintln(stderr, err) + return 1 } } fmt.Fprintln(stdout, strings.Join(vals, *sep)) diff --git a/cmd/fakes/main_test.go b/cmd/fakes/main_test.go index b02d392..3dddd9e 100644 --- a/cmd/fakes/main_test.go +++ b/cmd/fakes/main_test.go @@ -19,7 +19,7 @@ func runOut(args ...string) (int, string, string) { } func TestRunOutputsValue(t *testing.T) { - code, out, errb := runOut("-path", "person", svSE) + code, out, errb := runOut("-data-path", svSE, "person") if code != 0 { t.Fatalf("run = %d, stderr=%q", code, errb) } @@ -33,11 +33,11 @@ func TestRunOutputsValue(t *testing.T) { func TestRunDotPath(t *testing.T) { // person.last descends to just a surname — never the full "First Last". - code, full, _ := runOut("-seed", "7", "-path", "person", svSE) + code, full, _ := runOut("-seed", "7", "-data-path", svSE, "person") if code != 0 { t.Fatalf("person run = %d", code) } - code, last, errb := runOut("-seed", "7", "-path", "person.last", svSE) + code, last, errb := runOut("-seed", "7", "-data-path", svSE, "person.last") if code != 0 { t.Fatalf("person.last run = %d, stderr=%q", code, errb) } @@ -50,8 +50,8 @@ func TestRunDotPath(t *testing.T) { } func TestRunSeedDeterministic(t *testing.T) { - _, a, _ := runOut("-seed", "42", "-path", "address", svSE) - _, b, _ := runOut("-seed", "42", "-path", "address", svSE) + _, a, _ := runOut("-seed", "42", "-data-path", svSE, "address") + _, b, _ := runOut("-seed", "42", "-data-path", svSE, "address") if a != b { t.Errorf("same seed diverged: %q != %q", a, b) } @@ -59,7 +59,7 @@ func TestRunSeedDeterministic(t *testing.T) { func TestRunRepeat(t *testing.T) { // -repeat N prints N values, one per line by default. - code, out, errb := runOut("-seed", "1", "-repeat", "3", "-path", "word", svSE) + code, out, errb := runOut("-seed", "1", "-repeat", "3", "-data-path", svSE, "word") if code != 0 { t.Fatalf("run = %d, stderr=%q", code, errb) } @@ -70,8 +70,8 @@ func TestRunRepeat(t *testing.T) { } func TestRunSeparator(t *testing.T) { - // -separator joins the emitted values instead of newlines. - code, out, errb := runOut("-repeat", "3", "-separator", ",", "-path", "word", svSE) + // -separator joins the repeated values instead of newlines. + code, out, errb := runOut("-repeat", "3", "-separator", ",", "-data-path", svSE, "word") if code != 0 { t.Fatalf("run = %d, stderr=%q", code, errb) } @@ -85,7 +85,7 @@ func TestRunSeparator(t *testing.T) { func TestRunRepeatAdvancesRNG(t *testing.T) { // Each repeat is a fresh draw, not the same value N times. - code, out, errb := runOut("-seed", "1", "-repeat", "5", "-path", "person", svSE) + code, out, errb := runOut("-seed", "1", "-repeat", "5", "-data-path", svSE, "person") if code != 0 { t.Fatalf("run = %d, stderr=%q", code, errb) } @@ -98,39 +98,10 @@ func TestRunRepeatAdvancesRNG(t *testing.T) { } } -func TestRunMultiplePaths(t *testing.T) { - // -path repeats: each given path renders once, in order, one per line. - code, out, errb := runOut("-path", "person", "-path", "word", svSE) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - lines := strings.Split(strings.TrimRight(out, "\n"), "\n") - if len(lines) != 2 { - t.Errorf("two -path flags gave %d lines: %q", len(lines), out) - } - for _, l := range lines { - if strings.TrimSpace(l) == "" { - t.Errorf("empty value among %q", out) - } - } -} - -func TestRunMultiplePathsWithRepeat(t *testing.T) { - // repeat × paths values: 2 repeats over 2 paths => 4 lines. - code, out, errb := runOut("-repeat", "2", "-path", "person", "-path", "word", svSE) - if code != 0 { - t.Fatalf("run = %d, stderr=%q", code, errb) - } - lines := strings.Split(strings.TrimRight(out, "\n"), "\n") - if len(lines) != 4 { - t.Errorf("repeat=2 × 2 paths gave %d lines: %q", len(lines), out) - } -} - func TestRunRepeatInvalid(t *testing.T) { // A non-positive repeat is misuse. for _, r := range []string{"0", "-1"} { - code, _, errb := runOut("-repeat", r, "-path", "word", svSE) + code, _, errb := runOut("-repeat", r, "-data-path", svSE, "word") if code != 2 { t.Errorf("repeat=%s = %d, want 2", r, code) } @@ -141,8 +112,8 @@ func TestRunRepeatInvalid(t *testing.T) { } func TestRunUsageOnMissingArgs(t *testing.T) { - // Need at least one -path and one data dir; fewer is misuse. - for _, args := range [][]string{{}, {svSE}, {"-path", "person"}} { + // Need at least one -data-path and exactly one positional path; else misuse. + for _, args := range [][]string{{}, {"-data-path", svSE}, {"person"}} { code, _, errb := runOut(args...) if code != 2 { t.Errorf("run(%v) = %d, want 2", args, code) @@ -153,9 +124,9 @@ func TestRunUsageOnMissingArgs(t *testing.T) { } } -func TestRunMultipleDirs(t *testing.T) { - // Several data dirs after the path flags: all positionals are dirs. - code, out, errb := runOut("-path", "person", enUS, svSE) +func TestRunMultipleDataPaths(t *testing.T) { + // -data-path repeats: dirs merge, last wins; the path stays positional. + code, out, errb := runOut("-data-path", enUS, "-data-path", svSE, "person") if code != 0 { t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb) } @@ -165,7 +136,7 @@ func TestRunMultipleDirs(t *testing.T) { } func TestRunUnknownCategoryFails(t *testing.T) { - code, _, errb := runOut("-path", "nope", svSE) + code, _, errb := runOut("-data-path", svSE, "nope") if code != 1 { t.Fatalf("run = %d, want 1", code) } @@ -175,7 +146,7 @@ func TestRunUnknownCategoryFails(t *testing.T) { } func TestRunMissingDirFails(t *testing.T) { - code, _, errb := runOut("-path", "person", "../../data/nope") + code, _, errb := runOut("-data-path", "../../data/nope", "person") if code != 1 { t.Fatalf("run = %d, want 1", code) }