Answer stability round 1 and product-owner round 1: spell .. in the fence's and the link column's advice, take it in struct tags, and show the whole climb

This commit is contained in:
2026-10-05 00:15:13 +02:00
committed by Lillem4n
parent 1551087ddf
commit 406232e8f9
5 changed files with 45 additions and 9 deletions
+2 -2
View File
@@ -588,8 +588,8 @@ It steps up from a selected row, a drawn row or a [name](#names) bound to a row,
per `..`: `{l..municipality..region.name}`. `.country` after a row still reads the link per `..`: `{l..municipality..region.name}`. `.country` after a row still reads the link
column's cell, `NO`. column's cell, `NO`.
After a `..`, `.` steps down again, drawing afresh inside the row stepped up to, apart from After a `..`, `.` steps down again, drawing afresh inside the row stepped up to,
the row the path stepped up from. Paths stepping down from one `..` read one draw, as one independent of the row the path stepped up from, which it may draw again. Paths stepping down from one `..` read one draw, as one
reference path does: `{/city..country.city.name}` and `{/city..country.city.population}` reference path does: `{/city..country.city.name}` and `{/city..country.city.population}`
describe one city, within a render and [draw group](#draw-group), or within a name's pick. describe one city, within a render and [draw group](#draw-group), or within a name's pick.
+2 -1
View File
@@ -24,7 +24,8 @@ import (
const usage = `Usage: fejkdata [flags] <path|template> const usage = `Usage: fejkdata [flags] <path|template>
<path> a category, or a dotted path into one (person, person.last); <path> a category, or a dotted path into one (person, person.last);
a table's row by key or name: 'misc.territory[SE]', 'misc.territory[Sweden].tld' a table's row by key or name: 'misc.territory[SE]', 'misc.territory[Sweden].tld';
.. steps up to the row a row links to: 'geo.SE.locality..municipality.name'
<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"]}'
+2 -2
View File
@@ -232,8 +232,8 @@ func drawsApart(x, y pathRead) error {
if s.t != x.tr.headTable { if s.t != x.tr.headTable {
tail = append([]string{x.tr.headTable.segment}, tail...) tail = append([]string{x.tr.headTable.segment}, tail...)
} }
return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup", return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s}, or draw them apart with a drawGroup",
x.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling), s.spelling, joinSegments(tail)) x.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling), joinSegments(append([]string{s.spelling}, tail...)))
} }
return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup", return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup",
x.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling)) x.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling))
+22 -4
View File
@@ -137,11 +137,11 @@ func hasSelector(segs []string) bool {
func selectorOf(seg string) string { return seg[1 : len(seg)-1] } func selectorOf(seg string) string { return seg[1 : len(seg)-1] }
// nameSegments is the segments of a path that are names, its selectors left out. // nameSegments is the segments of a path that are names, its selectors and ".." left out.
func nameSegments(segs []string) []string { func nameSegments(segs []string) []string {
out := segs[:0:0] out := segs[:0:0]
for _, s := range segs { for _, s := range segs {
if !isSelector(s) { if !isSelector(s) && s != ".." {
out = append(out, s) out = append(out, s)
} }
} }
@@ -157,7 +157,7 @@ func stepInto(n node, seg string) (node, error) {
if c, ok := n.(*tableColumn); ok { if c, ok := n.(*tableColumn); ok {
return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column; put the ".." right after the row`, c.t.header[c.i]) return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column; put the ".." right after the row`, c.t.header[c.i])
} }
return nil, fmt.Errorf(`".." steps up from a table's row to the row it links to, and what it follows is not a table; step up only from a table`) return nil, fmt.Errorf(`".." steps up from a table's row to the row it links to, and what it follows is not a table; a path names every other level from the data root down`)
} }
switch n := n.(type) { switch n := n.(type) {
case *folder: case *folder:
@@ -170,6 +170,9 @@ func stepInto(n node, seg string) (node, error) {
return child, nil return child, nil
} }
case *tableColumn: case *tableColumn:
if n.i == n.t.parentIndex {
return nil, fmt.Errorf("no field %q: %q is the link column, holding its parent's key; to read the row it links to, step up with ..%s.%s", seg, n.t.header[n.i], n.t.header[n.i], seg)
}
return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", seg, n.t.header[n.i]) return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", seg, n.t.header[n.i])
case *nullItem: case *nullItem:
default: default:
@@ -230,14 +233,29 @@ func (t *table) stepUp(rest []string) error {
switch { switch {
case t.parentT == nil: case t.parentT == nil:
return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment) return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment)
case len(rest) > 0 && rest[0] == "":
return fmt.Errorf("path has an empty segment")
case len(rest) == 0 || rest[0] != t.parentT.segment: case len(rest) == 0 || rest[0] != t.parentT.segment:
return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: ..%s`, t.segment, t.parentT.segment) return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: %s`, t.segment, t.climbTo(rest))
case hasSelector(rest): case hasSelector(rest):
return fmt.Errorf(`a path selects its rows before a "..", since a row selected after it could lie outside the row it steps up to; select from the table instead, its path from the data root: %s`, joinSegments(append([]string{t.parentT.path}, rest[1:]...))) return fmt.Errorf(`a path selects its rows before a "..", since a row selected after it could lie outside the row it steps up to; select from the table instead, its path from the data root: %s`, joinSegments(append([]string{t.parentT.path}, rest[1:]...)))
} }
return nil return nil
} }
// climbTo spells the ".." steps from t up to the ancestor rest names first, or the one step to
// t's parent where no ancestor is named.
func (t *table) climbTo(rest []string) string {
var b strings.Builder
for a := t.parentT; a != nil; a = a.parentT {
b.WriteString(".." + a.segment)
if len(rest) > 0 && a.segment == rest[0] {
return b.String()
}
}
return ".." + t.parentT.segment
}
// pathStep is one step of a compiled path, taken at the node the steps before it // pathStep is one step of a compiled path, taken at the node the steps before it
// reached, a choice there resolved first; at indexes the tail where it is taken, and // reached, a choice there resolved first; at indexes the tail where it is taken, and
// only a table's step sits past the tail's end. A step holds no node, so a // only a table's step sits past the tail's end. A step holds no node, so a
+17
View File
@@ -310,6 +310,17 @@ func TestTableStepsUpToTheRowItLinksTo(t *testing.T) {
if !afresh["L4"] || !afresh["L7"] || !afresh["record L4"] || !afresh["record L7"] { if !afresh["L4"] || !afresh["L7"] || !afresh["record L4"] || !afresh["record L7"] {
t.Fatalf("locality[L4]..municipality.locality.code drew only %v in 200 renders, want a locality of Lund drawn afresh", afresh) t.Fatalf("locality[L4]..municipality.locality.code drew only %v in 200 renders, want a locality of Lund drawn afresh", afresh)
} }
var tagged struct {
M string `fake:"locality[L4]..municipality.name"`
}
if err := f.FakeStruct(&tagged); err != nil || tagged.M != "Lund" {
t.Errorf("a struct tag stepping up = %q, %v; want Lund", tagged.M, err)
}
if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{
"x.json": `"{/locality..municipality.code}|{/locality[L1].name}"`,
})))); err == nil || !strings.Contains(err.Error(), "{/locality[L1]..municipality.code}") {
t.Errorf("New = %v, want the fence to name {/locality[L1]..municipality.code}", err)
}
for _, p := range f.List() { for _, p := range f.List() {
if strings.Contains(p, "..") { if strings.Contains(p, "..") {
t.Fatalf("List() advertises %q; it lists direct descents only", p) t.Fatalf("List() advertises %q; it lists direct descents only", p)
@@ -323,6 +334,9 @@ func TestTableStepUpRefusals(t *testing.T) {
"region[12]..municipality.name": "no parent", "region[12]..municipality.name": "no parent",
"locality[L4]..": "..municipality", "locality[L4]..": "..municipality",
"locality[L4]..municipality[0180]": "municipality[0180]", "locality[L4]..municipality[0180]": "municipality[0180]",
"locality[L4]..region": "..municipality..region",
"locality[L4]...municipality": "empty segment",
"locality[L4].municipality.name": "..municipality.name",
"locality[L4].name..municipality": "column", "locality[L4].name..municipality": "column",
"x..y": "not a table", "x..y": "not a table",
} { } {
@@ -338,6 +352,9 @@ func TestTableStepUpRefusals(t *testing.T) {
t.Errorf("{/%s}: New = %v, want an error mentioning %s", path, err, want) t.Errorf("{/%s}: New = %v, want an error mentioning %s", path, err, want)
} }
} }
if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, map[string]string{"s.json": `{"format":"{a..b}","a":{"format":"{b}","b":"x"}}`}))); err == nil || !strings.Contains(err.Error(), "not a table") {
t.Errorf("{a..b} on a field: New = %v, want an error mentioning not a table", err)
}
} }
func TestLinkedTablesDrawConsistently(t *testing.T) { func TestLinkedTablesDrawConsistently(t *testing.T) {