Files
fejkdata/record.go
T

304 lines
9.5 KiB
Go

package fejkdata
import (
"encoding/json"
"errors"
"fmt"
"strings"
"unicode"
"unicode/utf8"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant"
)
// Column is one rendered column of a record. Value is the rendered text, which a
// serializer quotes for DataTypeString and writes bare for any other datatype; a Null
// column has no Value.
type Column struct {
Name string
DataType DataType
Value string
Null bool
}
// Record is one record rendered from a template: every direct field is a column,
// listed in name order. Each {…} draws afresh, while a pick bound by {x as n} is
// drawn once for the whole record.
type Record struct {
columns []Column
}
// Columns returns the record's columns in name order.
func (r *Record) Columns() []Column {
return append([]Column(nil), r.columns...)
}
// JSON renders the record as one JSON object.
func (r *Record) JSON() string {
var b strings.Builder
b.WriteByte('{')
for i, c := range r.columns {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(jsonString(c.Name))
b.WriteByte(':')
b.WriteString(literal(c, jsonString, "null"))
}
b.WriteByte('}')
return b.String()
}
func jsonString(s string) string {
b, _ := json.Marshal(s)
return string(b)
}
// CSVHeader renders the column names as one CSV header line.
func (r *Record) CSVHeader() string {
fields := make([]string, len(r.columns))
for i, c := range r.columns {
fields[i] = csvField(c.Name)
}
return strings.Join(fields, ",")
}
// CSVLine renders the column values as one CSV row: a null column an empty field and an
// empty string "", the convention PostgreSQL's COPY reads a null by. A record of one null
// column is a blank line, which COPY reads as null and most CSV readers skip.
func (r *Record) CSVLine() string {
fields := make([]string, len(r.columns))
for i, c := range r.columns {
fields[i] = literal(c, csvField, "")
}
return strings.Join(fields, ",")
}
// csvField quotes a field where encoding/csv would, and an empty one too.
func csvField(s string) string {
first, _ := utf8.DecodeRuneInString(s)
switch {
case s == "":
return `""`
case s == `\.` || strings.ContainsAny(s, "\",\r\n") || unicode.IsSpace(first):
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
}
return s
}
// SQLInsert renders the record as one INSERT statement into table, identifiers in ANSI
// double quotes.
func (r *Record) SQLInsert(table string) string {
cols := make([]string, len(r.columns))
vals := make([]string, len(r.columns))
for i, c := range r.columns {
cols[i] = quoteIdent(c.Name)
vals[i] = literal(c, sqlString, "NULL")
}
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s);", quoteIdent(table), strings.Join(cols, ", "), strings.Join(vals, ", "))
}
func sqlString(s string) string {
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}
func quoteIdent(s string) string {
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
}
// literal writes a string column quoted, a proven typed one bare, and a null as nullText.
func literal(c Column, quote func(string) string, nullText string) string {
switch {
case c.Null:
return nullText
case c.DataType == DataTypeString:
return quote(c.Value)
}
return c.Value
}
// 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) FakeRecord(path string) (*Record, error) {
f.mu.Lock()
defer f.mu.Unlock()
segments, err := grammar.SplitPath(path)
if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err)
}
f.loadShippedAt(segments)
_, n, tail, err := resolveCategory(f.root.children, segments)
if err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
}
var frames frameStack
sc := renderScope{frames: &frames}
if t, isTable := n.(*table); isTable {
if n, sc.row, err = tableRecord(f.drawState, t, tail); err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
}
} else 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])
}
record, err := recordOf(n)
if errors.Is(err, ErrNoColumns) {
ns := grammar.NameSegments(segments)
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, err, ns[len(ns)-1], path)
}
if err != nil {
return nil, fmt.Errorf("fejkdata: %s %w", path, err)
}
return renderRecord(f.drawState, record, sc), nil
}
// tableRecord walks a path's tail from a table to the table whose row is the record, and that
// row, selected or drawn.
func tableRecord(s *drawstate.State, t *table, tail []string) (node, renderedRow, error) {
n, pins, err := descend(s, t, tail)
if err != nil {
return nil, renderedRow{}, err
}
switch n := n.(type) {
case *table:
return n, renderedRow{n, n.rows.DrawIn(s, &pins)}, nil
case *tableRow:
return n.t, renderedRow{n.t, pins.MustRow(n.t.rows)}, nil
case *tableColumn:
return nil, renderedRow{}, fmt.Errorf("descends into %q, a column; a record is a table's row", n.name())
}
return n, renderedRow{}, nil
}
// RecordTemplate is an inline record compiled, referenced and validated once,
// ready to render many times with [RecordTemplate.Fake].
type RecordTemplate struct {
g *Generator
template *template
}
// Fake renders the record, one pick of each name across its columns.
func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock()
defer t.g.mu.Unlock()
var frames frameStack
return renderRecord(t.g.drawState, t.template, renderScope{frames: &frames})
}
// NewRecordTemplate compiles an inline record — a JSON object with a format and
// fields — and binds its references against the loaded tree.
func (f *Generator) NewRecordTemplate(input string) (*RecordTemplate, error) {
t, err := f.NewTemplate(input)
if err != nil {
return nil, err
}
record, err := recordOf(t.n)
if err != nil {
return nil, fmt.Errorf("fejkdata: an inline record %w", err)
}
return &RecordTemplate{g: f, template: record}, nil
}
// 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
}
return t.Fake(), nil
}
// ErrNoColumns is the one record fence a path can answer, so its entry point names
// the record to write instead.
var ErrNoColumns = errors.New("has no fields, so no columns")
// recordOf is the record template n is, the fence FakeRecord, NewRecordTemplate and FakeStruct
// pass; settleRecords fixed its columns at load.
func recordOf(n node) (*template, error) {
if tb, isTable := n.(*table); isTable {
n = tb.formatTemplate
}
t, ok := n.(*template)
if !ok {
return nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns")
}
if len(t.fields) == 0 {
return nil, ErrNoColumns
}
if !t.isRecord {
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)
}
if len(t.columns) != len(t.fields) {
panic(invariant.Broken("record %q was reached before its load settled its columns", t.format))
}
return t, nil
}
// recordColumn is one column of a record template: its name and datatype, the field drawing it,
// and the name the template binds to that whole field, whose pick the column renders so the row
// agrees with the columns reading the name.
type recordColumn struct {
name string
datatype DataType
field node
boundWhole *nameBinding
}
// settleRecords fixes the columns of every record in scope. bind runs it after checkNoCycles: a
// column's datatype walks the column, and a cycle would walk forever.
func settleRecords(scope nodeScope) {
_ = scope(func(_ string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
t.columns = recordColumns(t)
}
return nil
})
}
// recordColumns is t's columns in name order where t is a record with fields, else nil.
func recordColumns(t *template) []recordColumn {
if !t.isRecord || len(t.fields) == 0 {
return nil
}
whole := map[node]*nameBinding{}
for _, tok := range t.tokens {
if tok.Kind != grammar.NameBind {
continue
}
if b := t.nameScope.bindings[tok.Bound]; b.bindsField() && len(b.tail) == 0 && whole[b.head] == nil {
whole[b.head] = b
}
}
names := sortedNames(t.fields)
columns := make([]recordColumn, len(names))
for i, name := range names {
field := t.fields[name]
columns[i] = recordColumn{name: name, datatype: columnDatatype(field), field: field, boundWhole: whole[field]}
}
return columns
}
// renderRecord draws each column of t once, in name order, as one render, so the columns read one
// pick of each name; a table's columns read sc's row.
func renderRecord(s *drawstate.State, t *template, sc renderScope) *Record {
if mark := sc.renderFrame(t); mark >= 0 {
defer sc.frames.pop(mark)
}
r := &Record{columns: make([]Column, len(t.columns))}
for i, c := range t.columns {
var column readValue
if c.boundWhole != nil {
column = readName(s, sc, arm{kind: namedRead, named: c.boundWhole, levels: wholeLevels})
} else {
column = renderLeaf(s, c.field, sc)
}
r.columns[i] = Column{Name: c.name, DataType: c.datatype, Value: column.text, Null: column.null}
}
return r
}
// wholeLevels is the levels of a read of a whole name, as compileArm compiles {n}.
var wholeLevels = []pickKey{""}