Correct CLI: repeatable -data-path flag for dirs, dot path stays positional
This commit is contained in:
@@ -24,27 +24,25 @@ lacked the locale coverage and format control we needed.
|
|||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
Install the `fakes` command, then give it one or more `-path` flags and one or
|
Install the `fakes` command, then give it one or more `-data-path` directories
|
||||||
more data directories — it prints one value per path to stdout. Each dot segment
|
and a path — it prints one value to stdout. Each dot segment descends one level:
|
||||||
descends one level: folders, then the category (a JSON file), then fields inside it.
|
folders, then the category (a JSON file), then fields inside it.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install github.com/Timewave-AB/fakes/cmd/fakes@latest
|
go install github.com/Timewave-AB/fakes/cmd/fakes@latest
|
||||||
|
|
||||||
fakes -path person ./data/sv_SE # Sara Eriksson
|
fakes -data-path ./data/sv_SE person # Sara Eriksson
|
||||||
fakes -path person.last ./data/sv_SE # Eriksson (dotted path into a category)
|
fakes -data-path ./data/sv_SE person.last # Eriksson (dotted path into a category)
|
||||||
fakes -path sv_SE.person ./data # point at the tree; the folder is a segment
|
fakes -data-path ./data sv_SE.person # point at the tree; the folder is a segment
|
||||||
fakes -path person -path address ./data/sv_SE # several paths, one value each
|
fakes -data-path ./data/sv_SE -data-path ./mydata word # layer dirs; the last wins a name clash
|
||||||
fakes -path word ./data/sv_SE ./mydata # layer dirs; the last wins a name clash
|
fakes -seed 42 -data-path ./data/sv_SE address
|
||||||
fakes -seed 42 -path address ./data/sv_SE
|
fakes -repeat 3 -data-path ./data/sv_SE person # three values, one per line
|
||||||
fakes -repeat 3 -path person ./data/sv_SE # three values, one per line
|
fakes -repeat 3 -separator ', ' -data-path ./data/sv_SE word # nät, barn, sol
|
||||||
fakes -repeat 3 -separator ', ' -path word ./data/sv_SE # nät, barn, sol
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`-path` is repeatable, and flags come before the data directories. `-repeat N`
|
`-data-path` is repeatable (last wins a name clash) and the path comes last, after
|
||||||
renders each path N times — every render an independent draw — and all emitted
|
the flags. `-repeat N` renders the path N times — each an independent draw — joined
|
||||||
values (`repeat` × paths) are joined by `-separator` (default a newline, so
|
by `-separator` (default a newline, so values land one per line).
|
||||||
values land one per line).
|
|
||||||
|
|
||||||
Without installing, run it from a checkout with `go run ./cmd/fakes …`. Exit
|
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.
|
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).)
|
letter token — see [Data format](#data-format).)
|
||||||
|
|
||||||
```sh
|
```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');
|
# 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:
|
`-repeat` for more statements — together they build a whole seed file:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fakes -repeat 100 -path sql ./data/sv_SE > seed.sql
|
fakes -repeat 100 -data-path ./data/sv_SE sql > seed.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
## Library
|
## Library
|
||||||
|
|||||||
+23
-27
@@ -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 -data-path ./data/sv_SE person # a full person
|
||||||
// fakes -path person.last ./data/sv_SE # just the surname (dotted path)
|
// fakes -data-path ./data/sv_SE person.last # just the surname (dotted path)
|
||||||
// fakes -path sv_SE.person ./data # point at the tree, address by folder
|
// fakes -data-path ./data sv_SE.person # point at the tree, address by folder
|
||||||
// fakes -path person -path address ./data/sv_SE # several paths, one per line
|
// fakes -data-path ./data/sv_SE -data-path ./mydata person # layer custom data; last dir wins
|
||||||
// fakes -path person ./data/sv_SE ./mydata # layer custom data; last dir wins
|
// fakes -seed 42 -data-path ./data/sv_SE address
|
||||||
// fakes -seed 42 -path address ./data/sv_SE
|
|
||||||
//
|
//
|
||||||
// It is a thin CLI over the fakes library: New(dirs) then Fake(path).
|
// It is a thin CLI over the fakes library: New(dirs) then Fake(path).
|
||||||
package main
|
package main
|
||||||
@@ -20,13 +19,13 @@ import (
|
|||||||
"github.com/Timewave-AB/fakes"
|
"github.com/Timewave-AB/fakes"
|
||||||
)
|
)
|
||||||
|
|
||||||
const usage = `Usage: fakes -path P [-path P]... [-seed N] [-repeat N] [-separator S] <data-dir>...
|
const usage = `Usage: fakes -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] <path>
|
||||||
|
|
||||||
-path P a category, or a dotted path into one (person, person.last); repeatable
|
-data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash)
|
||||||
<data-dir> one or more data directories, e.g. ./data/sv_SE (last wins on clash)
|
<path> a category, or a dotted path into one (person, person.last)
|
||||||
-seed N seed for reproducible output
|
-seed N seed for reproducible output
|
||||||
-repeat N render each path N times (default 1)
|
-repeat N render the path N times (default 1)
|
||||||
-separator S string between emitted values (default newline)`
|
-separator S string between repeated values (default newline)`
|
||||||
|
|
||||||
// stringList collects a repeatable string flag, preserving order.
|
// stringList collects a repeatable string flag, preserving order.
|
||||||
type stringList []string
|
type stringList []string
|
||||||
@@ -44,14 +43,14 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
fs.SetOutput(stderr)
|
fs.SetOutput(stderr)
|
||||||
fs.Usage = func() { fmt.Fprintln(stderr, usage) }
|
fs.Usage = func() { fmt.Fprintln(stderr, usage) }
|
||||||
seed := fs.Uint64("seed", 0, "seed for reproducible output")
|
seed := fs.Uint64("seed", 0, "seed for reproducible output")
|
||||||
repeat := fs.Int("repeat", 1, "render each path this many times")
|
repeat := fs.Int("repeat", 1, "render the path this many times")
|
||||||
sep := fs.String("separator", "\n", "string between emitted values")
|
sep := fs.String("separator", "\n", "string between repeated values")
|
||||||
var paths stringList
|
var dirs stringList
|
||||||
fs.Var(&paths, "path", "a category or dotted path to render (repeatable)")
|
fs.Var(&dirs, "data-path", "a data directory to load (repeatable)")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if len(paths) == 0 || fs.NArg() < 1 {
|
if len(dirs) == 0 || fs.NArg() != 1 {
|
||||||
fs.Usage()
|
fs.Usage()
|
||||||
return 2
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
vals := make([]string, 0, *repeat*len(paths))
|
vals := make([]string, *repeat)
|
||||||
for range *repeat {
|
for i := range vals {
|
||||||
for _, p := range paths {
|
if vals[i], err = f.Fake(path); err != nil {
|
||||||
v, err := f.Fake(p)
|
fmt.Fprintln(stderr, err)
|
||||||
if err != nil {
|
return 1
|
||||||
fmt.Fprintln(stderr, err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
vals = append(vals, v)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Fprintln(stdout, strings.Join(vals, *sep))
|
fmt.Fprintln(stdout, strings.Join(vals, *sep))
|
||||||
|
|||||||
+17
-46
@@ -19,7 +19,7 @@ func runOut(args ...string) (int, string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunOutputsValue(t *testing.T) {
|
func TestRunOutputsValue(t *testing.T) {
|
||||||
code, out, errb := runOut("-path", "person", svSE)
|
code, out, errb := runOut("-data-path", svSE, "person")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
}
|
}
|
||||||
@@ -33,11 +33,11 @@ func TestRunOutputsValue(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunDotPath(t *testing.T) {
|
func TestRunDotPath(t *testing.T) {
|
||||||
// person.last descends to just a surname — never the full "First Last".
|
// 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 {
|
if code != 0 {
|
||||||
t.Fatalf("person run = %d", code)
|
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 {
|
if code != 0 {
|
||||||
t.Fatalf("person.last run = %d, stderr=%q", code, errb)
|
t.Fatalf("person.last run = %d, stderr=%q", code, errb)
|
||||||
}
|
}
|
||||||
@@ -50,8 +50,8 @@ func TestRunDotPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunSeedDeterministic(t *testing.T) {
|
func TestRunSeedDeterministic(t *testing.T) {
|
||||||
_, a, _ := runOut("-seed", "42", "-path", "address", svSE)
|
_, a, _ := runOut("-seed", "42", "-data-path", svSE, "address")
|
||||||
_, b, _ := runOut("-seed", "42", "-path", "address", svSE)
|
_, b, _ := runOut("-seed", "42", "-data-path", svSE, "address")
|
||||||
if a != b {
|
if a != b {
|
||||||
t.Errorf("same seed diverged: %q != %q", 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) {
|
func TestRunRepeat(t *testing.T) {
|
||||||
// -repeat N prints N values, one per line by default.
|
// -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 {
|
if code != 0 {
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,8 @@ func TestRunRepeat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunSeparator(t *testing.T) {
|
func TestRunSeparator(t *testing.T) {
|
||||||
// -separator joins the emitted values instead of newlines.
|
// -separator joins the repeated values instead of newlines.
|
||||||
code, out, errb := runOut("-repeat", "3", "-separator", ",", "-path", "word", svSE)
|
code, out, errb := runOut("-repeat", "3", "-separator", ",", "-data-path", svSE, "word")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func TestRunSeparator(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunRepeatAdvancesRNG(t *testing.T) {
|
func TestRunRepeatAdvancesRNG(t *testing.T) {
|
||||||
// Each repeat is a fresh draw, not the same value N times.
|
// 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 {
|
if code != 0 {
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
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) {
|
func TestRunRepeatInvalid(t *testing.T) {
|
||||||
// A non-positive repeat is misuse.
|
// A non-positive repeat is misuse.
|
||||||
for _, r := range []string{"0", "-1"} {
|
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 {
|
if code != 2 {
|
||||||
t.Errorf("repeat=%s = %d, want 2", r, code)
|
t.Errorf("repeat=%s = %d, want 2", r, code)
|
||||||
}
|
}
|
||||||
@@ -141,8 +112,8 @@ func TestRunRepeatInvalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunUsageOnMissingArgs(t *testing.T) {
|
func TestRunUsageOnMissingArgs(t *testing.T) {
|
||||||
// Need at least one -path and one data dir; fewer is misuse.
|
// Need at least one -data-path and exactly one positional path; else misuse.
|
||||||
for _, args := range [][]string{{}, {svSE}, {"-path", "person"}} {
|
for _, args := range [][]string{{}, {"-data-path", svSE}, {"person"}} {
|
||||||
code, _, errb := runOut(args...)
|
code, _, errb := runOut(args...)
|
||||||
if code != 2 {
|
if code != 2 {
|
||||||
t.Errorf("run(%v) = %d, want 2", args, code)
|
t.Errorf("run(%v) = %d, want 2", args, code)
|
||||||
@@ -153,9 +124,9 @@ func TestRunUsageOnMissingArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunMultipleDirs(t *testing.T) {
|
func TestRunMultipleDataPaths(t *testing.T) {
|
||||||
// Several data dirs after the path flags: all positionals are dirs.
|
// -data-path repeats: dirs merge, last wins; the path stays positional.
|
||||||
code, out, errb := runOut("-path", "person", enUS, svSE)
|
code, out, errb := runOut("-data-path", enUS, "-data-path", svSE, "person")
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb)
|
t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb)
|
||||||
}
|
}
|
||||||
@@ -165,7 +136,7 @@ func TestRunMultipleDirs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunUnknownCategoryFails(t *testing.T) {
|
func TestRunUnknownCategoryFails(t *testing.T) {
|
||||||
code, _, errb := runOut("-path", "nope", svSE)
|
code, _, errb := runOut("-data-path", svSE, "nope")
|
||||||
if code != 1 {
|
if code != 1 {
|
||||||
t.Fatalf("run = %d, want 1", code)
|
t.Fatalf("run = %d, want 1", code)
|
||||||
}
|
}
|
||||||
@@ -175,7 +146,7 @@ func TestRunUnknownCategoryFails(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunMissingDirFails(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 {
|
if code != 1 {
|
||||||
t.Fatalf("run = %d, want 1", code)
|
t.Fatalf("run = %d, want 1", code)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user