Fill embedded fields into their struct's record, add fake:"-", round integer ranges inward naming a wider kind, and refuse a lone reference in IsTemplate
Tests / vet + fmt + tests (pull_request) Successful in 1m2s
Tests / vet + fmt + tests (pull_request) Successful in 1m2s
This commit is contained in:
@@ -30,8 +30,9 @@ a JSON object, array or string, or that carries a `{` token, is instead an
|
|||||||
spot. Its tokens reach the data by reference from the root —
|
spot. Its tokens reach the data by reference from the root —
|
||||||
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike
|
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike
|
||||||
available. An inline template sits in no folder, so the folder-relative `{.name}`
|
available. An inline template sits in no folder, so the folder-relative `{.name}`
|
||||||
and `{..name}` are rejected naming the root spelling. A path never contains a
|
and `{..name}` are rejected naming the root spelling, and one reference alone —
|
||||||
brace, a bracket or a quote, so the two cannot collide (see
|
`{/sv_SE.person}` — is the path written as a template, rejected naming the path. A
|
||||||
|
path never contains a brace, a bracket or a quote, so the two cannot collide (see
|
||||||
[Decisions](#decisions)).
|
[Decisions](#decisions)).
|
||||||
|
|
||||||
| Flag | |
|
| Flag | |
|
||||||
@@ -186,13 +187,15 @@ type User struct {
|
|||||||
a column of one record, its tag a path or an inline template — told apart by
|
a column of one record, its tag a path or an inline template — told apart by
|
||||||
`IsTemplate`, as the CLI tells an argument — and its Go type the column's
|
`IsTemplate`, as the CLI tells an argument — and its Go type the column's
|
||||||
[datatype](#datatype): a string, bool, integer or float kind, or a pointer to one,
|
[datatype](#datatype): a string, bool, integer or float kind, or a pointer to one,
|
||||||
which a [`null`](#null) item leaves nil. A value the kind cannot hold, such as
|
which a [`null`](#null) item leaves nil. An integer stays within int64 whatever its
|
||||||
`{int(0,300)}` in a `uint8`, is refused as a typed column's is. A struct field, or a
|
kind, and a value the kind cannot hold, such as `{int(0,300)}` in a `uint8`, is refused
|
||||||
pointer to one, fills from its own tags as a record of its own, so its references draw
|
naming a kind that holds it. The fields an embedded struct promotes are columns of the
|
||||||
apart from its parent's. Untagged fields keep their values, and so does a pointer back
|
same record; a named struct field, or a pointer to one, fills from its own tags as a
|
||||||
to a struct already being filled. The first call for a type compiles its tags and
|
record of its own, so its references draw apart from its parent's, and `fake:"-"`
|
||||||
reports what they get wrong: a tag holding only `{/path}` names the path to write, and
|
leaves it unfilled. Untagged fields keep their values, and so does a pointer back to a
|
||||||
a `datatype` in a tag names the Go type that already sets it.
|
struct already being filled. The first call for a type compiles its tags and reports
|
||||||
|
what they get wrong, with the same error on every later call; a `datatype` in a tag
|
||||||
|
names the Go type that already sets it.
|
||||||
|
|
||||||
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
||||||
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
||||||
@@ -497,8 +500,8 @@ tokens add cost in proportion to the output.
|
|||||||
one is a load error naming the right one.
|
one is a load error naming the right one.
|
||||||
3. **Every mistake is a load error** — `New` rejects the data and `NewTemplate`
|
3. **Every mistake is a load error** — `New` rejects the data and `NewTemplate`
|
||||||
the inline template; on a loaded generator `Fake` fails only for an unknown
|
the inline template; on a loaded generator `Fake` fails only for an unknown
|
||||||
path, `FakeStruct` only for a type its tags do not describe, and
|
path, `FakeStruct` only for a non-struct argument or a type its tags do not
|
||||||
`Template.Fake` cannot fail at all.
|
describe, with the same error every call, and `Template.Fake` cannot fail at all.
|
||||||
4. **Zero to a value in one command** — `go install`, then `fejkdata sv_SE.person`:
|
4. **Zero to a value in one command** — `go install`, then `fejkdata sv_SE.person`:
|
||||||
no checkout, no flag. Flags are GNU-form (`--seed 42`, `-n 3`) in any position;
|
no checkout, no flag. Flags are GNU-form (`--seed 42`, `-n 3`) in any position;
|
||||||
the first custom template needs no escape and no option.
|
the first custom template needs no escape and no option.
|
||||||
@@ -535,7 +538,10 @@ tokens add cost in proportion to the output.
|
|||||||
whole — though only a leading one could collide — keeps one simple name rule
|
whole — though only a leading one could collide — keeps one simple name rule
|
||||||
instead of a leading-position special case. The JSON string is what makes the
|
instead of a leading-position special case. The JSON string is what makes the
|
||||||
library's own advice reachable: the error for an object holding only a format
|
library's own advice reachable: the error for an object holding only a format
|
||||||
names `"…"`, and that spelling has to work where it is printed.
|
names `"…"`, and that spelling has to work where it is printed. One reference alone,
|
||||||
|
`{/users}`, is refused naming the path `users`: both render the same text, and only
|
||||||
|
the path names a record. `IsTemplate` exports the rule, so the CLI, struct tags and
|
||||||
|
any other caller read one.
|
||||||
- **An inline template skips the cycle fence, and only that one.** `New` proves the
|
- **An inline template skips the cycle fence, and only that one.** `New` proves the
|
||||||
loaded tree acyclic, an inline node is a finite tree of its own, and nothing in
|
loaded tree acyclic, an inline node is a finite tree of its own, and nothing in
|
||||||
the tree can reference it, so no render of it reaches itself. Every other fence
|
the tree can reference it, so no render of it reaches itself. Every other fence
|
||||||
@@ -615,6 +621,16 @@ tokens add cost in proportion to the output.
|
|||||||
schema: the fields name the columns and their types are the datatypes, so a tag
|
schema: the fields name the columns and their types are the datatypes, so a tag
|
||||||
says only what to draw, and a `datatype` in it would be a second spelling of the
|
says only what to draw, and a `datatype` in it would be a second spelling of the
|
||||||
type.
|
type.
|
||||||
|
- **A struct's records follow Go's field access, and compile on first use.** The
|
||||||
|
fields an embedded struct promotes are the struct's own — `e.First`, as
|
||||||
|
`encoding/json` and SQL mappers read them — so they are columns of its record and
|
||||||
|
share its draws; a tagged field that another field hides is refused, not dropped. A
|
||||||
|
named struct field is another entity and a record of its own; `fake:"-"` leaves it
|
||||||
|
unfilled, whatever a category named `-` holds, and a pointer back to a struct
|
||||||
|
already being filled is left alone, since filling it would never end. `New` cannot
|
||||||
|
see a caller's types, so the first `FakeStruct` for a type compiles its tags and the
|
||||||
|
answer, error included, is kept per type: a test's first call is its load, and no
|
||||||
|
`NewStruct` handle is needed, as the cache already compiles once.
|
||||||
- **A record shares one reference draw per category.** Two columns that reference
|
- **A record shares one reference draw per category.** Two columns that reference
|
||||||
one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of
|
one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of
|
||||||
it, so a record's facts agree the way a template's [correlated
|
it, so a record's facts agree the way a template's [correlated
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ func (f *Generator) FakeTemplate(input string) (string, error) {
|
|||||||
// IsTemplate reports whether arg is an inline template rather than a path, by its shape: a {
|
// IsTemplate reports whether arg is an inline template rather than a path, by its shape: a {
|
||||||
// token, or a JSON object, array or string, is a template, and anything else is a path. A
|
// token, or a JSON object, array or string, is a template, and anything else is a path. A
|
||||||
// name never holds a bracket, a brace or a quote, so an arg holding one that is not valid
|
// name never holds a bracket, a brace or a quote, so an arg holding one that is not valid
|
||||||
// JSON names neither, and errors.
|
// JSON names neither, and errors; so does a template of one reference alone, which is a path
|
||||||
|
// written as a template.
|
||||||
func IsTemplate(arg string) (bool, error) {
|
func IsTemplate(arg string) (bool, error) {
|
||||||
inline, err := isTemplate(arg)
|
inline, err := isTemplate(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,6 +64,9 @@ func IsTemplate(arg string) (bool, error) {
|
|||||||
|
|
||||||
func isTemplate(arg string) (bool, error) {
|
func isTemplate(arg string) (bool, error) {
|
||||||
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
||||||
|
if path, lone := loneReference(arg); lone {
|
||||||
|
return false, fmt.Errorf("%s is the path %s written as a template; write %s", arg, path, path)
|
||||||
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
if i := strings.IndexAny(arg, `[]}"`); i >= 0 {
|
if i := strings.IndexAny(arg, `[]}"`); i >= 0 {
|
||||||
@@ -75,6 +79,24 @@ func isJSONStart(arg string) bool {
|
|||||||
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loneReference is the path a template spells when it is one reference token and nothing else.
|
||||||
|
func loneReference(arg string) (string, bool) {
|
||||||
|
format := arg
|
||||||
|
if strings.HasPrefix(arg, `"`) && json.Unmarshal([]byte(arg), &format) != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
var units []ftoken
|
||||||
|
if eachToken(format, func(t ftoken) error { units = append(units, t); return nil }) != nil || len(units) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
body := units[0].body
|
||||||
|
if units[0].kind != 'b' || !isRef(body) || strings.ContainsAny(body, "|(") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
_, path, err := refShape(body)
|
||||||
|
return path, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func compileInput(input string) (node, error) {
|
func compileInput(input string) (node, error) {
|
||||||
v, err := inputValue(input)
|
v, err := inputValue(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a
|
// FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a
|
||||||
// column of one record: its tag a path or an inline template, told apart as [IsTemplate]
|
// column of one record: its tag a path or an inline template, told apart as [IsTemplate]
|
||||||
// tells them, and its Go type the column's datatype. A struct field, or a pointer to one,
|
// tells them, and its Go type the column's datatype. The fields an embedded struct promotes
|
||||||
// fills from its own tags as a record of its own. The first call for a type compiles its
|
// are columns of that record too, while a named struct field, or a pointer to one, fills
|
||||||
// tags, so a later call for that type fails only as the first did.
|
// from its own tags as a record of its own. The first call for a type compiles its tags,
|
||||||
|
// so a later call for that type fails only as the first did.
|
||||||
func (f *Generator) FakeStruct(v any) error {
|
func (f *Generator) FakeStruct(v any) error {
|
||||||
p := reflect.ValueOf(v)
|
p := reflect.ValueOf(v)
|
||||||
if p.Kind() != reflect.Pointer || p.IsNil() || p.Elem().Kind() != reflect.Struct {
|
if p.Kind() != reflect.Pointer || p.IsNil() || p.Elem().Kind() != reflect.Struct {
|
||||||
@@ -41,7 +43,11 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
|
|||||||
if r, done := f.structs[t]; done {
|
if r, done := f.structs[t]; done {
|
||||||
return r.shape, r.err
|
return r.shape, r.err
|
||||||
}
|
}
|
||||||
shape, err := compileStruct(f.categories, t, t.String(), map[reflect.Type]bool{})
|
label := t.Name()
|
||||||
|
if label == "" {
|
||||||
|
label = "struct"
|
||||||
|
}
|
||||||
|
shape, err := compileStruct(f.categories, t, label, map[reflect.Type]bool{})
|
||||||
if err == nil && shape.empty() {
|
if err == nil && shape.empty() {
|
||||||
err = fmt.Errorf("%s has no fake tags, so nothing to fill", t)
|
err = fmt.Errorf("%s has no fake tags, so nothing to fill", t)
|
||||||
}
|
}
|
||||||
@@ -53,66 +59,138 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// structShape is a struct type compiled to fill: its tagged fields as one record, the field
|
// structShape is a struct type compiled to fill: its tagged fields as one record, the field
|
||||||
// index each column fills, and the struct fields carrying tags of their own.
|
// index path each column fills, and the struct fields carrying tags of their own.
|
||||||
type structShape struct {
|
type structShape struct {
|
||||||
record *template
|
record *template
|
||||||
columns []Column
|
columns []Column
|
||||||
fields []int
|
fields [][]int
|
||||||
nested []nestedStruct
|
nested []nestedStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
// nestedStruct is a struct field, or a pointer to one, filled as a record of its own.
|
// nestedStruct is a named struct field, or a pointer to one, filled as a record of its own.
|
||||||
type nestedStruct struct {
|
type nestedStruct struct {
|
||||||
index int
|
index []int
|
||||||
shape *structShape
|
shape *structShape
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 }
|
func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 }
|
||||||
|
|
||||||
// compileStruct compiles struct type t, naming its fields from label. visiting holds the types
|
// structFields gathers what one struct type fills: its tagged fields, those its embedded
|
||||||
// compiling above t, so a pointer back to one is left alone rather than filled without end.
|
// structs promote included, as the tags of one record, and its named struct fields as nested
|
||||||
|
// records. visiting holds the types compiling or embedded above, so a pointer back to one is
|
||||||
|
// left alone rather than filled without end.
|
||||||
|
type structFields struct {
|
||||||
|
root map[string]node
|
||||||
|
t reflect.Type
|
||||||
|
label string
|
||||||
|
visiting map[reflect.Type]bool
|
||||||
|
tags map[string]any
|
||||||
|
shape *structShape
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileStruct compiles struct type t, naming its fields from label.
|
||||||
func compileStruct(root map[string]node, t reflect.Type, label string, visiting map[reflect.Type]bool) (*structShape, error) {
|
func compileStruct(root map[string]node, t reflect.Type, label string, visiting map[reflect.Type]bool) (*structShape, error) {
|
||||||
visiting[t] = true
|
visiting[t] = true
|
||||||
defer delete(visiting, t)
|
defer delete(visiting, t)
|
||||||
shape := &structShape{}
|
c := &structFields{root: root, t: t, label: label, visiting: visiting, tags: map[string]any{}, shape: &structShape{}}
|
||||||
tags := map[string]any{}
|
if err := c.walk(t, nil); err != nil {
|
||||||
for i := 0; i < t.NumField(); i++ {
|
|
||||||
sf := t.Field(i)
|
|
||||||
tag, tagged := sf.Tag.Lookup("fake")
|
|
||||||
if !tagged {
|
|
||||||
if err := shape.addNested(root, sf, label, visiting); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
continue
|
if len(c.tags) > 0 {
|
||||||
}
|
if err := c.shape.compileRecord(root, t, label, c.tags); err != nil {
|
||||||
v, err := tagValue(sf, tag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s.%s: %w", label, sf.Name, err)
|
|
||||||
}
|
|
||||||
tags[sf.Name] = v
|
|
||||||
}
|
|
||||||
if len(tags) > 0 {
|
|
||||||
if err := shape.compileRecord(root, t, label, tags); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shape, nil
|
return c.shape, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addNested adds an untagged exported struct field, or a pointer to one, that carries tags.
|
// walk gathers the fields of struct type t, which sits at index within c.t.
|
||||||
func (s *structShape) addNested(root map[string]node, sf reflect.StructField, label string, visiting map[reflect.Type]bool) error {
|
func (c *structFields) walk(t reflect.Type, index []int) error {
|
||||||
t := sf.Type
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
sf := t.Field(i)
|
||||||
|
sf.Index = append(index[:len(index):len(index)], i)
|
||||||
|
if err := c.field(sf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *structFields) field(sf reflect.StructField) error {
|
||||||
|
tag, tagged := sf.Tag.Lookup("fake")
|
||||||
|
elem := structOf(sf.Type)
|
||||||
|
switch {
|
||||||
|
case tagged && tag == "-":
|
||||||
|
if elem == nil {
|
||||||
|
return fmt.Errorf(`%s.%s: fake:"-" leaves a struct field unfilled, and any other untagged field keeps its value already; drop the tag`, c.label, sf.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case tagged:
|
||||||
|
return c.column(sf, tag)
|
||||||
|
case elem == nil || c.visiting[elem]:
|
||||||
|
return nil
|
||||||
|
case sf.Anonymous:
|
||||||
|
return c.embed(sf, elem)
|
||||||
|
case sf.IsExported():
|
||||||
|
return c.nest(sf, elem)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// structOf is the struct type a field holds, by value or through a pointer; nil when none.
|
||||||
|
func structOf(t reflect.Type) reflect.Type {
|
||||||
if t.Kind() == reflect.Pointer {
|
if t.Kind() == reflect.Pointer {
|
||||||
t = t.Elem()
|
t = t.Elem()
|
||||||
}
|
}
|
||||||
if !sf.IsExported() || t.Kind() != reflect.Struct || visiting[t] {
|
if t.Kind() != reflect.Struct {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
nested, err := compileStruct(root, t, label+"."+sf.Name, visiting)
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// column adds a tagged field's tag to the record, refusing one another field hides.
|
||||||
|
func (c *structFields) column(sf reflect.StructField, tag string) error {
|
||||||
|
if visible, ok := c.t.FieldByName(sf.Name); !ok || !slices.Equal(visible.Index, sf.Index) {
|
||||||
|
return fmt.Errorf("%s.%s: hidden by another field named %s, so its fake tag cannot fill it; rename one", c.label, fieldPath(c.t, sf.Index), sf.Name)
|
||||||
|
}
|
||||||
|
v, err := tagValue(sf, tag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s.%s: %w", c.label, sf.Name, err)
|
||||||
|
}
|
||||||
|
c.tags[sf.Name] = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldPath names the field at index within t through each struct it is embedded in.
|
||||||
|
func fieldPath(t reflect.Type, index []int) string {
|
||||||
|
names := make([]string, len(index))
|
||||||
|
for i := range index {
|
||||||
|
names[i] = t.FieldByIndex(index[:i+1]).Name
|
||||||
|
}
|
||||||
|
return strings.Join(names, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed gathers the fields an embedded struct promotes into c's record.
|
||||||
|
func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error {
|
||||||
|
c.visiting[elem] = true
|
||||||
|
defer delete(c.visiting, elem)
|
||||||
|
tags, nested := len(c.tags), len(c.shape.nested)
|
||||||
|
if err := c.walk(elem, sf.Index); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sf.Type.Kind() == reflect.Pointer && !sf.IsExported() && (len(c.tags) > tags || len(c.shape.nested) > nested) {
|
||||||
|
return fmt.Errorf("%s.%s: an embedded pointer to an unexported type cannot be allocated, so the tags beneath it cannot fill; embed %s by value", c.label, fieldPath(c.t, sf.Index), elem)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nest adds a named struct field, or a pointer to one, that carries tags as a record of its own.
|
||||||
|
func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error {
|
||||||
|
nested, err := compileStruct(c.root, elem, c.label+"."+sf.Name, c.visiting)
|
||||||
if err != nil || nested.empty() {
|
if err != nil || nested.empty() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.nested = append(s.nested, nestedStruct{sf.Index[0], nested})
|
c.shape.nested = append(c.shape.nested, nestedStruct{sf.Index, nested})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +201,14 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
inline, err := isTemplate(tag)
|
inline, err := isTemplate(tag)
|
||||||
if err != nil {
|
switch {
|
||||||
|
case err != nil:
|
||||||
return nil, err
|
return nil, err
|
||||||
|
case inline:
|
||||||
|
return inputValue(tag)
|
||||||
|
case strings.HasPrefix(tag, "/"):
|
||||||
|
return nil, fmt.Errorf("path %q starts with /, and every path starts at the root already; write %s", tag, tag[1:])
|
||||||
}
|
}
|
||||||
if !inline {
|
|
||||||
for _, seg := range strings.Split(tag, ".") {
|
for _, seg := range strings.Split(tag, ".") {
|
||||||
if err := checkName(seg); err != nil {
|
if err := checkName(seg); err != nil {
|
||||||
return nil, fmt.Errorf("path %w", err)
|
return nil, fmt.Errorf("path %w", err)
|
||||||
@@ -134,34 +216,20 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
|
|||||||
}
|
}
|
||||||
return "{/" + tag + "}", nil
|
return "{/" + tag + "}", nil
|
||||||
}
|
}
|
||||||
v, err := inputValue(tag)
|
|
||||||
if s, isString := v.(string); isString && isLoneReference(s) {
|
|
||||||
path := s[2 : len(s)-1]
|
|
||||||
return nil, fmt.Errorf("%s is the path %s written as a template; write fake:%q", s, path, path)
|
|
||||||
}
|
|
||||||
return v, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// isLoneReference reports whether a format is one {/path} token alone, which a path tag spells.
|
|
||||||
func isLoneReference(format string) bool {
|
|
||||||
return len(format) > len("{/}") && strings.HasPrefix(format, "{/") && strings.HasSuffix(format, "}") &&
|
|
||||||
!strings.ContainsAny(format[1:len(format)-1], "{}|(")
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkTaggedType rejects a tagged field no column can fill.
|
// checkTaggedType rejects a tagged field no column can fill.
|
||||||
func checkTaggedType(sf reflect.StructField) error {
|
func checkTaggedType(sf reflect.StructField) error {
|
||||||
elem := sf.Type
|
_, holds := columnKinds[sf.Type.Kind()]
|
||||||
if elem.Kind() == reflect.Pointer {
|
if sf.Type.Kind() == reflect.Pointer {
|
||||||
elem = elem.Elem()
|
_, holds = columnKinds[sf.Type.Elem().Kind()]
|
||||||
}
|
}
|
||||||
_, holds := columnKinds[elem.Kind()]
|
|
||||||
switch {
|
switch {
|
||||||
case !sf.IsExported():
|
case !sf.IsExported():
|
||||||
return errors.New("unexported, so its fake tag cannot fill it")
|
return errors.New("unexported, so its fake tag cannot fill it")
|
||||||
case holds:
|
case holds:
|
||||||
return nil
|
return nil
|
||||||
case elem.Kind() == reflect.Struct:
|
case structOf(sf.Type) != nil:
|
||||||
return errors.New("a struct field fills from the tags on its own fields; drop this one")
|
return errors.New(`a struct field fills from the tags on its own fields; drop this one, or write fake:"-" to leave it unfilled`)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("a fake tag fills a string, bool, integer or float field, or a pointer to one, not %s", sf.Type)
|
return fmt.Errorf("a fake tag fills a string, bool, integer or float field, or a pointer to one, not %s", sf.Type)
|
||||||
}
|
}
|
||||||
@@ -182,7 +250,7 @@ func (s *structShape) compileRecord(root map[string]node, t reflect.Type, label
|
|||||||
return fmt.Errorf("%s: %w", label, err)
|
return fmt.Errorf("%s: %w", label, err)
|
||||||
}
|
}
|
||||||
proof := &valueProof{}
|
proof := &valueProof{}
|
||||||
s.fields = make([]int, len(columns))
|
s.fields = make([][]int, len(columns))
|
||||||
for i, c := range columns {
|
for i, c := range columns {
|
||||||
sf, _ := t.FieldByName(c.Name)
|
sf, _ := t.FieldByName(c.Name)
|
||||||
if c.DataType != DataTypeString {
|
if c.DataType != DataTypeString {
|
||||||
@@ -191,34 +259,44 @@ func (s *structShape) compileRecord(root map[string]node, t reflect.Type, label
|
|||||||
if err := proof.checkField(label+"."+c.Name, sf.Type, record.fields[c.Name]); err != nil {
|
if err := proof.checkField(label+"."+c.Name, sf.Type, record.fields[c.Name]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.fields[i] = sf.Index[0]
|
s.fields[i] = sf.Index
|
||||||
}
|
}
|
||||||
s.record, s.columns = record, columns
|
s.record, s.columns = record, columns
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// columnKind is what a field of one Go kind holds: the datatype its text proves as, and the
|
// columnKind is what a field of one Go kind holds: the datatype its text proves as, the range a
|
||||||
// range its value stays in.
|
// number of it stays in, and the kind to name when a value can pass that range.
|
||||||
type columnKind struct {
|
type columnKind struct {
|
||||||
datatype DataType
|
datatype DataType
|
||||||
lo, hi float64
|
lo, hi float64
|
||||||
|
wider reflect.Kind
|
||||||
}
|
}
|
||||||
|
|
||||||
var columnKinds = map[reflect.Kind]columnKind{
|
var columnKinds = map[reflect.Kind]columnKind{
|
||||||
reflect.Bool: {DataTypeBoolean, -math.MaxFloat64, math.MaxFloat64},
|
reflect.Bool: {datatype: DataTypeBoolean},
|
||||||
reflect.Float32: {DataTypeNumber, -math.MaxFloat32, math.MaxFloat32},
|
reflect.Float32: {DataTypeNumber, -math.MaxFloat32, math.MaxFloat32, reflect.Float64},
|
||||||
reflect.Float64: {DataTypeNumber, -math.MaxFloat64, math.MaxFloat64},
|
reflect.Float64: {DataTypeNumber, -math.MaxFloat64, math.MaxFloat64, reflect.Float64},
|
||||||
reflect.Int: {DataTypeInteger, math.MinInt, math.MaxInt},
|
reflect.Int: {DataTypeInteger, math.MinInt, math.MaxInt, reflect.Int64},
|
||||||
reflect.Int16: {DataTypeInteger, math.MinInt16, math.MaxInt16},
|
reflect.Int16: {DataTypeInteger, math.MinInt16, math.MaxInt16, reflect.Int64},
|
||||||
reflect.Int32: {DataTypeInteger, math.MinInt32, math.MaxInt32},
|
reflect.Int32: {DataTypeInteger, math.MinInt32, math.MaxInt32, reflect.Int64},
|
||||||
reflect.Int64: {DataTypeInteger, math.MinInt64, math.MaxInt64},
|
reflect.Int64: {DataTypeInteger, math.MinInt64, math.MaxInt64, reflect.Int64},
|
||||||
reflect.Int8: {DataTypeInteger, math.MinInt8, math.MaxInt8},
|
reflect.Int8: {DataTypeInteger, math.MinInt8, math.MaxInt8, reflect.Int64},
|
||||||
reflect.String: {DataTypeString, -math.MaxFloat64, math.MaxFloat64},
|
reflect.String: {datatype: DataTypeString},
|
||||||
reflect.Uint: {DataTypeInteger, 0, math.MaxUint},
|
reflect.Uint: {DataTypeInteger, 0, math.MaxUint, reflect.Int64},
|
||||||
reflect.Uint16: {DataTypeInteger, 0, math.MaxUint16},
|
reflect.Uint16: {DataTypeInteger, 0, math.MaxUint16, reflect.Int64},
|
||||||
reflect.Uint32: {DataTypeInteger, 0, math.MaxUint32},
|
reflect.Uint32: {DataTypeInteger, 0, math.MaxUint32, reflect.Int64},
|
||||||
reflect.Uint64: {DataTypeInteger, 0, math.MaxUint64},
|
reflect.Uint64: {DataTypeInteger, 0, math.MaxUint64, reflect.Int64},
|
||||||
reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8},
|
reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8, reflect.Int64},
|
||||||
|
}
|
||||||
|
|
||||||
|
// reach is the least and greatest value v proves a field of this kind receives. An integer
|
||||||
|
// prints whole, so its bounds round inward.
|
||||||
|
func (k columnKind) reach(v proven) (lo, hi float64) {
|
||||||
|
if k.datatype == DataTypeInteger {
|
||||||
|
return math.Ceil(v.lo), math.Floor(v.hi)
|
||||||
|
}
|
||||||
|
return v.lo, v.hi
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkField rejects a column some render of which a field of Go type ft cannot hold: a null
|
// checkField rejects a column some render of which a field of Go type ft cannot hold: a null
|
||||||
@@ -237,13 +315,19 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro
|
|||||||
}
|
}
|
||||||
for _, it := range items {
|
for _, it := range items {
|
||||||
v := p.of(it)
|
v := p.of(it)
|
||||||
reason := v.not[kind.datatype]
|
if reason := v.not[kind.datatype]; reason != "" {
|
||||||
if reason == "" && (v.lo < kind.lo || v.hi > kind.hi) {
|
|
||||||
reason = fmt.Sprintf("%q is not proven within %s", it.format, elem.Kind())
|
|
||||||
}
|
|
||||||
if reason != "" {
|
|
||||||
return fmt.Errorf("%s (%s): %s", label, ft, reason)
|
return fmt.Errorf("%s (%s): %s", label, ft, reason)
|
||||||
}
|
}
|
||||||
|
if kind.datatype == DataTypeBoolean {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lo, hi := kind.reach(v); lo < kind.lo || hi > kind.hi {
|
||||||
|
past := hi
|
||||||
|
if lo < kind.lo {
|
||||||
|
past = lo
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s (%s): %q can reach %s, past %s; make it %s", label, ft, it.format, strconv.FormatFloat(past, 'g', -1, 64), elem.Kind(), kind.wider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -252,11 +336,11 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro
|
|||||||
func (s *structShape) fill(sess *session, v reflect.Value) {
|
func (s *structShape) fill(sess *session, v reflect.Value) {
|
||||||
if s.record != nil {
|
if s.record != nil {
|
||||||
for i, c := range renderRecord(sess, s.record, s.columns).columns {
|
for i, c := range renderRecord(sess, s.record, s.columns).columns {
|
||||||
setColumn(v.Field(s.fields[i]), c)
|
setColumn(fieldAt(v, s.fields[i]), c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, n := range s.nested {
|
for _, n := range s.nested {
|
||||||
field := v.Field(n.index)
|
field := fieldAt(v, n.index)
|
||||||
if field.Kind() == reflect.Pointer {
|
if field.Kind() == reflect.Pointer {
|
||||||
if field.IsNil() {
|
if field.IsNil() {
|
||||||
field.Set(reflect.New(field.Type().Elem()))
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
@@ -267,6 +351,20 @@ func (s *structShape) fill(sess *session, v reflect.Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fieldAt is v's field at index, allocating each nil embedded pointer on the way.
|
||||||
|
func fieldAt(v reflect.Value, index []int) reflect.Value {
|
||||||
|
for _, i := range index {
|
||||||
|
if v.Kind() == reflect.Pointer {
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
v = v.Field(i)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// setColumn writes a drawn column into its field: a null as a nil pointer, a value through a
|
// setColumn writes a drawn column into its field: a null as a nil pointer, a value through a
|
||||||
// fresh pointer or straight into the field.
|
// fresh pointer or straight into the field.
|
||||||
func setColumn(field reflect.Value, c Column) {
|
func setColumn(field reflect.Value, c Column) {
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ The record API lands first, so the data update can use it.
|
|||||||
- two bare `{/sv_SE.word}` → two words
|
- two bare `{/sv_SE.word}` → two words
|
||||||
- Reference inheritance — settle whether a column that is exactly one reference to
|
- Reference inheritance — settle whether a column that is exactly one reference to
|
||||||
another record's column, like `{/src.score}`, takes that column's datatype and
|
another record's column, like `{/src.score}`, takes that column's datatype and
|
||||||
null. Today a null there writes `""`, and a typed column reading it is refused.
|
null. Today a null there writes `""`, a `*T` struct field reading it gets `""`
|
||||||
Settle before draw groups and the data update.
|
rather than nil, and a typed column reading it is refused. Settle before draw
|
||||||
|
groups and the data update.
|
||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user