98 lines
3.5 KiB
Go
98 lines
3.5 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Template is an inline template compiled, referenced and validated against a
|
|
// generator's loaded data once, ready to render many times with [Template.Fake].
|
|
// It is safe for concurrent use: Fake serializes on its generator's lock, so a
|
|
// seeded sequence is reproducible only when a generator — and its templates — are
|
|
// drawn from one goroutine.
|
|
type Template struct {
|
|
g *Generator
|
|
n node
|
|
}
|
|
|
|
// Fake renders the template with one draw.
|
|
func (t *Template) Fake() string {
|
|
t.g.mu.Lock()
|
|
defer t.g.mu.Unlock()
|
|
return render(t.g.rand, t.n)
|
|
}
|
|
|
|
// NewTemplate compiles an inline template — a format string or a JSON value — and
|
|
// binds its references against the loaded tree, so repeated renders pay the
|
|
// compile and validation once. It shares [New]'s guarantees: a bad template errors
|
|
// here, and rendering cannot fail.
|
|
//
|
|
// checkNoCycles is the one fence loadData runs that an inline node does not need:
|
|
// the loaded tree is proven acyclic at [New], the node is a finite tree, and no
|
|
// tree node can reference it, so nothing it renders can reach itself.
|
|
func (f *Generator) NewTemplate(input string) (*Template, error) {
|
|
n, err := compileInput(input)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
|
}
|
|
if err := linkNodeRefs(n, f.categories); err != nil {
|
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
|
}
|
|
if err := checkScope(inlineScope(n)); err != nil {
|
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
|
}
|
|
return &Template{g: f, n: n}, nil
|
|
}
|
|
|
|
// FakeTemplate compiles and renders an inline template in one call. It is
|
|
// [NewTemplate] then [Template.Fake]; to render the same template many times, hold
|
|
// the *Template and call its Fake.
|
|
func (f *Generator) FakeTemplate(input string) (string, error) {
|
|
t, err := f.NewTemplate(input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return t.Fake(), nil
|
|
}
|
|
|
|
// compileInput compiles an inline template: a JSON value, or a bare format string
|
|
// when the input is not JSON. A JSON string literal and a bare string compile
|
|
// alike (both are a template with no fields); the other JSON scalars — a number,
|
|
// bool or null — are no template, so they are rejected here, as a data file that
|
|
// was one would be at load. Padding is where the two readings would disagree — a
|
|
// format string renders it, JSON drops it — so a padded JSON value is rejected
|
|
// naming the one that renders.
|
|
func compileInput(input string) (node, error) {
|
|
var raw any
|
|
if err := json.Unmarshal([]byte(input), &raw); err != nil {
|
|
return compile(input)
|
|
}
|
|
if trimmed := strings.TrimSpace(input); trimmed != input {
|
|
return nil, fmt.Errorf("a JSON template may not be padded with spaces, which a format string would render; write %s", trimmed)
|
|
}
|
|
return compile(raw)
|
|
}
|
|
|
|
// linkNodeRefs binds the references in an inline node's templates against the
|
|
// loaded tree. An inline template sits in no folder, so . and .. name nothing and
|
|
// are rejected for the root spelling they would otherwise silently mean.
|
|
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
|
|
}
|
|
for _, name := range refTokens(t.format) {
|
|
sigil, rest, err := refShape(name)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
|
}
|
|
if sigil != "/" {
|
|
return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest)
|
|
}
|
|
}
|
|
return linkTemplateRefs(nil, path, t, root)
|
|
})
|
|
}
|