Add inline templates to the CLI and library #4
@@ -10,29 +10,36 @@ import (
|
|||||||
// visiting keys in sorted order so which of several broken nodes gets reported does
|
// visiting keys in sorted order so which of several broken nodes gets reported does
|
||||||
// not depend on map iteration.
|
// not depend on map iteration.
|
||||||
func walkNodes(root map[string]node, fn func(path string, n node) error) error {
|
func walkNodes(root map[string]node, fn func(path string, n node) error) error {
|
||||||
seen := map[node]bool{}
|
for _, name := range sortedNames(root) {
|
||||||
var visit func(string, node) error
|
if err := eachNode(root[name], name, fn); err != nil {
|
||||||
visit = func(path string, n node) error {
|
|
||||||
if n == nil || seen[n] {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen[n] = true
|
|
||||||
if err := fn(path, n); err != nil {
|
|
||||||
return err
|
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 {
|
if err := visit(join(path, c.name), c.node); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, name := range sortedNames(root) {
|
return visit(path, n)
|
||||||
if err := visit(name, root[name]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// namedNode is a contained child and the segment reaching it; a choice's items carry
|
// 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
|
// path, so nested repeats cannot build what one repeat may not. It runs after
|
||||||
// checkNoCycles, whose guarantee is what lets the walk terminate.
|
// checkNoCycles, whose guarantee is what lets the walk terminate.
|
||||||
func checkRepeatReach(root map[string]node) error {
|
func checkRepeatReach(root map[string]node) error {
|
||||||
reach := map[node]int{}
|
mem := reachMemo{}
|
||||||
var of func(n node) int
|
return walkNodes(root, func(path string, n node) error {
|
||||||
of = func(n node) int {
|
return repeatCheck(path, n, mem)
|
||||||
if r, done := reach[n]; done {
|
})
|
||||||
return r
|
}
|
||||||
}
|
|
||||||
r := 1
|
// checkNodeRepeatReach is checkRepeatReach for one inline node: each template in
|
||||||
for _, e := range renderEdges(n) {
|
// it is bounded, following reference edges into the already-validated tree the same
|
||||||
if c := of(e.to); c > r {
|
// way a repeat through a reference multiplies along a path.
|
||||||
r = c
|
func checkNodeRepeatReach(n node) error {
|
||||||
}
|
mem := reachMemo{}
|
||||||
}
|
return eachNode(n, "template", func(path string, m node) error {
|
||||||
if t, ok := n.(*template); ok {
|
return repeatCheck(path, m, mem)
|
||||||
r *= t.repeat
|
})
|
||||||
}
|
}
|
||||||
reach[n] = r
|
|
||||||
|
type reachMemo map[node]int
|
||||||
|
|
||||||
|
func (m reachMemo) of(n node) int {
|
||||||
|
if r, done := m[n]; done {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
return walkNodes(root, func(path string, n node) error {
|
r := 1
|
||||||
if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > MaxRepeat {
|
for _, e := range renderEdges(n) {
|
||||||
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), MaxRepeat)
|
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
|
// 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.
|
// It runs after checkNoCycles, whose guarantee is what lets the walk terminate.
|
||||||
func checkBoundLevelsHeld(root map[string]node) error {
|
func checkBoundLevelsHeld(root map[string]node) error {
|
||||||
return walkNodes(root, func(path string, n node) error {
|
return walkNodes(root, func(path string, n node) error {
|
||||||
t, ok := n.(*template)
|
return heldCheck(path, n)
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// 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
|
// in name order: a level read both ways is reported by the operand's fence, and
|
||||||
// which overlap is reported does not vary.
|
// which overlap is reported does not vary.
|
||||||
|
|||||||
@@ -122,6 +122,18 @@ func (t *template) compileFormat() error {
|
|||||||
return checkNoRepeatedRead(t.format, c, t.refs)
|
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) {
|
func compileChoice(items []any) (node, error) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil, fmt.Errorf("empty choice")
|
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.
|
// error, never a random render-time one.
|
||||||
func linkRefs(root map[string]node) error {
|
func linkRefs(root map[string]node) error {
|
||||||
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
||||||
names := refTokens(t.format)
|
return linkTemplateRefs(folder, path, t, root)
|
||||||
if len(names) == 0 {
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
if t.fields == nil {
|
return linkTemplateRefs(nil, path, t, root)
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,32 @@ func (f *Generator) Fake(path string) (string, error) {
|
|||||||
return render(f.rand, n), nil
|
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
|
// 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
|
// 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
|
// that does not exist. A choice consumes no segment, so the rest of the path must
|
||||||
|
|||||||
Reference in New Issue
Block a user