181 lines
5.2 KiB
Go
181 lines
5.2 KiB
Go
package fakes
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// refPrefix marks a {..path} token: a reference to a node elsewhere in the data
|
|
// root rather than a sibling field. The path after it is the same dot path Fake
|
|
// takes, resolved across every loaded directory (see linkRefs).
|
|
const refPrefix = ".."
|
|
|
|
func isRef(name string) bool { return strings.HasPrefix(name, refPrefix) }
|
|
|
|
// linkRefs resolves every {..path} reference in the assembled tree, binding the
|
|
// target node into the referring template's fields under the token's key so the
|
|
// ordinary resolver renders it like a sibling. It runs once, after all data is
|
|
// merged, so a reference sees the final (override-resolved) tree. A path that is
|
|
// unknown, names a folder, or steps through a multi-variant choice fails here,
|
|
// keeping a bad reference a New-time error, never a random render-time one. The
|
|
// seen set skips shared nodes (and keeps this walk finite if data is cyclic);
|
|
// checkNoCycles, run next, is what rejects an actual reference cycle.
|
|
func linkRefs(root map[string]node) error {
|
|
seen := map[node]bool{}
|
|
var visit func(node) error
|
|
visit = func(n node) error {
|
|
if n == nil || seen[n] {
|
|
return nil
|
|
}
|
|
seen[n] = true
|
|
switch n := n.(type) {
|
|
case *group:
|
|
for _, c := range n.children {
|
|
if err := visit(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case *choice:
|
|
for _, it := range n.items {
|
|
if err := visit(it); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case *template:
|
|
for _, name := range refTokens(n.format) {
|
|
target, err := lookup(root, strings.Split(name[len(refPrefix):], "."))
|
|
if err != nil {
|
|
return fmt.Errorf("reference {%s}: %w", name, err)
|
|
}
|
|
n.fields[name] = target
|
|
}
|
|
for _, c := range n.fields {
|
|
if err := visit(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
for _, c := range root {
|
|
if err := visit(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// lookup finds the single node a reference path names, walking groups and
|
|
// template fields by segment and descending a single-variant choice as a
|
|
// transparent wrapper. A missing segment, a folder target, or a step through a
|
|
// multi-variant choice (which has no one value to bind) is an error.
|
|
func lookup(root map[string]node, segments []string) (node, error) {
|
|
var n node = &group{children: root}
|
|
for i := 0; i < len(segments); i++ {
|
|
switch c := n.(type) {
|
|
case *group:
|
|
child, ok := c.children[segments[i]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no entry %q", segments[i])
|
|
}
|
|
n = child
|
|
case *template:
|
|
child, ok := c.fields[segments[i]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no field %q", segments[i])
|
|
}
|
|
n = child
|
|
case *choice:
|
|
if len(c.items) != 1 {
|
|
return nil, fmt.Errorf("%q steps through a %d-way choice", segments[i], len(c.items))
|
|
}
|
|
n, i = c.items[0], i-1 // a choice consumes no segment; reprocess it unwrapped
|
|
default:
|
|
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[i])
|
|
}
|
|
}
|
|
if _, ok := n.(*group); ok {
|
|
return nil, fmt.Errorf("names a folder, not a value")
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// refTokens returns just the {..path} reference names among a format's field
|
|
// tokens (linkRefs binds each into the template's fields).
|
|
func refTokens(format string) []string {
|
|
var refs []string
|
|
for _, name := range fieldTokens(format) {
|
|
if isRef(name) {
|
|
refs = append(refs, name)
|
|
}
|
|
}
|
|
return refs
|
|
}
|
|
|
|
// renderEdge is a child a node renders into, labelled by the token that reaches it
|
|
// (a field name, reference, or choice index) for a readable cycle report.
|
|
type renderEdge struct {
|
|
to node
|
|
label string
|
|
}
|
|
|
|
// renderEdges lists the children rendering n recurses into, mirroring expand: a
|
|
// choice's items, and a template's field/reference tokens plus its calc operands.
|
|
// A literal or group renders nothing, so it has no edges.
|
|
func renderEdges(n node) []renderEdge {
|
|
switch n := n.(type) {
|
|
case *choice:
|
|
es := make([]renderEdge, len(n.items))
|
|
for i, it := range n.items {
|
|
es[i] = renderEdge{it, fmt.Sprintf("[%d]", i)}
|
|
}
|
|
return es
|
|
case *template:
|
|
var es []renderEdge
|
|
for _, name := range append(fieldTokens(n.format), calcOperands(n.format)...) {
|
|
if c, ok := n.fields[name]; ok {
|
|
es = append(es, renderEdge{c, name})
|
|
}
|
|
}
|
|
return es
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// checkNoCycles rejects a reference cycle: a node whose rendering can reach itself
|
|
// — directly, mutually, or through a chain — never terminates, so it must fail at
|
|
// New rather than stack-overflow at render. It is a depth-first walk of the render
|
|
// graph (renderEdges); grey marks nodes on the current path so a back-edge to one
|
|
// is the cycle, while black lets a shared node (a DAG, not a cycle) be skipped.
|
|
func checkNoCycles(root map[string]node) error {
|
|
const (
|
|
grey = 1
|
|
black = 2
|
|
)
|
|
color := map[node]int{}
|
|
var visit func(n node, path string) error
|
|
visit = func(n node, path string) error {
|
|
switch color[n] {
|
|
case grey:
|
|
return fmt.Errorf("reference cycle: %s", path)
|
|
case black:
|
|
return nil
|
|
}
|
|
color[n] = grey
|
|
for _, e := range renderEdges(n) {
|
|
if err := visit(e.to, path+" -> "+e.label); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
color[n] = black
|
|
return nil
|
|
}
|
|
for name, c := range root {
|
|
if err := visit(c, name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|