Print the CSV header at -n 0, refuse --separator with a record format, refuse a date field in time(), read a bare number template as a format string, and say when no data is loaded
This commit is contained in:
+3
-2
@@ -29,7 +29,8 @@ replacement, and each removed path, column or flag.
|
||||
- First release: the CLI, the library and the shipped data set.
|
||||
- The CLI renders exactly the template on its stdin, `echo '{/sv_SE.person} hihi' |
|
||||
fejkdata`, and adds nothing under `--format text`: `echo`'s newline ends the render, and `--separator`
|
||||
defaults to empty. Whitespace around a JSON template is dropped. A lone reference,
|
||||
defaults to empty. Whitespace around a JSON object, array or string template is
|
||||
dropped, and a bare number or boolean is a format string, printed as written. A lone reference,
|
||||
`{/users}`, is the record `users` under `--format`. An argument other than a flag, or
|
||||
nothing piped in, exits 2; an empty or blank stdin is a template, and renders as it is.
|
||||
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
|
||||
@@ -100,7 +101,7 @@ replacement, and each removed path, column or flag.
|
||||
from a `sv_SE.birth-number` table under `sex`, in place of `sv_SE.ssn`, whose
|
||||
`ssn.mmdd`, `ssn.mmdd.m` and `ssn.mmdd.d` go with it. `en_US.ssn` now draws the
|
||||
ranges the SSA assigns and carries the columns `area`, `group` and `serial`, so
|
||||
`--format csv en_US.ssn` writes a header where it used to fail; `en_US.itin` is new
|
||||
`echo '{/en_US.ssn}' | fejkdata --format csv` writes a header where it used to fail; `en_US.itin` is new
|
||||
and carries the same three. `person.prefix` is null where a person has no title,
|
||||
where it used to be an empty string, so `--format sql` writes `NULL` and a `string`
|
||||
struct field reading it becomes `*string`.
|
||||
|
||||
@@ -30,8 +30,7 @@ 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,
|
||||
fejkdata renders the template on its stdin: a format string, or any JSON value. 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
|
||||
inside one: each dot segment descends one level — folders, then the category (a JSON
|
||||
@@ -40,9 +39,9 @@ these paths; a template reads one as `{/path}`.
|
||||
|
||||
Under `--format text`, the default, what you send is what renders, and fejkdata adds nothing. A format string keeps every
|
||||
byte, so `echo`, a file or a heredoc ends each render with its newline, and `printf '%s'`
|
||||
sends none; `-n` joins renders with `--separator`, empty by default. A JSON template is
|
||||
any JSON value, and the whitespace around it is dropped; `42` or `true` alone renders its
|
||||
text, and `null` alone renders nothing. An empty or blank stdin is a template too, and
|
||||
sends none; `-n` joins renders with `--separator`, empty by default. A JSON object, array
|
||||
or string drops the whitespace around it, and `null` alone renders nothing; `42` or `true`
|
||||
alone is a format string, so it prints as written, newline included. An empty or blank stdin is a template too, and
|
||||
renders as it is. A JSON template that should end
|
||||
each render in a newline writes `\n` in its format, from a quoted heredoc or a file. A
|
||||
lone reference, one `{/…}` and nothing else, is the record it names under `--format`, one
|
||||
@@ -55,8 +54,8 @@ template holding one from a quoted heredoc or a file. The rest of stdin's rules
|
||||
| `-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` | same seed, version and data: identical output |
|
||||
| `-n`, `--repeat N` | render the template N times (0 to 1048576), each an independent draw, streamed; `0` prints nothing, or `[]` under `--format json` |
|
||||
| `--separator S` | between repeated renders (default empty); a record `--format` ignores it |
|
||||
| `-n`, `--repeat N` | render the template N times (0 to 1048576), each an independent draw, streamed; `0` prints nothing, `[]` under `--format json`, or the header under `--format csv` |
|
||||
| `--separator S` | between repeated renders (default empty); refused with a record `--format`, which has its own separators |
|
||||
| `--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`, ignored under any other (default: a lone reference's last segment, such as `users` for `{/users}`, else `records`) |
|
||||
| `--list` | print every path, then exit; `--repeat`, `--separator`, `--format` and `--table` do nothing here |
|
||||
@@ -678,8 +677,10 @@ between the two days is reachable, so a layout with a clock draws the time too,
|
||||
`from` may equal `to`, which is that one day. The instant is UTC, so a zone in the
|
||||
layout prints `UTC` or `Z`.
|
||||
The quotes delimit a layout outside a selector only, so `[O'Fallon]` in an
|
||||
argument stays a name. A layout naming no field prints itself, `time` prints a date
|
||||
field as 1970-01-01, and `date` over one day with no clock field prints that day.
|
||||
argument stays a name. A layout naming no field prints itself, `date` with a clock-only
|
||||
layout prints the drawn instant's clock, and `date` over one day with no clock field prints
|
||||
that day; `time` with a layout naming a date field is refused, naming `date`, since `time`
|
||||
draws no date.
|
||||
Rejected at `New`: a bound that is no calendar date, or after the other, and an unquoted
|
||||
or double-quoted layout, naming the single-quoted one. `{seq()}` spans `Fake`
|
||||
calls and `repeat`, resets with a new generator, and is the natural primary key for
|
||||
|
||||
+54
-31
@@ -35,7 +35,7 @@ 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
|
||||
A template is a format string, or any JSON value. Its {…} tokens
|
||||
reach the data by reference from the root, {/sv_SE.person.last}, whether the data
|
||||
is shipped or layered with --data-path. A reference names a category or a dotted
|
||||
path into one ({/sv_SE.person.last}), a table's row by key or name
|
||||
@@ -45,8 +45,9 @@ path into one ({/sv_SE.person.last}), a table's row by key or name
|
||||
Under --format text, the default, what you send is what renders, and fejkdata adds
|
||||
nothing: a format string keeps every byte, so echo's newline ends each render, and
|
||||
printf '%s' sends none. -n joins renders with --separator, empty by default. A JSON
|
||||
template is any JSON value, and the whitespace around it is dropped; 42 or true alone
|
||||
renders its text, and null nothing. An empty or blank stdin renders as it is. A
|
||||
object, array or string drops the whitespace around it, and null alone renders nothing;
|
||||
42 or true alone is a format string, printed as written. An empty or blank stdin
|
||||
renders as it is. A
|
||||
quoted heredoc, <<'EOF', passes $, a backslash or a quote as written.
|
||||
|
||||
With --format json, ndjson, csv or sql the template must be a record — a JSON
|
||||
@@ -61,9 +62,11 @@ one INSERT.
|
||||
--list list the paths the data offers, each written after {/ in a template, then exit;
|
||||
--repeat, --separator, --format and --table do nothing here
|
||||
--no-shipped-data load only the --data-path directories
|
||||
-n, --repeat N render the template N times, 0..1048576 (default 1)
|
||||
-n, --repeat N render the template N times, 0..1048576 (default 1); 0 prints nothing,
|
||||
[] under --format json, or the header under --format csv
|
||||
-s, --seed N same seed, version and data: identical output
|
||||
--separator S string between repeated renders (default empty); a record --format ignores it
|
||||
--separator S string between repeated renders (default empty); refused with a record
|
||||
--format, which has its own separators
|
||||
--table T the INSERT target for --format sql, ignored under any other
|
||||
(default: a lone reference's last segment, else records)
|
||||
--version print the version, then exit
|
||||
@@ -73,19 +76,20 @@ A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3)
|
||||
`
|
||||
|
||||
type invocation struct {
|
||||
args []string
|
||||
dirs []string
|
||||
format string
|
||||
help bool
|
||||
list bool
|
||||
noShipped bool
|
||||
repeat int
|
||||
seed uint64
|
||||
seeded bool
|
||||
separator string
|
||||
table string
|
||||
tableSet bool
|
||||
version bool
|
||||
args []string
|
||||
dirs []string
|
||||
format string
|
||||
help bool
|
||||
list bool
|
||||
noShipped bool
|
||||
repeat int
|
||||
seed uint64
|
||||
seeded bool
|
||||
separator string
|
||||
separatorSet bool
|
||||
table string
|
||||
tableSet bool
|
||||
version bool
|
||||
}
|
||||
|
||||
type flagDef struct {
|
||||
@@ -117,7 +121,7 @@ var flagDefs = []flagDef{
|
||||
in.seed, in.seeded = n, true
|
||||
return nil
|
||||
}},
|
||||
{"separator", "", true, func(in *invocation, v string) error { in.separator = v; return nil }},
|
||||
{"separator", "", true, func(in *invocation, v string) error { in.separator, in.separatorSet = v, true; return nil }},
|
||||
{"table", "", true, func(in *invocation, v string) error { in.table, in.tableSet = v, true; return nil }},
|
||||
{"version", "", false, func(in *invocation, _ string) error { in.version = true; return nil }},
|
||||
}
|
||||
@@ -233,15 +237,16 @@ type recordFormat struct {
|
||||
open string
|
||||
close string
|
||||
sep string
|
||||
writes string // what it writes between values, which --separator would seem to set
|
||||
}
|
||||
|
||||
// recordFormats is every --format that writes records. json frames the records
|
||||
// as one array document; ndjson writes one object per line.
|
||||
var recordFormats = map[string]recordFormat{
|
||||
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }, sep: "\n"},
|
||||
"json": {line: jsonLine, open: "[", close: "]", sep: ",\n"},
|
||||
"ndjson": {line: jsonLine, sep: "\n"},
|
||||
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }, sep: "\n"},
|
||||
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }, sep: "\n", writes: `always separates fields with ","`},
|
||||
"json": {line: jsonLine, open: "[", close: "]", sep: ",\n", writes: "writes the records as one JSON array"},
|
||||
"ndjson": {line: jsonLine, sep: "\n", writes: "writes one record per line"},
|
||||
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }, sep: "\n", writes: "writes one INSERT per line"},
|
||||
}
|
||||
|
||||
func jsonLine(r *fejkdata.Record, _ string) string { return r.JSON() }
|
||||
@@ -268,6 +273,9 @@ func (in invocation) checkFlags() error {
|
||||
if in.tableSet && in.table == "" && in.format == "sql" {
|
||||
return errors.New("--table names the INSERT target, so it cannot be empty")
|
||||
}
|
||||
if f, records := recordFormats[in.format]; records && in.separatorSet && !in.list {
|
||||
return fmt.Errorf("--separator joins text renders; --format %s %s", in.format, f.writes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -400,16 +408,19 @@ func (in invocation) writeRecords(f *fejkdata.Generator, src input, w io.Writer)
|
||||
return out.Flush()
|
||||
}
|
||||
|
||||
// writeNoRecord writes zero records: an empty document where the format frames one, else nothing. It
|
||||
// draws one record and drops it, so a lone reference to nothing fails as it would with records.
|
||||
// writeNoRecord writes zero records: the header where the format writes one, an empty document
|
||||
// where it frames one, else nothing. It draws one record, which names the header's columns, so a
|
||||
// lone reference to nothing fails as it would with records.
|
||||
func writeNoRecord(record func() (*fejkdata.Record, error), format recordFormat, w io.Writer) error {
|
||||
if _, err := record(); err != nil {
|
||||
r, err := record()
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case format.header != nil:
|
||||
_, err = io.WriteString(w, format.header(r)+"\n")
|
||||
case format.open != "":
|
||||
_, err = io.WriteString(w, format.open+format.close+"\n")
|
||||
}
|
||||
if format.open == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := io.WriteString(w, format.open+format.close+"\n")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -595,7 +606,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
return 0
|
||||
}
|
||||
if err := in.write(f, src, stdout); err != nil {
|
||||
return fail(stderr, err)
|
||||
return fail(stderr, in.noDataCause(err))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -611,6 +622,18 @@ func readStdin(stdin io.Reader) (input, error) {
|
||||
return parseInput(string(raw)), nil
|
||||
}
|
||||
|
||||
// noDataCause adds the flags that left the generator without data to an error saying none is
|
||||
// loaded.
|
||||
func (in invocation) noDataCause(err error) error {
|
||||
if !in.noShipped || len(in.dirs) > 0 || !strings.HasSuffix(err.Error(), "no data is loaded") {
|
||||
return err
|
||||
}
|
||||
if te, isInput := err.(inputError); isInput {
|
||||
return inputError{fmt.Errorf("%w: --no-shipped-data and no --data-path", te.error)}
|
||||
}
|
||||
return fmt.Errorf("%w: --no-shipped-data and no --data-path", err)
|
||||
}
|
||||
|
||||
func fail(stderr io.Writer, err error) int {
|
||||
var te inputError
|
||||
if errors.As(err, &te) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/grammar"
|
||||
@@ -36,7 +37,7 @@ func (f *Generator) NewTemplate(input string) (*Template, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if err := loadInline(&f.root, n, "template", false); err != nil {
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
return nil, f.noDataNote(fmt.Errorf("fejkdata: %w", err))
|
||||
}
|
||||
return &Template{g: f, n: n}, nil
|
||||
}
|
||||
@@ -68,13 +69,18 @@ func compileInput(input string) (node, error) {
|
||||
return compile(inputValue(input))
|
||||
}
|
||||
|
||||
// inputValue reads an inline template as the value compile takes: the JSON value it holds, the
|
||||
// whitespace around it dropped, or the input itself as a format string when it is not JSON.
|
||||
// inputValue reads an inline template as the value compile takes: the JSON object, array, string
|
||||
// or null it holds, the whitespace around it dropped, or else the input itself as a format string,
|
||||
// a bare number or boolean among them.
|
||||
func inputValue(input string) jsonvalue.Value {
|
||||
raw, err := jsonvalue.Decode([]byte(input))
|
||||
if err != nil {
|
||||
return jsonvalue.String(input)
|
||||
}
|
||||
switch raw.Any().(type) {
|
||||
case json.Number, bool:
|
||||
return jsonvalue.String(input)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
|
||||
@@ -83,10 +83,19 @@ func timeArg(a []string) error {
|
||||
if err := layoutArity("time", 1, a); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := layoutArg(a[0])
|
||||
layout, err := layoutArg(a[0])
|
||||
if err == nil && namesADateField(layout) {
|
||||
return fmt.Errorf("time(layout): '%s' names a date field, and time draws no date; write date(from,to,layout)", layout)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// namesADateField reports a layout that prints two instants differing in their date alone apart.
|
||||
func namesADateField(layout string) bool {
|
||||
day, nextYear := time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC), time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC)
|
||||
return day.Format(layout) != nextYear.Format(layout)
|
||||
}
|
||||
|
||||
// datePrep draws a second in [from 00:00:00, to 23:59:59] UTC; the span is counted
|
||||
// in seconds, since a Duration overflows past 292 years.
|
||||
func datePrep(a []string) Call {
|
||||
|
||||
@@ -25,7 +25,7 @@ func childNamed(n node, seg string) (node, error) {
|
||||
if child, ok := n.children[seg]; ok {
|
||||
return child, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no entry %q", seg)
|
||||
return nil, noEntry{seg}
|
||||
case *template:
|
||||
if child, ok := n.fields[seg]; ok {
|
||||
return child, nil
|
||||
|
||||
@@ -136,7 +136,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
|
||||
f.loadShippedAt(segments)
|
||||
_, n, tail, err := resolveCategory(f.root.children, segments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
|
||||
return nil, f.noDataNote(fmt.Errorf("fejkdata: %s: %w", path, err))
|
||||
}
|
||||
var frames frameStack
|
||||
env := renderEnv{frames: &frames}
|
||||
|
||||
+17
-1
@@ -1,6 +1,7 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -77,7 +78,7 @@ func resolveCategory(root map[string]node, segments []string) (categorySegs []st
|
||||
}
|
||||
n, ok := g.children[segments[i]]
|
||||
if !ok {
|
||||
return nil, nil, nil, fmt.Errorf("no entry %q", segments[i])
|
||||
return nil, nil, nil, noEntry{segments[i]}
|
||||
}
|
||||
return segments[:i+1], n, segments[i+1:], nil
|
||||
}
|
||||
@@ -114,3 +115,18 @@ func refTokens(toks []grammar.Token) []string {
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
// noEntry is a path segment naming nothing in the folder it reads.
|
||||
type noEntry struct{ seg string }
|
||||
|
||||
func (e noEntry) Error() string { return fmt.Sprintf("no entry %q", e.seg) }
|
||||
|
||||
// noDataNote adds to err, where a segment names nothing and f holds no data at all, that no
|
||||
// data is loaded.
|
||||
func (f *Generator) noDataNote(err error) error {
|
||||
var ne noEntry
|
||||
if errors.As(err, &ne) && len(f.root.children) == 0 && len(f.root.unloaded) == 0 {
|
||||
return fmt.Errorf("%w; no data is loaded", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) {
|
||||
f.loadShippedAt(segments)
|
||||
n, pins, err := drawCallerPath(f.drawState, &f.root, segments)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %s: %w", path, err)
|
||||
return "", f.noDataNote(fmt.Errorf("fejkdata: %s: %w", path, err))
|
||||
}
|
||||
if _, ok := n.(*folder); ok {
|
||||
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
|
||||
|
||||
Reference in New Issue
Block a user