This is an automated email from the ASF dual-hosted git repository.

Alanxtl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/develop by this push:
     new 744af790b fix(generic): honor m tag options (#3709)
744af790b is described below

commit 744af790bf08e4c5fa2c7381271b5a72a6981517
Author: Oxidaner <[email protected]>
AuthorDate: Tue Sep 1 17:09:16 2026 +0800

    fix(generic): honor m tag options (#3709)
    
    * fix(generic): honor m tag options
    
    Signed-off-by: Oxidaner <[email protected]>
    
    * fix forma
    t
    
    Signed-off-by: Oxidaner <[email protected]>
    
    * fix(generic): preserve parent class when squashing POJO
    
    * fix(generic): lowercase the first rune, not the first byte
    
    toUnexport derived an untagged field's wire key with
    strings.ToLower(a[:1]) + a[1:], which slices one byte. For a field whose
    first rune is multi-byte that splits the rune, so an exported field named
    with a non-ASCII letter generalized to a key containing an invalid UTF-8
    fragment — and realization, matching case-insensitively against the intact
    Go name, could not find it again.
    
    Decoding the rune first keeps ASCII names byte-identical and makes the
    non-ASCII case round-trip.
    
    * style(generic): modernize unicode test type lookup
    
    ---------
    
    Signed-off-by: Oxidaner <[email protected]>
---
 filter/generic/generalizer/map.go      | 133 ++++++++++++++++++++++++++-------
 filter/generic/generalizer/map_test.go | 133 ++++++++++++++++++++++++++++++++-
 2 files changed, 237 insertions(+), 29 deletions(-)

diff --git a/filter/generic/generalizer/map.go 
b/filter/generic/generalizer/map.go
index b09cc2cb1..4c0122154 100644
--- a/filter/generic/generalizer/map.go
+++ b/filter/generic/generalizer/map.go
@@ -18,11 +18,14 @@
 package generalizer
 
 import (
+       "maps"
        "reflect"
        "strconv"
        "strings"
        "sync"
        "time"
+       "unicode"
+       "unicode/utf8"
 )
 
 import (
@@ -56,7 +59,10 @@ func GetMapGeneralizer() Generalizer {
 type MapGeneralizer struct{}
 
 func (g *MapGeneralizer) Generalize(obj any) (gobj any, err error) {
-       gobj = objToMap(obj)
+       gobj, err = objToMap(obj)
+       if err != nil {
+               return nil, perrors.Errorf("generalizing map failed, %v", err)
+       }
        if !getGenericIncludeClass() {
                gobj = removeClass(gobj)
        }
@@ -157,9 +163,9 @@ func removeClass(obj any) any {
 }
 
 // objToMap converts an object(any) to a map
-func objToMap(obj any) any {
+func objToMap(obj any) (any, error) {
        if obj == nil {
-               return obj
+               return obj, nil
        }
 
        t := reflect.TypeOf(obj)
@@ -183,40 +189,62 @@ func objToMap(obj any) any {
                for i := 0; i < t.NumField(); i++ {
                        field := t.Field(i)
                        value := v.Field(i)
+                       tag := parseMTag(field)
+                       if tag.ignore || tag.omitEmpty && isEmptyValue(value) {
+                               continue
+                       }
                        kind := value.Kind()
                        if !value.CanInterface() {
                                logger.Debugf("[Filter][Generic] objToMap is 
skipped because it couldn't be converted to interface, field=%v", field)
                                continue
                        }
                        valueIface := value.Interface()
+                       var generalizedValue any
+                       var err error
                        switch kind {
                        case reflect.Pointer:
                                if value.IsNil() {
-                                       setInMap(result, field, nil)
-                                       continue
+                                       generalizedValue = nil
+                                       break
                                }
-                               setInMap(result, field, objToMap(valueIface))
+                               generalizedValue, err = objToMap(valueIface)
                        case reflect.Struct, reflect.Slice, reflect.Map:
                                if isPrimitive(valueIface) {
                                        logger.Warnf("[Filter][Generic] %q is 
primitive. Cross-language transfer (e.g., dubbo-go <-> dubbo-java) may crash. 
Use basic types like string.", value.Type())
-                                       setInMap(result, field, valueIface)
-                                       continue
+                                       generalizedValue = valueIface
+                                       break
                                }
 
-                               setInMap(result, field, objToMap(valueIface))
+                               generalizedValue, err = objToMap(valueIface)
                        default:
-                               setInMap(result, field, valueIface)
+                               generalizedValue = valueIface
+                       }
+                       if err != nil {
+                               return nil, err
                        }
+                       if tag.squash {
+                               squashed, ok := 
generalizedValue.(map[string]any)
+                               if !ok {
+                                       return nil, perrors.Errorf("cannot 
squash non-struct type '%s'", value.Type())
+                               }
+                               delete(squashed, "class")
+                               maps.Copy(result, squashed)
+                               continue
+                       }
+                       result[tag.name] = generalizedValue
                }
-               return result
+               return result, nil
        case reflect.Array, reflect.Slice:
                value := reflect.ValueOf(obj)
                newTemps := make([]any, 0, value.Len())
                for i := 0; i < value.Len(); i++ {
-                       newTemp := objToMap(value.Index(i).Interface())
+                       newTemp, err := objToMap(value.Index(i).Interface())
+                       if err != nil {
+                               return nil, err
+                       }
                        newTemps = append(newTemps, newTemp)
                }
-               return newTemps
+               return newTemps, nil
        case reflect.Map:
                newTempMap := make(map[any]any, v.Len())
                iter := v.MapRange()
@@ -226,13 +254,17 @@ func objToMap(obj any) any {
                        }
                        key := iter.Key()
                        mapV := iter.Value().Interface()
-                       newTempMap[mapKey(key)] = objToMap(mapV)
+                       generalizedValue, err := objToMap(mapV)
+                       if err != nil {
+                               return nil, err
+                       }
+                       newTempMap[mapKey(key)] = generalizedValue
                }
-               return newTempMap
+               return newTempMap, nil
        case reflect.Pointer:
                return objToMap(v.Elem().Interface())
        default:
-               return obj
+               return obj, nil
        }
 }
 
@@ -254,20 +286,69 @@ func mapKey(key reflect.Value) any {
        }
 }
 
-// setInMap sets the struct into the map using the tag or the name of the 
struct as the key
-func setInMap(m map[string]any, structField reflect.StructField, value any) 
(result map[string]any) {
-       result = m
-       if tagName := structField.Tag.Get("m"); tagName == "" {
-               result[toUnexport(structField.Name)] = value
-       } else {
-               result[tagName] = value
+type mTag struct {
+       name      string
+       ignore    bool
+       omitEmpty bool
+       squash    bool
+}
+
+func parseMTag(field reflect.StructField) mTag {
+       tag := mTag{name: toUnexport(field.Name)}
+       tagValue := field.Tag.Get("m")
+       name, options, hasOptions := strings.Cut(tagValue, ",")
+       if name == "-" {
+               tag.ignore = true
+               return tag
        }
-       return
+       if name != "" {
+               tag.name = name
+       }
+       if !hasOptions {
+               return tag
+       }
+
+       for option := range strings.SplitSeq(options, ",") {
+               switch option {
+               case "omitempty":
+                       tag.omitEmpty = true
+               case "squash":
+                       tag.squash = true
+               }
+       }
+       return tag
 }
 
-// toUnexport is to lower the first letter
+func isEmptyValue(value reflect.Value) bool {
+       switch value.Kind() {
+       case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+               return value.Len() == 0
+       case reflect.Bool:
+               return !value.Bool()
+       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, 
reflect.Int64:
+               return value.Int() == 0
+       case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, 
reflect.Uint64, reflect.Uintptr:
+               return value.Uint() == 0
+       case reflect.Float32, reflect.Float64:
+               return value.Float() == 0
+       case reflect.Interface, reflect.Pointer:
+               return value.IsNil()
+       default:
+               return false
+       }
+}
+
+// toUnexport lowercases the first rune of a.
+//
+// Rune-based rather than byte-based: strings.ToLower(a[:1]) splits a 
multi-byte
+// leading rune, so an exported field named with a non-ASCII letter used to
+// generalize to a key containing an invalid UTF-8 fragment.
 func toUnexport(a string) string {
-       return strings.ToLower(a[:1]) + a[1:]
+       if a == "" {
+               return a
+       }
+       first, size := utf8.DecodeRuneInString(a)
+       return string(unicode.ToLower(first)) + a[size:]
 }
 
 // isPrimitive determines if the object is primitive
diff --git a/filter/generic/generalizer/map_test.go 
b/filter/generic/generalizer/map_test.go
index cdeaca910..e4fb40bd9 100644
--- a/filter/generic/generalizer/map_test.go
+++ b/filter/generic/generalizer/map_test.go
@@ -22,9 +22,12 @@ import (
        "strconv"
        "testing"
        "time"
+       "unicode/utf8"
 )
 
 import (
+       "github.com/mitchellh/mapstructure"
+
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 )
@@ -54,6 +57,34 @@ type testMTagObj struct {
        Name   string
 }
 
+type testMTagEmbedded struct {
+       City string `m:"city_name"`
+}
+
+type testMTagOptionsObj struct {
+       ID       string           `m:"id,omitempty"`
+       Empty    string           `m:"empty,omitempty"`
+       Ignored  string           `m:"-"`
+       Embedded testMTagEmbedded `m:",squash"`
+}
+
+type testMTagSquashedPOJO struct {
+       City string `m:"city"`
+}
+
+func (testMTagSquashedPOJO) JavaClassName() string {
+       return "org.apache.dubbo.testMTagSquashedPOJO"
+}
+
+type testMTagSquashParentPOJO struct {
+       Name     string               `m:"name"`
+       Embedded testMTagSquashedPOJO `m:",squash"`
+}
+
+func (testMTagSquashParentPOJO) JavaClassName() string {
+       return "org.apache.dubbo.testMTagSquashParentPOJO"
+}
+
 func TestObjToMap(t *testing.T) {
        obj := &testPlainObj{}
        obj.AaAa = "1"
@@ -64,7 +95,9 @@ func TestObjToMap(t *testing.T) {
        obj.CaCa.XxYy.Xx = "3"
        obj.DaDa = time.Date(2020, 10, 29, 2, 34, 0, 0, time.Local)
        obj.EeEe = 100
-       m := objToMap(obj).(map[string]any)
+       generalized, err := objToMap(obj)
+       require.NoError(t, err)
+       m := generalized.(map[string]any)
        assert.Equal(t, "1", m["aaAa"].(string))
        assert.Equal(t, "1", m["baBa"].(string))
        assert.Equal(t, "2", m["caCa"].(map[string]any)["aaAa"].(string))
@@ -93,6 +126,97 @@ func TestMTagRoundTrip(t *testing.T) {
        assert.Equal(t, original, realized)
 }
 
+func TestMTagOptionsRoundTrip(t *testing.T) {
+       original := testMTagOptionsObj{
+               ID:      "42",
+               Ignored: "secret",
+               Embedded: testMTagEmbedded{
+                       City: "Hangzhou",
+               },
+       }
+
+       generalized, err := mockMapGeneralizer.Generalize(original)
+       require.NoError(t, err)
+       generalizedMap, ok := generalized.(map[string]any)
+       require.True(t, ok)
+       expected := map[string]any{}
+       decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
+               Result:  &expected,
+               TagName: "m",
+       })
+       require.NoError(t, err)
+       require.NoError(t, decoder.Decode(original))
+       assert.Equal(t, expected, generalizedMap)
+
+       realized, err := mockMapGeneralizer.Realize(generalized, 
reflect.TypeFor[testMTagOptionsObj]())
+       require.NoError(t, err)
+       realizedObj, ok := realized.(testMTagOptionsObj)
+       require.True(t, ok)
+       assert.Equal(t, original.ID, realizedObj.ID)
+       assert.Empty(t, realizedObj.Empty)
+       assert.Empty(t, realizedObj.Ignored)
+       assert.Equal(t, original.Embedded.City, realizedObj.Embedded.City)
+}
+
+func TestMTagSquashPreservesParentPOJOClass(t *testing.T) {
+       original := testMTagSquashParentPOJO{
+               Name: "alice",
+               Embedded: testMTagSquashedPOJO{
+                       City: "Hangzhou",
+               },
+       }
+
+       generalized, err := GetMapGeneralizer().Generalize(original)
+       require.NoError(t, err)
+       generalizedMap, ok := generalized.(map[string]any)
+       require.True(t, ok)
+       assert.Equal(t, "org.apache.dubbo.testMTagSquashParentPOJO", 
generalizedMap["class"])
+       assert.Equal(t, "alice", generalizedMap["name"])
+       assert.Equal(t, "Hangzhou", generalizedMap["city"])
+}
+
+func TestMTagSquashRejectsNonStruct(t *testing.T) {
+       obj := struct {
+               Value string `m:",squash"`
+       }{Value: "invalid"}
+
+       _, err := mockMapGeneralizer.Generalize(obj)
+       require.ErrorContains(t, err, "cannot squash non-struct type")
+}
+
+func TestUntaggedNameLowercasesByRune(t *testing.T) {
+       // Lowercasing the first byte instead of the first rune split multi-byte
+       // leading runes, producing keys that were not valid UTF-8.
+       obj := struct {
+               Ünicode string
+               Ascii   string
+       }{Ünicode: "value", Ascii: "value"}
+
+       out, err := mockMapGeneralizer.Generalize(obj)
+       require.NoError(t, err)
+       m := out.(map[string]any)
+
+       assert.Contains(t, m, "ünicode")
+       assert.Contains(t, m, "ascii", "ASCII names must keep behaving exactly 
as before")
+       for key := range m {
+               assert.True(t, utf8.ValidString(key), "key %q is not valid 
UTF-8", key)
+       }
+}
+
+func TestUntaggedNameRoundTripsForUnicode(t *testing.T) {
+       type unicodeNamed struct {
+               Ünicode string
+       }
+       original := unicodeNamed{Ünicode: "value"}
+
+       generalized, err := mockMapGeneralizer.Generalize(original)
+       require.NoError(t, err)
+
+       realized, err := mockMapGeneralizer.Realize(generalized, 
reflect.TypeFor[unicodeNamed]())
+       require.NoError(t, err)
+       assert.Equal(t, original, realized)
+}
+
 type testStruct struct {
        AaAa string
        BaBa string `m:"baBa"`
@@ -116,7 +240,9 @@ func TestObjToMap_Slice(t *testing.T) {
        tmp.XxYy.xxXx = "3"
        tmp.XxYy.Xx = "3"
        testData.CaCa = append(testData.CaCa, tmp)
-       m := objToMap(testData).(map[string]any)
+       generalized, err := objToMap(testData)
+       require.NoError(t, err)
+       m := generalized.(map[string]any)
 
        assert.Equal(t, "1", m["aaAa"].(string))
        assert.Equal(t, "1", m["baBa"].(string))
@@ -151,7 +277,8 @@ func TestObjToMap_Map(t *testing.T) {
        testData.CaCa["k1"] = "v1"
        testData.CaCa["kv2"] = "v2"
        testData.IntMap[1] = 1
-       m := objToMap(testData)
+       m, err := objToMap(testData)
+       require.NoError(t, err)
 
        assert.Equal(t, reflect.Map, reflect.TypeOf(m).Kind())
        mappedStruct := m.(map[string]any)

Reply via email to