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)