diff --git a/graph.go b/graph.go index 887c0ec..a5b8868 100644 --- a/graph.go +++ b/graph.go @@ -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 diff --git a/hold.go b/hold.go index bb5c3fe..53a0a8f 100644 --- a/hold.go +++ b/hold.go @@ -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. diff --git a/node.go b/node.go index fb6aee9..d228839 100644 --- a/node.go +++ b/node.go @@ -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") diff --git a/reference.go b/reference.go index 664521b..1327d83 100644 --- a/reference.go +++ b/reference.go @@ -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) }) } diff --git a/render.go b/render.go index 2d998fb..4d35b8c 100644 --- a/render.go +++ b/render.go @@ -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