Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
+1
-1
@@ -31,7 +31,7 @@ replacement, and each removed path, column or flag.
|
||||
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,
|
||||
`{/users}`, is the record `users` under `--format`. An argument other than a flag, an
|
||||
empty or blank stdin, or nothing piped in, exits 2.
|
||||
empty or blank stdin, one holding only `null`, 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
|
||||
|
||||
@@ -41,8 +41,8 @@ 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
|
||||
an object, array or string, and the whitespace around it is dropped; `null` alone
|
||||
renders nothing, and `42` or `true` alone is refused. A JSON template that should end
|
||||
an object, array or string, and the whitespace around it is dropped; `null`, `42` or
|
||||
`true` alone is refused. 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
|
||||
newline after it allowed. Some shells' `echo` reads a backslash as an escape, so pipe a
|
||||
@@ -65,7 +65,7 @@ template holding one from a quoted heredoc or a file. The rest of stdin's rules
|
||||
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 under `--format`), `2` misuse — a bad flag, an argument
|
||||
other than a flag, an empty, blank or unreadable stdin, nothing piped in, or a
|
||||
other than a flag, an empty, blank, `null` or unreadable stdin, nothing piped in, or a
|
||||
template that does not compile. From a checkout:
|
||||
`go run ./cmd/fejkdata …`.
|
||||
|
||||
@@ -336,7 +336,7 @@ generator:
|
||||
with the same error every call.
|
||||
- `Template.Fake` cannot fail.
|
||||
|
||||
A path, wherever a call takes one, may start with `/`, as a reference does.
|
||||
A path, wherever a call takes one, may start with one `/`, as a reference does.
|
||||
|
||||
A `*Record` carries its columns via `Columns()` — each a `Column` of `Name`,
|
||||
`DataType`, rendered `Value` and `Null` — and serializes them with `JSON()`
|
||||
@@ -409,8 +409,8 @@ are two draws each. Only a [name](#names) keeps one pick for every read of it.
|
||||
|
||||
Renders e.g. `100 Main St, Apt 4B — tel 555-0199`. Random characters come from
|
||||
the sample functions, so a phone pattern is `070-{digits(3)} {digits(2)} {digits(2)}`.
|
||||
A lone `}` is a load error naming `}}`; the arms of `{a|b}` must differ, a
|
||||
repeated arm being a second spelling of [weight](#weight).
|
||||
A lone `}` is a load error naming `}}`. A repeated arm counts as written, so
|
||||
`{a|a|b}` draws `a` two times in three.
|
||||
|
||||
### Weight
|
||||
|
||||
@@ -425,10 +425,10 @@ An item of a choice may carry a `weight` (default `1`) to skew its odds:
|
||||
```
|
||||
|
||||
Renders `070-412 38 91` ten times as often as `08-…`. A string item is weighted by
|
||||
writing it as `{ "format": "AB", "weight": 3 }`. Rejected at load: a weight that
|
||||
is negative, non-numeric, `0` (never drawn — remove the item) or outside a choice,
|
||||
and a repeated item — `["a", "a", "b"]` is `[{ "format": "a", "weight": 2 }, "b"]`,
|
||||
and the error says so.
|
||||
writing it as `{ "format": "AB", "weight": 3 }`. A repeated item counts as written,
|
||||
so `["a", "a", "b"]` draws `a` two times in three. Rejected at load: a weight that is
|
||||
negative, non-numeric or `0` (never drawn — remove the item), and one where it does
|
||||
nothing: outside a choice, or on the item of a one-item choice.
|
||||
|
||||
### Repeat
|
||||
|
||||
@@ -472,9 +472,9 @@ order.id: datatype integer: "1{digits(2)}" is not one value; write one literal o
|
||||
A column whose format is one read of a record's column and nothing else —
|
||||
`"score": "{/src.score}"`, `"score": "{s.score}"` with `s` bound to `/src`, or `"a": "{p}"`
|
||||
beside `{place as p}` in its own record — is that column: it takes the column's datatype and
|
||||
is null where the column is, and a struct field tagged `src.score` is nil there. A `datatype` of its own types the
|
||||
column's values where they prove it, and one restating the datatype it takes is refused.
|
||||
Any other read renders the column's text, a null as `""`.
|
||||
is null where the column is, and a struct field tagged `src.score` is nil there. A
|
||||
`datatype` of its own types the column's values where they prove it, and `"string"`
|
||||
reads them as text. Any other read renders the column's text, a null as `""`.
|
||||
|
||||
A typed column's `{calc()}` must be proven to print a number: each operand a number
|
||||
literal, an `{int()}`, `{float()}`, `{seq()}` or `{digits()}` call, a calc, or a read of
|
||||
|
||||
+16
-12
@@ -45,8 +45,8 @@ 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 an object, array or string, and the whitespace around it is dropped; null
|
||||
alone renders nothing, and 42 or true alone is refused. A quoted heredoc, <<'EOF',
|
||||
template is an object, array or string, and the whitespace around it is dropped; null,
|
||||
42 or true alone is refused. 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
|
||||
@@ -291,13 +291,13 @@ func operandError(arg string) error {
|
||||
if fi, err := os.Stat(arg); err == nil && fi.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s; read a file from stdin: fejkdata < %s", unexpected, shellQuoted(arg))
|
||||
}
|
||||
example := pipeHint
|
||||
example, ref := pipeHint, "{/"+strings.TrimPrefix(arg, "/")+"}"
|
||||
if _, lone := loneReference(arg); lone {
|
||||
example = piped(arg)
|
||||
} else if inline, err := fejkdata.IsTemplate(arg); inline {
|
||||
example = piped(arg)
|
||||
} else if _, lone := loneReference("{/" + arg + "}"); lone && err == nil {
|
||||
example = piped("{/" + arg + "}")
|
||||
} else if _, lone := loneReference(ref); lone && err == nil {
|
||||
example = piped(ref)
|
||||
}
|
||||
return fmt.Errorf("%s; the template is read from stdin: %s", unexpected, example)
|
||||
}
|
||||
@@ -448,24 +448,27 @@ type input struct {
|
||||
record string
|
||||
}
|
||||
|
||||
// parseInput reads stdin. A format string is every byte of stdin; a JSON template drops
|
||||
// the whitespace around it; a lone reference names a record, with one newline ending it
|
||||
// parseInput reads stdin as the template, and as a record where it is a lone reference: JSON
|
||||
// with the whitespace around it dropped, else a format string with one newline ending it
|
||||
// allowed.
|
||||
func parseInput(raw string) (input, error) {
|
||||
trimmed := strings.Trim(raw, jsonSpace)
|
||||
if trimmed == "" {
|
||||
switch trimmed {
|
||||
case "":
|
||||
return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint)
|
||||
case "null":
|
||||
return input{}, fmt.Errorf("stdin holds only null, as jq prints for a missing key: %s", pipeHint)
|
||||
}
|
||||
template := raw
|
||||
text := raw
|
||||
if json.Valid([]byte(trimmed)) {
|
||||
template = trimmed
|
||||
text = trimmed
|
||||
}
|
||||
text, found := strings.CutSuffix(template, "\n")
|
||||
text, found := strings.CutSuffix(text, "\n")
|
||||
if found {
|
||||
text = strings.TrimSuffix(text, "\r")
|
||||
}
|
||||
record, _ := loneReference(text)
|
||||
return input{template: template, record: record}, nil
|
||||
return input{template: raw, record: record}, nil
|
||||
}
|
||||
|
||||
// loneReference returns the path that text reads when it is one reference from the root
|
||||
@@ -485,6 +488,7 @@ func loneReference(text string) (string, bool) {
|
||||
|
||||
// isPath reports whether a reference's body after its / is a path alone: no second /, no
|
||||
// folder sigil, and none of the |, ( or " as " a token reads outside a selector.
|
||||
// It copies the grammar, since the CLI reads only the library's public API.
|
||||
func isPath(body string) bool {
|
||||
if body == "" || strings.ContainsAny(body[:1], "/.") {
|
||||
return false
|
||||
|
||||
+4
-13
@@ -100,7 +100,7 @@ func columnDatatype(n node) DataType {
|
||||
|
||||
// itemDatatype is the datatype a column item declares, else that of the column it reads.
|
||||
func itemDatatype(t *template) DataType {
|
||||
if t.datatype == DataTypeString && t.readsColumn != nil {
|
||||
if !t.declared && t.readsColumn != nil {
|
||||
return columnDatatype(t.readsColumn.column)
|
||||
}
|
||||
return t.datatype
|
||||
@@ -139,7 +139,7 @@ const (
|
||||
|
||||
func kindOf(t *template) itemKind {
|
||||
switch {
|
||||
case t.datatype != DataTypeString:
|
||||
case t.declared:
|
||||
return kindDeclares
|
||||
case itemDatatype(t) != DataTypeString:
|
||||
return kindReads
|
||||
@@ -187,12 +187,12 @@ func (c clash) retypeRead() error {
|
||||
if c.fits() {
|
||||
return fmt.Errorf("%s, so %s", c.held, typedAs(c.fix, c.want))
|
||||
}
|
||||
return fmt.Errorf("%s, so to read %q as text, %s", c.held, c.fix.format, asText(c.fix))
|
||||
return fmt.Errorf("%s, so to read %q as text, %s", c.held, c.fix.format, typedAs(c.fix, DataTypeString))
|
||||
}
|
||||
|
||||
func (c clash) textBesideRead() error {
|
||||
if !c.fits() {
|
||||
return fmt.Errorf(`item %q is not %s, the datatype item %q takes from the column it reads; to read that column as text, %s`, c.fix.format, datatype.Noun(c.want), c.other.format, asText(c.other))
|
||||
return fmt.Errorf(`item %q is not %s, the datatype item %q takes from the column it reads; to read that column as text, %s`, c.fix.format, datatype.Noun(c.want), c.other.format, typedAs(c.other, DataTypeString))
|
||||
}
|
||||
return c.declareText()
|
||||
}
|
||||
@@ -212,12 +212,3 @@ func typedAs(t *template, d DataType) string {
|
||||
}
|
||||
return fmt.Sprintf(`give %q "datatype": %q`, t.format, d)
|
||||
}
|
||||
|
||||
// asText names the spelling that reads the column a column-read item reads as text, keeping the
|
||||
// other keys an object item carries.
|
||||
func asText(t *template) string {
|
||||
if t.fromString {
|
||||
return fmt.Sprintf(`write {"format":"{text}","text":%q}`, t.format)
|
||||
}
|
||||
return fmt.Sprintf(`set its "format" to "{text}" and add "text": %q`, t.format)
|
||||
}
|
||||
|
||||
@@ -61,13 +61,7 @@ func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error)
|
||||
case grammar.NameBind:
|
||||
return nil, checkBind(t, fields)
|
||||
case grammar.PathRead:
|
||||
unbound, err := checkReads(t, fields, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Last, so an arm broken on its own terms is reported as that: a repeat is
|
||||
// the consequence of such a mistake, not the mistake itself.
|
||||
return unbound, checkNoRepeatedArm(t.Body, t.Arms)
|
||||
return checkReads(t, fields, false)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -178,23 +172,6 @@ func tokenReads(t grammar.Token) []string {
|
||||
return t.Arms
|
||||
}
|
||||
|
||||
// checkNoRepeatedArm rejects {a|a|b}: an alternation draws its arms evenly, so a
|
||||
// repeated one is a second spelling of weight. The error names the spelling that
|
||||
// does skew a draw.
|
||||
func checkNoRepeatedArm(body string, names []string) error {
|
||||
if len(names) < 2 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]bool, len(names))
|
||||
for _, name := range names {
|
||||
if seen[name] {
|
||||
return fmt.Errorf("token {%s}: arm %q is repeated; an alternation draws its arms evenly, so skew the odds with a choice's weights instead", body, name)
|
||||
}
|
||||
seen[name] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatOps is a compiled format: its ops, and the size of its literal text.
|
||||
type formatOps struct {
|
||||
ops []op
|
||||
|
||||
@@ -16,12 +16,23 @@ func IsTemplate(arg string) (bool, error) {
|
||||
if strings.HasPrefix(strings.TrimSpace(arg), "[") {
|
||||
return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg)
|
||||
}
|
||||
_, err := SplitPath(CallerPath(arg))
|
||||
path, err := CallerPath(arg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, err = SplitPath(path)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// CallerPath is a path as a caller passed it, without the slashes it may start with.
|
||||
func CallerPath(path string) string { return strings.TrimLeft(path, "/") }
|
||||
// CallerPath is a path as a caller passed it, without the one / it may start with, as a
|
||||
// reference does.
|
||||
func CallerPath(path string) (string, error) {
|
||||
rest := strings.TrimPrefix(path, "/")
|
||||
if strings.HasPrefix(rest, "/") {
|
||||
return "", fmt.Errorf("path %s starts with more than one /; write /%s", path, strings.TrimLeft(rest, "/"))
|
||||
}
|
||||
return rest, nil
|
||||
}
|
||||
|
||||
func isJSONStart(arg string) bool {
|
||||
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
@@ -53,6 +52,7 @@ type template struct {
|
||||
repeat int
|
||||
separator string
|
||||
datatype DataType
|
||||
declared bool // carries a "datatype", which wins over the datatype of a column it reads
|
||||
fromString bool // written as a JSON string rather than an object
|
||||
isRecord bool // compiled at the top without a repeat, so its fields are record columns
|
||||
unbound []unboundRead // the heads its tokens read that no field holds
|
||||
@@ -186,17 +186,11 @@ func compileChoice(items []any, pos position) (node, error) {
|
||||
if pos == inColumn {
|
||||
itemPos = inColumn
|
||||
}
|
||||
if len(items) == 0 {
|
||||
switch len(items) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("empty choice")
|
||||
}
|
||||
if len(items) == 1 {
|
||||
if _, err := weightOf(items[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return compileItem(items[0], pos)
|
||||
}
|
||||
if err := checkNoRepeatedItem(items); err != nil {
|
||||
return nil, err
|
||||
case 1:
|
||||
return compileAt(items[0], pos)
|
||||
}
|
||||
c := &choice{items: make([]node, len(items))}
|
||||
cum := make([]float64, len(items))
|
||||
@@ -228,34 +222,6 @@ func compileChoice(items []any, pos position) (node, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// checkNoRepeatedItem rejects a choice that lists one item twice: a draw is even
|
||||
// over the items, so a repeat is a second spelling of weight. The error names the
|
||||
// spelling that does skew a draw.
|
||||
func checkNoRepeatedItem(items []any) error {
|
||||
seen := make(map[string]int, len(items))
|
||||
for i, raw := range items {
|
||||
key, isString := raw.(string)
|
||||
if !isString {
|
||||
b, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key = "\x00" + string(b)
|
||||
}
|
||||
if j, dup := seen[key]; dup {
|
||||
if s, isString := raw.(string); isString {
|
||||
return fmt.Errorf("choice item %q is repeated; skew the odds with a weight instead: { \"format\": %q, \"weight\": 2 }", s, s)
|
||||
}
|
||||
if raw == nil {
|
||||
return fmt.Errorf("choice item %d repeats null; a null takes no weight, so weight the other items instead", i)
|
||||
}
|
||||
return fmt.Errorf("choice item %d repeats item %d; skew the odds with a weight on one of them instead", i, j)
|
||||
}
|
||||
seen[key] = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileTemplate(m map[string]any, pos position) (node, error) {
|
||||
if _, isTable := m["rows"]; isTable {
|
||||
return nil, fmt.Errorf("rows names a TSV beside a category's file, so only a category is a table; an inline template has no file beside it")
|
||||
@@ -276,12 +242,13 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, isRecord: fieldPos == inColumn, unbound: unbound}, nil
|
||||
return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, declared: o.declared, isRecord: fieldPos == inColumn, unbound: unbound}, nil
|
||||
}
|
||||
|
||||
// templateOptions is what a template object's option keys say.
|
||||
type templateOptions struct {
|
||||
datatype DataType
|
||||
declared bool
|
||||
format string
|
||||
repeat int
|
||||
separator string
|
||||
@@ -299,6 +266,7 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) {
|
||||
return o, err
|
||||
}
|
||||
o.repeat = repeat
|
||||
_, o.declared = m["datatype"]
|
||||
if o.datatype, err = datatypeOf(m, pos); err != nil {
|
||||
return o, err
|
||||
}
|
||||
|
||||
@@ -125,7 +125,10 @@ func literal(c Column, quote func(string) string, nullText string) string {
|
||||
func (f *Generator) FakeRecord(path string) (*Record, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
path = grammar.CallerPath(path)
|
||||
path, err := grammar.CallerPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
segments, err := grammar.SplitPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||
|
||||
@@ -16,7 +16,10 @@ import (
|
||||
func (f *Generator) Fake(path string) (string, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
path = grammar.CallerPath(path)
|
||||
path, err := grammar.CallerPath(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %w", err)
|
||||
}
|
||||
segments, err := grammar.SplitPath(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fejkdata: %w", err)
|
||||
|
||||
+2
-2
@@ -101,10 +101,10 @@ type columnRead struct {
|
||||
}
|
||||
|
||||
// columnReadOf is the record's column t's format reads, where the format only reads one reference
|
||||
// or name, and nil where it does not.
|
||||
// or name, and nil where it does not or t declares a string, which reads the column as text.
|
||||
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
|
||||
ops := t.compiled.ops
|
||||
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
|
||||
if t.declared && t.datatype == DataTypeString || t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
|
||||
return nil
|
||||
}
|
||||
a := ops[0].arms[0]
|
||||
|
||||
@@ -228,7 +228,10 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
|
||||
case inline:
|
||||
return inputValue(tag), nil
|
||||
}
|
||||
path := grammar.CallerPath(tag)
|
||||
path, err := grammar.CallerPath(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := grammar.CheckPathIdentifiers(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-7
@@ -16,17 +16,12 @@ type valueProof struct {
|
||||
columnMemo map[node]proven.Facts
|
||||
}
|
||||
|
||||
// checkDatatype rejects a typed column item some render of which is not text of its datatype,
|
||||
// and one restating the datatype of the column it is.
|
||||
// docs/decisions.md#a-column-that-only-reads-one-reference-or-name-is-the-column-it-reads
|
||||
// checkDatatype rejects a typed column item some render of which is not text of its datatype.
|
||||
func (p *valueProof) checkDatatype(label string, n node) error {
|
||||
t, ok := n.(*template)
|
||||
if !ok || t.datatype == DataTypeString {
|
||||
return nil
|
||||
}
|
||||
if r := t.readsColumn; r != nil && columnDatatype(r.column) == t.datatype {
|
||||
return fmt.Errorf(`%s: %s takes datatype %s from the column it reads; drop "datatype"`, label, t.format, t.datatype)
|
||||
}
|
||||
if reason := p.proveColumnItem(t).Not[t.datatype]; reason != "" {
|
||||
return fmt.Errorf("%s: datatype %s: %s", label, t.datatype, reason)
|
||||
}
|
||||
@@ -38,7 +33,7 @@ func (p *valueProof) checkDatatype(label string, n node) error {
|
||||
func (p *valueProof) checkField(label string, ft reflect.Type, column node) error {
|
||||
items, _ := columnItems(column)
|
||||
for _, it := range items {
|
||||
if it.datatype != DataTypeString {
|
||||
if it.declared {
|
||||
return fmt.Errorf("%s: its Go type %s sets the datatype; drop \"datatype\"", label, ft)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user