Add record output to JSON, CSV and SQL #8

Open
lilleman wants to merge 21 commits from record-output into main
2 changed files with 31 additions and 33 deletions
Showing only changes of commit 83e11074ac - Show all commits
+5 -9
View File
@@ -225,8 +225,8 @@ var recordFormats = map[string]recordFormat{
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }}, "sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }},
} }
// recordFormat reports whether the format writes records rather than plain text. // writesRecords reports whether the format writes records rather than plain text.
func (in invocation) recordFormat() bool { func (in invocation) writesRecords() bool {
return in.format != "text" return in.format != "text"
} }
@@ -248,7 +248,7 @@ func (in invocation) checkFlags() error {
if in.tableSet && in.format != "sql" { if in.tableSet && in.format != "sql" {
return errors.New("--table names the INSERT target, so it needs --format sql") return errors.New("--table names the INSERT target, so it needs --format sql")
} }
if in.recordFormat() && in.separatorSet { if in.writesRecords() && in.separatorSet {
return errors.New("--separator joins text values, so it has no effect with --format " + in.format) return errors.New("--separator joins text values, so it has no effect with --format " + in.format)
} }
return nil return nil
@@ -298,10 +298,6 @@ func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) err
if err != nil { if err != nil {
return err return err
} }
separator := in.separator
if in.recordFormat() {
separator = "\n"
}
out := bufio.NewWriter(w) out := bufio.NewWriter(w)
for i := 0; i < in.repeat; i++ { for i := 0; i < in.repeat; i++ {
v, err := draw() v, err := draw()
@@ -309,7 +305,7 @@ func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) err
return err return err
} }
if i > 0 { if i > 0 {
out.WriteString(separator) out.WriteString(in.separator)
} }
out.WriteString(v) out.WriteString(v)
} }
@@ -320,7 +316,7 @@ func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) err
// draw builds what one render yields: the value's text, or the record's line in // draw builds what one render yields: the value's text, or the record's line in
// the chosen format, the header carried ahead of the first one. // the chosen format, the header carried ahead of the first one.
func (in invocation) draw(f *fejkdata.Generator, kind argKind, arg string) (func() (string, error), error) { func (in invocation) draw(f *fejkdata.Generator, kind argKind, arg string) (func() (string, error), error) {
if !in.recordFormat() { if !in.writesRecords() {
if kind != argTemplate { if kind != argTemplate {
return func() (string, error) { return f.Fake(arg) }, nil return func() (string, error) { return f.Fake(arg) }, nil
} }
+26 -24
View File
@@ -100,25 +100,26 @@ func (f *Generator) Record(path string) (*Record, error) {
if len(tail) > 0 { 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]) return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0])
} }
t, err := recordOf(n) t, columns, err := recordOf(n)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %s %w", path, err) return nil, fmt.Errorf("fejkdata: %s %w", path, err)
} }
return renderRecord(f.rand, t), nil return renderRecord(f.rand, t, columns), nil
} }
// RecordTemplate is an inline record compiled, referenced and validated once, // RecordTemplate is an inline record compiled, referenced and validated once,
// ready to render many times with [RecordTemplate.Fake]. // ready to render many times with [RecordTemplate.Fake].
type RecordTemplate struct { type RecordTemplate struct {
g *Generator g *Generator
t *template t *template
columns []string
} }
// Fake renders the record with one draw. // Fake renders the record with one draw.
func (t *RecordTemplate) Fake() *Record { func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
return renderRecord(t.g.rand, t.t) return renderRecord(t.g.rand, t.t, t.columns)
} }
// NewRecordTemplate compiles an inline record — a JSON object with a format and // NewRecordTemplate compiles an inline record — a JSON object with a format and
@@ -128,11 +129,11 @@ func (f *Generator) NewRecordTemplate(input string) (*RecordTemplate, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
rt, err := recordOf(t.n) tm, columns, err := recordOf(t.n)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: an inline record %w", err) return nil, fmt.Errorf("fejkdata: an inline record %w", err)
} }
return &RecordTemplate{g: f, t: rt}, nil return &RecordTemplate{g: f, t: tm, columns: columns}, nil
} }
// FakeRecord compiles and renders an inline record in one call. // FakeRecord compiles and renders an inline record in one call.
@@ -144,32 +145,33 @@ func (f *Generator) FakeRecord(input string) (*Record, error) {
return t.Fake(), nil return t.Fake(), nil
} }
// recordOf is the fence both record entry points pass: the node is a // recordOf is the fence both record entry points pass: the node is a template, it
// category-level template, it carries no repeat — which composes the format // carries no repeat — which composes the format rather than projecting columns —
// rather than projecting columns — and it offers at least one column. // and it offers at least one column. The columns come back with it, fixed for
func recordOf(n node) (*template, error) { // every draw the caller goes on to make.
func recordOf(n node) (*template, []string, error) {
t, ok := n.(*template) t, ok := n.(*template)
if !ok { if !ok {
return nil, errors.New("names a choice, not a template; only a category-level template is a record") return nil, nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns")
} }
if t.repeat != 1 { if t.repeat != 1 {
return nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat) return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
} }
if len(recordColumns(t)) == 0 { columns := recordColumns(t)
return nil, errors.New("has no fields, so no columns") if len(columns) == 0 {
return nil, nil, errors.New("has no fields, so no columns")
} }
return t, nil return t, columns, nil
} }
// renderRecord projects a template's direct fields as columns, drawn once each, // renderRecord draws each column once, in the name order recordOf fixed. The
// in name order. A {/path} binding is a render edge, not a column, so it is // columns share one reference scope, so two columns that reference one category
// skipped the same way List and the graph do. The columns share one reference // read one draw of it.
// scope, so two columns that reference one category read one draw of it. func renderRecord(s *session, t *template, columns []string) *Record {
func renderRecord(s *session, t *template) *Record {
scope := &draws{variant: map[string]node{}, value: map[string]string{}} scope := &draws{variant: map[string]node{}, value: map[string]string{}}
r := &Record{} r := &Record{columns: make([]Column, len(columns))}
for _, name := range recordColumns(t) { for i, name := range columns {
r.columns = append(r.columns, Column{Name: name, Value: render(s, t.fields[name], scope)}) r.columns[i] = Column{Name: name, Value: render(s, t.fields[name], scope)}
} }
return r return r
} }