From 93e17c1770af3d7c95ef9161ab7d061db679ca58 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 18 Sep 2026 19:52:50 +0200 Subject: [PATCH] Name a runnable layout, path and record in the errors that reject one, and let a date span one day --- builtins.go | 43 +++++++++++++++++++++++++++++++++++-------- record.go | 10 +++++++++- table.go | 22 +++++++++++++++++----- template.go | 2 +- 4 files changed, 62 insertions(+), 15 deletions(-) diff --git a/builtins.go b/builtins.go index 6a83172..2eb8041 100644 --- a/builtins.go +++ b/builtins.go @@ -472,17 +472,28 @@ func iban(r rng, cc string) string { const dayLayout = "2006-01-02" -// Alike in no field; layoutDay differs from layoutProbe in its date fields only. +// Alike in no field; layoutDay differs from layoutProbe in its date fields only, +// layoutClock in its clock fields only. var ( layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC) layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC) layoutDay = time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC) + layoutClock = time.Date(2001, 2, 3, 13, 14, 15, 0, time.UTC) ) +// quotedLayout reports whether an arg carries the single quotes a layout is written in. +func quotedLayout(a string) bool { + return len(a) >= 2 && a[0] == '\'' && a[len(a)-1] == '\'' +} + // layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant. func layoutArg(a string) (string, error) { - if len(a) < 2 || a[0] != '\'' || a[len(a)-1] != '\'' { - return "", fmt.Errorf("layout %s is not quoted; write '%s'", a, strings.Trim(a, "'")) + if !quotedLayout(a) { + bare := strings.Trim(a, `'"`) + if strings.HasPrefix(a, `"`) || strings.HasSuffix(a, `"`) { + return "", fmt.Errorf("layout %s is double-quoted; write '%s'", a, bare) + } + return "", fmt.Errorf("layout %s is not quoted; write '%s', which a shell keeps only inside a double-quoted argument", a, bare) } layout := a[1 : len(a)-1] if layoutProbe.Format(layout) == layoutProbe2.Format(layout) { @@ -501,18 +512,31 @@ func layoutOf(a string) string { } // layoutArity checks a call ending in a layout takes n args, naming the quoted -// layout when an unquoted one split into more. +// layout where an unquoted one split into more. func layoutArity(name string, n int, a []string) error { if len(a) == n { return nil } hint := "" - if len(a) > n { + if len(a) > n && !split(a[n-1:]) { hint = fmt.Sprintf("; a layout holding a comma is quoted: '%s'", strings.Join(a[n-1:], ", ")) } - return fmt.Errorf("%s takes %d args, got %d%s", name, n, len(a), hint) + return fmt.Errorf("%s takes %d argument%s, got %d%s", name, n, plural(n), len(a), hint) } +// split reports whether the surplus args already hold a quoted layout, which no +// comma split apart. +func split(a []string) bool { + for _, arg := range a { + if quotedLayout(arg) { + return true + } + } + return false +} + +func plural(n int) string { return map[bool]string{true: "s"}[n != 1] } + func dateArgs(_ map[string]node, a []string) error { if err := layoutArity("date", 3, a); err != nil { return err @@ -525,8 +549,8 @@ func dateArgs(_ map[string]node, a []string) error { if err != nil { return fmt.Errorf("date(from,to,layout): to %q is not a YYYY-MM-DD date", a[1]) } - if !from.Before(to) { - return fmt.Errorf("date(from,to,layout): from %s is not before to %s", a[0], a[1]) + if to.Before(from) { + return fmt.Errorf("date(from,to,layout): from %s is after to %s", a[0], a[1]) } layout, err := layoutArg(a[2]) if err != nil { @@ -535,6 +559,9 @@ func dateArgs(_ map[string]node, a []string) error { if layoutProbe.Format(layout) == layoutDay.Format(layout) { return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, layout) } + if from.Equal(to) && layoutProbe.Format(layout) == layoutClock.Format(layout) { + return fmt.Errorf("date(%s,%s,'%s') is the constant %q; write it as text", a[0], a[1], layout, from.Format(layout)) + } return nil } diff --git a/record.go b/record.go index 97514af..464dcb1 100644 --- a/record.go +++ b/record.go @@ -140,6 +140,10 @@ func (f *Generator) FakeRecord(path string) (*Record, error) { return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0]) } shape := f.recordShapeOf(n) + if errors.Is(shape.err, errNoColumns) { + column := path[strings.LastIndex(path, ".")+1:] + return nil, fmt.Errorf("fejkdata: %s %w; render it as a column of one: '{\"format\":\"\",\"%s\":\"{/%s}\"}'", path, shape.err, column, path) + } if shape.err != nil { return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err) } @@ -227,6 +231,10 @@ func (f *Generator) FakeRecordTemplate(input string) (*Record, error) { return t.Fake(), nil } +// errNoColumns is the one record fence a path can answer, so its entry point names +// the record to write instead. +var errNoColumns = errors.New("has no fields, so no columns") + // recordOf is the fence both record entry points pass. The columns come back with // the template, fixed for every draw the caller goes on to make. func recordOf(n node) (*template, []Column, error) { @@ -239,7 +247,7 @@ func recordOf(n node) (*template, []Column, error) { } names := recordColumns(t) if len(names) == 0 { - return nil, nil, errors.New("has no fields, so no columns") + return nil, nil, errNoColumns } if !t.record { return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat) diff --git a/table.go b/table.go index 3aae198..6b34865 100644 --- a/table.go +++ b/table.go @@ -14,6 +14,7 @@ import ( // cell of the row a render pinned; a cell carrying tokens compiles to a string node. type table struct { category string + path string // the category's path from the data root, set when the tables are linked file string format *template // fields are the column nodes columns []string @@ -350,6 +351,7 @@ func linkTables(root map[string]node) error { return err } case *table: + n.path = path if n.parent < 0 { continue } @@ -508,7 +510,7 @@ func (t *table) under(r int, a *table, pr int) bool { // naming several rows resolves inside the ancestors pinned in d. func (t *table) find(sel string, d *draws) (int, error) { if t.key < 0 && t.name < 0 { - return 0, fmt.Errorf("%s has no key or name column to select a row by", t.category) + return 0, fmt.Errorf("%s has no key or name column to select a row by", t.spelledPath()) } if r, ok := t.byKey[sel]; ok { return r, nil @@ -521,7 +523,7 @@ func (t *table) find(sel string, d *draws) (int, error) { case 1: return rows[0], nil case 0: - return 0, fmt.Errorf("no row of %s has key or name %q", t.category, sel) + return 0, fmt.Errorf("no row of %s has key or name %q", t.spelledPath(), sel) } keys := make([]string, len(rows)) for i, r := range rows { @@ -531,14 +533,24 @@ func (t *table) find(sel string, d *draws) (int, error) { keys[i] = t.cell(r, t.key) } } + listed := strings.Join(keys, ", ") if t.key < 0 { - return 0, fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %v", sel, len(rows), t.category, t.parentT.category, keys) + return 0, fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %s", sel, len(rows), t.spelledPath(), t.parentT.spelledPath(), listed) } inside := "" if t.parentT != nil { inside = fmt.Sprintf(", or select it inside its %s", t.parentT.category) } - return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %v%s", sel, len(rows), t.category, keys, inside) + return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %s%s", sel, len(rows), t.spelledPath(), listed, inside) +} + +// spelledPath is the path a selector on t is written at, the category's own name +// until the tables are linked. +func (t *table) spelledPath() string { + if t.path == "" { + return t.category + } + return t.path } // selectorSpelling is how a path writes a selected row, for messages: by key, or @@ -546,7 +558,7 @@ func (t *table) find(sel string, d *draws) (int, error) { func (t *table) selectorSpelling(r int) string { switch { case t.key >= 0: - return t.category + "[" + t.cell(r, t.key) + "]" + return t.spelledPath() + "[" + t.cell(r, t.key) + "]" case t.name >= 0: return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.category + "[" + t.cell(r, t.name) + "]" } diff --git a/template.go b/template.go index d2ce7d2..cc8ea2a 100644 --- a/template.go +++ b/template.go @@ -125,7 +125,7 @@ func checkFunc(body string, fields map[string]node) error { return fmt.Errorf("token {%s}: unknown function %q", body, name) } if b.arity >= 0 && len(args) != b.arity { - return fmt.Errorf("token {%s}: %s takes %d args, got %d", body, name, b.arity, len(args)) + return fmt.Errorf("token {%s}: %s takes %d argument%s, got %d", body, name, b.arity, plural(b.arity), len(args)) } if b.check != nil { if err := b.check(fields, args); err != nil {