From 60a9312057bead9ae10c16b1e3f43cc1afbf2862 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 10 Feb 2026 11:43:59 +0800 Subject: [PATCH] chore: structure support remain-tagged field --- common/structure/structure.go | 39 ++++++++++++++++++++++++++++-- common/structure/structure_test.go | 22 +++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/common/structure/structure.go b/common/structure/structure.go index a16e9dd0..1bce1516 100644 --- a/common/structure/structure.go +++ b/common/structure/structure.go @@ -421,6 +421,11 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e field reflect.StructField val reflect.Value } + + // remainField is set to a valid field set with the "remain" tag if + // we are keeping track of remaining values. + var remainField *field + var fields []field for len(structs) > 0 { structVal := structs[0] @@ -438,6 +443,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e // If "squash" is specified in the tag, we squash the field down. squash := fieldVal.Kind() == reflect.Struct && fieldType.Anonymous + remain := false // We always parse the tags cause we're looking for other tags too tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",") @@ -446,6 +452,11 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e squash = true break } + + if tag == "remain" { + remain = true + break + } } if squash { @@ -458,8 +469,13 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e continue } - // Normal struct field, store it away - fields = append(fields, field{fieldType, fieldVal}) + // Build our field + if remain { + remainField = &field{fieldType, fieldVal} + } else { + // Normal struct field, store it away + fields = append(fields, field{fieldType, fieldVal}) + } } } @@ -545,6 +561,25 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e } } + // If we have a "remain"-tagged field and we have unused keys then + // we put the unused keys directly into the remain field. + if remainField != nil && len(dataValKeysUnused) > 0 { + // Build a map of only the unused values + remain := map[interface{}]interface{}{} + for key := range dataValKeysUnused { + remain[key] = dataVal.MapIndex(reflect.ValueOf(key)).Interface() + } + + // Decode it as-if we were just decoding this map onto our map. + if err := d.decodeMap(name, remain, remainField.val); err != nil { + errors = append(errors, err.Error()) + } + + // Set the map to nil so we have none so that the next check will + // not error (ErrorUnused) + dataValKeysUnused = nil + } + if len(targetValKeysUnused) > 0 { keys := make([]string, 0, len(targetValKeysUnused)) for rawKey := range targetValKeysUnused { diff --git a/common/structure/structure_test.go b/common/structure/structure_test.go index 5de24823..1e0cbaa7 100644 --- a/common/structure/structure_test.go +++ b/common/structure/structure_test.go @@ -160,6 +160,28 @@ func TestStructure_DoubleNest(t *testing.T) { assert.Equal(t, s.Bar.BazOptional, goal) } +func TestStructure_Remain(t *testing.T) { + rawMap := map[string]any{ + "foo": 1, + "bar": "test", + "extra": false, + } + + goal := &Baz{ + Foo: 1, + Bar: "test", + } + + s := &struct { + Baz + Remain map[string]any `test:",remain"` + }{} + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, *goal, s.Baz) + assert.Equal(t, map[string]any{"extra": false}, s.Remain) +} + func TestStructure_SliceNilValue(t *testing.T) { rawMap := map[string]any{ "foo": 1,