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 47 additions and 18 deletions
Showing only changes of commit 3cd3f8f317 - Show all commits
+4 -3
View File
@@ -193,9 +193,10 @@ naming a kind that holds it. The fields an embedded struct promotes are columns
same record; a named struct field, or a pointer to one, fills from its own tags as a same record; a named struct field, or a pointer to one, fills from its own tags as a
record of its own, so its references draw apart from its parent's. `fake:"-"` leaves a record of its own, so its references draw apart from its parent's. `fake:"-"` leaves a
struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep
their values, and so does a pointer back to a struct already being filled. The first call for a type compiles its tags and reports their values, and so does a pointer back to a struct already being filled; a type
what they get wrong, with the same error on every later call; a `datatype` in a tag whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The
names the Go type that already sets it. 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
+13 -2
View File
@@ -65,7 +65,7 @@ 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 { 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 false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path))
} }
return true, nil return true, nil
} }
@@ -73,7 +73,7 @@ func isTemplate(arg string) (bool, error) {
return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1]) return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
} }
if path := strings.TrimLeft(arg, "/"); path != arg && path != "" { if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
return false, fmt.Errorf("path %s starts with /, and every path starts at the root already; write %s", arg, path) return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
} }
return false, nil return false, nil
} }
@@ -82,6 +82,14 @@ func isJSONStart(arg string) bool {
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`) return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
} }
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
func pathAdvice(path, refusal string) error {
if err := checkPathNames(path); err != nil {
return err
}
return fmt.Errorf("%s; write %s", refusal, path)
}
// loneReference is the path a template spells when the value it holds — a format string, a // loneReference is the path a template spells when the value it holds — a format string, a
// JSON string, or an object holding only a format — is one reference token and nothing else. // JSON string, or an object holding only a format — is one reference token and nothing else.
func loneReference(arg string) (string, bool) { func loneReference(arg string) (string, bool) {
@@ -101,6 +109,9 @@ func loneReference(arg string) (string, bool) {
if units[0].kind != 'b' || !isRef(body) || strings.ContainsAny(body, "|(") { if units[0].kind != 'b' || !isRef(body) || strings.ContainsAny(body, "|(") {
return "", false return "", false
} }
if strings.HasPrefix(body, "/") {
body = "/" + strings.TrimLeft(body, "/")
}
_, path, err := refShape(body) _, path, err := refShape(body)
return path, err == nil return path, err == nil
} }
+10
View File
@@ -396,6 +396,16 @@ func checkName(name string) error {
return nil return nil
} }
// checkPathNames rejects a dotted path with a segment no name may be.
func checkPathNames(path string) error {
for _, seg := range strings.Split(path, ".") {
if err := checkName(seg); err != nil {
return fmt.Errorf("path %w", err)
}
}
return nil
}
// isOption reports whether a template key configures the node instead of naming a // isOption reports whether a template key configures the node instead of naming a
// field. These names can never be fields. // field. These names can never be fields.
func isOption(name string) bool { func isOption(name string) bool {
+20 -13
View File
@@ -36,9 +36,8 @@ type structResult struct {
err error err error
} }
// maxStructRecords caps the records one struct type fills, which pointers between struct // maxStructs caps the structs compiling one type walks through its fields.
// types multiply along every path. const maxStructs = 1 << 10
const maxStructRecords = 1 << 10
// structShapeOf compiles a struct type once and remembers the answer. Callers hold the // structShapeOf compiles a struct type once and remembers the answer. Callers hold the
// generator's lock. // generator's lock.
@@ -50,7 +49,7 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
if label == "" { if label == "" {
label = "struct" label = "struct"
} }
sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, records: maxStructRecords} sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, structs: maxStructs}
shape, err := sc.record(t, label) shape, err := sc.record(t, label)
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)
@@ -79,13 +78,13 @@ type nestedStruct struct {
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 }
// structCompile is what compiling one struct type shares across the records it reaches: the // structCompile is what compiling one struct type shares across the structs it reaches: the
// loaded tree, the types compiling or embedded above, so a pointer back to one is left alone // loaded tree, the types compiling or embedded above, so a pointer back to one is left alone
// rather than filled without end, and how many more records it may build. // rather than filled without end, and how many more structs it may walk.
type structCompile struct { type structCompile struct {
root map[string]node root map[string]node
visiting map[reflect.Type]bool visiting map[reflect.Type]bool
records int structs int
} }
// structFields gathers what one struct type fills: its tagged fields, those its embedded // structFields gathers what one struct type fills: its tagged fields, those its embedded
@@ -100,8 +99,8 @@ type structFields struct {
} }
func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) { func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) {
if sc.records--; sc.records < 0 { if err := sc.spend(label); err != nil {
return nil, fmt.Errorf(`%s: the struct fields reach more than %d records; leave a pointer unfilled with fake:"-"`, label, maxStructRecords) return nil, err
} }
sc.visiting[t] = true sc.visiting[t] = true
defer delete(sc.visiting, t) defer delete(sc.visiting, t)
@@ -117,6 +116,13 @@ func (sc *structCompile) record(t reflect.Type, label string) (*structShape, err
return c.shape, nil return c.shape, nil
} }
func (sc *structCompile) spend(label string) error {
if sc.structs--; sc.structs >= 0 {
return nil
}
return fmt.Errorf(`%s: the struct fields reach more than %d structs; leave a struct field unfilled with fake:"-"`, label, maxStructs)
}
// walk gathers the fields of struct type t, which sits at index within c.t. // walk gathers the fields of struct type t, which sits at index within c.t.
func (c *structFields) walk(t reflect.Type, index []int) error { func (c *structFields) walk(t reflect.Type, index []int) error {
for i := 0; i < t.NumField(); i++ { for i := 0; i < t.NumField(); i++ {
@@ -184,6 +190,9 @@ func fieldPath(t reflect.Type, index []int) string {
} }
func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error { func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error {
if err := c.spend(c.label + "." + fieldPath(c.t, sf.Index)); err != nil {
return err
}
c.visiting[elem] = true c.visiting[elem] = true
defer delete(c.visiting, elem) defer delete(c.visiting, elem)
tags, nested := len(c.tags), len(c.shape.nested) tags, nested := len(c.tags), len(c.shape.nested)
@@ -218,10 +227,8 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
case inline: case inline:
return inputValue(tag) return inputValue(tag)
} }
for _, seg := range strings.Split(tag, ".") { if err := checkPathNames(tag); err != nil {
if err := checkName(seg); err != nil { return nil, err
return nil, fmt.Errorf("path %w", err)
}
} }
return "{/" + tag + "}", nil return "{/" + tag + "}", nil
} }