Reject field descent in Record and pin the shared-draw scope
Tests / vet + fmt + tests (pull_request) Successful in 1m2s
Tests / vet + fmt + tests (pull_request) Successful in 1m2s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"}`,
|
||||
|
||||
Reference in New Issue
Block a user