Files
fejkdata/draw.go
T

271 lines
7.4 KiB
Go

package fejkdata
import (
"fmt"
"sort"
"strings"
)
// drawSet is one render's reference draws: the unnamed group's, and each named group's.
type drawSet struct {
unnamed draws
named map[string]*draws
}
// drawScope is where a render reads its reference paths: a draw set, in the group of the
// template rendering.
type drawScope struct {
set *drawSet
group string
}
// 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.
func newDrawSet() drawSet {
return drawSet{unnamed: draws{variant: map[string]node{}, value: map[string]draw{}}}
}
// renderOnce renders n as one render, over draws of its own.
func renderOnce(s *session, n node) string {
var set drawSet
return render(s, n, drawScope{set: &set})
}
// in is the scope t renders in: the group it names, else its caller's.
func (sc drawScope) in(t *template) drawScope {
if t.drawGroup != "" {
sc.group = t.drawGroup
}
return sc
}
// draws is the set's draws for sc's group.
func (sc drawScope) draws() *draws {
if sc.group == "" {
return &sc.set.unnamed
}
d, drew := sc.set.named[sc.group]
if !drew {
if sc.set.named == nil {
sc.set.named = map[string]*draws{}
}
d = &draws{variant: map[string]node{}, value: map[string]draw{}}
sc.set.named[strings.Clone(sc.group)] = d // a key from sc would leak sc, and with it every render's draw set, to the heap
}
return d
}
// groupOf reads a template's "group" (default "").
func groupOf(m map[string]any) (string, error) {
v, ok := m["group"]
if !ok {
return "", nil
}
name, ok := v.(string)
switch {
case !ok:
return "", fmt.Errorf("group must be a string, got %T", v)
case name == "":
return "", fmt.Errorf(`group "" is the default, so it has no effect; drop it`)
}
return name, nil
}
// drawCheck fences each template of a scope as a render of its own, remembering which nodes
// read a reference path.
type drawCheck struct {
reads map[node]bool
}
func (c *drawCheck) check(path string, n node) error {
t, ok := n.(*template)
switch {
case !ok:
return nil
case !c.readsPath(t) && t.drawGroup != "":
return fmt.Errorf("%s: group %q splits nothing, since nothing it renders reads a reference path; drop it", path, t.drawGroup)
case !c.readsPath(t):
return nil
}
w := newDrawWalk(nil)
for _, e := range renderEdges(t) {
w.edge(t, e, t.drawGroup, drawRoute{e.reached(), e.label}, "", false)
}
if err := w.check(); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
return nil
}
// readsPath reports whether rendering n reads a reference path, however deep.
func (c *drawCheck) readsPath(n node) bool {
if r, done := c.reads[n]; done {
return r
}
r := false
for _, e := range renderEdges(n) {
if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || c.readsPath(e.to) {
r = true
break
}
}
if c.reads == nil {
c.reads = map[node]bool{}
}
c.reads[n] = r
return r
}
// refRead is the reference an edge of n reads, and the node it is bound to; false when the
// edge reads none.
func refRead(n node, label string) (arm, node, bool) {
t, isTemplate := n.(*template)
if !isTemplate {
return arm{}, nil, false
}
a := splitArm(label, t.refs)
return a, t.fields[a.key], isRef(a.key)
}
// checkColumnDraws fences a record's columns as one render.
func checkColumnDraws(t *template, columns []string) error {
w := newDrawWalk(t)
for _, name := range columns {
w.walk(t.fields[name], t.drawGroup, drawRoute{spelling: fmt.Sprintf("column %q", name)}, "", false)
}
return w.check()
}
// drawWalk gathers what one render reads through its draws and what it draws afresh, each by
// group, for check to compare. record is set for a record's columns, which may not read the
// record back.
type drawWalk struct {
record *template
reads []pathRead
read map[drawKey]bool
fresh []freshDraw
seen map[drawVisit]bool
err error
}
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
type drawRoute struct{ spelling, label string }
type pathRead struct {
group string
route drawRoute
a arm
target node
}
type freshDraw struct {
group string
route drawRoute
via string
n node
}
type drawVisit struct {
n node
group string
held bool
}
type drawKey struct {
group string
key any
}
func newDrawWalk(record *template) *drawWalk {
return &drawWalk{record: record, read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
}
// walk follows what rendering n renders. held says n renders inside a reference path's draw, so
// what it draws belongs to that draw; a repeat renders over draws of its own, so the walk stops there.
func (w *drawWalk) walk(n node, group string, route drawRoute, via string, held bool) {
v := drawVisit{n, group, held}
if w.seen[v] || w.err != nil {
return
}
w.seen[v] = true
if !held {
w.fresh = append(w.fresh, freshDraw{group, route, via, n})
}
t, isTemplate := n.(*template)
if isTemplate && t.repeat > 1 {
return
}
if isTemplate && t.drawGroup != "" {
group = t.drawGroup
}
for _, e := range renderEdges(n) {
w.edge(n, e, group, route, via, held)
}
}
func (w *drawWalk) edge(from node, e renderEdge, group string, route drawRoute, via string, held bool) {
a, target, reads := refRead(from, e.label)
switch {
case !reads:
w.walk(e.to, group, route, via, held)
case w.record != nil && target == node(w.record):
w.err = fmt.Errorf("%s reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", route.spelling, a.name)
case len(a.tail) > 0:
if k := (drawKey{group, a.path}); !w.read[k] {
w.read[k] = true
w.reads = append(w.reads, pathRead{group, route, a, target})
}
w.walk(e.to, group, route, a.name, true)
default:
w.walk(e.to, group, route, a.name, false)
}
}
// check refuses what one draw per reference path cannot answer for: a path read into a level
// another read renders, and a node drawn afresh beside a path whose draw holds it.
func (w *drawWalk) check() error {
if w.err != nil {
return w.err
}
sort.SliceStable(w.reads, func(i, j int) bool {
if w.reads[i].group != w.reads[j].group {
return w.reads[i].group < w.reads[j].group
}
return w.reads[i].a.path < w.reads[j].a.path
})
pins := map[drawKey]pathRead{}
for i, level := range w.reads {
for _, into := range w.reads[i+1:] {
if into.group == level.group && strings.HasPrefix(into.a.path, level.a.path+".") {
return overlap(level.route, level.a.name, into)
}
}
held := map[node]bool{}
coverPath(level.target, level.a.tail, held)
for n := range held {
if _, pinned := pins[drawKey{level.group, n}]; !pinned {
pins[drawKey{level.group, n}] = level
}
}
}
for _, f := range w.fresh {
if r, pinned := pins[drawKey{f.group, f.n}]; pinned {
return overlap(f.route, f.via, r)
}
}
return nil
}
func overlap(route drawRoute, ref string, into pathRead) error {
return fmt.Errorf("%s renders a level that %s reads a path into; name the fields you want instead, or draw them apart with a group", route.spelled(ref), into.route.spelled(into.a.name))
}
// spelled names the route, and the reference it reaches a draw by where its root edge is not
// that reference.
func (r drawRoute) spelled(ref string) string {
if ref == "" || ref == r.label {
return r.spelling
}
return fmt.Sprintf("%s with {%s}", r.spelling, ref)
}