diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c983d2..26c3a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,8 +40,6 @@ replacement, and each removed path, column or flag. at `New`: a `name` without a `key` or a `parent`, a name repeating inside one parent row, a name spelling another row's key, and a table named like a column of any table above it. -- `New` refuses a root choice of templates sharing one format and one set of string - fields, naming the rows TSV to write instead. - `misc.territory`, `misc.currency`, `misc.language`, `misc.httpstatus` and `misc.mimetype` are tables. `misc.territory` is every ISO 3166-1 territory that has a capital, a currency and a TLD, with the columns `calling-code`, `capital`, diff --git a/README.md b/README.md index 80a2ed3..3a455f3 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,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 -an object, array or string, and the whitespace around it is dropped; `42`, `true` or -`null` 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 +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 +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 template holding one from a quoted heredoc or a file. The rest of stdin's rules are under @@ -336,6 +336,8 @@ 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 `*Record` carries its columns via `Columns()` — each a `Column` of `Name`, `DataType`, rendered `Value` and `Null` — and serializes them with `JSON()` (one object), `CSVHeader()`/`CSVLine()`, or `SQLInsert(table)` — the shapes the @@ -363,7 +365,7 @@ naming a kind that holds it. The tags are a record's columns, so they share its describe one person, while two path tags into `sv_SE.person` are two draws. The fields an embedded struct promotes are columns of the same record; a named struct field, or a pointer to one, fills from its own tags as a record of its own, so its names and picks are -its own. `fake:"-"` leaves a struct field, embedded or named, or a pointer to one, unfilled. +its own. `fake:"-"` leaves a field unfilled. Untagged fields keep their values, and so does a pointer back to a struct already being filled; a type whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The first call for a type compiles its tags and reports what they get wrong, with the same @@ -424,28 +426,28 @@ 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), `1` (the default) -or outside a choice, and a repeated item — `["a", "a", "b"]` is `[{ "format": "a", "weight": 2 }, "b"]`, +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. ### Repeat -A template may carry `repeat` (an integer above `1`) to render its format that -many times — each an independent draw — joined by `separator` (default `""`): +A template may carry `repeat` (a positive integer, default `1`) to render its format +that many times — each an independent draw — joined by `separator` (default `""`): ```json { "format": "{word}", "repeat": 3, "separator": " ", "word": ["foo", "bar", "baz"] } ``` -Renders e.g. `bar foo baz`. Rejected at load: a `separator` without a `repeat`, -a `separator` of `""` (the default), and a `repeat` that multiplies to more than -1 048 576 renders along any path of nested repeats. +Renders e.g. `bar foo baz`. Rejected at load: a `separator` without a `repeat` above +`1`, and a `repeat` that multiplies to more than 1 048 576 renders along any path of +nested repeats. ### Datatype A record column may declare `datatype` — `integer`, `number` or `boolean` — so `json` -writes `42` rather than `"42"` and `sql` a bare literal; a column without one is a -string: +writes `42` rather than `"42"` and `sql` a bare literal; a column without one, or with +`string`, is a string: ```json { "format": "", @@ -455,10 +457,10 @@ string: ``` Writes e.g. `{"id":1,"paid":true,"total":59.97}`. A column is a field of the top-level -template, or an item of a choice standing in for one; `datatype` anywhere else is a -load error. A typed column holds one value, alone in its format: a literal, one -`{int()}`, `{float()}`, `{seq()}` or `{calc()}` call, or a read that lands only on such -values. `integer` is an int64 written `0|-?[1-9][0-9]*` — `{float()}` prints one at +template, or an item of a choice standing in for one; `datatype` anywhere else, bar +`string`, is a load error. A typed column holds one value, alone in its format: a +literal, one `{int()}`, `{float()}`, `{seq()}` or `{calc()}` call, or a read that lands only on such +values. `integer` is an int64 written `-?(0|[1-9][0-9]*)` — `{float()}` prints one at `0` decimals within int64 — `number` a JSON number, `boolean` `true` or `false`. A value its datatype cannot hold is a load error naming it: @@ -493,9 +495,9 @@ renders a null as `""`. The other items' weights skew its odds: { "format": "", "deleted_at": null, "middle": [null, { "format": "{n}", "n": ["Ann", "Eva"], "weight": 3 }] } ``` -`deleted_at` is null every draw, `middle` a name three draws in four. Rejected at -load: `null` anywhere but a column, naming `""`, and a column whose items hold -different datatypes. +`deleted_at` is null every draw, `middle` a name three draws in four. Anywhere but a +column, `null` is `""`. Rejected at load: a column whose items hold different +datatypes. ### Table @@ -542,10 +544,6 @@ is refused; a name spelling another row's key is refused, since the key would select first and the name never. The table's options are its own — `rows`, `key`, `name`, `weight` and `parent` — so a column may be named `name`, as one usually is. -A choice of templates sharing one format and one set of string fields is a table -written by hand, and `New` refuses it in a data file naming the TSV to write; an -inline template has no file beside it, so there it stays a choice. - ### Row selection `[key]` or `[name]` after a table's name selects one row: `misc.territory[SE]` and @@ -625,15 +623,13 @@ reference, `..` is the folder above ([References](#references)). `format`, `weight`, `repeat`, `separator` and `datatype` are the only options; **any other key is a field** (see [Decisions](docs/decisions.md#options-and-fields-share-one-namespace)), and `rows` makes a category a [table](#table), so no template carries a field of that name. A [name](#names) shares the -namespace too. An object that does nothing a -string can't — only a `format` — is rejected naming the string, as is a one-item -choice naming its item. +namespace too. ### Functions A `{name(args)}` token calls a builtin. Arguments are checked at `New`: a bad -count, range, country or expression fails fast; an integer is written plain -(`5`, not `+5` or `05`); bounds are finite; a sample that could only ever emit one +count, range, country or expression fails fast; an integer may carry a sign or leading +zeros (`+5` and `05` are `5`); bounds are finite; a sample that could only ever emit one value (`int(5,5)`, `float(1,1,2)`) is rejected naming the text to write instead; and a length, count or decimal place beyond a sane maximum is rejected, so a fat-fingered `hex(2000000000)` never tries to allocate gigabytes. Every builtin draws only from the seed — a @@ -778,10 +774,7 @@ Refused at `New`, each error naming what to write instead: one; - a read of a name inside the field bound to it: read it outside that field; - a binding in a choice's item, which every other item would leave unbound; -- a binding nothing reads, or one read only once, unless that read sits in a `repeat` - nested where it is bound: write what it binds there instead, `{/word}` for - `{/word as w}{w}` and `{/region.name}` for `{/region as r}{r.name}`; a calc cannot read a - reference or a path, so a calc's one read of a name bound to either is not refused; +- a binding nothing reads; - a path through a name that selects a row; - `{n}` beside `{n.w}` where `n`'s category reads `w` twice, as in `{w}-{w}` or `{w.a} {w.b}`; diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index af9bfb8..5574195 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -45,9 +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 an object, array or string, and the whitespace around it is dropped; 42, -true or null alone is refused. A quoted heredoc, <<'EOF', passes $, a backslash or a -quote as written. +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', +passes $, a backslash or a quote as written. With --format json, ndjson, csv or sql the template must be a record — a JSON template whose fields are its columns, or a lone reference such as {/sv_SE.person}: @@ -410,7 +410,13 @@ func defaultTable(path string) string { if path == "" { return "records" } - var names strings.Builder // the path with its [selectors] cut out + segments := strings.Split(withoutSelectors(path), ".") + return segments[len(segments)-1] +} + +// withoutSelectors is path with its [selectors] cut out. +func withoutSelectors(path string) string { + var names strings.Builder for depth, i := 0, 0; i < len(path); i++ { switch { case path[i] == '[': @@ -421,8 +427,7 @@ func defaultTable(path string) string { names.WriteByte(path[i]) } } - segments := strings.Split(names.String(), ".") - return segments[len(segments)-1] + return names.String() } // inputError marks a failure that is the input's own fault — no template, one that does @@ -464,25 +469,34 @@ func parseInput(raw string) (input, error) { } // loneReference returns the path that text reads when it is one reference from the root -// and nothing else, bare or as a JSON string: the path IsTemplate advises writing instead. +// and nothing else, bare or as a JSON string. func loneReference(text string) (string, bool) { var unquoted string if strings.HasPrefix(text, `"`) && strings.HasSuffix(text, `"`) && json.Unmarshal([]byte(text), &unquoted) == nil { text = unquoted } - if !strings.HasPrefix(text, "{/") || !strings.HasSuffix(text, "}") { - return "", false - } - if inline, err := fejkdata.IsTemplate(text); inline || err == nil { - return "", false - } - path := text[2 : len(text)-1] - if inline, err := fejkdata.IsTemplate(path); inline || err != nil { + path, opens := strings.CutPrefix(text, "{/") + path, closes := strings.CutSuffix(path, "}") + if !opens || !closes || !isPath(path) { return "", false } return path, true } +// 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. +func isPath(body string) bool { + if body == "" || strings.ContainsAny(body[:1], "/.") { + return false + } + names := withoutSelectors(body) + if strings.ContainsAny(names, "|(") || strings.Contains(names, " as ") { + return false + } + inline, err := fejkdata.IsTemplate(body) + return !inline && err == nil +} + // spacedReference adds to a record refusal that whitespace around a lone reference made // the template text. func spacedReference(template string, err error) error { diff --git a/datatype.go b/datatype.go index 39cb605..9775b18 100644 --- a/datatype.go +++ b/datatype.go @@ -31,7 +31,7 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) { return 0, fmt.Errorf("datatype must be a string, got %T", v) } if name == DataTypeString.String() { - return 0, fmt.Errorf("datatype %q is the default, so it has no effect; drop it", name) + return DataTypeString, nil } for d := DataTypeInteger; d < datatype.Count; d++ { if name != d.String() { @@ -42,7 +42,7 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) { } return d, nil } - return 0, fmt.Errorf(`datatype takes "integer", "number" or "boolean", got %q`, name) + return 0, fmt.Errorf(`datatype takes "string", "integer", "number" or "boolean", got %q`, name) } // checkColumns rejects a record column whose items hold different datatypes, checking a column diff --git a/inline.go b/inline.go index 3de886c..6ee548f 100644 --- a/inline.go +++ b/inline.go @@ -3,7 +3,6 @@ package fejkdata import ( "encoding/json" "fmt" - "strings" "github.com/larvit/fejkdata/internal/grammar" ) @@ -55,9 +54,8 @@ func (f *Generator) FakeTemplate(input string) (string, error) { // IsTemplate reports whether arg is an inline template rather than a path, by its shape: an // arg holding a {, or a valid JSON array or string, is a template, and anything else is a -// path. A path errors where it holds a } or a quote, starts with [, or is no valid path; so -// do a template of one reference alone, which is a path written as a template, and a path -// written with a leading /. +// path. A path errors where it holds a } or a quote, starts with [, or is no valid path. A path +// may start with /, as a reference does, and every call taking a path drops it. func IsTemplate(arg string) (bool, error) { inline, err := grammar.IsTemplate(arg) if err != nil { @@ -67,24 +65,17 @@ func IsTemplate(arg string) (bool, error) { } func compileInput(input string) (node, error) { - v, err := inputValue(input) - if err != nil { - return nil, err - } - return compile(v) + return compile(inputValue(input)) } -// inputValue reads an inline template as the value compile takes: the JSON value it holds, or -// the input itself as a format string when it is not JSON. -func inputValue(input string) (any, error) { +// 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. +func inputValue(input string) any { var raw any if err := json.Unmarshal([]byte(input), &raw); err != nil { - return input, nil + return input } - if trimmed := strings.TrimSpace(input); trimmed != input { - return nil, fmt.Errorf("a JSON template may not be padded with spaces, which a format string would render; write %s", trimmed) - } - return raw, nil + return raw } // loadInline loads the shipped categories n reads, then runs n through the pipeline against root. diff --git a/internal/builtinfunc/builtinfunc.go b/internal/builtinfunc/builtinfunc.go index ea206c0..27ea21d 100644 --- a/internal/builtinfunc/builtinfunc.go +++ b/internal/builtinfunc/builtinfunc.go @@ -28,8 +28,6 @@ type builtin struct { // operands names the fields the call reads, which a render reads for it; nil // for a builtin that reads none. operands func(args []string) []string - // noRefOperands says no operand can be a reference. - noRefOperands bool // proveNumber bounds the number a call's text reads as, token its body, and says which // datatypes that text is not; set it where every render reads as a finite number, which // makes the call a calc operand, and leave it nil otherwise. @@ -79,9 +77,6 @@ func Operands(name string, args []string) []string { return b.operands(args) } -// NoRefOperands reports a builtin no operand of which can be a reference. -func NoRefOperands(name string) bool { return builtins[name].noRefOperands } - // IsTransform reports a builtin that rewrites the text of its one operand. func IsTransform(name string) bool { _, isTransform := transforms[name] diff --git a/internal/builtinfunc/builtinfunc_test.go b/internal/builtinfunc/builtinfunc_test.go index a9afc0c..1b6080b 100644 --- a/internal/builtinfunc/builtinfunc_test.go +++ b/internal/builtinfunc/builtinfunc_test.go @@ -2,11 +2,9 @@ package builtinfunc import ( "fmt" - "slices" "strings" "testing" - "github.com/larvit/fejkdata/internal/grammar" "github.com/larvit/fejkdata/internal/invariant" ) @@ -23,17 +21,6 @@ func TestRegistryShapes(t *testing.T) { } } -func TestNoRefOperandsMatchesTheOperandGrammar(t *testing.T) { - for name, b := range builtins { - if b.operands == nil { - continue - } - if takesRef := slices.ContainsFunc(b.operands([]string{"/x"}), grammar.IsRef); b.noRefOperands == takesRef { - t.Errorf("%s: noRefOperands = %v, yet a reference /x read as an operand: %v", name, b.noRefOperands, takesRef) - } - } -} - func TestCheckNamesWhatACallGetsWrong(t *testing.T) { for _, c := range []struct { name string diff --git a/internal/builtinfunc/builtins.go b/internal/builtinfunc/builtins.go index c2cb05f..aac40ed 100644 --- a/internal/builtinfunc/builtins.go +++ b/internal/builtinfunc/builtins.go @@ -66,7 +66,7 @@ var builtins = withTransforms(map[string]builtin{ }}, "date": {arity: -1, checkArgs: dateArgs, prep: datePrep}, "time": {arity: -1, checkArgs: timeArg, prep: timePrep}, - CalcName: {arity: -1, checkArgs: checkCalc, prep: calcPrep, operands: calcOperands, noRefOperands: true}, + CalcName: {arity: -1, checkArgs: checkCalc, prep: calcPrep, operands: calcOperands}, // seq is the one stateful builtin: a per-generator counter from 1, advancing on // each call. An optional name selects an independent counter; no name uses the // default one. Deterministic by construction, so seeded output stays stable. @@ -148,8 +148,7 @@ func randChars(r rng, n int, alphabet string) string { return string(b) } -// plainInt parses an integer arg written the one way: no sign, no leading zero. -func plainInt(s string) (int, error) { +func intArg(s string) (int, error) { n, err := strconv.Atoi(s) if errors.Is(err, strconv.ErrRange) { return 0, fmt.Errorf("%q is past the integer range: %w", s, err) @@ -157,13 +156,10 @@ func plainInt(s string) (int, error) { if err != nil { return 0, fmt.Errorf("%q is not an integer", s) } - if strconv.Itoa(n) != s { - return 0, fmt.Errorf("%q is not a plain integer; write %d", s, n) - } return n, nil } func posIntArg(a []string) error { - n, err := plainInt(a[0]) + n, err := intArg(a[0]) if errors.Is(err, strconv.ErrRange) { return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen) } @@ -179,11 +175,11 @@ func posIntArg(a []string) error { return nil } func intRangeArgs(a []string) error { - lo, err := plainInt(a[0]) + lo, err := intArg(a[0]) if err != nil { return fmt.Errorf("int(min,max): min %w", err) } - hi, err := plainInt(a[1]) + hi, err := intArg(a[1]) if err != nil { return fmt.Errorf("int(min,max): max %w", err) } @@ -204,7 +200,7 @@ func floatArgs(a []string) error { if e1 != nil || e2 != nil { return fmt.Errorf("float(min,max,dp) needs numeric bounds, got %q,%q", a[0], a[1]) } - dp, err := plainInt(a[2]) + dp, err := intArg(a[2]) if err != nil { return fmt.Errorf("float(min,max,dp): decimals %w", err) } diff --git a/internal/builtinfunc/calc.go b/internal/builtinfunc/calc.go index c5e2e83..8105238 100644 --- a/internal/builtinfunc/calc.go +++ b/internal/builtinfunc/calc.go @@ -56,7 +56,7 @@ func checkCalc(args []string) error { return fmt.Errorf("calc(%q): %w", args[0], err) } if len(args) == 2 { - dp, err := plainInt(args[1]) + dp, err := intArg(args[1]) if err != nil { return fmt.Errorf("calc decimals %w", err) } diff --git a/internal/grammar/template.go b/internal/grammar/template.go index 444230d..5d1e7b7 100644 --- a/internal/grammar/template.go +++ b/internal/grammar/template.go @@ -8,9 +8,6 @@ import ( func IsTemplate(arg string) (bool, error) { if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) { - if path, lone := loneReference(arg); lone { - return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path)) - } return true, nil } if i := strings.IndexAny(arg, `}"`); i >= 0 { @@ -19,60 +16,13 @@ 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) } - if _, err := SplitPath(arg); err != nil { - return false, err - } - if path := strings.TrimLeft(arg, "/"); path != arg && path != "" { - return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg)) - } - return false, nil + _, err := SplitPath(CallerPath(arg)) + 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, "/") } + func isJSONStart(arg string) bool { return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`) } - -// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it. -func pathAdvice(path, refusal string) error { - if err := CheckPathIdentifiers(path); err != nil { - return err - } - return fmt.Errorf("%s; write %s", refusal, path) -} - -// loneReference is the path a template spells when the value it holds — a format string, a -// JSON string, or an object holding only a format — is one reference token and nothing else. -func loneReference(arg string) (string, bool) { - var raw any - if json.Unmarshal([]byte(arg), &raw) != nil { - raw = arg - } - if m, isObject := raw.(map[string]any); isObject && len(m) == 1 { - raw = m["format"] - } - format, isString := raw.(string) - if !isString { - return "", false - } - toks, err := ParseFormat(format) - if err != nil { - return "", false - } - body, lone := loneRef(toks) - if !lone { - return "", false - } - if strings.HasPrefix(body, "/") { - body = "/" + strings.TrimLeft(body, "/") - } - _, path, err := RefShape(body) - return path, err == nil -} - -// loneRef is the reference a format of one reference token and nothing else reads. -func loneRef(toks []Token) (string, bool) { - if len(toks) != 1 || toks[0].Kind != PathRead || len(toks[0].Arms) != 1 || !IsRef(toks[0].Arms[0]) { - return "", false - } - return toks[0].Arms[0], true -} diff --git a/internal/proven/proven.go b/internal/proven/proven.go index 2006f39..d05f3eb 100644 --- a/internal/proven/proven.go +++ b/internal/proven/proven.go @@ -192,19 +192,5 @@ func Literal(text string) Facts { if text != "true" && text != "false" { v.Not[datatype.Boolean] = fmt.Sprintf("%q is not a boolean", text) } - return signedZero(text, v) -} - -// signedZero refuses a zero written with a sign as a typed value, naming it unsigned. -func signedZero(text string, v Facts) Facts { - mantissa, _, _ := strings.Cut(strings.ToLower(text), "e") - if !strings.HasPrefix(text, "-") || v.NotOperand != "" || strings.Trim(mantissa, "-0.") != "" { - return v - } - for _, d := range []datatype.DataType{datatype.Integer, datatype.Number} { - if v.Not[d] == "" { - v.Not[d] = fmt.Sprintf("%q is zero written with a sign; write %q", text, text[1:]) - } - } return v } diff --git a/internal/rows/rows.go b/internal/rows/rows.go index 0b94edf..a3a08b2 100644 --- a/internal/rows/rows.go +++ b/internal/rows/rows.go @@ -103,7 +103,7 @@ func (t *Table[O]) parseRows(data string) error { t.col[name] = i } if header == data { - return fmt.Errorf("has no rows below its header; a table is at least two rows") + return fmt.Errorf("has no rows below its header; a table is at least one row") } m := len(t.header) t.cells = make([]string, 0, m*(strings.Count(rest, "\n")+1)) @@ -118,9 +118,6 @@ func (t *Table[O]) parseRows(data string) error { } rest = more } - if t.Len() < 2 { - return fmt.Errorf("has one row, which is a template; write it as one") - } return nil } @@ -141,13 +138,10 @@ func (t *Table[O]) appendRow(row string, line int) error { return nil } -// bindOptions resolves each option to its column, refuses a name option naming the key column, and -// proves what the options claim of the cells: a key is unique, a weight a positive number. +// bindOptions resolves each option to its column, and proves what the options claim of the cells: a +// key is unique, a weight a positive number. func (t *Table[O]) bindOptions() error { o := t.options - if o.Key != "" && o.Key == o.Name { - return fmt.Errorf("name names the key column %q, which a selector already reads; drop it", o.Name) - } for _, opt := range []struct { name, value string into *int diff --git a/name.go b/name.go index d7c2b25..8c53d06 100644 --- a/name.go +++ b/name.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/grammar" ) @@ -37,15 +36,6 @@ type nameBinding struct { addressed map[pickKey]string } -// nameUse is one read of a name: the path it reads into the name, "" for the name itself; the -// template reading it; whether a builtin reads it as an operand, and whether no reference can -// stand there; and whether it sits in a repeat nested inside the name's scope. -type nameUse struct { - tail string - in *template - operand, noRef, nested bool -} - // nameTarget is what a binding resolves to in the assembled tree: start, the node its head names, and // the path it reads into that node. type nameTarget struct { @@ -239,12 +229,10 @@ func (t *template) isName(head string) bool { return !grammar.IsRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil } -// namedReadAt is one read of a name in a format: the op holding it, the compiled arm, and whether a -// builtin reads it as an operand. +// namedReadAt is one read of a name in a format: the op holding it, and the compiled arm. type namedReadAt struct { - o *op - a *arm - operand bool + o *op + a *arm } // namedReads is every read of a name t's format makes, in format order. @@ -259,7 +247,7 @@ func namedReads(t *template) []namedReadAt { } for j := range o.operands { if o.operands[j].kind == namedRead { - out = append(out, namedReadAt{o: o, a: &o.operands[j], operand: true}) + out = append(out, namedReadAt{o: o, a: &o.operands[j]}) } } } @@ -299,20 +287,3 @@ func addressedKeys(ts []templateSite, targets map[*nameBinding]nameTarget) map[* } return keys } - -func nameUses(ts []templateSite) map[*nameBinding][]nameUse { - uses := map[*nameBinding][]nameUse{} - for _, s := range ts { - for _, r := range namedReads(s.t) { - b := r.a.named - uses[b] = append(uses[b], nameUse{ - tail: grammar.JoinSegments(r.a.tail), - in: s.t, - operand: r.operand, - noRef: r.operand && builtinfunc.NoRefOperands(r.o.Fn), - nested: s.t.nameScope != b.scope, - }) - } - } - return uses -} diff --git a/namefence.go b/namefence.go index a22151b..6c18317 100644 --- a/namefence.go +++ b/namefence.go @@ -3,24 +3,13 @@ package fejkdata import ( "fmt" "slices" - "strings" "github.com/larvit/fejkdata/internal/grammar" - "github.com/larvit/fejkdata/internal/invariant" ) -// checkNameReads refuses each binding of t that refuseSingleRead refuses, a read of a name inside -// the field bound to it, and a read {n} beside {n.w} where n's pick renders w twice. +// checkNameReads refuses a read of a name inside the field bound to it, and a read {n} beside {n.w} +// where n's pick renders w twice. func checkNameReads(label string, t *template, names resolvedNames) error { - for _, tok := range t.tokens { - if tok.Kind != grammar.NameBind { - continue - } - b := t.nameScope.bindings[tok.Bound] - if err := b.refuseSingleRead(names.uses[b]); err != nil { - return fmt.Errorf("%s: token {%s}: %w", label, tok.Body, err) - } - } for _, r := range namedReads(t) { b, target := r.a.named, names.targets[r.a.named] if b.bindsField() && rendersInside(compilePath(target.start, target.tail).leaves, t) { @@ -62,66 +51,6 @@ func rendersInside(nodes []node, t *template) bool { return false } -// refuseSingleRead refuses b when its one read sits in b's own scope, not in a repeat nested inside -// it, at a spot where the bound spelling could stand. That spelling draws the same without the -// name: write {/word} for {/word as w}{w}. A bound field can stand in only where the reading -// template reaches the binder through fields. A single read as a calc operand stands where a calc -// could not read the spelling, a reference or a path. So {/misc.coordinate.lat as lat}{calc(lat * 60)} -// stands, since {calc(/misc.coordinate.lat * 60)} does not compile. -// A whole struct tag of that spelling is refused, naming the bare path. -func (b *nameBinding) refuseSingleRead(uses []nameUse) error { - if len(uses) == 0 { - panic(invariant.Broken("name %q has no read at resolve, though its compile found one", b.name)) - } - r := uses[0] - if len(uses) > 1 || r.nested { - return nil - } - spelling := b.ref - if b.bindsField() { - down, reaches := fieldPathTo(r.in, b.binder) - if !reaches { - return nil - } - spelling = grammar.JoinSegments(append(down, b.ref)) - } - switch { - case strings.HasPrefix(r.tail, ".."): - spelling += r.tail - case r.tail != "": - spelling += "." + r.tail - } - if r.noRef && !calcReads(spelling) { - return nil - } - if !r.operand { - spelling = "{" + spelling + "}" - } - return fmt.Errorf("name %q is read once, so it keeps no pick for another read; write %s where it is read, and drop the token", b.name, spelling) -} - -// fieldPathTo is the fields leading from t down to the template target, if t reaches it so. -func fieldPathTo(t, target *template) ([]string, bool) { - if t == target { - return nil, true - } - for _, name := range sortedNames(t.fields) { - if sub, isTemplate := t.fields[name].(*template); isTemplate { - if rest, reaches := fieldPathTo(sub, target); reaches { - return append([]string{name}, rest...), true - } - } - } - return nil, false -} - -// calcReads reports whether a calc reads spelling as one operand. -func calcReads(spelling string) bool { - c, err := grammar.ParseCalc(spelling) - v, isVar := c.Expr.(grammar.CalcVar) - return err == nil && isVar && v.Name == spelling -} - // refuseTwiceDrawn refuses read, a read of a name that lands on n, where n renders a field twice and // another read of the name addresses that field. The pick keeps one draw of the field: where n binds // a category whose format is {w}-{w}, a {n.w} beside {n} cannot say which draw it reads. key is n's diff --git a/node.go b/node.go index 9cf6976..ae255c6 100644 --- a/node.go +++ b/node.go @@ -4,9 +4,6 @@ import ( "encoding/json" "fmt" "math" - "slices" - "sort" - "strings" "github.com/larvit/fejkdata/internal/datafiles" "github.com/larvit/fejkdata/internal/grammar" @@ -99,70 +96,16 @@ func compile(v any) (node, error) { } // compileCategory compiles a data file's value, which may be a table over a rows -// file beside it, and refuses a choice that is a table written as templates. +// file beside it. func compileCategory(c datafiles.Category) (node, error) { if m, ok := c.JSON.(map[string]any); ok { if _, isTable := m["rows"]; isTable { return compileTable(m, c.Folders, c.Name, c.ReadRows) } } - if items, ok := c.JSON.([]any); ok { - if err := checkNotRows(items, c.Name); err != nil { - return nil, err - } - } return compile(c.JSON) } -// checkNotRows refuses a choice of templates sharing one format and one set of -// string fields: each item is a row, and the rows file is the spelling for that. -// docs/decisions.md#the-choice-of-rows-fence-guards-a-data-files-root-and-requires-string-fields -func checkNotRows(items []any, name string) error { - var format string - var fields []string - weighted := false - for i, raw := range items { - f, keys, w, isRow := rowShape(raw) - if !isRow || i > 0 && (f != format || !slices.Equal(keys, fields)) { - return nil - } - format, fields, weighted = f, keys, weighted || w - } - if len(items) < 2 || len(fields) == 0 { - return nil - } - weight := "" - if weighted { - weight = `, a weight column, and "weight" naming it` - } - return fmt.Errorf("a choice of %d templates with one format and the fields %v is a table; write the rows in %s.tsv with the header %q%s, and the category as {\"format\": %q, \"rows\": \"%s.tsv\"}", - len(items), fields, name, strings.Join(fields, "\t"), weight, format, name) -} - -// rowShape reads a choice item as a row: a template whose every field is a string, -// with its format, its sorted field names, and whether it carries a weight. -func rowShape(raw any) (format string, fields []string, weighted, isRow bool) { - m, ok := raw.(map[string]any) - if !ok { - return "", nil, false, false - } - format, _ = m["format"].(string) - for k, v := range m { - if k == "weight" { - weighted = true - continue - } - if _, isString := v.(string); !isString || isOption(k) && k != "format" { - return "", nil, false, false - } - if k != "format" { - fields = append(fields, k) - } - } - sort.Strings(fields) - return format, fields, weighted, true -} - // position is where a JSON value sits, which decides whether it may carry a datatype or // be null. type position int @@ -199,7 +142,7 @@ func compileItem(v any, pos position) (node, error) { return compileTemplate(v, pos) case nil: if pos != inColumn { - return nil, fmt.Errorf(`null is a record column's value; here it only renders "", so write ""`) + return compileString("") } return &nullItem{}, nil default: @@ -247,7 +190,10 @@ func compileChoice(items []any, pos position) (node, error) { return nil, fmt.Errorf("empty choice") } if len(items) == 1 { - return nil, fmt.Errorf("a one-item choice is its item; write the item") + if _, err := weightOf(items[0]); err != nil { + return nil, err + } + return compileItem(items[0], pos) } if err := checkNoRepeatedItem(items); err != nil { return nil, err @@ -326,9 +272,6 @@ func compileTemplate(m map[string]any, pos position) (node, error) { if err != nil { return nil, err } - if len(fields) == 0 && o.repeat == 1 && !o.weighted && o.datatype == DataTypeString { - return nil, fmt.Errorf("an object holding only a format is a string; write %q", o.format) - } toks, unbound, err := parseChecked(o.format, fields) if err != nil { return nil, err @@ -342,7 +285,6 @@ type templateOptions struct { format string repeat int separator string - weighted bool } func readOptions(m map[string]any, pos position) (templateOptions, error) { @@ -367,11 +309,7 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) { if repeat == 1 { return o, fmt.Errorf("separator joins repeated renders, so it has no effect without a repeat above 1") } - if o.separator == "" { - return o, fmt.Errorf("separator \"\" is the default, so it has no effect; drop it") - } } - _, o.weighted = m["weight"] return o, nil } @@ -394,7 +332,7 @@ func compileFields(m map[string]any, pos position) (map[string]node, error) { } // repeatOf reads a template's "repeat" (default 1): how many times its format -// is rendered and concatenated. A present one must be an integer above 1. +// is rendered and concatenated. A present one must be a positive integer. func repeatOf(m map[string]any) (int, error) { rv, ok := m["repeat"] if !ok { @@ -407,9 +345,6 @@ func repeatOf(m map[string]any) (int, error) { if math.IsNaN(r) || math.IsInf(r, 0) || r < 1 || r != math.Trunc(r) { return 0, fmt.Errorf("repeat must be a positive integer, got %v", rv) } - if r == 1 { - return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it") - } if r > MaxRepeat { // caps the renders one repeat asks for; repeatCheck bounds what nested ones multiply to return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, MaxRepeat) } @@ -437,9 +372,6 @@ func weightOf(raw any) (float64, error) { if w == 0 { return 0, fmt.Errorf("weight 0 means the item is never drawn; remove the item instead") } - if w == 1 { - return 0, fmt.Errorf("weight 1 is the default, so it has no effect; drop it") - } return w, nil } diff --git a/record.go b/record.go index 5623b50..3b321ae 100644 --- a/record.go +++ b/record.go @@ -125,6 +125,7 @@ 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) segments, err := grammar.SplitPath(path) if err != nil { return nil, fmt.Errorf("fejkdata: %w", err) diff --git a/render.go b/render.go index 077224d..b203c67 100644 --- a/render.go +++ b/render.go @@ -16,6 +16,7 @@ import ( func (f *Generator) Fake(path string) (string, error) { f.mu.Lock() defer f.mu.Unlock() + path = grammar.CallerPath(path) segments, err := grammar.SplitPath(path) if err != nil { return "", fmt.Errorf("fejkdata: %w", err) diff --git a/resolve.go b/resolve.go index f89daf7..7787f3b 100644 --- a/resolve.go +++ b/resolve.go @@ -45,11 +45,10 @@ func resolveInlineTemplates(nodes nodeSet, root map[string]node) error { } // resolvedNames is what resolveTemplates resolved about the names of a set of templates: each binding's -// target, the keys its reads address, and its reads. +// target, and the keys its reads address. type resolvedNames struct { targets map[*nameBinding]nameTarget addressed map[*nameBinding]map[pickKey]string - uses map[*nameBinding][]nameUse } // resolveTemplates resolves ts in steps, each over every template before the next starts. The @@ -71,7 +70,7 @@ func resolveTemplates(ts []templateSite, root map[string]node) error { } s.t.compiled = compiled } - names := resolvedNames{targets: targets, addressed: addressedKeys(ts, targets), uses: nameUses(ts)} + names := resolvedNames{targets: targets, addressed: addressedKeys(ts, targets)} for _, s := range ts { s.t.readsColumn = columnReadOf(s.t, targets) } diff --git a/struct.go b/struct.go index 6b5e3d5..a323ad2 100644 --- a/struct.go +++ b/struct.go @@ -144,9 +144,6 @@ func (c *structFields) gatherField(sf reflect.StructField) error { elem := structOf(sf.Type) switch { case tagged && tag == "-": - if elem == nil { - return fmt.Errorf(`%s.%s: fake:"-" leaves a struct field unfilled, and any other untagged field keeps its value already; drop the tag`, c.label, sf.Name) - } return nil case tagged: return c.addTag(sf, tag) @@ -229,12 +226,13 @@ func tagValue(sf reflect.StructField, tag string) (any, error) { case err != nil: return nil, err case inline: - return inputValue(tag) + return inputValue(tag), nil } - if err := grammar.CheckPathIdentifiers(tag); err != nil { + path := grammar.CallerPath(tag) + if err := grammar.CheckPathIdentifiers(path); err != nil { return nil, err } - return "{/" + tag + "}", nil + return "{/" + path + "}", nil } // checkTaggedType rejects a tagged field no column can fill.