From 71c5039f0406914f3b9d19621b1142c938fa2abb Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 8 Oct 2026 11:57:06 +0200 Subject: [PATCH] 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 --- README.md | 4 +-- datatype.go | 2 +- internal/jsonvalue/jsonvalue.go | 2 +- node.go | 59 +++++++++++++++++++-------------- struct.go | 2 +- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index feefe5c..a1f7169 100644 --- a/README.md +++ b/README.md @@ -389,8 +389,8 @@ Every value is a **node**, nestable without limit: | choice | `["a", "b", …]` | one item, picked at random | | template | `{"format": "…", …}` | its format, with `{name}` tokens rendering the named fields | | table | `{"format": "…", "rows": "x.tsv", …}` | its format over one row of the TSV beside it ([Table](#table)) | -| number | `5`, `1.50` | its text as written; refused in a record column, where it could be text, `"5"`, or a typed value, `{"format": "5", "datatype": "integer"}` | -| boolean | `true` | its text; refused in a record column, where it could be text, `"true"`, or a typed value, `{"format": "true", "datatype": "boolean"}` | +| number | `5`, `1.50` | its text as written; refused in a record column, where it could be text, `"5"`, or a typed value, `{"format": "5", "datatype": "integer"}`, bar a struct tag's, which its field's Go type types | +| boolean | `true` | its text; refused in a record column, where it could be text, `"true"`, or a typed value, `{"format": "true", "datatype": "boolean"}`, bar a struct tag's, which its field's Go type types | ### Format string diff --git a/datatype.go b/datatype.go index 035563c..cb8924b 100644 --- a/datatype.go +++ b/datatype.go @@ -35,7 +35,7 @@ func datatypeOf(m map[string]any, pos position) (*DataType, error) { if name != d.String() { continue } - if pos != inColumn { + if !pos.column() { return nil, nil } return &d, nil diff --git a/internal/jsonvalue/jsonvalue.go b/internal/jsonvalue/jsonvalue.go index 1060ac6..c477f51 100644 --- a/internal/jsonvalue/jsonvalue.go +++ b/internal/jsonvalue/jsonvalue.go @@ -7,7 +7,7 @@ import ( ) // Value is a JSON value that only Decode, String and Object make, so a template never compiles -// from a value encoding/json decoded with its numbers as float64. +// from a value encoding/json decoded with its numbers as float64. The zero Value is JSON null. type Value struct{ v any } // Decode decodes one JSON value, keeping each number as the json.Number it is written as. diff --git a/node.go b/node.go index c149c4d..836305d 100644 --- a/node.go +++ b/node.go @@ -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. diff --git a/struct.go b/struct.go index 986ac5b..61a41eb 100644 --- a/struct.go +++ b/struct.go @@ -257,7 +257,7 @@ func checkTaggedType(sf reflect.StructField) error { // only what its field's Go type can. func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]jsonvalue.Value) error { tags["format"] = jsonvalue.String("") - n, err := compile(jsonvalue.Object(tags)) + n, err := compileFrom(jsonvalue.Object(tags), atGoTop) if err != nil { return fmt.Errorf("%s: %w", label, err) }