This commit is contained in:
@@ -41,7 +41,7 @@ brace, a bracket or a quote, so the two cannot collide (see
|
|||||||
| `-s`, `--seed N` | reproducible output |
|
| `-s`, `--seed N` | reproducible output |
|
||||||
| `-n`, `--repeat N` | render the value 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) |
|
| `--separator S` | between repeated values (default a newline) |
|
||||||
| `--format F` | `text` (default), `json`, `csv` or `sql` — a record's columns, one record per row |
|
| `--format F` | `text` (default), `json`, `ndjson`, `csv` or `sql` — a record's columns, one record per row (json frames them as an array) |
|
||||||
| `--table T` | the INSERT target for `--format sql` (default: the path's last segment, or `records` for an inline template) |
|
| `--table T` | the INSERT target for `--format sql` (default: the path's last segment, or `records` for an inline template) |
|
||||||
| `--list` | print every path, then exit |
|
| `--list` | print every path, then exit |
|
||||||
| `--version`, `-h`, `--help` | print, then exit |
|
| `--version`, `-h`, `--help` | print, then exit |
|
||||||
@@ -81,7 +81,7 @@ For structured output a record writes the row for you.
|
|||||||
### Records
|
### Records
|
||||||
|
|
||||||
A record is a template seen as columns: its fields are the columns, its `format`
|
A record is a template seen as columns: its fields are the columns, its `format`
|
||||||
the whole. `--format json|csv|sql` streams one record per row; the library's
|
the whole. `--format json|ndjson|csv|sql` writes the records; the library's
|
||||||
`Record` (below) hands back the columns. Every column is a string — this is the
|
`Record` (below) hands back the columns. Every column is a string — this is the
|
||||||
out-of-scope of typed scalars, see [Decisions](#decisions). Save
|
out-of-scope of typed scalars, see [Decisions](#decisions). Save
|
||||||
`mydata/users.json`:
|
`mydata/users.json`:
|
||||||
@@ -95,17 +95,17 @@ out-of-scope of typed scalars, see [Decisions](#decisions). Save
|
|||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fejkdata --seed 1 --data-path ./mydata --format json users # {"first":"Bo","last":"Lovelace"}
|
fejkdata --seed 1 --data-path ./mydata --format json users # [{"first":"Bo","last":"Lovelace"}]
|
||||||
|
fejkdata --seed 1 --data-path ./mydata --format ndjson users # {"first":"Bo","last":"Lovelace"}
|
||||||
fejkdata --seed 1 --data-path ./mydata --format csv users # first,last → Bo,Lovelace
|
fejkdata --seed 1 --data-path ./mydata --format csv users # first,last → Bo,Lovelace
|
||||||
fejkdata --seed 1 --data-path ./mydata --format sql users # INSERT INTO "users" ("first", "last") VALUES ('Bo', 'Lovelace');
|
fejkdata --seed 1 --data-path ./mydata --format sql users # INSERT INTO "users" ("first", "last") VALUES ('Bo', 'Lovelace');
|
||||||
fejkdata --seed 1 --data-path ./mydata --format json --repeat 3 users # three objects, one per line
|
|
||||||
fejkdata --seed 1 --data-path ./mydata --format sql --table people users # INSERT into another table
|
fejkdata --seed 1 --data-path ./mydata --format sql --table people users # INSERT into another table
|
||||||
```
|
```
|
||||||
|
|
||||||
`--repeat` streams that many records — newline-delimited JSON (one object per
|
`--repeat` streams that many records — `json` frames them as one array document,
|
||||||
line, NDJSON), a CSV row after a header, or an INSERT in SQL. To fold NDJSON into
|
`ndjson` writes one object per line, `csv` a row after a header, `sql` one INSERT
|
||||||
a single array, `fejkdata … --format json | jq -s .`. Only a category-level
|
per line. Only a category-level template is a record; a field, choice or folder
|
||||||
template is a record; a field, choice or folder errors, and so does a `repeat` on
|
errors, and so does a `repeat` on
|
||||||
the template itself, which composes the format into one string rather than
|
the template itself, which composes the format into one string rather than
|
||||||
projecting columns — ask for more records with `--repeat`. A `repeat` on a column
|
projecting columns — ask for more records with `--repeat`. A `repeat` on a column
|
||||||
is fine.
|
is fine.
|
||||||
@@ -165,9 +165,9 @@ r, err = f.FakeRecord(`{"format":"{x}","x":["a","b"]}`) // compile + render inli
|
|||||||
| `WithoutShippedData()` | load only what you give |
|
| `WithoutShippedData()` | load only what you give |
|
||||||
|
|
||||||
A `*Record` carries its columns via `Columns()`, and serializes them with `JSON()`
|
A `*Record` carries its columns via `Columns()`, and serializes them with `JSON()`
|
||||||
(one object), `CSVHeader()`/`CSVLine()`, or `SQLInsert(table)` — the same three
|
(one object), `CSVHeader()`/`CSVLine()`, or `SQLInsert(table)` — the shapes the
|
||||||
shapes the CLI's `--format` streams. `Record` and `FakeRecord` take a record; a
|
CLI's `--format` writes. `Record` and `FakeRecord` take a record; a path or
|
||||||
path or template that is not one — a bare string, a choice, or a folder — errors.
|
template that is not one — a bare string, a choice, or a folder — errors.
|
||||||
|
|
||||||
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
||||||
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
||||||
|
|||||||
+63
-25
@@ -35,12 +35,12 @@ or a quote). Templates reach the data by reference from the root —
|
|||||||
argument carrying a bracket, a closing brace or a quote but no valid JSON names
|
argument carrying a bracket, a closing brace or a quote but no valid JSON names
|
||||||
neither.
|
neither.
|
||||||
|
|
||||||
With --format json, csv or sql the argument must name a record — a template whose
|
With --format json, ndjson, csv or sql the argument must name a record — a
|
||||||
fields are its columns — and each render is streamed as one JSON object, one CSV
|
template whose fields are its columns — and the rows are written as one JSON
|
||||||
row (after a header), or one INSERT.
|
array, one JSON object per line, one CSV row (after a header), or one INSERT.
|
||||||
|
|
||||||
-d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash)
|
-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, csv or sql
|
--format F output form: text (default), json, ndjson, csv or sql
|
||||||
-h, --help print this help, then exit
|
-h, --help print this help, then exit
|
||||||
--list list the paths the data offers, then exit
|
--list list the paths the data offers, then exit
|
||||||
--no-shipped-data load only the --data-path directories
|
--no-shipped-data load only the --data-path directories
|
||||||
@@ -209,20 +209,28 @@ func parseArgs(argv []string) (invocation, error) {
|
|||||||
return in, nil
|
return in, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordFormat is one way to write a record out: the line it renders, and the
|
// recordFormat is one way to write a record out: the line each record renders,
|
||||||
// header that precedes the first one, if the format has one.
|
// the header that precedes the first one, and the open/close frame plus the
|
||||||
|
// between-record separator a document form needs.
|
||||||
type recordFormat struct {
|
type recordFormat struct {
|
||||||
header func(*fejkdata.Record) string
|
header func(*fejkdata.Record) string
|
||||||
line func(r *fejkdata.Record, table string) string
|
line func(r *fejkdata.Record, table string) string
|
||||||
|
open string
|
||||||
|
close string
|
||||||
|
sep string
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordFormats is every --format that writes records.
|
// recordFormats is every --format that writes records. json frames the records
|
||||||
|
// as one array document; ndjson is the same column, one object per line.
|
||||||
var recordFormats = map[string]recordFormat{
|
var recordFormats = map[string]recordFormat{
|
||||||
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }},
|
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }, sep: "\n"},
|
||||||
"json": {line: func(r *fejkdata.Record, _ string) string { return r.JSON() }},
|
"json": {line: jsonLine, open: "[", close: "]", sep: ",\n"},
|
||||||
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }},
|
"ndjson": {line: jsonLine, sep: "\n"},
|
||||||
|
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }, sep: "\n"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func jsonLine(r *fejkdata.Record, _ string) string { return r.JSON() }
|
||||||
|
|
||||||
// writesRecords reports whether the format writes records rather than plain text.
|
// writesRecords reports whether the format writes records rather than plain text.
|
||||||
func (in invocation) writesRecords() bool {
|
func (in invocation) writesRecords() bool {
|
||||||
return in.format != "text"
|
return in.format != "text"
|
||||||
@@ -295,7 +303,10 @@ func (in invocation) options() []fejkdata.Option {
|
|||||||
// so a render failure comes before anything is written; a write failure surfaces
|
// so a render failure comes before anything is written; a write failure surfaces
|
||||||
// from Flush, bufio keeping the first one.
|
// from Flush, bufio keeping the first one.
|
||||||
func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
||||||
draw, err := in.draw(f, kind, in.paths[0])
|
if in.writesRecords() {
|
||||||
|
return in.writeRecords(f, kind, w)
|
||||||
|
}
|
||||||
|
draw, err := in.textDraw(f, kind, in.paths[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -314,10 +325,9 @@ func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) err
|
|||||||
return out.Flush()
|
return out.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw builds what one render yields: the value's text, or the record's line in
|
// textDraw builds what one text render yields: the value, from a path or an
|
||||||
// the chosen format, the header carried ahead of the first one.
|
// inline template.
|
||||||
func (in invocation) draw(f *fejkdata.Generator, kind argKind, arg string) (func() (string, error), error) {
|
func (in invocation) textDraw(f *fejkdata.Generator, kind argKind, arg string) (func() (string, error), error) {
|
||||||
if !in.writesRecords() {
|
|
||||||
if kind != argTemplate {
|
if kind != argTemplate {
|
||||||
return func() (string, error) { return f.Fake(arg) }, nil
|
return func() (string, error) { return f.Fake(arg) }, nil
|
||||||
}
|
}
|
||||||
@@ -327,30 +337,58 @@ func (in invocation) draw(f *fejkdata.Generator, kind argKind, arg string) (func
|
|||||||
}
|
}
|
||||||
return func() (string, error) { return t.Fake(), nil }, nil
|
return func() (string, error) { return t.Fake(), nil }, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordStream builds the record drawer for the argument, plus the INSERT table
|
||||||
|
// a sql format names.
|
||||||
|
func (in invocation) recordStream(f *fejkdata.Generator, kind argKind, arg string) (func() (*fejkdata.Record, error), string, error) {
|
||||||
record := func() (*fejkdata.Record, error) { return f.Record(arg) }
|
record := func() (*fejkdata.Record, error) { return f.Record(arg) }
|
||||||
if kind == argTemplate {
|
if kind == argTemplate {
|
||||||
t, err := f.NewRecordTemplate(arg)
|
t, err := f.NewRecordTemplate(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, templateError{err}
|
return nil, "", templateError{err}
|
||||||
}
|
}
|
||||||
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
||||||
}
|
}
|
||||||
format, table, first := recordFormats[in.format], in.table, true
|
table := in.table
|
||||||
if table == "" {
|
if table == "" {
|
||||||
table = defaultTable(arg, kind)
|
table = defaultTable(arg, kind)
|
||||||
}
|
}
|
||||||
return func() (string, error) {
|
return record, table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeRecords streams a record per line in the chosen format, framing a document
|
||||||
|
// form with its open/close brackets and a header preceding the first record.
|
||||||
|
func (in invocation) writeRecords(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
||||||
|
record, table, err := in.recordStream(f, kind, in.paths[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
format := recordFormats[in.format]
|
||||||
|
out := bufio.NewWriter(w)
|
||||||
|
if format.open != "" {
|
||||||
|
out.WriteString(format.open)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
for i := 0; i < in.repeat; i++ {
|
||||||
r, err := record()
|
r, err := record()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
line := format.line(r, table)
|
if i == 0 && format.header != nil {
|
||||||
if first && format.header != nil {
|
out.WriteString(format.header(r))
|
||||||
line = format.header(r) + "\n" + line
|
out.WriteByte('\n')
|
||||||
}
|
}
|
||||||
first = false
|
if i > 0 {
|
||||||
return line, nil
|
out.WriteString(format.sep)
|
||||||
}, nil
|
}
|
||||||
|
out.WriteString(format.line(r, table))
|
||||||
|
}
|
||||||
|
if format.close != "" {
|
||||||
|
out.WriteByte('\n')
|
||||||
|
out.WriteString(format.close)
|
||||||
|
}
|
||||||
|
out.WriteByte('\n')
|
||||||
|
return out.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultTable names the INSERT target when --table is absent: the path's last
|
// defaultTable names the INSERT target when --table is absent: the path's last
|
||||||
|
|||||||
Reference in New Issue
Block a user