diff --git a/README.md b/README.md index 835227b..35768d6 100644 --- a/README.md +++ b/README.md @@ -518,12 +518,15 @@ tokens add cost in proportion to the output. need a per-column `kind`, a parallel scalar system in a format whose promise is "text means what it says". A column's check digit, number or id is still valid-by-construction through a builtin; it is serialized as text. -- **A record's columns share one reference draw.** Two columns that reference one - category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of it, - so a record's facts agree the way a template's [correlated - fields](#correlated-fields) do. Only references share across columns: a sibling - field is local to its own column, so `first` does not silently bind to a - `first` in the column next to it. +- **A record shares one reference draw per category.** Two columns that reference + one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of + it, so a record's facts agree the way a template's [correlated + fields](#correlated-fields) do. The draw is one per record, so it spans a + column's `repeat` and nested templates too (one record is one coherent unit); + a bare reference — `{/currency}`, no field — stays an independent draw every + time, the rule a format string already follows. Only references share: a sibling + field is local to its own column, so a `first` column does not silently bind to + a `first` in the column next to it. - **Filling a Go struct is out of scope.** `Record.Fields()` returns the columns a caller maps onto a struct themselves. gofakeit's `fake:"{firstname}"` tags reflect over an arbitrary struct type and cast into its fields — a different diff --git a/record.go b/record.go index 0b7d1d3..be485ba 100644 --- a/record.go +++ b/record.go @@ -88,17 +88,17 @@ func quoteIdent(s string) string { } // Record renders a path as one record: the template it names, with each direct -// field drawn as a column. A path naming a folder or a value that is not a -// template — a bare string or a choice, which have no fields — is an error. +// field drawn as a column. Only a category-level template is a record — a path +// that descends into a field, or that names a folder or a choice, is an error. func (f *Generator) Record(path string) (*Record, error) { f.mu.Lock() defer f.mu.Unlock() - n, err := descend(f.rand, &group{children: f.categories}, strings.Split(path, ".")) + _, n, tail, err := resolveRef(f.categories, strings.Split(path, ".")) if err != nil { return nil, fmt.Errorf("fejkdata: %s: %w", path, err) } - if _, ok := n.(*group); ok { - return nil, fmt.Errorf("fejkdata: %s names a folder, not a value", path) + if len(tail) > 0 { + return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0]) } t, ok := n.(*template) if !ok { diff --git a/record_test.go b/record_test.go index 7493c88..8c0b638 100644 --- a/record_test.go +++ b/record_test.go @@ -139,6 +139,19 @@ func TestRecordSQLInsert(t *testing.T) { } } +func TestRecordRejectsFieldDescent(t *testing.T) { + dir := writeData(t, map[string]string{ + "cat": `{"format":"{sub}","sub":{"format":"{x}","x":"1"}}`, + "row": `[{"format":"{x}","x":"1"},{"format":"{x}","x":"2"}]`, + }) + f := newGenerator(t, dir, WithSeed(1)) + for _, path := range []string{"cat.sub", "row.x"} { + if _, err := f.Record(path); err == nil || !strings.Contains(err.Error(), "field") { + t.Errorf("Record(%q) = %v, want a 'descends into a field' error", path, err) + } + } +} + func TestRecordSharesAReferenceAcrossColumns(t *testing.T) { dir := writeData(t, map[string]string{ "currency": `[{"format":"{code}","code":"AUD","symbol":"$"},{"format":"{code}","code":"EUR","symbol":"€"}]`, @@ -169,6 +182,24 @@ func TestRecordSharesAReferenceAcrossColumns(t *testing.T) { } } +func TestRecordSharesAReferenceIntoAColumnRepeat(t *testing.T) { + dir := writeData(t, map[string]string{ + "currency": `[{"format":"{code}","code":"AUD"},{"format":"{code}","code":"EUR"}]`, + "order": `{"format":"","codes":{"format":"{/currency.code}","repeat":3,"separator":"-"}}`, + }) + f := newGenerator(t, dir, WithSeed(1)) + for i := 0; i < 50; i++ { + r, err := f.Record("order") + if err != nil { + t.Fatal(err) + } + parts := strings.Split(r.Fields()[0].Value, "-") + if len(parts) != 3 || parts[0] != parts[1] || parts[1] != parts[2] { + t.Fatalf("codes column = %q, want one shared draw across its repeat", r.Fields()[0].Value) + } + } +} + func TestRecordSQLQuotesIdentifiers(t *testing.T) { dir := writeData(t, map[string]string{ "row": `{"format": "", "postal-code": "1", "street-number": "2"}`,