Add FakeTemplate: compile an inline template and render it against the loaded tree
This commit is contained in:
@@ -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 {
|
||||
return r
|
||||
}
|
||||
r := 1
|
||||
for _, e := range renderEdges(n) {
|
||||
if c := of(e.to); c > r {
|
||||
r = c
|
||||
}
|
||||
}
|
||||
if t, ok := n.(*template); ok {
|
||||
r *= t.repeat
|
||||
}
|
||||
reach[n] = r
|
||||
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
|
||||
}
|
||||
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)
|
||||
r := 1
|
||||
for _, e := range renderEdges(n) {
|
||||
if c := m.of(e.to); c > r {
|
||||
r = c
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if t, ok := n.(*template); ok {
|
||||
r *= t.repeat
|
||||
}
|
||||
m[n] = r
|
||||
return r
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -15,20 +15,32 @@ 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 {
|
||||
t, ok := n.(*template)
|
||||
if !ok || len(t.held) == 0 {
|
||||
return nil
|
||||
}
|
||||
readers := boundReaders(t.format, t.bound, t.refs)
|
||||
for _, head := range heldHeads(t) {
|
||||
if err := checkHeadHeld(t, head, readers); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
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
|
||||
}
|
||||
readers := boundReaders(t.format, t.bound, t.refs)
|
||||
for _, head := range heldHeads(t) {
|
||||
if err := checkHeadHeld(t, head, readers); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// heldHeads lists a template's held names, operand heads first, then paths, each
|
||||
// in name order: a level read both ways is reported by the operand's fence, and
|
||||
// which overlap is reported does not vary.
|
||||
|
||||
@@ -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")
|
||||
|
||||
+46
-26
@@ -72,34 +72,54 @@ 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 {
|
||||
names := refTokens(t.format)
|
||||
if len(names) == 0 {
|
||||
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
|
||||
}
|
||||
if t.fields == nil {
|
||||
t.fields = map[string]node{}
|
||||
}
|
||||
t.refs = make(map[string]refBinding, len(names))
|
||||
for _, name := range names {
|
||||
segments, err := refSegments(name, folder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
head, target, tail, err := resolveRef(root, segments)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
key := "/" + strings.Join(head, ".")
|
||||
if err := checkPath(target, tail, key); err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
t.fields[key] = target
|
||||
t.refs[name] = refBinding{key, tail}
|
||||
}
|
||||
if err := t.compileFormat(); err != nil {
|
||||
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
|
||||
}
|
||||
if t.fields == nil {
|
||||
t.fields = map[string]node{}
|
||||
}
|
||||
t.refs = make(map[string]refBinding, len(names))
|
||||
for _, name := range names {
|
||||
segments, err := refSegments(name, folder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
head, target, tail, err := resolveRef(root, segments)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
key := "/" + strings.Join(head, ".")
|
||||
if err := checkPath(target, tail, key); err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
t.fields[key] = target
|
||||
t.refs[name] = refBinding{key, tail}
|
||||
}
|
||||
if err := t.compileFormat(); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
return linkTemplateRefs(nil, path, t, root)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user