Add session-scoped seq() builtin; move uuid v4 from locales to misc only
This commit is contained in:
@@ -162,7 +162,7 @@ Each shipped locale carries these categories, formatted per locale (e.g. `date`
|
|||||||
is `MM/DD/YYYY` in `en_US`, `YYYY-MM-DD` in `sv_SE`; `ssn` is a US SSN vs a
|
is `MM/DD/YYYY` in `en_US`, `YYYY-MM-DD` in `sv_SE`; `ssn` is a US SSN vs a
|
||||||
Swedish personnummer): `address`, `color`, `company`, `date`, `email`, `ip`,
|
Swedish personnummer): `address`, `color`, `company`, `date`, `email`, `ip`,
|
||||||
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
||||||
`uuid`, `version`, `word`.
|
`version`, `word`.
|
||||||
|
|
||||||
`data/misc` carries locale-neutral categories: `uuid` (a proper random v4),
|
`data/misc` carries locale-neutral categories: `uuid` (a proper random v4),
|
||||||
`mac`, and `creditcard` (per-network numbers ending in a valid `{luhn()}` digit).
|
`mac`, and `creditcard` (per-network numbers ending in a valid `{luhn()}` digit).
|
||||||
@@ -230,14 +230,15 @@ form prefixes the century outside the checksummed core:
|
|||||||
"core": { "format": "00{mmdd}-000{luhn()}", "mmdd": [ … ] } }
|
"core": { "format": "00{mmdd}-000{luhn()}", "mmdd": [ … ] } }
|
||||||
```
|
```
|
||||||
|
|
||||||
A function must be deterministic in the seeded rng (no wall-clock), so a seeded
|
A function must be deterministic (no wall-clock), so a seeded faker stays
|
||||||
faker stays reproducible. A time-based id (UUID v7, ULID) therefore draws its
|
reproducible. A time-based id (UUID v7, ULID) therefore draws its timestamp from
|
||||||
timestamp from the rng, not the clock — the result is a valid, reproducible value,
|
the rng, not the clock — the result is a valid, reproducible value, not a real
|
||||||
not a real point in time.
|
point in time.
|
||||||
|
|
||||||
There are two kinds. **Derivations** read the digits emitted so far, so put them
|
There are three kinds. **Derivations** read the digits emitted so far, so put them
|
||||||
after their payload; **generators** read only the rng, so they stand alone.
|
after their payload; **generators** read only the rng, so they stand alone; one
|
||||||
Arguments are validated at `New` (a bad count, range, or country fails fast).
|
**session counter** (`seq`) advances state held on the faker. Arguments are
|
||||||
|
validated at `New` (a bad count, range, or country fails fast).
|
||||||
|
|
||||||
| Function | Kind | Emits |
|
| Function | Kind | Emits |
|
||||||
|----------|------|-------|
|
|----------|------|-------|
|
||||||
@@ -253,6 +254,7 @@ Arguments are validated at `New` (a bad count, range, or country fails fast).
|
|||||||
| `{int(min,max)}` | generator | uniform integer in `[min, max]` |
|
| `{int(min,max)}` | generator | uniform integer in `[min, max]` |
|
||||||
| `{float(min,max,dp)}` | generator | number in `[min, max]` with `dp` decimals |
|
| `{float(min,max,dp)}` | generator | number in `[min, max]` with `dp` decimals |
|
||||||
| `{iban(CC)}` | generator | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
|
| `{iban(CC)}` | generator | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
|
||||||
|
| `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this faker's sequence; `name` selects an independent counter |
|
||||||
|
|
||||||
`{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979
|
`{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979
|
||||||
prefix in data and call `{ean()}`). `{iban()}` is a generator, not a derivation:
|
prefix in data and call `{ean()}`). `{iban()}` is a generator, not a derivation:
|
||||||
@@ -260,6 +262,10 @@ an IBAN's check digits sit *before* the account number, which a left-to-right
|
|||||||
reader can't reach, so it emits the whole value (a generic numeric BBAN — valid
|
reader can't reach, so it emits the whole value (a generic numeric BBAN — valid
|
||||||
length and checksum, not real bank routing).
|
length and checksum, not real bank routing).
|
||||||
|
|
||||||
|
`{seq()}`'s counter lives on the faker, so it spans `Fake` calls (and `repeat`)
|
||||||
|
and resets when you build a new faker — `seq` is reproducible by being ordered,
|
||||||
|
not random. It's the natural fit for a primary-key column in the SQL example above.
|
||||||
|
|
||||||
**References.** A `{..path}` token renders a node from the **data root** instead
|
**References.** A `{..path}` token renders a node from the **data root** instead
|
||||||
of a sibling field — the dot path is the one `Fake` takes, resolved across every
|
of a sibling field — the dot path is the one `Fake` takes, resolved across every
|
||||||
loaded directory. One category can borrow another, even across folders or layered
|
loaded directory. One category can borrow another, even across folders or layered
|
||||||
|
|||||||
+35
-15
@@ -15,22 +15,32 @@ import (
|
|||||||
// the wall clock. Add a builtin only for what data can't express: a random v4
|
// the wall clock. Add a builtin only for what data can't express: a random v4
|
||||||
// UUID already ships as data (data/misc/uuid.json), so the builtin is v7.
|
// UUID already ships as data (data/misc/uuid.json), so the builtin is v7.
|
||||||
var builtins = map[string]builtin{
|
var builtins = map[string]builtin{
|
||||||
"luhn": {arity: 0, call: func(_ rng, e string, _ []string) string { return string(rune('0' + luhnCheck(e))) }},
|
"luhn": {arity: 0, call: func(_ *session, e string, _ []string) string { return string(rune('0' + luhnCheck(e))) }},
|
||||||
"mod11": {arity: 0, call: func(_ rng, e string, _ []string) string { return mod11Check(e) }},
|
"mod11": {arity: 0, call: func(_ *session, e string, _ []string) string { return mod11Check(e) }},
|
||||||
"ean": {arity: 0, call: func(_ rng, e string, _ []string) string { return eanCheck(e) }},
|
"ean": {arity: 0, call: func(_ *session, e string, _ []string) string { return eanCheck(e) }},
|
||||||
"uuid": {arity: 0, call: func(r rng, _ string, _ []string) string { return uuidV7(r) }},
|
"uuid": {arity: 0, call: func(s *session, _ string, _ []string) string { return uuidV7(s) }},
|
||||||
"ulid": {arity: 0, call: func(r rng, _ string, _ []string) string { return ulid(r) }},
|
"ulid": {arity: 0, call: func(s *session, _ string, _ []string) string { return ulid(s) }},
|
||||||
"objectid": {arity: 0, call: func(r rng, _ string, _ []string) string { return randHex(r, 24) }},
|
"objectid": {arity: 0, call: func(s *session, _ string, _ []string) string { return randHex(s, 24) }},
|
||||||
"nanoid": {arity: 1, check: posIntArg, call: func(r rng, _ string, a []string) string { return nanoid(r, atoi(a[0])) }},
|
"nanoid": {arity: 1, check: posIntArg, call: func(s *session, _ string, a []string) string { return nanoid(s, atoi(a[0])) }},
|
||||||
"hex": {arity: 1, check: posIntArg, call: func(r rng, _ string, a []string) string { return randHex(r, atoi(a[0])) }},
|
"hex": {arity: 1, check: posIntArg, call: func(s *session, _ string, a []string) string { return randHex(s, atoi(a[0])) }},
|
||||||
"base64": {arity: 1, check: posIntArg, call: func(r rng, _ string, a []string) string {
|
"base64": {arity: 1, check: posIntArg, call: func(s *session, _ string, a []string) string {
|
||||||
return base64.StdEncoding.EncodeToString(randBytes(r, atoi(a[0])))
|
return base64.StdEncoding.EncodeToString(randBytes(s, atoi(a[0])))
|
||||||
}},
|
}},
|
||||||
"int": {arity: 2, check: intRangeArgs, call: func(r rng, _ string, a []string) string {
|
"int": {arity: 2, check: intRangeArgs, call: func(s *session, _ string, a []string) string {
|
||||||
return strconv.Itoa(atoi(a[0]) + r.IntN(atoi(a[1])-atoi(a[0])+1))
|
return strconv.Itoa(atoi(a[0]) + s.IntN(atoi(a[1])-atoi(a[0])+1))
|
||||||
}},
|
}},
|
||||||
"float": {arity: 3, check: floatArgs, call: floatCall},
|
"float": {arity: 3, check: floatArgs, call: floatCall},
|
||||||
"iban": {arity: 1, check: ibanArg, call: func(r rng, _ string, a []string) string { return iban(r, a[0]) }},
|
"iban": {arity: 1, check: ibanArg, call: func(s *session, _ string, a []string) string { return iban(s, a[0]) }},
|
||||||
|
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
||||||
|
// each call. An optional name selects an independent counter; no name uses the
|
||||||
|
// default one. Deterministic by construction, so a seeded faker stays stable.
|
||||||
|
"seq": {arity: -1, check: seqArg, call: func(s *session, _ string, a []string) string {
|
||||||
|
key := ""
|
||||||
|
if len(a) == 1 {
|
||||||
|
key = a[0]
|
||||||
|
}
|
||||||
|
return strconv.FormatUint(s.next(key), 10)
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
const hexDigits = "0123456789abcdef"
|
const hexDigits = "0123456789abcdef"
|
||||||
@@ -89,10 +99,20 @@ func floatArgs(a []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func floatCall(r rng, _ string, a []string) string {
|
func floatCall(s *session, _ string, a []string) string {
|
||||||
lo, _ := strconv.ParseFloat(a[0], 64)
|
lo, _ := strconv.ParseFloat(a[0], 64)
|
||||||
hi, _ := strconv.ParseFloat(a[1], 64)
|
hi, _ := strconv.ParseFloat(a[1], 64)
|
||||||
return strconv.FormatFloat(lo+r.Float64()*(hi-lo), 'f', atoi(a[2]), 64)
|
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', atoi(a[2]), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func seqArg(a []string) error {
|
||||||
|
if len(a) > 1 {
|
||||||
|
return fmt.Errorf("seq takes at most one name, got %d args", len(a))
|
||||||
|
}
|
||||||
|
if len(a) == 1 && a[0] == "" {
|
||||||
|
return fmt.Errorf("seq name must not be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use (order is irrelevant
|
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use (order is irrelevant
|
||||||
|
|||||||
@@ -96,6 +96,32 @@ func TestBuiltinChecksums(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuiltinSeqPerSession pins seq's contract: a counter from 1, advancing on
|
||||||
|
// each call, named counters independent, and the whole thing scoped to one faker
|
||||||
|
// (session) so a fresh faker restarts at 1.
|
||||||
|
func TestBuiltinSeqPerSession(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != strconv.Itoa(i) {
|
||||||
|
t.Fatalf("seq call %d = %q, want %d", i, got, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := mustRender(t, f, `{"format":"{seq(orders)}"}`); got != "1" {
|
||||||
|
t.Fatalf("named seq(orders) = %q, want its own count from 1", got)
|
||||||
|
}
|
||||||
|
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != "6" {
|
||||||
|
t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got)
|
||||||
|
}
|
||||||
|
// repeat drives one counter forward across its renders...
|
||||||
|
if got := mustRender(t, engine(2), `{"format":"{seq()}","repeat":3,"separator":","}`); got != "1,2,3" {
|
||||||
|
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
||||||
|
}
|
||||||
|
// ...and a fresh session restarts from 1.
|
||||||
|
if got := mustRender(t, engine(2), `{"format":"{seq()}"}`); got != "1" {
|
||||||
|
t.Fatalf("new session seq = %q, want 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuiltinIBAN(t *testing.T) {
|
func TestBuiltinIBAN(t *testing.T) {
|
||||||
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
|
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"format": "{x}{x}{x}{x}{x}{x}{x}{x}-{x}{x}{x}{x}-{x}{x}{x}{x}-{x}{x}{x}{x}-{x}{x}{x}{x}{x}{x}{x}{x}{x}{x}{x}{x}",
|
|
||||||
"x": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"format": "{x}{x}{x}{x}{x}{x}{x}{x}-{x}{x}{x}{x}-{x}{x}{x}{x}-{x}{x}{x}{x}-{x}{x}{x}{x}{x}{x}{x}{x}{x}{x}{x}{x}",
|
|
||||||
"x": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -14,7 +14,6 @@ func TestShippedDataCategories(t *testing.T) {
|
|||||||
letters := regexp.MustCompile(`^[\pL'-]+$`)
|
letters := regexp.MustCompile(`^[\pL'-]+$`)
|
||||||
semver := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$`)
|
semver := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$`)
|
||||||
ip := regexp.MustCompile(`^((\d{1,3}\.){3}\d{1,3}|([0-9a-f]{4}:){7}[0-9a-f]{4})$`)
|
ip := regexp.MustCompile(`^((\d{1,3}\.){3}\d{1,3}|([0-9a-f]{4}:){7}[0-9a-f]{4})$`)
|
||||||
uuid := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
|
||||||
color := regexp.MustCompile(`^(#[0-9a-f]{6}|[\pL ]+)$`)
|
color := regexp.MustCompile(`^(#[0-9a-f]{6}|[\pL ]+)$`)
|
||||||
url := regexp.MustCompile(`^https?://([a-z0-9-]+\.)+[a-z]{2,}(/[a-z0-9./-]*)?$`)
|
url := regexp.MustCompile(`^https?://([a-z0-9-]+\.)+[a-z]{2,}(/[a-z0-9./-]*)?$`)
|
||||||
email := regexp.MustCompile(`^[a-z0-9._-]+@([a-z0-9-]+\.)+[a-z]{2,}$`)
|
email := regexp.MustCompile(`^[a-z0-9._-]+@([a-z0-9-]+\.)+[a-z]{2,}$`)
|
||||||
@@ -45,7 +44,6 @@ func TestShippedDataCategories(t *testing.T) {
|
|||||||
regexp.MustCompile(`^.+ (AB|HB|KB)$`)},
|
regexp.MustCompile(`^.+ (AB|HB|KB)$`)},
|
||||||
{"color", color, color},
|
{"color", color, color},
|
||||||
{"url", url, url},
|
{"url", url, url},
|
||||||
{"uuid", uuid, uuid},
|
|
||||||
{"username", uname, uname},
|
{"username", uname, uname},
|
||||||
{"price",
|
{"price",
|
||||||
regexp.MustCompile(`^\$\d{1,3}(,\d{3})?\.\d{2}$`),
|
regexp.MustCompile(`^\$\d{1,3}(,\d{3})?\.\d{2}$`),
|
||||||
|
|||||||
@@ -25,10 +25,25 @@ import (
|
|||||||
|
|
||||||
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
|
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
|
||||||
type Fakes struct {
|
type Fakes struct {
|
||||||
rand *rand.Rand
|
rand *session
|
||||||
categories map[string]node // root namespace: name -> compiled node tree
|
categories map[string]node // root namespace: name -> compiled node tree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// session is one faker's mutable render state: the seeded rng plus the {seq()}
|
||||||
|
// counters. Scoping it to the Fakes means sequences (and randomness) belong to
|
||||||
|
// that faker and reset when you create a new one. Embedding *rand.Rand makes a
|
||||||
|
// *session satisfy the rng interface the renderer draws from.
|
||||||
|
type session struct {
|
||||||
|
*rand.Rand
|
||||||
|
counters map[string]uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// next returns the next value (counting from 1) of the named {seq()} counter.
|
||||||
|
func (s *session) next(key string) uint64 {
|
||||||
|
s.counters[key]++
|
||||||
|
return s.counters[key]
|
||||||
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
seed uint64
|
seed uint64
|
||||||
seeded bool
|
seeded bool
|
||||||
@@ -60,11 +75,12 @@ func New(paths []string, opts ...Option) (*Fakes, error) {
|
|||||||
return &Fakes{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
return &Fakes{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRand(seed uint64, seeded bool) *rand.Rand {
|
func newRand(seed uint64, seeded bool) *session {
|
||||||
|
r := rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15))
|
||||||
if !seeded {
|
if !seeded {
|
||||||
var b [16]byte
|
var b [16]byte
|
||||||
_, _ = crand.Read(b[:])
|
_, _ = crand.Read(b[:])
|
||||||
return rand.New(rand.NewPCG(binary.LittleEndian.Uint64(b[:8]), binary.LittleEndian.Uint64(b[8:])))
|
r = rand.New(rand.NewPCG(binary.LittleEndian.Uint64(b[:8]), binary.LittleEndian.Uint64(b[8:])))
|
||||||
}
|
}
|
||||||
return rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15))
|
return &session{Rand: r, counters: map[string]uint64{}}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-25
@@ -70,7 +70,7 @@ func (f *Fakes) Fake(path string) (string, error) {
|
|||||||
// descend walks named fields, resolving choices it meets along the way. It is
|
// descend walks named fields, resolving choices it meets along the way. It is
|
||||||
// the one render-side step that can fail, because the path comes from the
|
// the one render-side step that can fail, because the path comes from the
|
||||||
// caller and may name a field that does not exist.
|
// caller and may name a field that does not exist.
|
||||||
func descend(r rng, n node, segments []string) (node, error) {
|
func descend(s *session, n node, segments []string) (node, error) {
|
||||||
if len(segments) == 0 {
|
if len(segments) == 0 {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
@@ -80,15 +80,15 @@ func descend(r rng, n node, segments []string) (node, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no entry %q", segments[0])
|
return nil, fmt.Errorf("no entry %q", segments[0])
|
||||||
}
|
}
|
||||||
return descend(r, child, segments[1:])
|
return descend(s, child, segments[1:])
|
||||||
case *template:
|
case *template:
|
||||||
child, ok := n.fields[segments[0]]
|
child, ok := n.fields[segments[0]]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no field %q", segments[0])
|
return nil, fmt.Errorf("no field %q", segments[0])
|
||||||
}
|
}
|
||||||
return descend(r, child, segments[1:])
|
return descend(s, child, segments[1:])
|
||||||
case *choice:
|
case *choice:
|
||||||
return descend(r, pick(r, n), segments) // a choice consumes no path segment
|
return descend(s, pick(s, n), segments) // a choice consumes no path segment
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[0])
|
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[0])
|
||||||
}
|
}
|
||||||
@@ -96,22 +96,22 @@ func descend(r rng, n node, segments []string) (node, error) {
|
|||||||
|
|
||||||
// render evaluates a compiled node to a string. compile validates every node up
|
// render evaluates a compiled node to a string. compile validates every node up
|
||||||
// front, so rendering a compiled tree cannot fail.
|
// front, so rendering a compiled tree cannot fail.
|
||||||
func render(r rng, n node) string {
|
func render(s *session, n node) string {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case literal:
|
case literal:
|
||||||
return string(n)
|
return string(n)
|
||||||
case *choice:
|
case *choice:
|
||||||
return render(r, pick(r, n))
|
return render(s, pick(s, n))
|
||||||
case *template:
|
case *template:
|
||||||
if n.repeat == 1 {
|
if n.repeat == 1 {
|
||||||
return expand(r, n.format, n.fields)
|
return expand(s, n.format, n.fields)
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i := 0; i < n.repeat; i++ {
|
for i := 0; i < n.repeat; i++ {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
b.WriteString(n.separator)
|
b.WriteString(n.separator)
|
||||||
}
|
}
|
||||||
b.WriteString(expand(r, n.format, n.fields))
|
b.WriteString(expand(s, n.format, n.fields))
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
default:
|
default:
|
||||||
@@ -137,7 +137,7 @@ func pick(r rng, c *choice) node {
|
|||||||
// any other "{token}" is substituted via resolve; every other rune is literal.
|
// any other "{token}" is substituted via resolve; every other rune is literal.
|
||||||
// checkTokens validated the braces and tokens at compile time, so this scan
|
// checkTokens validated the braces and tokens at compile time, so this scan
|
||||||
// cannot fail.
|
// cannot fail.
|
||||||
func expand(r rng, format string, fields map[string]node) string {
|
func expand(s *session, format string, fields map[string]node) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
rs := []rune(format)
|
rs := []rune(format)
|
||||||
for i := 0; i < len(rs); i++ {
|
for i := 0; i < len(rs); i++ {
|
||||||
@@ -149,13 +149,13 @@ func expand(r rng, format string, fields map[string]node) string {
|
|||||||
b.WriteRune('#')
|
b.WriteRune('#')
|
||||||
}
|
}
|
||||||
case '0':
|
case '0':
|
||||||
b.WriteByte(byte('0' + r.IntN(10)))
|
b.WriteByte(byte('0' + s.IntN(10)))
|
||||||
case '1':
|
case '1':
|
||||||
b.WriteByte(byte('1' + r.IntN(9)))
|
b.WriteByte(byte('1' + s.IntN(9)))
|
||||||
case 'A':
|
case 'A':
|
||||||
b.WriteByte(byte('A' + r.IntN(26)))
|
b.WriteByte(byte('A' + s.IntN(26)))
|
||||||
case 'a':
|
case 'a':
|
||||||
b.WriteByte(byte('a' + r.IntN(26)))
|
b.WriteByte(byte('a' + s.IntN(26)))
|
||||||
case '{':
|
case '{':
|
||||||
end := i + 1
|
end := i + 1
|
||||||
for rs[end] != '}' { // checkTokens guarantees a closing '}'
|
for rs[end] != '}' { // checkTokens guarantees a closing '}'
|
||||||
@@ -163,9 +163,9 @@ func expand(r rng, format string, fields map[string]node) string {
|
|||||||
}
|
}
|
||||||
body := string(rs[i+1 : end])
|
body := string(rs[i+1 : end])
|
||||||
if name, args, ok := funcCall(body); ok {
|
if name, args, ok := funcCall(body); ok {
|
||||||
b.WriteString(builtins[name].call(r, b.String(), args)) // b.String() is the output so far
|
b.WriteString(builtins[name].call(s, b.String(), args)) // b.String() is the output so far
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(resolve(r, body, fields))
|
b.WriteString(resolve(s, body, fields))
|
||||||
}
|
}
|
||||||
i = end
|
i = end
|
||||||
default:
|
default:
|
||||||
@@ -178,22 +178,25 @@ func expand(r rng, format string, fields map[string]node) string {
|
|||||||
// resolve renders a "{token}" body: one or more names separated by '|', one
|
// resolve renders a "{token}" body: one or more names separated by '|', one
|
||||||
// picked at random. A name is a sibling field or a {..path} reference, which
|
// picked at random. A name is a sibling field or a {..path} reference, which
|
||||||
// linkRefs bound into fields too; checkTokens and linkRefs guarantee both exist.
|
// linkRefs bound into fields too; checkTokens and linkRefs guarantee both exist.
|
||||||
func resolve(r rng, token string, fields map[string]node) string {
|
func resolve(s *session, token string, fields map[string]node) string {
|
||||||
names := strings.Split(token, "|")
|
names := strings.Split(token, "|")
|
||||||
return render(r, fields[names[r.IntN(len(names))]])
|
return render(s, fields[names[s.IntN(len(names))]])
|
||||||
}
|
}
|
||||||
|
|
||||||
// builtin is a format-string function invoked as {name(args)}. It receives the
|
// builtin is a format-string function invoked as {name(args)}. It receives the
|
||||||
// rng, the output emitted so far in the current expansion (for derivations such
|
// session (its rng, and the {seq()} counters), the output emitted so far in the
|
||||||
// as a checksum over preceding digits), and its args. A builtin must be pure
|
// current expansion (for derivations such as a checksum over preceding digits),
|
||||||
// over those inputs — no wall-clock, no crypto/rand — so seeding stays
|
// and its args. Almost all are pure over (rng, emitted, args) — no wall-clock, no
|
||||||
// reproducible; a time-based id derives its time from the rng. The optional
|
// crypto/rand — so seeding stays reproducible; a time-based id derives its time
|
||||||
// check validates args at compile time (their values, beyond the arity count).
|
// from the rng. seq is the one exception: it advances per-session counter state,
|
||||||
// The registry lives in builtins.go.
|
// 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). The registry lives in
|
||||||
|
// builtins.go.
|
||||||
type builtin struct {
|
type builtin struct {
|
||||||
arity int
|
arity int
|
||||||
check func(args []string) error
|
check func(args []string) error
|
||||||
call func(r rng, emitted string, args []string) string
|
call func(s *session, emitted string, args []string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
||||||
@@ -230,7 +233,7 @@ func checkFunc(body string) error {
|
|||||||
if !known {
|
if !known {
|
||||||
return fmt.Errorf("token {%s}: unknown function %q", body, name)
|
return fmt.Errorf("token {%s}: unknown function %q", body, name)
|
||||||
}
|
}
|
||||||
if len(args) != b.arity {
|
if b.arity >= 0 && len(args) != b.arity {
|
||||||
return fmt.Errorf("token {%s}: %s takes %d args, got %d", body, name, b.arity, len(args))
|
return fmt.Errorf("token {%s}: %s takes %d args, got %d", body, name, b.arity, len(args))
|
||||||
}
|
}
|
||||||
if b.check != nil {
|
if b.check != nil {
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ func TestCompileErrors(t *testing.T) {
|
|||||||
`{"format":"{float(1,2)}"}`, // wrong arity
|
`{"format":"{float(1,2)}"}`, // wrong arity
|
||||||
`{"format":"{float(1,2,-1)}"}`, // negative decimals
|
`{"format":"{float(1,2,-1)}"}`, // negative decimals
|
||||||
`{"format":"{iban(US)}"}`, // unsupported country
|
`{"format":"{iban(US)}"}`, // unsupported country
|
||||||
|
`{"format":"{seq(a,b)}"}`, // seq takes at most one name
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, bad)); err == nil {
|
if _, err := compile(parse(t, bad)); err == nil {
|
||||||
t.Errorf("compile(%s) = nil error, want error", bad)
|
t.Errorf("compile(%s) = nil error, want error", bad)
|
||||||
|
|||||||
Reference in New Issue
Block a user