Struct-filling #14

Merged
lilleman merged 10 commits from struct-filling into main 2026-09-15 17:52:29 +02:00
4 changed files with 237 additions and 100 deletions
Showing only changes of commit e6bd4c5972 - Show all commits
+28 -12
View File
@@ -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
+23 -1
View File
@@ -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 {
+178 -80
View File
@@ -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) {
+3 -2
View File
@@ -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