Files
fejkdata/resolve.go
T

133 lines
4.3 KiB
Go

package fejkdata
import (
"fmt"
"github.com/larvit/fejkdata/internal/grammar"
)
// templateSite is a template to resolve, with its folder and category, both empty for an inline
// template, and eachNode's label for it.
type templateSite struct {
t *template
folder []string
category, label string
}
// resolveCategoryTemplates resolves every template of the categories, once all data is merged, so a
// reference sees the override-resolved tree.
func resolveCategoryTemplates(sites []categorySite, root map[string]node) error {
var ts []templateSite
for _, s := range sites {
if err := eachNode(s.n, s.path, func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
ts = append(ts, templateSite{t: t, folder: s.dir, category: s.path, label: label})
}
return nil
}); err != nil {
return err
}
}
return resolveTemplates(ts, root)
}
func resolveInlineTemplates(nodes nodeSet, root map[string]node) error {
var ts []templateSite
if err := nodes(func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
ts = append(ts, templateSite{t: t, label: label})
}
return nil
}); err != nil {
return err
}
return resolveTemplates(ts, root)
}
// resolvedNames is what resolveTemplates resolved about the names of a set of templates: each binding's
// target, and the keys its reads address.
type resolvedNames struct {
targets map[*nameBinding]nameTarget
addressed map[*nameBinding]map[pickKey]string
}
// resolveTemplates resolves ts in steps, each over every template before the next starts. The
// names' targets, keys and reads pass to later steps as arguments, and the bindings take their
// targets and keys only once every check has passed.
func resolveTemplates(ts []templateSite, root map[string]node) error {
for _, s := range ts {
refs, err := s.t.resolveRefs(s.folder, s.label, s.category, root)
if err != nil {
return err
}
s.t.refs = refs
}
targets := nameTargets(ts)
for _, s := range ts {
compiled, err := compileFormat(s.t, targets)
if err != nil {
return fmt.Errorf("%s: %w", s.label, err)
}
s.t.compiled = compiled
}
names := resolvedNames{targets: targets, addressed: addressedKeys(ts, targets)}
for _, s := range ts {
s.t.readsColumn = columnReadOf(s.t, targets)
}
for _, s := range ts {
if err := checkNameReads(s.label, s.t, names); err != nil {
return err
}
}
for _, s := range ts {
if err := checkCalcNames(s.label, s.t); err != nil {
return err
}
}
for b, target := range targets {
b.target, b.addressed = target, names.addressed[b]
}
return nil
}
// columnRead is a record's column read by a format that only reads one reference or name,
// which is the column: it takes the column's datatype and null. category and field name it;
// category is "" for a column of the reading template's own record, which checkColumns reaches
// on its own.
type columnRead struct {
a arm
category, field string
column node
}
// columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not.
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
ops := t.compiled.ops
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
return nil
}
a := ops[0].arms[0]
var start node
var category string
var tail []string
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
// {f as n}{n}: column f of the record binding n, in t's own category.
start, tail = b.binder, append(append([]string{splitArm(b.ref, nil).head}, targets[b].tail...), a.tail...)
case a.kind == namedRead:
// {/c as n}{n.x} or {/c.x as n}{n}: column x of category c.
start, category = targets[b].start, categoryOf(b.binder.refs.byName[b.ref].head)
tail = append(targets[b].tail[:len(targets[b].tail):len(targets[b].tail)], a.tail...)
case grammar.IsRef(a.head):
// {/c.x}: column x of category c.
start, category, tail = t.startOf(a.head), categoryOf(a.head), a.tail
default:
return nil
}
if target, isTemplate := start.(*template); isTemplate && target.isRecord && len(tail) == 1 {
return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}