Replay each path's selectors through the walk for the family fence, require a key for a name, refuse a name shadowing a key, a table reading its own family and an ancestor's column clash, add the row node and family.go, and pin eight rows inline
Tests / vet + fmt + tests (pull_request) Successful in 1m8s
Tests / Gitea release from CHANGELOG.md (pull_request) Has been skipped

This commit is contained in:
2026-09-17 12:54:24 +02:00
parent fb83aa6178
commit 047237f361
14 changed files with 419 additions and 277 deletions
+5 -1
View File
@@ -13,7 +13,11 @@ replacement, and each removed path, column or flag.
`key`, `name`, `weight` and `parent`; a path selects a row by key or name, `key`, `name`, `weight` and `parent`; a path selects a row by key or name,
`misc.country[SE]`, and descends to a linked table by name; linked tables draw `misc.country[SE]`, and descends to a linked table by name; linked tables draw
consistently within one render and draw group. `rows` is an option, so no consistently within one render and draw group. `rows` is an option, so no
template may carry a field of that name. template may carry a field of that name. Refused at `New`: a `name` without a
`key`, a name spelling another row's key, a table named like a column of any
table above it, a table whose format or cell references its own family, and,
within one render and draw group, a path drawing a table another path selects a
row of, or two paths pinning different rows of one table.
- `New` refuses a root choice of templates sharing one format and one set of string - `New` refuses a root choice of templates sharing one format and one set of string
fields, naming the rows TSV to write instead. fields, naming the rows TSV to write instead.
- `misc.country`, `misc.currency`, `misc.language`, `misc.httpstatus` and - `misc.country`, `misc.currency`, `misc.language`, `misc.httpstatus` and
+27 -10
View File
@@ -382,8 +382,10 @@ is refused, since a cell has no sibling. `New` proves the header, the options an
cell token, and refuses a TSV no category names, a key that is empty or repeats, a cell token, and refuses a TSV no category names, a key that is empty or repeats, a
weight that is not a positive number, and a key or name holding `[`, `]`, `{`, `}`, weight that is not a positive number, and a key or name holding `[`, `]`, `{`, `}`,
`"` or `|`, which a selector cannot spell; the rows are indexed on the first draw that `"` or `|`, which a selector cannot spell; the rows are indexed on the first draw that
selects one. The table's options are its own — `rows`, `key`, `name`, `weight` and selects one. A `name` needs a `key`, since a name naming several rows is reported by
`parent` — so a column may be named `name`, as one usually is. their keys, and 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 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 written by hand, and `New` refuses it in a data file naming the TSV to write; an
@@ -393,9 +395,8 @@ inline template has no file beside it, so there it stays a choice.
`[key]` or `[name]` after a table's name selects one row: `misc.country[SE]` and `[key]` or `[name]` after a table's name selects one row: `misc.country[SE]` and
`misc.country[Sweden]` name one row, and `misc.country[SE].capital` reads its column. `misc.country[Sweden]` name one row, and `misc.country[SE].capital` reads its column.
A key wins over a name that spells the same, and a name naming several rows is an A name naming several rows is an error listing their keys, unless a row selected
error listing their keys, unless a row selected before it settles which before it settles which ([Linked tables](#linked-tables)). A selector is part of the path, so it works
([Linked tables](#linked-tables)). A selector is part of the path, so it works
wherever a path does: `Fake`, `FakeRecord`, a `{/misc.country[SE].capital}` reference wherever a path does: `Fake`, `FakeRecord`, a `{/misc.country[SE].capital}` reference
and a struct tag. A dot inside the brackets belongs to the key or name, so and a struct tag. A dot inside the brackets belongs to the key or name, so
`city[St. Louis]` selects it. A path starts with a name, and `[` still opens a JSON `city[St. Louis]` selects it. A path starts with a name, and `[` still opens a JSON
@@ -436,9 +437,14 @@ tables in one render and group selects the same rows: one that selects none besi
one that does is refused naming the spelling that does, `{/country[SE].city.name}` one that does is refused naming the spelling that does, `{/country[SE].city.name}`
beside `{/country[SE].name}`, and two selecting different rows are refused naming a beside `{/country[SE].name}`, and two selecting different rows are refused naming a
`drawGroup` to draw them apart in. A bare `{/city}` beside a path into its family is `drawGroup` to draw them apart in. A bare `{/city}` beside a path into its family is
refused too, since a bare reference draws each time. `New` also refuses a link cell refused too, since a bare reference draws each time, and so is a path that draws a
that is no key of the parent, a parent row no child links to, a chain of parents that table another path in the group selects a row of, `{/city.name}` beside
closes, and a child named like one of its parent's columns. `{/country[SE].name}`, which the first token rendered would otherwise decide. `New`
also refuses a link cell that is no key of the parent, a parent row no child links
to, a chain of parents that closes, a table named like a column of any table above
it, and a cell or format of a table that references a table of its own family, since
a row rendered whole would draw the family apart from itself: read the family from a
template beside it, or add the value as a column.
### Options and fields ### Options and fields
@@ -907,8 +913,18 @@ renamed or retyped line is a major.
- **A selector is bracketed, and a dot inside it is literal.** `municipality[0180]` - **A selector is bracketed, and a dot inside it is literal.** `municipality[0180]`
reads as selection to anyone who has indexed an array, and `[St. Louis]` keeps a reads as selection to anyone who has indexed an array, and `[St. Louis]` keeps a
name whole where a colon or a dot-separated spelling could not; zsh needs the name whole where a colon or a dot-separated spelling could not; zsh needs the
brackets quoted, which the README's examples show. A key wins over a name that brackets quoted, which the README's examples show. A name that spells another
spells the same, since a key names one row by contract and a name may not. row's key is refused at load rather than shadowed: a key names one row by
contract, so the name could never select its own, and the check is one lookup per
row against the key index that already exists.
- **`parent` names the link column and the table alike.** One word says both, so a
child table sits in its parent's folder and links on a column of the parent's
name; the geo plan wants exactly that, and a table needing another folder or
another column name would be asking for a second spelling.
- **After a row, a path names a column or a linked table.** `locality[Lund].address`,
a template beside the family under a selected row, is refused today; admitting it
later is additive, since a refused spelling gains a meaning and no accepted one
changes, so the door stays open for the address records the plan describes.
- **A table read into is pinned; a table rendered whole draws afresh.** A path into a - **A table read into is pinned; a table rendered whole draws afresh.** A path into a
table pins its row for the render and group, as a reference path pins its level, table pins its row for the render and group, as a reference path pins its level,
and a bare `{/city}` draws each time, as a bare reference does; so a bare table and a bare `{/city}` draws each time, as a bare reference does; so a bare table
@@ -1015,6 +1031,7 @@ inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplat
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
hold.go the hold: one draw per expansion for paths and operands, and its fences hold.go the hold: one draw per expansion for paths and operands, and its fences
draw.go one reference draw per render and group: draw sets, the group option, and its fence draw.go one reference draw per render and group: draw sets, the group option, and its fence
family.go a family of linked tables: the rows a render pins, and the fence over paths into one family
reference.go reference sigils, and binding references across the tree reference.go reference sigils, and binding references across the tree
graph.go the render graph: edges, cycles, the repeat bound, tree walks graph.go the render graph: edges, cycles, the repeat bound, tree walks
builtins.go the {name()} function registry and its implementations builtins.go the {name()} function registry and its implementations
+1 -213
View File
@@ -21,110 +21,6 @@ type drawScope struct {
row int row int
} }
// pinned is the row the render pinned for t, if any.
func (d *draws) pinned(t *table) (int, bool) {
for _, p := range d.pins[:d.npins] {
if p.t == t {
return p.row, true
}
}
r, ok := d.more[t]
return r, ok
}
// mustRow is the row pinned for t, which the walk reaching a column pinned.
func (d *draws) mustRow(t *table) int {
r, ok := d.pinned(t)
if !ok {
panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category))
}
return r
}
// pin pins row r of t, and the rows of t's ancestors it links to.
func (d *draws) pin(t *table, r int) {
for {
if _, done := d.pinned(t); done {
return
}
if d.npins < len(d.pins) {
d.pins[d.npins] = tablePin{t, r}
d.npins++
} else {
if d.more == nil {
d.more = map[*table]int{}
}
d.more[t] = r
}
if t.parentT == nil {
return
}
t, r = t.parentT, t.parentRow(r)
}
}
// rowOf is the render's row of t: the one pinned, else one drawn inside the
// nearest pinned ancestor — its parent drawn inside that first where the ancestor
// is further up — or over the whole table, and pinned with its ancestors.
func (d *draws) rowOf(t *table) int {
if r, ok := d.pinned(t); ok {
return r
}
r := -1
for a := t.parentT; a != nil && r < 0; a = a.parentT {
if _, ok := d.pinned(a); !ok {
continue
}
if t.parentT != a {
d.rowOf(t.parentT)
}
pr, _ := d.pinned(t.parentT)
r = t.drawUnder(d.s, pr)
}
if r < 0 {
r = t.draw(d.s)
}
d.pin(t, r)
return r
}
// selectRow pins the row a selector names, refusing one outside the rows pinned
// before it.
func (d *draws) selectRow(t *table, sel string) error {
r, err := t.find(sel, d)
if err != nil {
return err
}
if pr, ok := d.pinned(t); ok && pr != r {
return fmt.Errorf("%s is not %s, the row already drawn", t.selectorSpelling(r), t.selectorSpelling(pr))
}
for a := t.parentT; a != nil; a = a.parentT {
if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) {
return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa))
}
}
d.pin(t, r)
return nil
}
// inside keeps the rows of t that sit inside every pinned ancestor.
func (d *draws) inside(t *table, rows []int) []int {
for a := t.parentT; a != nil; a = a.parentT {
pa, ok := d.pinned(a)
if !ok {
continue
}
var kept []int
for _, r := range rows {
if t.under(r, a, pa) {
kept = append(kept, r)
}
}
rows = kept
}
return rows
}
// newDrawSet makes the unnamed group's maps where the set is declared, keeping them on that frame's // newDrawSet makes the unnamed group's maps where the set is declared, keeping them on that frame's
// stack for a render that reads through them; a zero drawSet makes them on its first read instead. // stack for a render that reads through them; a zero drawSet makes them on its first read instead.
func newDrawSet(s *session) drawSet { func newDrawSet(s *session) drawSet {
@@ -368,78 +264,6 @@ type pathRead struct {
tr *tableRead // set where the reference names a table tr *tableRead // set where the reference names a table
} }
// tableRead is what a reference reads of a table family: the table named, the rows
// its selectors pin along the way, and whether it lands on a row rendered whole.
type tableRead struct {
head *table
sels []tableSel
whole bool
}
// tableSel is one selector on the way: the table it selects a row of, and the
// selector's spelling up to its closing bracket.
type tableSel struct {
t *table
row int
spelling string
}
// tableReadOf reads what a reference path does of a table, replaying its selectors
// over a walk of its own — checkPath proved each names a row — so two paths naming
// one row by key and by name compare equal.
func tableReadOf(head node, a arm, leaf node) *tableRead {
t, isTable := head.(*table)
if !isTable {
return nil
}
tr := &tableRead{head: t}
var pins draws
cur, seen := t, a.name
for _, seg := range a.tail {
switch {
case isSelector(seg):
end := strings.Index(seen, "]") + 1
_ = pins.selectRow(cur, selectorOf(seg))
row, _ := pins.pinned(cur)
tr.sels = append(tr.sels, tableSel{cur, row, a.name[:len(a.name)-len(seen)+end]})
seen = seen[end:]
case cur.children[seg] != nil:
cur = cur.children[seg]
}
}
c, isColumn := leaf.(*column)
tr.whole = isColumn && c.i < 0
return tr
}
// family is the table a chain of parents ends at.
func (t *table) family() *table {
for t.parentT != nil {
t = t.parentT
}
return t
}
// selected is the selector in r that pins t, or the nearest ancestor of t it pins.
func (r *tableRead) selected(t *table) (tableSel, bool) {
for ; t != nil; t = t.parentT {
for _, s := range r.sels {
if s.t == t {
return s, true
}
}
}
return tableSel{}, false
}
func (r *tableRead) selection() string {
parts := make([]string, len(r.sels))
for i, s := range r.sels {
parts[i] = fmt.Sprintf("%s[%d]", s.t.category, s.row)
}
return strings.Join(parts, " ")
}
type drawVisit struct { type drawVisit struct {
n node n node
group string group string
@@ -501,45 +325,9 @@ func (w *drawWalk) check() error {
if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) { if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) {
return overlapError(level.at.route, level.a.name, into) return overlapError(level.at.route, level.a.name, into)
} }
if err := checkFamily(level, into); err != nil {
return err
}
} }
} }
return nil return checkFamilies(w.reads)
}
// checkFamily refuses two reads of one table family in one group that cannot read one consistent
// draw: a table rendered whole beside a path into the family, and two paths selecting different rows.
func checkFamily(a, b pathRead) error {
if a.tr == nil || b.tr == nil || a.tr.head.family() != b.tr.head.family() {
return nil
}
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
bare, path := pair[0], pair[1]
if len(bare.a.tail) == 0 && len(path.a.tail) > 0 {
return overlapError(bare.at.route, bare.a.name, path)
}
}
if len(a.a.tail) == 0 || len(b.a.tail) == 0 || a.tr.selection() == b.tr.selection() {
return nil
}
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
selected, plain := pair[0], pair[1]
if len(plain.tr.sels) > 0 {
continue
}
if s, ok := selected.tr.selected(plain.tr.head); ok {
tail := plain.a.tail
if s.t != plain.tr.head {
tail = append([]string{plain.tr.head.category}, tail...)
}
return fmt.Errorf("%s reads %s without the row %s selects; write {%s.%s}, or draw them apart with a drawGroup",
plain.at.route.spelled(plain.a.name), plain.tr.head.category, selected.at.route.spelled(selected.a.name), s.spelling, strings.Join(tail, "."))
}
}
return fmt.Errorf("%s and %s select different rows of one table family; select the same rows in both, or draw them apart with a drawGroup",
a.at.route.spelled(a.a.name), b.at.route.spelled(b.a.name))
} }
func overlapError(route drawRoute, ref string, into pathRead) error { func overlapError(route drawRoute, ref string, into pathRead) error {
+300
View File
@@ -0,0 +1,300 @@
package fejkdata
import (
"fmt"
"sort"
"strings"
)
// tablePin is one table's pinned row.
type tablePin struct {
t *table
row int
}
// pinned is the row the render pinned for t, if any.
func (d *draws) pinned(t *table) (int, bool) {
for _, p := range d.pins[:d.npins] {
if p.t == t {
return p.row, true
}
}
r, ok := d.more[t]
return r, ok
}
// mustRow is the row pinned for t, which the walk reaching a column pinned.
func (d *draws) mustRow(t *table) int {
r, ok := d.pinned(t)
if !ok {
panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category))
}
return r
}
// pin pins row r of t, and the rows of t's ancestors it links to.
func (d *draws) pin(t *table, r int) {
for {
if _, done := d.pinned(t); done {
return
}
if d.npins < len(d.pins) {
d.pins[d.npins] = tablePin{t, r}
d.npins++
} else {
if d.more == nil {
d.more = map[*table]int{}
}
d.more[t] = r
}
if t.parentT == nil {
return
}
t, r = t.parentT, t.parentRow(r)
}
}
// each calls fn for every pinned row, in pin order, the spilled ones by table name.
func (d *draws) each(fn func(t *table, r int)) {
for _, p := range d.pins[:d.npins] {
fn(p.t, p.row)
}
spilled := make([]*table, 0, len(d.more))
for t := range d.more {
spilled = append(spilled, t)
}
sort.Slice(spilled, func(i, j int) bool { return spilled[i].category < spilled[j].category })
for _, t := range spilled {
fn(t, d.more[t])
}
}
// rowOf is the render's row of t: the one pinned, else one drawn inside the
// nearest pinned ancestor — its parent drawn inside that first where the ancestor
// is further up — or over the whole table, and pinned with its ancestors.
func (d *draws) rowOf(t *table) int {
if r, ok := d.pinned(t); ok {
return r
}
r := -1
for a := t.parentT; a != nil && r < 0; a = a.parentT {
if _, ok := d.pinned(a); !ok {
continue
}
if t.parentT != a {
d.rowOf(t.parentT)
}
pr, _ := d.pinned(t.parentT)
r = t.drawUnder(d.s, pr)
}
if r < 0 {
r = t.draw(d.s)
}
d.pin(t, r)
return r
}
// pinRow pins row r of t where it agrees with the rows pinned before it.
func (d *draws) pinRow(t *table, r int) error {
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)
}
for a := t.parentT; a != nil; a = a.parentT {
if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) {
return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa))
}
}
d.pin(t, r)
return nil
}
// selectRow pins the row a selector names.
func (d *draws) selectRow(t *table, sel string) error {
r, err := t.find(sel, d)
if err != nil {
return err
}
return d.pinRow(t, r)
}
// inside keeps the rows of t that sit inside every pinned ancestor.
func (d *draws) inside(t *table, rows []int) []int {
for a := t.parentT; a != nil; a = a.parentT {
pa, ok := d.pinned(a)
if !ok {
continue
}
var kept []int
for _, r := range rows {
if t.under(r, a, pa) {
kept = append(kept, r)
}
}
rows = kept
}
return rows
}
// tableRead is what a reference path reads of a table family: the table its head
// names, the rows its selectors pin, the tables it draws — those it walks with no
// row pinned, and their unpinned ancestors — each selector's spelling, and whether
// it lands on a row rendered whole.
type tableRead struct {
head *table
pins draws
drawn map[*table]bool
sels []tableSel
whole bool
}
// tableSel is one selector on the way: the table it selects a row of, and the path
// as written up to and including it.
type tableSel struct {
t *table
spelling string
}
// tableReadOf replays a reference's selectors through the walk a render uses, so
// two paths pinning one row by different routes compare equal. checkPath proved
// each selector names a row.
func tableReadOf(head node, a arm, leaf node) *tableRead {
t, isTable := head.(*table)
if !isTable {
return nil
}
tr := &tableRead{head: t, drawn: map[*table]bool{}}
_, _ = walkPath(t, a.tail, pathWalk{pins: &tr.pins})
written := a.name[:len(a.name)-len(joinSegments(a.tail))]
cur := t
tr.draws(cur)
for i, seg := range a.tail {
switch d := cur.descendant(seg); {
case isSelector(seg):
tr.sels = append(tr.sels, tableSel{cur, written + joinSegments(a.tail[:i+1])})
case d != nil:
cur = d
tr.draws(cur)
}
}
_, tr.whole = leaf.(*row)
return tr
}
// draws marks t and its ancestors drawn, up to the first the read pins.
func (r *tableRead) draws(t *table) {
for ; t != nil; t = t.parentT {
if _, pinned := r.pins.pinned(t); pinned {
return
}
r.drawn[t] = true
}
}
// joinSegments spells segments as a path: a selector attaches to the name before it.
func joinSegments(segs []string) string {
var b strings.Builder
for _, s := range segs {
if b.Len() > 0 && !isSelector(s) {
b.WriteByte('.')
}
b.WriteString(s)
}
return b.String()
}
// family is the table a chain of parents ends at.
func (t *table) family() *table {
for t.parentT != nil {
t = t.parentT
}
return t
}
// selected is the selector in r on t, or on the nearest ancestor of t it selects.
func (r *tableRead) selected(t *table) (tableSel, bool) {
for ; t != nil; t = t.parentT {
for _, s := range r.sels {
if s.t == t {
return s, true
}
}
}
return tableSel{}, false
}
// checkFamilies refuses reads of one table family in one draw group that cannot
// read one consistent draw: a table rendered whole beside a path into the family,
// a table one read draws that another pins, and two reads pinning different rows.
// The reads come sorted by group and path, so which pair is reported does not vary.
func checkFamilies(reads []pathRead) error {
shared := map[string]*draws{}
for i, r := range reads {
if r.tr == nil {
continue
}
for _, o := range reads[:i] {
if o.tr == nil || o.at.group != r.at.group || o.tr.head.family() != r.tr.head.family() {
continue
}
if err := checkFamilyPair(o, r); err != nil {
return err
}
}
d := shared[r.at.group]
if d == nil {
d = &draws{}
shared[r.at.group] = d
}
if err := r.tr.replay(d); err != nil {
return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.name), err)
}
}
return nil
}
// replay pins the read's rows into d, where they agree with the rows pinned before.
func (r *tableRead) replay(d *draws) error {
var err error
r.pins.each(func(t *table, row int) {
if err == nil {
err = d.pinRow(t, row)
}
})
return err
}
// checkFamilyPair refuses a table read whole beside a path into its family, and a
// table one read draws that the other pins, since which token renders first would
// then decide the row.
func checkFamilyPair(a, b pathRead) error {
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
x, y := pair[0], pair[1]
if len(x.a.tail) == 0 && len(y.a.tail) > 0 {
return overlapError(x.at.route, x.a.name, y)
}
if drawn := x.tr.drawnOf(&y.tr.pins); drawn != nil {
if s, ok := y.tr.selected(x.tr.head); ok && len(x.tr.sels) == 0 {
tail := x.a.tail
if s.t != x.tr.head {
tail = append([]string{x.tr.head.category}, tail...)
}
return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup",
x.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name), s.spelling, joinSegments(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",
x.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name))
}
}
return nil
}
// drawnOf is a table the read draws that pins holds a row of, if any.
func (r *tableRead) drawnOf(pins *draws) *table {
var found *table
pins.each(func(t *table, _ int) {
if found == nil && r.drawn[t] {
found = t
}
})
return found
}
+3 -3
View File
@@ -46,7 +46,7 @@ type Generator struct {
mu sync.Mutex mu sync.Mutex
rand *session rand *session
categories map[string]node categories map[string]node
root *folder // the categories as the node a path walks from root folder // the categories as the node a path walks from, owned here so a walk allocates none
set drawSet // one Fake's draws, owned here so a walk pinning rows keeps them off the heap set drawSet // one Fake's draws, owned here so a walk pinning rows keeps them off the heap
records map[node]recordShape records map[node]recordShape
structs map[reflect.Type]structResult structs map[reflect.Type]structResult
@@ -125,7 +125,7 @@ func New(opts ...Option) (*Generator, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
return &Generator{rand: rng, categories: cats, root: &folder{children: cats}}, nil return &Generator{rand: rng, categories: cats}, nil
} }
// List returns the sorted dotted paths Fake can render: every category, the dotted // List returns the sorted dotted paths Fake can render: every category, the dotted
@@ -170,7 +170,7 @@ func paths(n node) []string {
return []string{""} return []string{""}
case *table: case *table:
return tablePaths(n) return tablePaths(n)
case *column: case *column, *row:
return []string{""} return []string{""}
case *choice: case *choice:
out := []string{""} out := []string{""}
+2 -6
View File
@@ -63,9 +63,6 @@ func contained(n node) []namedNode {
case *table: case *table:
return append([]namedNode{{node: n.format}}, named(n.fields)...) return append([]namedNode{{node: n.format}}, named(n.fields)...)
case *column: case *column:
if n.i < 0 {
return nil
}
var out []namedNode var out []namedNode
for r := 0; r < n.t.rows(); r++ { for r := 0; r < n.t.rows(); r++ {
if cell := n.t.cellNode(r, n.i); cell != nil { if cell := n.t.cellNode(r, n.i); cell != nil {
@@ -162,10 +159,9 @@ func renderEdges(n node) []renderEdge {
return es return es
case *table: case *table:
return []renderEdge{{to: n.format, label: "format"}} return []renderEdge{{to: n.format, label: "format"}}
case *row:
return []renderEdge{{to: n.t.format, label: "format"}}
case *column: case *column:
if n.i < 0 {
return []renderEdge{{to: n.t.format, label: "format"}}
}
var es []renderEdge var es []renderEdge
for _, c := range contained(n) { for _, c := range contained(n) {
es = append(es, renderEdge{to: c.node, label: n.t.columns[n.i]}) es = append(es, renderEdge{to: c.node, label: n.t.columns[n.i]})
+6 -9
View File
@@ -262,18 +262,12 @@ func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding)
type draws struct { type draws struct {
variant map[string]node variant map[string]node
value map[string]draw value map[string]draw
pins [4]tablePin // the rows pinned, inline so a render pinning a few tables stays off the heap pins [8]tablePin // the rows pinned, inline so a render over a country's five-deep geo tree stays off the heap
npins int npins int
more map[*table]int // the rows pinned past the inline four more map[*table]int // the rows pinned past the inline eight
s *session // what draws a row; nil where a walk only proves selectors s *session // what draws a row; nil where a walk only proves selectors
} }
// tablePin is one table's pinned row.
type tablePin struct {
t *table
row int
}
// draw is what one read drew: its text, and whether it landed on a null. // draw is what one read drew: its text, and whether it landed on a null.
type draw struct { type draw struct {
text string text string
@@ -298,7 +292,7 @@ func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw {
if r, done := d.value[a.path]; done { if r, done := d.value[a.path]; done {
return r return r
} }
leaf, _ := walkPath(t.fields[a.key], a.tail, pathWalk{ leaf, err := walkPath(t.fields[a.key], a.tail, pathWalk{
// Hold the draw at every level passed through, so two paths sharing a // Hold the draw at every level passed through, so two paths sharing a
// prefix share it. // prefix share it.
choice: func(c *choice, rest []string) ([]node, error) { choice: func(c *choice, rest []string) ([]node, error) {
@@ -318,6 +312,9 @@ func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw {
}, },
pins: d, pins: d,
}) })
if err != nil {
panic(fmt.Sprintf("fejkdata: %q: %v; a fence should have refused this at New", a.name, err))
}
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
if d.value == nil { if d.value == nil {
d.value = map[string]draw{} d.value = map[string]draw{}
+12 -1
View File
@@ -102,6 +102,15 @@ func splitOutside(s string, c byte) []string {
// isSelector reports whether a segment is a [key or name] rather than a name. // isSelector reports whether a segment is a [key or name] rather than a name.
func isSelector(seg string) bool { return strings.HasPrefix(seg, "[") } func isSelector(seg string) bool { return strings.HasPrefix(seg, "[") }
func hasSelector(segs []string) bool {
for _, s := range segs {
if isSelector(s) {
return true
}
}
return false
}
// selectorOf is the key or name a selector segment holds. // selectorOf is the key or name a selector segment holds.
func selectorOf(seg string) string { return seg[1 : len(seg)-1] } func selectorOf(seg string) string { return seg[1 : len(seg)-1] }
@@ -214,8 +223,10 @@ func walkTable(t *table, tail []string, w pathWalk, descended bool) (node, error
return nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.category, sel) return nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.category, sel)
} }
} }
// A selector further down pins this table by ancestry, so the walk draws only
// where none follows; drawing first could pick a row the selector is not inside.
if w.pins != nil { if w.pins != nil {
if err := readRow(w.pins, t, sel, descended || len(tail) > 0); err != nil { if err := readRow(w.pins, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil {
return nil, err return nil, err
} }
} }
+2 -3
View File
@@ -157,10 +157,9 @@ func tableRecord(s *session, t *table, tail []string, sc drawScope) (node, error
case *table: case *table:
sc.draws(s).rowOf(n) sc.draws(s).rowOf(n)
return n, nil return n, nil
case *row:
return n.t, nil
case *column: case *column:
if n.i < 0 {
return n.t, nil
}
return nil, fmt.Errorf("descends into %q, a column; a record is a table's row", n.t.columns[n.i]) return nil, fmt.Errorf("descends into %q, a column; a record is a table's row", n.t.columns[n.i])
} }
return n, nil return n, nil
+19
View File
@@ -109,6 +109,9 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root
if category != "" && key == "/"+category { if category != "" && key == "/"+category {
return fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", path, name) return fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", path, name)
} }
if err := checkNotOwnFamily(root, category, target); err != nil {
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
}
if err := checkPath(target, tail, key); err != nil { if err := checkPath(target, tail, key); err != nil {
return fmt.Errorf("%s: reference {%s}: %w", path, name, err) return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
} }
@@ -122,6 +125,22 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root
return nil return nil
} }
// checkNotOwnFamily refuses a reference from a table's format or cell into a table of
// its own family: a table rendered whole draws its row without pinning it, so the
// family would draw apart from the row being rendered.
func checkNotOwnFamily(root map[string]node, category string, target node) error {
if category == "" {
return nil
}
_, own, _, err := resolveCategory(root, strings.Split(category, "."))
from, isTable := own.(*table)
into, targetIsTable := target.(*table)
if err != nil || !isTable || !targetIsTable || from.family() != into.family() {
return nil
}
return fmt.Errorf("a cell or format of %s reads %s, a table of its own family, which a row of %s rendered whole would draw apart from; read the family from a template beside it, or add the value as a column", from.category, into.category, from.category)
}
// columnRead is a record's column read by a format of that one reference alone, which is the // columnRead is a record's column read by a format of that one reference alone, which is the
// column: it takes the column's datatype and null. // column: it takes the column's datatype and null.
type columnRead struct { type columnRead struct {
+5 -7
View File
@@ -25,10 +25,8 @@ func (f *Generator) Fake(path string) (string, error) {
} }
f.set = drawSet{unnamed: draws{s: f.rand}} f.set = drawSet{unnamed: draws{s: f.rand}}
sc := drawScope{set: &f.set} sc := drawScope{set: &f.set}
if f.root == nil { f.root.children = f.categories
f.root = &folder{children: f.categories} n, err := descend(f.rand, &f.root, segments, sc)
}
n, err := descend(f.rand, f.root, segments, sc)
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %s: %w", path, err) return "", fmt.Errorf("fejkdata: %s: %w", path, err)
} }
@@ -60,13 +58,13 @@ func render(s *session, n node, sc drawScope) string {
case *table: case *table:
sc.t, sc.row = n, n.draw(s) sc.t, sc.row = n, n.draw(s)
return expand(s, n.format, sc) return expand(s, n.format, sc)
case *row:
sc.t, sc.row = n.t, sc.draws(s).mustRow(n.t)
return expand(s, n.t.format, sc)
case *column: case *column:
if sc.t != n.t { if sc.t != n.t {
sc.t, sc.row = n.t, sc.draws(s).mustRow(n.t) sc.t, sc.row = n.t, sc.draws(s).mustRow(n.t)
} }
if n.i < 0 {
return expand(s, n.t.format, sc)
}
if cell := n.t.cellNode(sc.row, n.i); cell != nil { if cell := n.t.cellNode(sc.row, n.i); cell != nil {
return render(s, cell, sc) return render(s, cell, sc)
} }
+28 -14
View File
@@ -19,7 +19,7 @@ type table struct {
columns []string columns []string
col map[string]int col map[string]int
fields map[string]node // column nodes, the format's fields fields map[string]node // column nodes, the format's fields
whole *column // the pinned row rendered by the format whole *row // the pinned row rendered by the format
cells []string // rows × columns, flat cells []string // rows × columns, flat
tokens map[int]*template tokens map[int]*template
key int // column index, or -1 key int // column index, or -1
@@ -44,8 +44,7 @@ type tableIndex struct {
func (*table) isNode() {} func (*table) isNode() {}
// column is one column of a table, rendered as the cell of the row the render // column is one column of a table, rendered as the cell of the row the render pinned.
// pinned; i < 0 is the whole row rendered by the table's format.
type column struct { type column struct {
t *table t *table
i int i int
@@ -53,6 +52,11 @@ type column struct {
func (*column) isNode() {} func (*column) isNode() {}
// row is the row of a table the render pinned, rendered by the table's format.
type row struct{ t *table }
func (*row) isNode() {}
func (t *table) rows() int { return len(t.cells) / len(t.columns) } func (t *table) rows() int { return len(t.cells) / len(t.columns) }
func (t *table) cell(row, col int) string { return t.cells[row*len(t.columns)+col] } func (t *table) cell(row, col int) string { return t.cells[row*len(t.columns)+col] }
@@ -144,7 +148,7 @@ func readTableOptions(m map[string]any) (tableOptionValues, error) {
// parseRows reads the TSV: the header line names the columns, each following line // parseRows reads the TSV: the header line names the columns, each following line
// is a row of as many cells. Cells are substrings of data, so the file is held once. // is a row of as many cells. Cells are substrings of data, so the file is held once.
func (t *table) parseRows(data string) error { func (t *table) parseRows(data string) error {
data = strings.TrimSuffix(data, "\n") data = strings.TrimSuffix(strings.TrimPrefix(data, "\xEF\xBB\xBF"), "\n")
header, rest, _ := strings.Cut(data, "\n") header, rest, _ := strings.Cut(data, "\n")
if data == "" { if data == "" {
return fmt.Errorf("has no header line naming its columns") return fmt.Errorf("has no header line naming its columns")
@@ -202,6 +206,9 @@ func (t *table) bindOptions(o tableOptionValues) error {
} }
*opt.into = i *opt.into = i
} }
if t.name >= 0 && t.key < 0 {
return fmt.Errorf("name selects a row as a key does, and lists the rows it matches by their keys, so it needs a key column; add key")
}
if err := t.indexKeys(); err != nil { if err := t.indexKeys(); err != nil {
return err return err
} }
@@ -224,6 +231,13 @@ func (t *table) indexKeys() error {
} }
t.byKey[k] = r t.byKey[k] = r
} }
for r := 0; r < t.rows() && t.name >= 0; r++ {
if n := t.cell(r, t.name); t.byKey[n] != r {
if other, isKey := t.byKey[n]; isKey {
return fmt.Errorf("%s line %d: name %q is the key of line %d, which a selector reads first, so the name could never select this row", t.file, r+2, n, other+2)
}
}
}
return nil return nil
} }
@@ -282,7 +296,7 @@ func (t *table) compileFormat(format string) error {
return err return err
} }
t.format = &template{format: format, fields: t.fields, repeat: 1, record: true, table: t} t.format = &template{format: format, fields: t.fields, repeat: 1, record: true, table: t}
t.whole = &column{t, -1} t.whole = &row{t}
return t.format.compileFormat() return t.format.compileFormat()
} }
@@ -300,7 +314,6 @@ func linkTables(root map[string]node) error {
return err return err
} }
case *table: case *table:
n.category = name
if n.parent < 0 { if n.parent < 0 {
continue continue
} }
@@ -325,17 +338,21 @@ func (t *table) linkParent(path string, siblings map[string]node) error {
case p.key < 0: case p.key < 0:
return fmt.Errorf("parent %q has no key column to link to", name) return fmt.Errorf("parent %q has no key column to link to", name)
} }
var ancestors []*table
for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.columns[q.parent]].(*table) { for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.columns[q.parent]].(*table) {
if seen[q] { if seen[q] {
return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.category) return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.category)
} }
seen[q] = true seen[q] = true
ancestors = append(ancestors, q)
if q.parent < 0 { if q.parent < 0 {
break break
} }
} }
if _, clash := p.col[t.category]; clash { for _, q := range ancestors {
return fmt.Errorf("%q is named like a column of its parent %q, so %s.%s could read either; rename one", t.category, name, name, t.category) if _, clash := q.col[t.category]; clash {
return fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.category, q.category, q.category, t.category)
}
} }
linked := make(map[string]bool, p.rows()) linked := make(map[string]bool, p.rows())
for r := 0; r < t.rows(); r++ { for r := 0; r < t.rows(); r++ {
@@ -450,8 +467,8 @@ func (t *table) under(r int, a *table, pr int) bool {
// find is the row a selector names: by key first, then by name, where a name // find is the row a selector names: by key first, then by name, where a name
// 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 {
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 column to select a row by", t.category)
} }
if r, ok := t.byKey[sel]; ok { if r, ok := t.byKey[sel]; ok {
return r, nil return r, nil
@@ -479,8 +496,5 @@ func (t *table) find(sel string, d *draws) (int, error) {
// selectorSpelling is how a path writes a selected row, for messages. // selectorSpelling is how a path writes a selected row, for messages.
func (t *table) selectorSpelling(r int) string { func (t *table) selectorSpelling(r int) string {
if t.key >= 0 { return t.category + "[" + t.cell(r, t.key) + "]"
return t.category + "[" + t.cell(r, t.key) + "]"
}
return t.category + "[" + t.cell(r, t.name) + "]"
} }
+7 -7
View File
@@ -337,11 +337,11 @@ func TestTableSelectionFences(t *testing.T) {
// table are refused at load rather than found at render. // table are refused at load rather than found at render.
func TestTableFamilyFenceReplaysPins(t *testing.T) { func TestTableFamilyFenceReplaysPins(t *testing.T) {
accepted := map[string]string{ accepted := map[string]string{
"skip-level selectors": `"{/region[12].locality[L4].name}|{/region[12].name}"`, "skip-level selectors": `"{/region[12].locality[L4].name}|{/region[12].name}"`,
"descendant then ancestor": `"{/locality[L4].name}|{/region[12].name}"`, "descendant then ancestor": `"{/locality[L4].name}|{/region[12].name}"`,
"two selected levels": `"{/municipality[1281].name}|{/locality[L4].name}"`, "two selected levels": `"{/municipality[1281].name}|{/locality[L4].name}"`,
"nested prefixes": `"{/region[12].municipality[1281].name}|{/municipality[1281].locality[L4].name}"`, "nested prefixes": `"{/region[12].municipality[1281].name}|{/municipality[1281].locality[L4].name}"`,
"selected above a draw": `"{/region[12].locality[L4].name}|{/region[12].municipality.code}"`, "a draw under the selection": `"{/region[12].name}|{/region[12].municipality.locality[L4].name}"`,
} }
for name, json := range accepted { for name, json := range accepted {
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1)) f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1))
@@ -358,7 +358,7 @@ func TestTableFamilyFenceReplaysPins(t *testing.T) {
} }
rejected := map[string]struct{ json, want string }{ rejected := map[string]struct{ json, want string }{
"two rows of one table": {`"{/region[12].locality[L4].name} {/region[12].locality[L7].name}"`, "drawGroup"}, "two rows of one table": {`"{/region[12].locality[L4].name} {/region[12].locality[L7].name}"`, "drawGroup"},
"a row outside a selected ancestor": {`"{/region[14].name} {/locality[L4].name}"`, "not inside"}, "a row outside a selected ancestor": {`"{/region[14].name} {/locality[L4].name}"`, "drawGroup"},
} }
for name, c := range rejected { for name, c := range rejected {
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": c.json})))) _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": c.json}))))
@@ -392,7 +392,7 @@ func TestTableFences(t *testing.T) {
"a brace in a name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\tx{1}\ny\ty\n"}, `"{"`}, "a brace in a name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\tx{1}\ny\ty\n"}, `"{"`},
"name without a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"a"}`, "t.tsv": "a\nx\nx\n"}, "key"}, "name without a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"a"}`, "t.tsv": "a\nx\nx\n"}, "key"},
"a name that is another row's key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\ty\ny\tz\n"}, `"y"`}, "a name that is another row's key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\ty\ny\tz\n"}, `"y"`},
"a cell reading its family": {with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\nL1\t{/municipality.code}\t0180\nL2\tSolna\t0184\nL3\tMalmö\t1280\nL4\tLund\t1281\nL5\tGöteborg\t1480\n"}), "family"}, "a cell reading its family": {with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\tnote\nL1\tStockholm\t0180\t{/municipality.code}\nL2\tSolna\t0184\t-\nL3\tMalmö\t1280\t-\nL4\tLund\t1281\t-\nL5\tGöteborg\t1480\t-\n"}), "family"},
"a format reading its family": {with(geo(), map[string]string{"locality.json": `{"format":"{name} {/region.name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`}), "family"}, "a format reading its family": {with(geo(), map[string]string{"locality.json": `{"format":"{name} {/region.name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`}), "family"},
"a descendant named like an ancestor's column": {with(geo(), map[string]string{"region.tsv": "code\tname\tpopulation\tlocality\n01\tStockholms län\t2400000\tx\n12\tSkåne län\t1400000\ty\n14\tVästra Götalands län\t1750000\tz\n"}), `"locality"`}, "a descendant named like an ancestor's column": {with(geo(), map[string]string{"region.tsv": "code\tname\tpopulation\tlocality\n01\tStockholms län\t2400000\tx\n12\tSkåne län\t1400000\ty\n14\tVästra Götalands län\t1750000\tz\n"}), `"locality"`},
"weight not a number": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\tmany\ny\t2\n"}, `"many"`}, "weight not a number": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\tmany\ny\t2\n"}, `"many"`},
+2 -3
View File
@@ -87,6 +87,8 @@ func (p *valueProof) of(n node) proven {
v = p.template(n) v = p.template(n)
case *column: case *column:
v = p.cells(n) v = p.cells(n)
case *row:
v = unproven(fmt.Sprintf("%q renders a row of %s, which is composed text", n.t.format.format, n.t.category))
default: default:
v = unproven(`it reads a null, which renders "" outside its own column`) v = unproven(`it reads a null, which renders "" outside its own column`)
} }
@@ -96,9 +98,6 @@ func (p *valueProof) of(n node) proven {
// cells proves a table column over every cell it may render. // cells proves a table column over every cell it may render.
func (p *valueProof) cells(c *column) proven { func (p *valueProof) cells(c *column) proven {
if c.i < 0 {
return unproven(fmt.Sprintf("%q renders a row of %s, which is composed text", c.t.format.format, c.t.category))
}
var v proven var v proven
for r := 0; r < c.t.rows(); r++ { for r := 0; r < c.t.rows(); r++ {
var w proven var w proven