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:
@@ -389,8 +389,8 @@ Every value is a **node**, nestable without limit:
|
|||||||
| choice | `["a", "b", …]` | one item, picked at random |
|
| choice | `["a", "b", …]` | one item, picked at random |
|
||||||
| template | `{"format": "…", …}` | its format, with `{name}` tokens rendering the named fields |
|
| 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)) |
|
| 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"}` |
|
| 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"}` |
|
| 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
|
### Format string
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ func datatypeOf(m map[string]any, pos position) (*DataType, error) {
|
|||||||
if name != d.String() {
|
if name != d.String() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if pos != inColumn {
|
if !pos.column() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return &d, nil
|
return &d, nil
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Value is a JSON value that only Decode, String and Object make, so a template never compiles
|
// 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 }
|
type Value struct{ v any }
|
||||||
|
|
||||||
// Decode decodes one JSON value, keeping each number as the json.Number it is written as.
|
// Decode decodes one JSON value, keeping each number as the json.Number it is written as.
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/datafiles"
|
"github.com/larvit/fejkdata/internal/datafiles"
|
||||||
"github.com/larvit/fejkdata/internal/grammar"
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
"github.com/larvit/fejkdata/internal/invariant"
|
"github.com/larvit/fejkdata/internal/invariant"
|
||||||
"github.com/larvit/fejkdata/internal/jsonvalue"
|
"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,
|
// 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,
|
// compile converts parsed JSON — a category or an inline template — into a node tree,
|
||||||
// validating structure up front.
|
// validating structure up front.
|
||||||
func compile(v jsonvalue.Value) (node, error) {
|
func compile(v jsonvalue.Value) (node, error) { return compileFrom(v, atTop) }
|
||||||
n, err := compileItem(v.Any(), 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -115,11 +118,16 @@ func compileCategory(c datafiles.Category) (node, error) {
|
|||||||
type position int
|
type position int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
inFormat position = iota // rendered by a format, so neither
|
inFormat position = iota // rendered by a format, so neither
|
||||||
atTop // a category or an inline template, whose fields may be columns
|
atTop // a category or an inline template, whose fields may be columns
|
||||||
inColumn // a column, or a choice item standing in for one
|
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.
|
// compileItem compiles one node; a weight on it skews only a choice drawing it.
|
||||||
func compileItem(v any, pos position) (node, error) {
|
func compileItem(v any, pos position) (node, error) {
|
||||||
if _, err := weightOf(v); err != nil {
|
if _, err := weightOf(v); err != nil {
|
||||||
@@ -137,13 +145,13 @@ func compileItem(v any, pos position) (node, error) {
|
|||||||
case map[string]any:
|
case map[string]any:
|
||||||
return compileTemplate(v, pos)
|
return compileTemplate(v, pos)
|
||||||
case nil:
|
case nil:
|
||||||
if pos != inColumn {
|
if !pos.column() {
|
||||||
return compileString("")
|
return compileString("")
|
||||||
}
|
}
|
||||||
return &nullItem{}, nil
|
return &nullItem{}, nil
|
||||||
case json.Number:
|
case json.Number:
|
||||||
if pos == inColumn {
|
if pos == inColumn {
|
||||||
return nil, typedOrText(string(v), numberType(v))
|
return nil, typedOrText(string(v), DataTypeInteger, DataTypeNumber)
|
||||||
}
|
}
|
||||||
return compileString(string(v))
|
return compileString(string(v))
|
||||||
case bool:
|
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
|
// 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.
|
// text it spells or a typed value, naming the text and the first of ds that holds it.
|
||||||
func typedOrText(text string, d DataType) error {
|
func typedOrText(text string, ds ...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)
|
held := proven.Literal(text)
|
||||||
}
|
for _, d := range ds {
|
||||||
|
if held.Not[d] == "" {
|
||||||
// numberType is the datatype a JSON number spells: an integer where it has no fraction or
|
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)
|
||||||
// exponent, else a number.
|
}
|
||||||
func numberType(n json.Number) DataType {
|
|
||||||
if strings.ContainsAny(string(n), ".eE") {
|
|
||||||
return DataTypeNumber
|
|
||||||
}
|
}
|
||||||
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
|
// 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) {
|
func compileChoice(items []any, pos position) (node, error) {
|
||||||
itemPos := inFormat
|
itemPos := inFormat
|
||||||
if pos == inColumn {
|
if pos.column() {
|
||||||
itemPos = inColumn
|
itemPos = pos
|
||||||
}
|
}
|
||||||
switch len(items) {
|
switch len(items) {
|
||||||
case 0:
|
case 0:
|
||||||
return nil, fmt.Errorf("empty choice")
|
return nil, fmt.Errorf("empty choice")
|
||||||
case 1:
|
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 nil, fmt.Errorf("every weight is 0, so the choice has nothing to draw")
|
||||||
}
|
}
|
||||||
return compileItem(items[0], pos)
|
return compileItem(items[0], pos)
|
||||||
@@ -263,8 +272,8 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fieldPos := inFormat
|
fieldPos := inFormat
|
||||||
if pos == atTop && o.repeat == 1 {
|
if o.repeat == 1 {
|
||||||
fieldPos = inColumn
|
fieldPos = map[position]position{atTop: inColumn, atGoTop: inGoColumn}[pos]
|
||||||
}
|
}
|
||||||
fields, err := compileFields(m, fieldPos)
|
fields, err := compileFields(m, fieldPos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -274,7 +283,7 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// templateOptions is what a template object's option keys say.
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ func checkTaggedType(sf reflect.StructField) error {
|
|||||||
// only what its field's Go type can.
|
// 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 {
|
func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]jsonvalue.Value) error {
|
||||||
tags["format"] = jsonvalue.String("")
|
tags["format"] = jsonvalue.String("")
|
||||||
n, err := compile(jsonvalue.Object(tags))
|
n, err := compileFrom(jsonvalue.Object(tags), atGoTop)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", label, err)
|
return fmt.Errorf("%s: %w", label, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user