Let a struct field's Go type type a bare number or boolean in its tag, and offer only a datatype that holds a refused column's number

This commit is contained in:
2026-10-08 11:57:06 +02:00
committed by Lillem4n
parent 9e392d02fd
commit 71c5039f04
5 changed files with 39 additions and 30 deletions
+34 -25
View File
@@ -5,12 +5,12 @@ import (
"fmt"
"math"
"strconv"
"strings"
"github.com/larvit/fejkdata/internal/datafiles"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant"
"github.com/larvit/fejkdata/internal/jsonvalue"
"github.com/larvit/fejkdata/internal/proven"
)
// node is a compiled element of the namespace tree: a folder, choice, null,
@@ -91,8 +91,11 @@ func (t *template) startOf(name string) node {
// compile converts parsed JSON — a category or an inline template — into a node tree,
// validating structure up front.
func compile(v jsonvalue.Value) (node, error) {
n, err := compileItem(v.Any(), atTop)
func compile(v jsonvalue.Value) (node, error) { return compileFrom(v, atTop) }
// compileFrom is compile with the root at pos: atTop, or atGoTop for a struct's tags.
func compileFrom(v jsonvalue.Value, pos position) (node, error) {
n, err := compileItem(v.Any(), pos)
if err != nil {
return nil, err
}
@@ -115,11 +118,16 @@ func compileCategory(c datafiles.Category) (node, error) {
type position int
const (
inFormat position = iota // rendered by a format, so neither
atTop // a category or an inline template, whose fields may be columns
inColumn // a column, or a choice item standing in for one
inFormat position = iota // rendered by a format, so neither
atTop // a category or an inline template, whose fields may be columns
inColumn // a column, or a choice item standing in for one
atGoTop // a struct's tags, whose fields are columns a Go type types
inGoColumn // a column a struct field's Go type types, or a choice item in one
)
// column reports a position that is a record column.
func (p position) column() bool { return p == inColumn || p == inGoColumn }
// compileItem compiles one node; a weight on it skews only a choice drawing it.
func compileItem(v any, pos position) (node, error) {
if _, err := weightOf(v); err != nil {
@@ -137,13 +145,13 @@ func compileItem(v any, pos position) (node, error) {
case map[string]any:
return compileTemplate(v, pos)
case nil:
if pos != inColumn {
if !pos.column() {
return compileString("")
}
return &nullItem{}, nil
case json.Number:
if pos == inColumn {
return nil, typedOrText(string(v), numberType(v))
return nil, typedOrText(string(v), DataTypeInteger, DataTypeNumber)
}
return compileString(string(v))
case bool:
@@ -156,18 +164,15 @@ func compileItem(v any, pos position) (node, error) {
}
// typedOrText refuses a bare JSON number or boolean in a record column, which could be the
// text it spells or a value of datatype d, naming both spellings.
func typedOrText(text string, d DataType) error {
return fmt.Errorf(`%s in a record column could be text or a value of datatype %s; write %q for text, or {"format":%q,"datatype":%q}`, text, d, text, text, d)
}
// numberType is the datatype a JSON number spells: an integer where it has no fraction or
// exponent, else a number.
func numberType(n json.Number) DataType {
if strings.ContainsAny(string(n), ".eE") {
return DataTypeNumber
// text it spells or a typed value, naming the text and the first of ds that holds it.
func typedOrText(text string, ds ...DataType) error {
held := proven.Literal(text)
for _, d := range ds {
if held.Not[d] == "" {
return fmt.Errorf(`%s in a record column could be text or a value of datatype %s; write %q for text, or {"format":%q,"datatype":%q}`, text, d, text, text, d)
}
}
return DataTypeInteger
return fmt.Errorf(`%s in a record column could be text or a typed value, though no datatype holds it; write %q for text`, text, text)
}
// jsonKind names the kind of a JSON value an option cannot take, in the data format's own
@@ -209,14 +214,18 @@ func (t *template) fixedText() (string, bool) {
func compileChoice(items []any, pos position) (node, error) {
itemPos := inFormat
if pos == inColumn {
itemPos = inColumn
if pos.column() {
itemPos = pos
}
switch len(items) {
case 0:
return nil, fmt.Errorf("empty choice")
case 1:
if w, err := weightOf(items[0]); err == nil && w == 0 {
w, err := weightOf(items[0])
if err != nil {
return nil, err
}
if w == 0 {
return nil, fmt.Errorf("every weight is 0, so the choice has nothing to draw")
}
return compileItem(items[0], pos)
@@ -263,8 +272,8 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
return nil, err
}
fieldPos := inFormat
if pos == atTop && o.repeat == 1 {
fieldPos = inColumn
if o.repeat == 1 {
fieldPos = map[position]position{atTop: inColumn, atGoTop: inGoColumn}[pos]
}
fields, err := compileFields(m, fieldPos)
if err != nil {
@@ -274,7 +283,7 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
if err != nil {
return nil, err
}
return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, isRecord: fieldPos == inColumn, unbound: unbound}, nil
return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, isRecord: fieldPos.column(), unbound: unbound}, nil
}
// templateOptions is what a template object's option keys say.