Files
fejkdata/path.go
T

379 lines
11 KiB
Go

package fejkdata
import (
"fmt"
"sort"
"strings"
)
// splitPath splits a dotted path into segments, a selector — [key or name] after a
// table's name — becoming a segment of its own, brackets kept. A dot inside a
// selector is part of the key or name.
func splitPath(path string) ([]string, error) {
segs := make([]string, 0, strings.Count(path, ".")+2*strings.Count(path, "[")+1)
start, open := 0, -1
for i := 0; i < len(path); i++ {
switch path[i] {
case '[':
if err := checkOpen(path, i, start, open); err != nil {
return nil, err
}
segs = append(segs, path[start:i])
start, open = i, i
case ']':
if err := checkClose(path, i, open); err != nil {
return nil, err
}
segs = append(segs, path[start:i+1])
start, open = i+2, -1
i++
case '.':
if open < 0 {
segs = append(segs, path[start:i])
start = i + 1
}
}
}
if open >= 0 {
return nil, fmt.Errorf(`%q opens a selector with "[" and never closes it with "]"`, path)
}
if start <= len(path) {
segs = append(segs, path[start:])
}
return segs, nil
}
// checkOpen refuses a "[" at i that opens no selector: one inside a selector, or one
// following no name.
func checkOpen(path string, i, start, open int) error {
switch {
case open >= 0:
return fmt.Errorf(`%q holds a "[" inside a selector, which no key or name may`, path)
case i == 0:
return fmt.Errorf(`%q starts with "["; a path starts with a name, and a selector follows a table's name`, path)
case i == start:
return fmt.Errorf(`%q: a selector follows its table's name; write %s`, path, path[:i-1]+path[i:])
}
return nil
}
// checkClose refuses a "]" at i that closes no selector, closes an empty one, or is
// followed by anything but a dot or the end.
func checkClose(path string, i, open int) error {
switch {
case open < 0:
return fmt.Errorf(`%q holds a "]" that closes no "["`, path)
case i == open+1:
return fmt.Errorf("%q holds an empty selector; a selector names a row by key or name", path)
case i+1 < len(path) && path[i+1] != '.':
return fmt.Errorf(`%q: a "]" ends its selector, so a dot or the end must follow it`, path)
}
return nil
}
// indexOutside is the first c in s outside a [selector], or -1.
func indexOutside(s string, c byte) int {
depth := 0
for i := 0; i < len(s); i++ {
switch {
case s[i] == '[':
depth++
case s[i] == ']' && depth > 0:
depth--
case s[i] == c && depth == 0:
return i
}
}
return -1
}
// splitOutside splits s on c outside any [selector].
func splitOutside(s string, c byte) []string {
var parts []string
for {
i := indexOutside(s, c)
if i < 0 {
return append(parts, s)
}
parts, s = append(parts, s[:i]), s[i+1:]
}
}
// isSelector reports whether a segment is a [key or name] rather than a name.
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.
func selectorOf(seg string) string { return seg[1 : len(seg)-1] }
// names is the segments of a path that are names, its selectors left out.
func names(segs []string) []string {
out := segs[:0:0]
for _, s := range segs {
if !isSelector(s) {
out = append(out, s)
}
}
return out
}
// walkMode is what a walk does at a choice, and whether it reads the rows a path
// selects and draws.
type walkMode uint8
const (
// walkEvery proves every variant carries the rest of the path and walks each,
// pinning the rows it selects in pins.
walkEvery walkMode = iota
// walkCover stops at a choice, handing it whole to leaf, and reads no row.
walkCover
// walkProbe proves as walkEvery does, walking one variant, so a path that
// resolves resolves whichever variants and rows are drawn.
walkProbe
// walkDraw draws the rows and variants a proved path reads; drawPath is its
// one entry.
walkDraw
)
// pathWalk is one walk of a dotted path: level runs at each template a segment
// descends into, leaf where the walk ends, and a nil action is skipped.
type pathWalk struct {
mode walkMode
level func(t *template, rest []string) error
leaf func(n node) error
pins *pinSet
draws *pathDraws
}
// pathDraws is what a walk draws with: the session, and, for a held read, the hold
// keeping the variant drawn at each level of a, so paths sharing a prefix share it.
type pathDraws struct {
s *session
held *hold
a *arm
}
// walkPath descends tail from n and returns the node it ends at: a folder or
// template by its next segment, a choice by its mode, which consumes no segment, a
// table by walkTable. A missing segment is an error, so no walk reaches past what
// the data holds.
func walkPath(n node, tail []string, w pathWalk) (node, error) {
if len(tail) == 0 {
return n, w.atLeaf(n)
}
if t, isTable := n.(*table); isTable {
return walkTable(t, tail, w, false)
}
if isSelector(tail[0]) {
return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(tail[0]))
}
switch n := n.(type) {
case *folder:
child, ok := n.children[tail[0]]
if !ok {
return nil, fmt.Errorf("no entry %q", tail[0])
}
return walkPath(child, tail[1:], w)
case *template:
if w.level != nil {
if err := w.level(n, tail); err != nil {
return nil, err
}
}
child, ok := n.fields[tail[0]]
if !ok {
return nil, fmt.Errorf("no field %q", tail[0])
}
return walkPath(child, tail[1:], w)
case *choice:
return walkChoice(n, tail, w)
case *column:
return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", tail[0], n.t.columns[n.i])
}
return nil, fmt.Errorf("no field %q", tail[0])
}
func (w pathWalk) atLeaf(n node) error {
if w.leaf != nil {
return w.leaf(n)
}
return nil
}
// drawPath draws the rows and variants a path reads, pinning them in pins. The path
// must be proved first, by a probe or by New's fences, so the walk cannot fail.
// head is what n is reached by, for the panic.
func drawPath(n node, tail []string, head string, pins *pinSet, draws *pathDraws) node {
leaf, err := walkPath(n, tail, pathWalk{mode: walkDraw, pins: pins, draws: draws})
if err != nil {
panic(fmt.Sprintf("fejkdata: %s: %v; the path should have been proved before it was drawn", join(head, joinSegments(tail)), err))
}
return leaf
}
func walkChoice(c *choice, tail []string, w pathWalk) (node, error) {
switch w.mode {
case walkCover:
return c, w.atLeaf(c)
case walkDraw:
return walkPath(w.draws.variant(c, tail), tail, w)
}
if err := carriedByAll(c, tail); err != nil {
return nil, err
}
if w.mode == walkProbe {
return walkPath(c.items[0], tail, w)
}
var last node
for _, item := range c.items {
var err error
if last, err = walkPath(item, tail, w); err != nil {
return nil, err
}
}
return last, nil
}
// walkTable descends tail from a table: a selector first, then a column or a
// linked table by name. The walk reads a row wherever a segment follows or the
// table was reached from another; a table reached whole, with no selector, is left
// to a render's own draw.
func walkTable(t *table, tail []string, w pathWalk, descended bool) (node, error) {
sel, tail, err := t.selector(tail)
if err != nil {
return nil, err
}
column, child, err := t.step(tail)
if err != nil {
return nil, err
}
// 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.mode != walkCover {
if err := readRow(w, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil {
return nil, err
}
}
switch {
case len(tail) == 0 && sel == "" && !descended:
return walkPath(t, nil, w)
case len(tail) == 0:
return walkPath(t.whole, nil, w)
case child != nil:
return walkTable(child, tail[1:], w, true)
}
return walkPath(column, tail[1:], w)
}
// selector splits the selector a tail starts with from the rest of it.
func (t *table) selector(tail []string) (sel string, rest []string, err error) {
if len(tail) == 0 || !isSelector(tail[0]) {
return "", tail, nil
}
sel, rest = selectorOf(tail[0]), tail[1:]
if len(rest) > 0 && isSelector(rest[0]) {
return "", nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.category, sel)
}
return sel, rest, nil
}
// step is what a tail's first segment names in t: a column, or a table linked to it.
func (t *table) step(tail []string) (column node, child *table, err error) {
if len(tail) == 0 {
return nil, nil, nil
}
if i, ok := t.col[tail[0]]; ok {
return t.fields[t.columns[i]], nil, nil
}
if child = t.descendant(tail[0]); child == nil {
return nil, nil, fmt.Errorf("no column or linked table %q in %s", tail[0], t.category)
}
return nil, child, nil
}
// readRow pins the row a path reads of t: the one its selector names, or, where the
// walk draws, one drawn where the path reads into the table.
func readRow(w pathWalk, t *table, sel string, draw bool) error {
if sel != "" {
return w.pins.selectRow(t, sel)
}
if draw && w.mode == walkDraw {
t.drawIn(w.draws.s, w.pins)
}
return nil
}
// variant is the variant of c the walk continues into: the one held for this level
// of the read, drawn once, where the walk holds its draws; else one drawn afresh.
func (d *pathDraws) variant(c *choice, rest []string) node {
if d.held == nil {
return pick(d.s, c)
}
key := d.a.levels[len(d.a.tail)-len(rest)]
n, drew := d.held.variant[key]
if !drew {
n = drawn(d.s, c)
if d.held.variant == nil {
d.held.variant = map[string]node{}
}
d.held.variant[key] = n
}
return n
}
// carriedByAll is the choice rule a path that must resolve on every call obeys:
// the rest of the tail must be one every variant carries.
func carriedByAll(c *choice, rest []string) error {
if want := strings.Join(rest, "."); !c.shared[want] {
return unreachableInChoice(c, want)
}
return nil
}
// unreachableInChoice reports that a path cannot step through this choice, listing
// what every variant does carry. It reads the precomputed set, so a failing path
// costs no more than a rendering one.
func unreachableInChoice(c *choice, want string) error {
if len(c.shared) == 0 {
return fmt.Errorf("no variant of this %d-way choice carries %q", len(c.items), want)
}
offered := make([]string, 0, len(c.shared))
for p := range c.shared {
offered = append(offered, p)
}
sort.Strings(offered)
return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered)
}
// checkPath proves a dotted tail resolves whichever way the draws go — a choice
// must carry the rest of the path in the set every variant shares, a selector must
// name one row inside the rows selected before it — and that no level a path reads
// carries a repeat or a drawGroup, which one draw of it could not apply. So a path
// that validates here resolves on every render, and a typo is a New-time error.
func checkPath(n node, tail []string, level string) error {
var pins pinSet
_, err := walkPath(n, tail, pathWalk{
mode: walkEvery,
level: func(t *template, rest []string) error {
name := join(level, strings.Join(tail[:len(tail)-len(rest)], "."))
switch {
case t.repeat > 1:
return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", name)
case t.drawGroup != "":
return fmt.Errorf("the level %q carries a drawGroup, which a path reading into it cannot apply", name)
}
return nil
},
pins: &pins,
})
return err
}