Give every table its path in one pass, name it in every message, read the layout probes through predicates, and leave the shell to the CLI
This commit is contained in:
+24
-13
@@ -472,8 +472,9 @@ func iban(r rng, cc string) string {
|
|||||||
|
|
||||||
const dayLayout = "2006-01-02"
|
const dayLayout = "2006-01-02"
|
||||||
|
|
||||||
// Alike in no field; layoutDay differs from layoutProbe in its date fields only,
|
// The instants a layout is proved against: layoutProbe2 is alike in no field, while
|
||||||
// layoutClock in its clock fields only.
|
// layoutDay differs from layoutProbe in its date fields alone and layoutClock in its
|
||||||
|
// clock fields alone, so formatting two of them tells which kind a layout names.
|
||||||
var (
|
var (
|
||||||
layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
|
layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
|
||||||
layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC)
|
layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC)
|
||||||
@@ -481,6 +482,18 @@ var (
|
|||||||
layoutClock = time.Date(2001, 2, 3, 13, 14, 15, 0, time.UTC)
|
layoutClock = time.Date(2001, 2, 3, 13, 14, 15, 0, time.UTC)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func namesAField(layout string) bool {
|
||||||
|
return layoutProbe.Format(layout) != layoutProbe2.Format(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func namesADateField(layout string) bool {
|
||||||
|
return layoutProbe.Format(layout) != layoutDay.Format(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func namesAClockField(layout string) bool {
|
||||||
|
return layoutProbe.Format(layout) != layoutClock.Format(layout)
|
||||||
|
}
|
||||||
|
|
||||||
// quotedLayout reports whether an arg carries the single quotes a layout is written in.
|
// quotedLayout reports whether an arg carries the single quotes a layout is written in.
|
||||||
func quotedLayout(a string) bool {
|
func quotedLayout(a string) bool {
|
||||||
return len(a) >= 2 && a[0] == '\'' && a[len(a)-1] == '\''
|
return len(a) >= 2 && a[0] == '\'' && a[len(a)-1] == '\''
|
||||||
@@ -493,10 +506,10 @@ func layoutArg(a string) (string, error) {
|
|||||||
if strings.HasPrefix(a, `"`) || strings.HasSuffix(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 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)
|
return "", fmt.Errorf("layout %s is not quoted; write '%s'", a, bare)
|
||||||
}
|
}
|
||||||
layout := a[1 : len(a)-1]
|
layout := a[1 : len(a)-1]
|
||||||
if layoutProbe.Format(layout) == layoutProbe2.Format(layout) {
|
if !namesAField(layout) {
|
||||||
return "", fmt.Errorf("layout '%s' names no field, so it is the constant %q; write it as text", layout, layout)
|
return "", fmt.Errorf("layout '%s' names no field, so it is the constant %q; write it as text", layout, layout)
|
||||||
}
|
}
|
||||||
return layout, nil
|
return layout, nil
|
||||||
@@ -518,15 +531,15 @@ func layoutArity(name string, n int, a []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
hint := ""
|
hint := ""
|
||||||
if len(a) > n && !split(a[n-1:]) {
|
if len(a) > n && !holdsQuotedLayout(a[n-1:]) {
|
||||||
hint = fmt.Sprintf("; a layout holding a comma is quoted: '%s'", strings.Join(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 argument%s, got %d%s", name, n, plural(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
|
// holdsQuotedLayout reports whether the surplus args already carry a quoted layout,
|
||||||
// comma split apart.
|
// which no comma split apart.
|
||||||
func split(a []string) bool {
|
func holdsQuotedLayout(a []string) bool {
|
||||||
for _, arg := range a {
|
for _, arg := range a {
|
||||||
if quotedLayout(arg) {
|
if quotedLayout(arg) {
|
||||||
return true
|
return true
|
||||||
@@ -535,8 +548,6 @@ func split(a []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func plural(n int) string { return map[bool]string{true: "s"}[n != 1] }
|
|
||||||
|
|
||||||
func dateArgs(_ map[string]node, a []string) error {
|
func dateArgs(_ map[string]node, a []string) error {
|
||||||
if err := layoutArity("date", 3, a); err != nil {
|
if err := layoutArity("date", 3, a); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -556,10 +567,10 @@ func dateArgs(_ map[string]node, a []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if layoutProbe.Format(layout) == layoutDay.Format(layout) {
|
if !namesADateField(layout) {
|
||||||
return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, 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) {
|
if from.Equal(to) && !namesAClockField(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 fmt.Errorf("date(%s,%s,'%s') is the constant %q; write it as text", a[0], a[1], layout, from.Format(layout))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -573,7 +584,7 @@ func timeArg(_ map[string]node, a []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if layoutProbe.Format(layout) != layoutDay.Format(layout) {
|
if namesADateField(layout) {
|
||||||
return fmt.Errorf("time(layout): '%s' names a date field; write date(from,to,layout)", layout)
|
return fmt.Errorf("time(layout): '%s' names a date field; write date(from,to,layout)", layout)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const usage = `Usage: fejkdata [flags] <path|template>
|
|||||||
<template> a format string or JSON value to render inline, e.g.
|
<template> a format string or JSON value to render inline, e.g.
|
||||||
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
|
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
|
||||||
|
|
||||||
|
A layout inside a template is single-quoted, so quote the whole argument with " to
|
||||||
|
keep it: "{date(1990-01-01,2010-12-31,'2006-01-02')}".
|
||||||
|
|
||||||
An argument containing a { token, or a JSON object, array or string, is a
|
An argument containing a { token, or a JSON object, array or string, is a
|
||||||
template; any other argument is a path (a path never contains a brace or a quote,
|
template; any other argument is a path (a path never contains a brace or a quote,
|
||||||
and a bracket only as a [selector] after a table's name). Templates reach the
|
and a bracket only as a [selector] after a table's name). Templates reach the
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func loadData(sources []dataSource) (map[string]node, error) {
|
|||||||
if len(root) == 0 {
|
if len(root) == 0 {
|
||||||
return nil, fmt.Errorf("no .json data found")
|
return nil, fmt.Errorf("no .json data found")
|
||||||
}
|
}
|
||||||
|
setTablePaths(root)
|
||||||
if err := linkTables(root); err != nil {
|
if err := linkTables(root); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func (d *draws) rowOf(t *table) int {
|
|||||||
// pinRow pins row r of t where it agrees with the rows pinned before it.
|
// pinRow pins row r of t where it agrees with the rows pinned before it.
|
||||||
func (d *draws) pinRow(t *table, r int) error {
|
func (d *draws) pinRow(t *table, r int) error {
|
||||||
if pr, ok := d.pinned(t); ok && pr != r {
|
if pr, ok := d.pinned(t); ok && pr != r {
|
||||||
return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.category)
|
return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.path)
|
||||||
}
|
}
|
||||||
for a := t.parentT; a != nil; a = a.parentT {
|
for a := t.parentT; a != nil; a = a.parentT {
|
||||||
if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) {
|
if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) {
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
|
|||||||
}
|
}
|
||||||
shape := f.recordShapeOf(n)
|
shape := f.recordShapeOf(n)
|
||||||
if errors.Is(shape.err, errNoColumns) {
|
if errors.Is(shape.err, errNoColumns) {
|
||||||
column := path[strings.LastIndex(path, ".")+1:]
|
ns := names(segments)
|
||||||
return nil, fmt.Errorf("fejkdata: %s %w; render it as a column of one: '{\"format\":\"\",\"%s\":\"{/%s}\"}'", path, shape.err, column, path)
|
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, shape.err, ns[len(ns)-1], path)
|
||||||
}
|
}
|
||||||
if shape.err != nil {
|
if shape.err != nil {
|
||||||
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err)
|
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// cell of the row a render pinned; a cell carrying tokens compiles to a string node.
|
// cell of the row a render pinned; a cell carrying tokens compiles to a string node.
|
||||||
type table struct {
|
type table struct {
|
||||||
category string
|
category string
|
||||||
path string // the category's path from the data root, set when the tables are linked
|
path string // the category's path from the data root, which a selector is written at
|
||||||
file string
|
file string
|
||||||
format *template // fields are the column nodes
|
format *template // fields are the column nodes
|
||||||
columns []string
|
columns []string
|
||||||
@@ -340,6 +340,23 @@ func (t *table) compileFormat(format string) error {
|
|||||||
// linkTables binds every table's parent to the table beside it, and proves the
|
// linkTables binds every table's parent to the table beside it, and proves the
|
||||||
// links: a parent has a key, every link cell is one, every parent row is linked
|
// links: a parent has a key, every link cell is one, every parent row is linked
|
||||||
// to, no chain of parents closes, and no child is named like a parent's column.
|
// to, no chain of parents closes, and no child is named like a parent's column.
|
||||||
|
// setTablePaths gives every table the path a selector on it is written at, before a
|
||||||
|
// link or a draw can name one.
|
||||||
|
func setTablePaths(root map[string]node) {
|
||||||
|
var walk func(dir string, children map[string]node)
|
||||||
|
walk = func(dir string, children map[string]node) {
|
||||||
|
for name, child := range children {
|
||||||
|
switch n := child.(type) {
|
||||||
|
case *folder:
|
||||||
|
walk(join(dir, name), n.children)
|
||||||
|
case *table:
|
||||||
|
n.path = join(dir, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk("", root)
|
||||||
|
}
|
||||||
|
|
||||||
func linkTables(root map[string]node) error {
|
func linkTables(root map[string]node) error {
|
||||||
var walk func(dir string, children map[string]node) error
|
var walk func(dir string, children map[string]node) error
|
||||||
walk = func(dir string, children map[string]node) error {
|
walk = func(dir string, children map[string]node) error {
|
||||||
@@ -351,7 +368,6 @@ func linkTables(root map[string]node) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case *table:
|
case *table:
|
||||||
n.path = path
|
|
||||||
if n.parent < 0 {
|
if n.parent < 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -510,7 +526,7 @@ func (t *table) under(r int, a *table, pr int) bool {
|
|||||||
// naming several rows resolves inside the ancestors pinned in d.
|
// naming several rows resolves inside the ancestors pinned in d.
|
||||||
func (t *table) find(sel string, d *draws) (int, error) {
|
func (t *table) find(sel string, d *draws) (int, error) {
|
||||||
if t.key < 0 && t.name < 0 {
|
if t.key < 0 && t.name < 0 {
|
||||||
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.spelledPath())
|
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.path)
|
||||||
}
|
}
|
||||||
if r, ok := t.byKey[sel]; ok {
|
if r, ok := t.byKey[sel]; ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
@@ -523,34 +539,31 @@ func (t *table) find(sel string, d *draws) (int, error) {
|
|||||||
case 1:
|
case 1:
|
||||||
return rows[0], nil
|
return rows[0], nil
|
||||||
case 0:
|
case 0:
|
||||||
return 0, fmt.Errorf("no row of %s has key or name %q", t.spelledPath(), sel)
|
return 0, fmt.Errorf("no row of %s has key or name %q", t.path, sel)
|
||||||
}
|
}
|
||||||
keys := make([]string, len(rows))
|
return 0, t.ambiguous(sel, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ambiguous names each row a selector could have meant: a keyed table by its keys,
|
||||||
|
// and one told apart only by its parent by the path that selects it.
|
||||||
|
func (t *table) ambiguous(sel string, rows []int) error {
|
||||||
|
spellings := make([]string, len(rows))
|
||||||
for i, r := range rows {
|
for i, r := range rows {
|
||||||
if t.key < 0 {
|
if t.key < 0 {
|
||||||
keys[i] = t.selectorSpelling(r)
|
spellings[i] = t.selectorSpelling(r)
|
||||||
} else {
|
} else {
|
||||||
keys[i] = t.cell(r, t.key)
|
spellings[i] = t.cell(r, t.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listed := strings.Join(keys, ", ")
|
listed := strings.Join(spellings, ", ")
|
||||||
if t.key < 0 {
|
if t.key < 0 {
|
||||||
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)
|
return fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %s", sel, len(rows), t.path, t.parentT.path, listed)
|
||||||
}
|
}
|
||||||
inside := ""
|
inside := ""
|
||||||
if t.parentT != nil {
|
if t.parentT != nil {
|
||||||
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.category)
|
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.path)
|
||||||
}
|
}
|
||||||
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)
|
return fmt.Errorf("%q names %d rows of %s; select one by key, one of %s%s", sel, len(rows), t.path, 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
|
// selectorSpelling is how a path writes a selected row, for messages: by key, or
|
||||||
@@ -558,7 +571,7 @@ func (t *table) spelledPath() string {
|
|||||||
func (t *table) selectorSpelling(r int) string {
|
func (t *table) selectorSpelling(r int) string {
|
||||||
switch {
|
switch {
|
||||||
case t.key >= 0:
|
case t.key >= 0:
|
||||||
return t.spelledPath() + "[" + t.cell(r, t.key) + "]"
|
return t.path + "[" + t.cell(r, t.key) + "]"
|
||||||
case t.name >= 0:
|
case t.name >= 0:
|
||||||
return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.category + "[" + t.cell(r, t.name) + "]"
|
return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.category + "[" + t.cell(r, t.name) + "]"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ func splitArgs(s string) []string {
|
|||||||
return append(args, strings.TrimSpace(s[start:]))
|
return append(args, strings.TrimSpace(s[start:]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func plural(n int) string {
|
||||||
|
if n == 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "s"
|
||||||
|
}
|
||||||
|
|
||||||
// checkFunc validates a function token at compile time: well-formed, naming a
|
// checkFunc validates a function token at compile time: well-formed, naming a
|
||||||
// known builtin, with the arg count that builtin takes and args its check accepts.
|
// known builtin, with the arg count that builtin takes and args its check accepts.
|
||||||
// fields is passed through for the one builtin (calc) that validates against them.
|
// fields is passed through for the one builtin (calc) that validates against them.
|
||||||
|
|||||||
Reference in New Issue
Block a user