Add record output to JSON, CSV and SQL #8

Open
lilleman wants to merge 21 commits from record-output into main
3 changed files with 45 additions and 11 deletions
Showing only changes of commit 93ac610192 - Show all commits
+9 -6
View File
@@ -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
+5 -5
View File
@@ -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 {
+31
View File
@@ -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"}`,