diff --git a/README.md b/README.md index a86800e..02c7f22 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ For structured output a record writes the row for you. A record is a template seen as columns: its fields are the columns, its `format` the whole. `--format json|ndjson|csv|sql` writes the records; the library's -`Record` (below) hands back the columns. Every column is a string — typed scalars +`FakeRecord` (below) hands back the columns. Every column is a string — typed scalars are on the release checklist, see [`todo.md`](todo.md). Save `mydata/users.json`: @@ -152,9 +152,9 @@ paths := f.List() // every path Fake accepts, sorted v, err = f.FakeTemplate("name: {/sv_SE.person.last}") // compile + render in one call t, err := f.NewTemplate(`{"format":"name: {x}","x":["bosse","lina"]}`) // compile once v = t.Fake() // render many times, no re-parse -r, err := f.Record("users") // one record: each field a column +r, err := f.FakeRecord("users") // one record: each field a column s := r.JSON() // {"first":"Ada","last":"Lovelace"} -r, err = f.FakeRecord(`{"format":"{x}","x":["a","b"]}`) // compile + render inline +r, err = f.FakeRecordTemplate(`{"format":"{x}","x":["a","b"]}`) // compile + render inline ``` | Option | | @@ -166,8 +166,8 @@ r, err = f.FakeRecord(`{"format":"{x}","x":["a","b"]}`) // compile + render inli A `*Record` carries its columns via `Columns()`, and serializes them with `JSON()` (one object), `CSVHeader()`/`CSVLine()`, or `SQLInsert(table)` — the shapes the -CLI's `--format` writes. `Record` and `FakeRecord` take a record; a path or -template that is not one — a bare string, a choice, or a folder — errors. +CLI's `--format` writes. `FakeRecord` and `FakeRecordTemplate` take a record; a +path or template that is not one — a bare string, a choice, or a folder — errors. A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only when drawn from one goroutine. Changing how a value is composed shifts the seeded @@ -526,7 +526,7 @@ tokens add cost in proportion to the output. letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on whether the argument looks like a number. - **A record is a template seen as columns, not a second schema format.** A - template's `format` composes its fields into one string; `Record` and + template's `format` composes its fields into one string; `FakeRecord` and `--format` project the same fields as columns. Two views of one dataset, so a record author writes the same JSON they already know, and a column is the same field `Fake` renders by dotted path. The `format` is inert to a record — a diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index cfd5909..36b605f 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -341,7 +341,7 @@ func (in invocation) textDraw(f *fejkdata.Generator, kind argKind, arg string) ( // recordStream builds the record drawer for the argument, plus the INSERT table // a sql format names. func (in invocation) recordStream(f *fejkdata.Generator, kind argKind, arg string) (func() (*fejkdata.Record, error), string, error) { - record := func() (*fejkdata.Record, error) { return f.Record(arg) } + record := func() (*fejkdata.Record, error) { return f.FakeRecord(arg) } if kind == argTemplate { t, err := f.NewRecordTemplate(arg) if err != nil { diff --git a/perf_test.go b/perf_test.go index fc5821c..a3b750d 100644 --- a/perf_test.go +++ b/perf_test.go @@ -61,11 +61,11 @@ func TestNoRecordAllocRegression(t *testing.T) { if err != nil { t.Fatalf("New(%s): %v", s.name, err) } - if _, err := f.Record("x"); err != nil { - t.Fatalf("Record(%s): %v", s.name, err) // else the gate would measure the error path + if _, err := f.FakeRecord("x"); err != nil { + t.Fatalf("FakeRecord(%s): %v", s.name, err) // else the gate would measure the error path } const base = 4.0 - if allocs := testing.AllocsPerRun(10000, func() { f.Record("x") }); allocs > base*1.10 { + if allocs := testing.AllocsPerRun(10000, func() { f.FakeRecord("x") }); allocs > base*1.10 { t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a record fence running per draw is the usual cause", s.name, allocs, base*1.10, base) } } diff --git a/readme_test.go b/readme_test.go index 4cc569e..18790e9 100644 --- a/readme_test.go +++ b/readme_test.go @@ -55,7 +55,7 @@ func TestReadmeRecordExample(t *testing.T) { if err != nil { t.Fatal(err) } - r, err := f.Record("users") + r, err := f.FakeRecord("users") if err != nil { t.Fatal(err) } diff --git a/record.go b/record.go index 4a70eaf..803e041 100644 --- a/record.go +++ b/record.go @@ -92,10 +92,10 @@ func quoteIdent(s string) string { return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` } -// Record renders a path as one record: the template it names, with each direct +// FakeRecord renders a path as one record: the template it names, with each direct // 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) { +func (f *Generator) FakeRecord(path string) (*Record, error) { f.mu.Lock() defer f.mu.Unlock() _, n, tail, err := resolveCategory(f.categories, strings.Split(path, ".")) @@ -164,8 +164,8 @@ func (f *Generator) NewRecordTemplate(input string) (*RecordTemplate, error) { return &RecordTemplate{g: f, t: tm, columns: columns}, nil } -// FakeRecord compiles and renders an inline record in one call. -func (f *Generator) FakeRecord(input string) (*Record, error) { +// FakeRecordTemplate compiles and renders an inline record in one call. +func (f *Generator) FakeRecordTemplate(input string) (*Record, error) { t, err := f.NewRecordTemplate(input) if err != nil { return nil, err diff --git a/record_test.go b/record_test.go index 01c5d20..ee2f754 100644 --- a/record_test.go +++ b/record_test.go @@ -21,7 +21,7 @@ func recordCat(t *testing.T) *Generator { func TestRecordProjectsFieldsAsColumns(t *testing.T) { f := recordCat(t) - r, err := f.Record("users") + r, err := f.FakeRecord("users") if err != nil { t.Fatal(err) } @@ -37,8 +37,8 @@ func TestRecordProjectsFieldsAsColumns(t *testing.T) { func TestRecordIsDeterministic(t *testing.T) { a, b := recordCat(t), recordCat(t) for i := 0; i < 20; i++ { - x, _ := a.Record("users") - y, _ := b.Record("users") + x, _ := a.FakeRecord("users") + y, _ := b.FakeRecord("users") if x.JSON() != y.JSON() { t.Fatalf("same seed diverged: %s != %s", x.JSON(), y.JSON()) } @@ -51,7 +51,7 @@ func TestRecordSkipsReferenceBindings(t *testing.T) { "user": `{"format": "they are {/name}", "id": "1"}`, }) f := newGenerator(t, dir, WithSeed(1)) - r, err := f.Record("user") + r, err := f.FakeRecord("user") if err != nil { t.Fatal(err) } @@ -77,15 +77,15 @@ func TestRecordErrors(t *testing.T) { {"group", "names a folder"}, {"nope", "no entry"}, } { - if _, err := f.Record(c.path); err == nil || !strings.Contains(err.Error(), c.want) { - t.Errorf("Record(%q) = %v, want an error containing %q", c.path, err, c.want) + if _, err := f.FakeRecord(c.path); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("FakeRecord(%q) = %v, want an error containing %q", c.path, err, c.want) } } } func TestRecordJSON(t *testing.T) { f := recordCat(t) - r, err := f.Record("users") + r, err := f.FakeRecord("users") if err != nil { t.Fatal(err) } @@ -110,7 +110,7 @@ func TestRecordCSV(t *testing.T) { f := newGenerator(t, dir, WithSeed(1)) seen := map[string]bool{} for i := 0; i < 200 && len(seen) < 4; i++ { - r, err := f.Record("note") + r, err := f.FakeRecord("note") if err != nil { t.Fatal(err) } @@ -135,7 +135,7 @@ func TestRecordCSV(t *testing.T) { func TestRecordCSVEmptyValueStaysARow(t *testing.T) { dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`}) f := newGenerator(t, dir, WithSeed(1)) - r, err := f.Record("blank") + r, err := f.FakeRecord("blank") if err != nil { t.Fatal(err) } @@ -158,9 +158,9 @@ func TestRecordRejectsOverlappingReferenceColumns(t *testing.T) { {"as a builtin operand", `{"format":"","whole":"{uppercase(/cat.a)}","inner":"{/cat.a.b}"}`}, } { f := newGenerator(t, writeData(t, map[string]string{"cat": cat, "row": c.row}), WithSeed(1)) - _, err := f.Record("row") + _, err := f.FakeRecord("row") if err == nil || !strings.Contains(err.Error(), "reads a path into") { - t.Errorf("%s: Record = %v, want the overlap rejected the way one format is", c.name, err) + t.Errorf("%s: FakeRecord = %v, want the overlap rejected the way one format is", c.name, err) continue } if !strings.Contains(err.Error(), `"whole"`) || !strings.Contains(err.Error(), `"inner"`) { @@ -177,7 +177,7 @@ func TestInlineRecordRejectsOverlappingColumns(t *testing.T) { "cat": `{"format":"","a":[{"format":"A={b}","b":"1"},{"format":"A={b}","b":"2"}]}`, }) f := newGenerator(t, dir, WithSeed(1)) - _, err := f.FakeRecord(`{"format":"","whole":"{/cat.a}","inner":"{/cat.a.b}"}`) + _, err := f.FakeRecordTemplate(`{"format":"","whole":"{/cat.a}","inner":"{/cat.a.b}"}`) if err == nil || !strings.Contains(err.Error(), "reads a path into") { t.Fatalf("inline record over an overlapping pair = %v, want the inline entry point to refuse it too", err) } @@ -191,8 +191,8 @@ func TestRecordRejectsAColumnReadingItsOwnRecord(t *testing.T) { } { person := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"],` + c.column + `}` f := newGenerator(t, writeData(t, map[string]string{"person": person}), WithSeed(1)) - if _, err := f.Record("person"); err == nil || !strings.Contains(err.Error(), "points back at this record") { - t.Errorf("%s: Record = %v, want it refused; the column would contradict the columns beside it", c.name, err) + if _, err := f.FakeRecord("person"); err == nil || !strings.Contains(err.Error(), "points back at this record") { + t.Errorf("%s: FakeRecord = %v, want it refused; the column would contradict the columns beside it", c.name, err) } } } @@ -205,7 +205,7 @@ func TestRecordBareReferenceStaysIndependentAsAnOperand(t *testing.T) { f := newGenerator(t, dir, WithSeed(1)) sawMismatch := false for i := 0; i < 200 && !sawMismatch; i++ { - r, err := f.Record("row") + r, err := f.FakeRecord("row") if err != nil { t.Fatal(err) } @@ -234,7 +234,7 @@ func TestRecordSQLInsert(t *testing.T) { "person": `{"format": "{last}", "last": "O'Brien"}`, }) f := newGenerator(t, dir, WithSeed(1)) - r, err := f.Record("person") + r, err := f.FakeRecord("person") if err != nil { t.Fatal(err) } @@ -251,8 +251,8 @@ func TestRecordRejectsFieldDescent(t *testing.T) { }) 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) + if _, err := f.FakeRecord(path); err == nil || !strings.Contains(err.Error(), "field") { + t.Errorf("FakeRecord(%q) = %v, want a 'descends into a field' error", path, err) } } } @@ -264,7 +264,7 @@ func TestRecordSharesAReferenceAcrossColumns(t *testing.T) { }) f := newGenerator(t, dir, WithSeed(1)) for i := 0; i < 100; i++ { - r, err := f.Record("price") + r, err := f.FakeRecord("price") if err != nil { t.Fatal(err) } @@ -294,7 +294,7 @@ func TestRecordSharesAReferenceIntoAColumnRepeat(t *testing.T) { }) f := newGenerator(t, dir, WithSeed(1)) for i := 0; i < 50; i++ { - r, err := f.Record("order") + r, err := f.FakeRecord("order") if err != nil { t.Fatal(err) } @@ -313,7 +313,7 @@ func TestRecordBareReferenceStaysIndependent(t *testing.T) { f := newGenerator(t, dir, WithSeed(1)) sawMismatch := false for i := 0; i < 100; i++ { - r, err := f.Record("order") + r, err := f.FakeRecord("order") if err != nil { t.Fatal(err) } @@ -336,7 +336,7 @@ func TestRecordSQLQuotesIdentifiers(t *testing.T) { "row": `{"format": "", "postal-code": "1", "street-number": "2"}`, }) f := newGenerator(t, dir, WithSeed(1)) - r, err := f.Record("row") + r, err := f.FakeRecord("row") if err != nil { t.Fatal(err) } @@ -346,15 +346,15 @@ func TestRecordSQLQuotesIdentifiers(t *testing.T) { } } -func TestFakeRecordAndTemplate(t *testing.T) { +func TestFakeRecordTemplateAndNewRecordTemplate(t *testing.T) { f := recordCat(t) in := `{"format":"{x} {y}","x":["1","2"],"y":["3","4"]}` - want, err := f.FakeRecord(in) + want, err := f.FakeRecordTemplate(in) if err != nil { - t.Fatalf("FakeRecord: %v", err) + t.Fatalf("FakeRecordTemplate: %v", err) } if len(want.Columns()) != 2 { - t.Fatalf("FakeRecord columns = %v, want two columns", want.Columns()) + t.Fatalf("FakeRecordTemplate columns = %v, want two columns", want.Columns()) } reusable, err := f.NewRecordTemplate(in) if err != nil { @@ -377,8 +377,8 @@ func TestInlineRecordErrors(t *testing.T) { {`["a","b"]`, "a record is a template whose fields are its columns"}, {`{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`, "carries repeat 3"}, } { - if _, err := f.FakeRecord(c.input); err == nil || !strings.Contains(err.Error(), c.want) { - t.Errorf("FakeRecord(%q) = %v, want an error naming %q", c.input, err, c.want) + if _, err := f.FakeRecordTemplate(c.input); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("FakeRecordTemplate(%q) = %v, want an error naming %q", c.input, err, c.want) } } } @@ -388,8 +388,8 @@ func TestRecordRejectsATopLevelRepeat(t *testing.T) { "rep": `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`, }) f := newGenerator(t, dir, WithSeed(1)) - if _, err := f.Record("rep"); err == nil || !strings.Contains(err.Error(), "carries repeat 3") { - t.Errorf("Record on a repeating template = %v, want an error naming the repeat", err) + if _, err := f.FakeRecord("rep"); err == nil || !strings.Contains(err.Error(), "carries repeat 3") { + t.Errorf("FakeRecord on a repeating template = %v, want an error naming the repeat", err) } if v, err := f.Fake("rep"); err != nil || v != "x-|x-|x-" { t.Errorf("Fake(rep) = %q, %v, want the repeat still composed for the string view", v, err) diff --git a/shipped_data_test.go b/shipped_data_test.go index 374b7a8..193a96b 100644 --- a/shipped_data_test.go +++ b/shipped_data_test.go @@ -108,7 +108,7 @@ func TestFakeIsSafeForConcurrentUse(t *testing.T) { return } f.List() - if _, err := f.Record("sv_SE.person"); err != nil { + if _, err := f.FakeRecord("sv_SE.person"); err != nil { t.Error(err) return } diff --git a/todo.md b/todo.md index dae3a44..0c2b28f 100644 --- a/todo.md +++ b/todo.md @@ -6,8 +6,6 @@ The record API lands first, so the data update can use it. ### Record API -- Rename the record entry points after the string ones: `Record(path)` becomes - `FakeRecord(path)`, and `FakeRecord(inline)` becomes `FakeRecordTemplate(inline)`. - Typed columns — a column declares its type, so `json` writes `42` rather than `"42"` and `sql` an unquoted literal: string, integer, number, boolean, and a way to write null. A template that can render a value its type rejects is a