diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index c3163fb..17531f1 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -225,8 +225,8 @@ var recordFormats = map[string]recordFormat{ "sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }}, } -// recordFormat reports whether the format writes records rather than plain text. -func (in invocation) recordFormat() bool { +// writesRecords reports whether the format writes records rather than plain text. +func (in invocation) writesRecords() bool { return in.format != "text" } @@ -248,7 +248,7 @@ func (in invocation) checkFlags() error { if in.tableSet && in.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 nil @@ -298,10 +298,6 @@ func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) err if err != nil { return err } - separator := in.separator - if in.recordFormat() { - separator = "\n" - } out := bufio.NewWriter(w) for i := 0; i < in.repeat; i++ { v, err := draw() @@ -309,7 +305,7 @@ func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) err return err } if i > 0 { - out.WriteString(separator) + out.WriteString(in.separator) } 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 // 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) { - if !in.recordFormat() { + if !in.writesRecords() { if kind != argTemplate { return func() (string, error) { return f.Fake(arg) }, nil } diff --git a/record.go b/record.go index 3eadb16..f4ce75a 100644 --- a/record.go +++ b/record.go @@ -100,25 +100,26 @@ func (f *Generator) Record(path string) (*Record, error) { 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, err := recordOf(n) + t, columns, err := recordOf(n) if err != nil { 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, // ready to render many times with [RecordTemplate.Fake]. type RecordTemplate struct { - g *Generator - t *template + g *Generator + t *template + columns []string } // Fake renders the record with one draw. func (t *RecordTemplate) Fake() *Record { t.g.mu.Lock() 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 @@ -128,11 +129,11 @@ func (f *Generator) NewRecordTemplate(input string) (*RecordTemplate, error) { if err != nil { return nil, err } - rt, err := recordOf(t.n) + tm, columns, err := recordOf(t.n) if err != nil { 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. @@ -144,32 +145,33 @@ func (f *Generator) FakeRecord(input string) (*Record, error) { return t.Fake(), nil } -// recordOf is the fence both record entry points pass: the node is a -// category-level template, it carries no repeat — which composes the format -// rather than projecting columns — and it offers at least one column. -func recordOf(n node) (*template, error) { +// recordOf is the fence both record entry points pass: the node is a template, it +// carries no repeat — which composes the format rather than projecting columns — +// and it offers at least one column. The columns come back with it, fixed for +// every draw the caller goes on to make. +func recordOf(n node) (*template, []string, error) { t, ok := n.(*template) 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 { - 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 { - return nil, errors.New("has no fields, so no columns") + columns := recordColumns(t) + 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, -// in name order. A {/path} binding is a render edge, not a column, so it is -// skipped the same way List and the graph do. The columns share one reference -// scope, so two columns that reference one category read one draw of it. -func renderRecord(s *session, t *template) *Record { +// renderRecord draws each column once, in the name order recordOf fixed. The +// columns share one reference scope, so two columns that reference one category +// read one draw of it. +func renderRecord(s *session, t *template, columns []string) *Record { scope := &draws{variant: map[string]node{}, value: map[string]string{}} - r := &Record{} - for _, name := range recordColumns(t) { - r.columns = append(r.columns, Column{Name: name, Value: render(s, t.fields[name], scope)}) + r := &Record{columns: make([]Column, len(columns))} + for i, name := range columns { + r.columns[i] = Column{Name: name, Value: render(s, t.fields[name], scope)} } return r }