141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// DataType is what a record column holds, which decides how a record writes its value.
|
|
type DataType int
|
|
|
|
// The datatypes a column declares with "datatype"; a column without one is a string.
|
|
const (
|
|
DataTypeString DataType = iota
|
|
DataTypeInteger
|
|
DataTypeNumber
|
|
DataTypeBoolean
|
|
)
|
|
|
|
var dataTypeNames = [...]string{"string", "integer", "number", "boolean"}
|
|
|
|
// String is the datatype as data spells it.
|
|
func (d DataType) String() string {
|
|
if d < 0 || int(d) >= len(dataTypeNames) {
|
|
return fmt.Sprintf("DataType(%d)", int(d))
|
|
}
|
|
return dataTypeNames[d]
|
|
}
|
|
|
|
// position is where a JSON value sits, which decides whether it may carry a datatype or
|
|
// be null.
|
|
type position int
|
|
|
|
const (
|
|
inFormat position = iota // rendered by a format, so neither
|
|
atTop // a category or an inline template, whose fields are the columns
|
|
inColumn // a column, or a choice item standing in for one
|
|
)
|
|
|
|
// datatypeOf reads a template's "datatype" (default DataTypeString).
|
|
func datatypeOf(m map[string]any, pos position) (DataType, error) {
|
|
v, ok := m["datatype"]
|
|
if !ok {
|
|
return DataTypeString, nil
|
|
}
|
|
name, ok := v.(string)
|
|
if !ok {
|
|
return 0, fmt.Errorf("datatype must be a string, got %T", v)
|
|
}
|
|
if name == DataTypeString.String() {
|
|
return 0, fmt.Errorf("datatype %q is the default, so it has no effect; drop it", name)
|
|
}
|
|
for d := DataTypeInteger; d <= DataTypeBoolean; d++ {
|
|
if name != d.String() {
|
|
continue
|
|
}
|
|
if pos != inColumn {
|
|
return 0, errors.New("datatype only types a record column — a field of the top-level template — so it has no effect here")
|
|
}
|
|
return d, nil
|
|
}
|
|
return 0, fmt.Errorf(`datatype takes "integer", "number" or "boolean", got %q`, name)
|
|
}
|
|
|
|
// columnDatatype is the datatype a column's items declare. They must agree, since a
|
|
// column holds one; a column only ever null is a string.
|
|
func columnDatatype(n node) (DataType, error) {
|
|
var declared []DataType
|
|
var collect func(node)
|
|
collect = func(n node) {
|
|
switch n := n.(type) {
|
|
case *choice:
|
|
for _, it := range n.items {
|
|
collect(it)
|
|
}
|
|
case *template:
|
|
declared = append(declared, n.datatype)
|
|
}
|
|
}
|
|
collect(n)
|
|
if len(declared) == 0 {
|
|
return DataTypeString, nil
|
|
}
|
|
for _, d := range declared {
|
|
if d != declared[0] {
|
|
return declared[0], fmt.Errorf("its items declare %s and %s; a column holds one datatype, so give every item the same", declared[0], d)
|
|
}
|
|
}
|
|
return declared[0], nil
|
|
}
|
|
|
|
// datatypeSpec is what a datatype's text must satisfy: a grammar, the states a render
|
|
// may end in, and how an error names the datatype.
|
|
type datatypeSpec struct {
|
|
grammar *grammar
|
|
accept uint32
|
|
noun string
|
|
}
|
|
|
|
var datatypeSpecs = map[DataType]datatypeSpec{
|
|
DataTypeInteger: {numberGrammar, integerAccept, "an integer"},
|
|
DataTypeNumber: {numberGrammar, numberAccept, "a number"},
|
|
DataTypeBoolean: {booleanGrammar, booleanAccept, "a boolean"},
|
|
}
|
|
|
|
// datatypeCheck proves every render of a typed column is text its datatype takes. One
|
|
// check covers a scope, so a node several columns reach is read once per grammar.
|
|
type datatypeCheck struct {
|
|
languages map[*grammar]*textLanguage
|
|
proof *calcProof
|
|
}
|
|
|
|
func (c *datatypeCheck) check(path string, n node) error {
|
|
t, ok := n.(*template)
|
|
if !ok || t.datatype == DataTypeString {
|
|
return nil
|
|
}
|
|
spec := datatypeSpecs[t.datatype]
|
|
w, escapes := c.language(spec.grammar).node(t, nil).escape(spec.accept)
|
|
if !escapes {
|
|
return nil
|
|
}
|
|
msg := fmt.Sprintf("%s: datatype %s, but it can render %s, which is not %s", path, t.datatype, w, spec.noun)
|
|
if w.why != "" {
|
|
msg += ": " + w.why
|
|
}
|
|
return errors.New(msg)
|
|
}
|
|
|
|
func (c *datatypeCheck) language(g *grammar) *textLanguage {
|
|
if c.proof == nil {
|
|
c.proof = newCalcProof()
|
|
c.languages = map[*grammar]*textLanguage{}
|
|
}
|
|
l, made := c.languages[g]
|
|
if !made {
|
|
l = newTextLanguage(g, c.proof)
|
|
c.languages[g] = l
|
|
}
|
|
return l
|
|
}
|