fix: decode untagged embedded structs and maps inline
Test / test (push) Successful in 1m26s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-17 20:05:50 +02:00
parent 93c36cf376
commit fc50e3c49a
4 changed files with 226 additions and 28 deletions
+94 -20
View File
@@ -105,16 +105,31 @@ func (d *decoder) assignTable(tbl map[string]any, dst reflect.Value) error {
}
func (d *decoder) assignStruct(tbl map[string]any, dst reflect.Value) error {
fields := structFields(dst.Type())
schema := newStructSchema(dst.Type())
for key, val := range tbl {
field, ok := fields[strings.ToLower(key)]
field, ok := schema.byName[strings.ToLower(key)]
if !ok {
if d.disallowUnknown {
return fmt.Errorf("interpres: unknown field %q for %s", key, dst.Type())
}
if schema.embedMaps != nil {
// Leftover keys land in an untagged embedded map, the inverse
// of the encoder inlining that map's entries.
mv, err := fieldByIndex(dst, schema.embedMaps[0])
if err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := d.assignMap(map[string]any{key: val}, mv); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
}
continue
}
if err := d.assign(val, dst.Field(field)); err != nil {
fv, err := fieldByIndex(dst, field.index)
if err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := d.assign(val, fv); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
}
@@ -219,26 +234,85 @@ func setFloat(dst reflect.Value, v float64) error {
}
}
// structFields builds a lower-cased lookup of field name → field index for the
// exported fields of t, honouring `toml:"name"` tags.
func structFields(t reflect.Type) map[string]int {
fields := make(map[string]int, t.NumField())
for i := range t.NumField() {
f := t.Field(i)
if f.PkgPath != "" { // unexported
continue
}
name := f.Name
if tag, ok := f.Tag.Lookup("toml"); ok {
tag, _, _ = strings.Cut(tag, ",")
if tag == "-" {
// structFieldLoc locates one destination field by its index path from the
// struct root and by the depth the field sits at, which breaks name clashes
// in favour of the shallower field.
type structFieldLoc struct {
index []int
depth int
}
// structSchema flattens the exported fields of t for decode, mirroring the
// encoder: an untagged embedded struct is inlined, so its own fields match
// keys of the same table, and an untagged embedded map is recorded in
// embedMaps (first declaration first) as the destination for leftover keys.
// When two fields resolve to one name, the shallower wins, then the later
// declaration.
type structSchema struct {
byName map[string]structFieldLoc
embedMaps [][]int
}
func newStructSchema(t reflect.Type) structSchema {
s := structSchema{byName: make(map[string]structFieldLoc, t.NumField())}
var walk func(t reflect.Type, prefix []int, depth int)
walk = func(t reflect.Type, prefix []int, depth int) {
for i := range t.NumField() {
f := t.Field(i)
if f.PkgPath != "" { // unexported
continue
}
if tag != "" {
name = tag
path := append(append([]int{}, prefix...), i)
name := ""
if tag, ok := f.Tag.Lookup("toml"); ok {
name, _, _ = strings.Cut(tag, ",")
if name == "-" {
continue
}
}
if f.Anonymous && name == "" {
ft := f.Type
for ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
switch {
case ft.Kind() == reflect.Struct && !isScalarStruct(ft):
walk(ft, path, depth+1)
continue
case ft.Kind() == reflect.Map && ft.Key().Kind() == reflect.String:
s.embedMaps = append(s.embedMaps, path)
continue
}
name = f.Name
}
if name == "" {
name = f.Name
}
key := strings.ToLower(name)
if existing, ok := s.byName[key]; !ok || depth < existing.depth {
s.byName[key] = structFieldLoc{index: path, depth: depth}
}
}
fields[strings.ToLower(name)] = i
}
return fields
walk(t, nil, 0)
return s
}
// fieldByIndex walks an index path from a struct value, allocating nil
// pointers along the way so a key can reach through an embedded pointer
// struct. Every field on the path is exported, so each step is settable.
func fieldByIndex(v reflect.Value, path []int) (reflect.Value, error) {
for i, x := range path {
v = v.Field(x)
if i < len(path)-1 && v.Kind() == reflect.Pointer {
if v.IsNil() {
if !v.CanSet() {
return reflect.Value{}, fmt.Errorf("cannot allocate nil embedded pointer")
}
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
}
return v, nil
}