Add record output to JSON, CSV and SQL #8
@@ -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
|
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
|
"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.
|
valid-by-construction through a builtin; it is serialized as text.
|
||||||
- **A record's columns share one reference draw.** Two columns that reference one
|
- **A record shares one reference draw per category.** Two columns that reference
|
||||||
category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of it,
|
one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of
|
||||||
so a record's facts agree the way a template's [correlated
|
it, so a record's facts agree the way a template's [correlated
|
||||||
fields](#correlated-fields) do. Only references share across columns: a sibling
|
fields](#correlated-fields) do. The draw is one per record, so it spans a
|
||||||
field is local to its own column, so `first` does not silently bind to a
|
column's `repeat` and nested templates too (one record is one coherent unit);
|
||||||
`first` in the column next to it.
|
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
|
- **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
|
caller maps onto a struct themselves. gofakeit's `fake:"{firstname}"` tags
|
||||||
reflect over an arbitrary struct type and cast into its fields — a different
|
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
|
// 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
|
// field drawn as a column. Only a category-level template is a record — a path
|
||||||
// template — a bare string or a choice, which have no fields — is an error.
|
// that descends into a field, or that names a folder or a choice, is an error.
|
||||||
func (f *Generator) Record(path string) (*Record, error) {
|
func (f *Generator) Record(path string) (*Record, error) {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
|
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
|
||||||
}
|
}
|
||||||
if _, ok := n.(*group); ok {
|
if len(tail) > 0 {
|
||||||
return nil, fmt.Errorf("fejkdata: %s names a folder, not a value", path)
|
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)
|
t, ok := n.(*template)
|
||||||
if !ok {
|
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) {
|
func TestRecordSharesAReferenceAcrossColumns(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"currency": `[{"format":"{code}","code":"AUD","symbol":"$"},{"format":"{code}","code":"EUR","symbol":"€"}]`,
|
"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) {
|
func TestRecordSQLQuotesIdentifiers(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"row": `{"format": "", "postal-code": "1", "street-number": "2"}`,
|
"row": `{"format": "", "postal-code": "1", "street-number": "2"}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user