diff --git a/builtins.go b/builtins.go index a26405a..bf1889b 100644 --- a/builtins.go +++ b/builtins.go @@ -24,13 +24,11 @@ const ( // clock. Add a builtin only for what data can't express: a random v4 UUID and a // 24-hex ObjectID both ship as data, so the uuid builtin is v7. var builtins = map[string]builtin{ - "luhn": {arity: 0, call: func(_ *session, e string, _ map[string]node, _ []string) string { - return string(rune('0' + luhnCheck(e))) - }}, - "mod11": {arity: 0, call: func(_ *session, e string, _ map[string]node, _ []string) string { return mod11Check(e) }}, - "ean": {arity: 0, call: func(_ *session, e string, _ map[string]node, _ []string) string { return eanCheck(e) }}, - "uuid": {arity: 0, call: func(s *session, _ string, _ map[string]node, _ []string) string { return uuidV7(s) }}, - "ulid": {arity: 0, call: func(s *session, _ string, _ map[string]node, _ []string) string { return ulid(s) }}, + "luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })}, + "mod11": {arity: 0, prep: derive(mod11Check)}, + "ean": {arity: 0, prep: derive(eanCheck)}, + "uuid": {arity: 0, prep: generate(uuidV7)}, + "ulid": {arity: 0, prep: generate(ulid)}, "nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn { n := atoi(a[0]) return func(s *session, _ string, _ map[string]node) string { return nanoid(s, n) } @@ -57,7 +55,10 @@ var builtins = map[string]builtin{ return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64) } }}, - "iban": {arity: 1, check: ibanArg, call: func(s *session, _ string, _ map[string]node, a []string) string { return iban(s, a[0]) }}, + "iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn { + cc := a[0] + return func(s *session, _ string, _ map[string]node) string { return iban(s, cc) } + }}, // calc is the one builtin that reads the sibling fields (to render its operands); // every other ignores them. 1 or 2 args: the expression and an optional decimals. "calc": {arity: -1, check: checkCalc, prep: calcPrep}, @@ -75,6 +76,21 @@ var builtins = map[string]builtin{ }}, } +// derive and generate are the two argument-free builtin shapes the README names: a +// derivation reads the output emitted so far, a generator reads only the rng. Each +// lifts that one function into the prep every registry entry supplies. +func derive(f func(emitted string) string) func([]string) callFn { + return func([]string) callFn { + return func(_ *session, emitted string, _ map[string]node) string { return f(emitted) } + } +} + +func generate(f func(rng) string) func([]string) callFn { + return func([]string) callFn { + return func(s *session, _ string, _ map[string]node) string { return f(s) } + } +} + const hexDigits = "0123456789abcdef" // atoi parses an arg already validated by a builtin's check, so it cannot fail. diff --git a/builtins_test.go b/builtins_test.go index 3fe6e6b..0a41f3a 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -181,18 +181,14 @@ func ibanValid(s string) bool { return rem == 1 } -// TestRegistryShapes pins the builtin contract bind relies on: exactly one of prep -// or call, and args parsed at compile only behind a check. A registry entry with -// neither would compile and then panic inside a render. +// TestRegistryShapes pins the builtin contract compileOps relies on: every entry +// supplies prep, and args parsed at compile only behind a check. func TestRegistryShapes(t *testing.T) { for name, b := range builtins { - switch { - case b.prep == nil && b.call == nil: - t.Errorf("builtin %q has neither prep nor call: bind would return a nil-func closure", name) - case b.prep != nil && b.call != nil: - t.Errorf("builtin %q has both prep and call; call is dead", name) + if b.prep == nil { + t.Errorf("builtin %q has no prep: compileOps would call a nil func", name) } - if b.prep != nil && b.arity != 0 && b.check == nil { + if b.arity != 0 && b.check == nil { t.Errorf("builtin %q parses args in prep with no check", name) } } diff --git a/template.go b/template.go index 1d660a2..06c2036 100644 --- a/template.go +++ b/template.go @@ -67,15 +67,12 @@ func eachToken(format string, fn func(ftoken) error) error { // seq is the one exception: it advances per-session counter state, which is itself // deterministic (1, 2, 3 …). arity is the exact arg count, or -1 for variadic // (then check does all the validation). The optional check validates args at -// compile time (their values, beyond the count). A builtin supplies exactly one of -// prep (args parsed once, at compile) or call (args parsed per render). The registry -// lives in builtins.go. +// compile time (their values, beyond the count). The registry lives in builtins.go. type builtin struct { arity int // prep parses validated args once, at compile time, into the closure expand calls. prep func(args []string) callFn check func(fields map[string]node, args []string) error - call func(s *session, emitted string, fields map[string]node, args []string) string } // funcCall splits a "{token}" body shaped name(args) into its parts; ok is false @@ -327,7 +324,7 @@ func compileOps(format string) ([]op, int, map[string]string) { case 'b': flush() if name, args, ok := funcCall(t.body); ok { - ops = append(ops, op{kind: 'b', call: builtins[name].bind(args)}) + ops = append(ops, op{kind: 'b', call: builtins[name].prep(args)}) } else { arms := splitArms(t.body) for _, a := range arms { @@ -348,14 +345,3 @@ func compileOps(format string) ([]op, int, map[string]string) { flush() return ops, grow, bound } - -// bind returns the closure for one call site, parsing args once via prep if present. -func (b builtin) bind(args []string) callFn { - if b.prep != nil { - return b.prep(args) - } - call := b.call - return func(s *session, emitted string, fields map[string]node) string { - return call(s, emitted, fields, args) - } -}