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
This commit is contained in:
+5
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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{""}
|
||||||
|
|||||||
@@ -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]})
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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"`},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user