Add FakeTemplate: compile an inline template and render it against the loaded tree

This commit is contained in:
2026-09-03 17:27:54 +02:00
parent 5ba38a8281
commit 3bc3e35045
5 changed files with 166 additions and 73 deletions
+48 -25
View File
@@ -10,29 +10,36 @@ import (
// visiting keys in sorted order so which of several broken nodes gets reported does
// not depend on map iteration.
func walkNodes(root map[string]node, fn func(path string, n node) error) error {
seen := map[node]bool{}
var visit func(string, node) error
visit = func(path string, n node) error {
if n == nil || seen[n] {
return nil
}
seen[n] = true
if err := fn(path, n); err != nil {
for _, name := range sortedNames(root) {
if err := eachNode(root[name], name, fn); err != nil {
return err
}
for _, c := range contained(n) {
}
return nil
}
// eachNode visits n and every node contained within it once, passing the dot path
// that reaches each. It never crosses a reference edge — a bound {/path} field is
// skipped, as in walkNodes — so a single inline node is walked on its own.
func eachNode(n node, path string, fn func(path string, n node) error) error {
seen := map[node]bool{}
var visit func(string, node) error
visit = func(path string, m node) error {
if m == nil || seen[m] {
return nil
}
seen[m] = true
if err := fn(path, m); err != nil {
return err
}
for _, c := range contained(m) {
if err := visit(join(path, c.name), c.node); err != nil {
return err
}
}
return nil
}
for _, name := range sortedNames(root) {
if err := visit(name, root[name]); err != nil {
return err
}
}
return nil
return visit(path, n)
}
// namedNode is a contained child and the segment reaching it; a choice's items carry
@@ -162,30 +169,46 @@ func pathLeaves(n node, tail []string) []node {
// path, so nested repeats cannot build what one repeat may not. It runs after
// checkNoCycles, whose guarantee is what lets the walk terminate.
func checkRepeatReach(root map[string]node) error {
reach := map[node]int{}
var of func(n node) int
of = func(n node) int {
if r, done := reach[n]; done {
mem := reachMemo{}
return walkNodes(root, func(path string, n node) error {
return repeatCheck(path, n, mem)
})
}
// checkNodeRepeatReach is checkRepeatReach for one inline node: each template in
// it is bounded, following reference edges into the already-validated tree the same
// way a repeat through a reference multiplies along a path.
func checkNodeRepeatReach(n node) error {
mem := reachMemo{}
return eachNode(n, "template", func(path string, m node) error {
return repeatCheck(path, m, mem)
})
}
type reachMemo map[node]int
func (m reachMemo) of(n node) int {
if r, done := m[n]; done {
return r
}
r := 1
for _, e := range renderEdges(n) {
if c := of(e.to); c > r {
if c := m.of(e.to); c > r {
r = c
}
}
if t, ok := n.(*template); ok {
r *= t.repeat
}
reach[n] = r
m[n] = r
return r
}
return walkNodes(root, func(path string, n node) error {
if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > MaxRepeat {
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), MaxRepeat)
func repeatCheck(path string, n node, mem reachMemo) error {
if t, ok := n.(*template); ok && t.repeat > 1 && mem.of(n) > MaxRepeat {
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, mem.of(n), MaxRepeat)
}
return nil
})
}
// checkNoCycles rejects a reference cycle: a node whose rendering can reach itself
+13 -1
View File
@@ -15,6 +15,19 @@ import (
// It runs after checkNoCycles, whose guarantee is what lets the walk terminate.
func checkBoundLevelsHeld(root map[string]node) error {
return walkNodes(root, func(path string, n node) error {
return heldCheck(path, n)
})
}
// checkNodeBoundLevelsHeld is checkBoundLevelsHeld for one inline node: the same
// held fence, over its own templates and the shared tree they reference.
func checkNodeBoundLevelsHeld(n node) error {
return eachNode(n, "template", func(path string, m node) error {
return heldCheck(path, m)
})
}
func heldCheck(path string, n node) error {
t, ok := n.(*template)
if !ok || len(t.held) == 0 {
return nil
@@ -26,7 +39,6 @@ func checkBoundLevelsHeld(root map[string]node) error {
}
}
return nil
})
}
// heldHeads lists a template's held names, operand heads first, then paths, each
+12
View File
@@ -122,6 +122,18 @@ func (t *template) compileFormat() error {
return checkNoRepeatedRead(t.format, c, t.refs)
}
// compileInput compiles an inline template: a JSON value, or a bare format string
// when the input is not JSON. A format string is a template with no fields, so a
// string that is itself valid JSON (a JSON string literal) and a bare string
// compile alike.
func compileInput(input string) (node, error) {
var raw any
if err := json.Unmarshal([]byte(input), &raw); err != nil {
return compile(input)
}
return compile(raw)
}
func compileChoice(items []any) (node, error) {
if len(items) == 0 {
return nil, fmt.Errorf("empty choice")
+20
View File
@@ -72,6 +72,14 @@ func refSegments(name string, folder []string) ([]string, error) {
// error, never a random render-time one.
func linkRefs(root map[string]node) error {
return eachTemplate(root, func(folder []string, path string, t *template) error {
return linkTemplateRefs(folder, path, t, root)
})
}
// linkTemplateRefs binds one template's references against root. A template with
// none is left untouched, so an inline format that references nothing costs only
// the refTokens scan.
func linkTemplateRefs(folder []string, path string, t *template, root map[string]node) error {
names := refTokens(t.format)
if len(names) == 0 {
return nil
@@ -100,6 +108,18 @@ func linkRefs(root map[string]node) error {
return fmt.Errorf("%s: %w", path, err)
}
return nil
}
// linkNodeRefs binds the references in an inline node's templates against the
// loaded tree. The node has no folder of its own, so every sigil is root-relative:
// / and . mean the root, .. has no folder above it.
func linkNodeRefs(n node, root map[string]node) error {
return eachNode(n, "template", func(path string, m node) error {
t, ok := m.(*template)
if !ok {
return nil
}
return linkTemplateRefs(nil, path, t, root)
})
}
+26
View File
@@ -30,6 +30,32 @@ func (f *Generator) Fake(path string) (string, error) {
return render(f.rand, n), nil
}
// FakeTemplate renders an inline template — a format string or a JSON value — the
// way a category's data is compiled and rendered, references reaching the loaded
// tree with {/path}. It shares [Fake]'s lock: an inline node is compiled and
// referenced per draw, so a seeded sequence is reproducible when drawn from one
// goroutine.
func (f *Generator) FakeTemplate(input string) (string, error) {
f.mu.Lock()
defer f.mu.Unlock()
n, err := compileInput(input)
if err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
if err := linkNodeRefs(n, f.categories); err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
// No cycle is possible: a reference binds only into the loaded tree, which has
// no path into this node, so rendering it cannot reach itself.
if err := checkNodeRepeatReach(n); err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
if err := checkNodeBoundLevelsHeld(n); err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
return render(f.rand, n), nil
}
// descend walks named fields to the node a path names. It is the one render-side
// step that can fail, because the path comes from the caller and may name a field
// that does not exist. A choice consumes no segment, so the rest of the path must