This is an automated email from the ASF dual-hosted git repository.
AlexStocks 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 81abe9ddb Fix(generic): complete typed result and generic mode
handling (#3618)
81abe9ddb is described below
commit 81abe9ddb6d0ffd4a8fd7e1b895894e444654949
Author: XiaoFei <[email protected]>
AuthorDate: Wed Aug 12 15:40:53 2026 +0800
Fix(generic): complete typed result and generic mode handling (#3618)
* fix(generic): complete typed result handling
---
client/client.go | 5 +-
client/client_test.go | 114 +++++++++++++++++++++++
client/options_test.go | 17 ++++
filter/generic/filter.go | 28 ++++--
filter/generic/filter_test.go | 159 ++++++++++++++++++++++++++++++--
filter/generic/generalizer/bean.go | 3 +
filter/generic/generalizer/gson.go | 4 +
filter/generic/generalizer/gson_test.go | 4 +-
filter/generic/service.go | 51 +++++++---
filter/generic/service_test.go | 121 ++++++++++++++++++++++++
filter/generic/util.go | 78 ++++++++++++++++
internal/config.go | 8 +-
internal/config_test.go | 3 +
13 files changed, 559 insertions(+), 36 deletions(-)
diff --git a/client/client.go b/client/client.go
index b7406409f..21c39beb9 100644
--- a/client/client.go
+++ b/client/client.go
@@ -167,10 +167,13 @@ func (cli *Client) NewGenericService(referenceStr string,
opts ...ReferenceOptio
finalOpts = append(finalOpts, opts...)
genericService := generic.NewGenericService(referenceStr)
- _, err := cli.DialWithService(referenceStr, genericService,
finalOpts...)
+ connection, err := cli.DialWithService(referenceStr, genericService,
finalOpts...)
if err != nil {
return nil, err
}
+ if err :=
genericService.SetGenericType(connection.refOpts.Reference.Generic); err != nil
{
+ return nil, err
+ }
return genericService, nil
}
diff --git a/client/client_test.go b/client/client_test.go
index 837925a67..028e00e06 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -30,11 +30,16 @@ import (
)
import (
+ _ "dubbo.apache.org/dubbo-go/v3/cluster/cluster/available"
+ _ "dubbo.apache.org/dubbo-go/v3/cluster/router/condition"
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
+ "dubbo.apache.org/dubbo-go/v3/common/extension"
+ _ "dubbo.apache.org/dubbo-go/v3/filter/graceful_shutdown"
"dubbo.apache.org/dubbo-go/v3/global"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
"dubbo.apache.org/dubbo-go/v3/protocol/result"
+ _ "dubbo.apache.org/dubbo-go/v3/proxy/proxy_factory"
)
type fakeInvoker struct {
@@ -43,6 +48,55 @@ type fakeInvoker struct {
res result.Result
}
+type genericClientTestUser struct {
+ Name string `json:"name"`
+ Age int `json:"age"`
+}
+
+func (genericClientTestUser) JavaClassName() string {
+ return "org.apache.dubbo.test.User"
+}
+
+type genericResultProtocol struct {
+ *base.BaseProtocol
+}
+
+func (p *genericResultProtocol) Refer(url *common.URL) base.Invoker {
+ return &genericResultInvoker{BaseInvoker: base.NewBaseInvoker(url)}
+}
+
+type genericResultInvoker struct {
+ *base.BaseInvoker
+}
+
+func (i *genericResultInvoker) Invoke(_ context.Context, inv base.Invocation)
result.Result {
+ mode := inv.GetAttachmentWithDefaultValue(
+ constant.GenericKey,
+ i.GetURL().GetParam(constant.GenericKey,
constant.GenericSerializationDefault),
+ )
+ var response any
+ switch mode {
+ case constant.GenericSerializationGson,
constant.GenericSerializationProtobufJson:
+ response = `{"name":"gsonUser","age":42}`
+ default:
+ response = map[string]any{"name": "mapUser", "age": 41}
+ }
+ reply := inv.Reply().(*any)
+ *reply = response
+ return &result.RPCResult{Rest: inv.Reply()}
+}
+
+func registerGenericResultProtocol(t *testing.T, protocolName string) {
+ t.Helper()
+ extension.SetProtocol(protocolName, func() base.Protocol {
+ proto := base.NewBaseProtocol()
+ return &genericResultProtocol{BaseProtocol: &proto}
+ })
+ t.Cleanup(func() {
+ extension.UnregisterProtocol(protocolName)
+ })
+}
+
func (f *fakeInvoker) GetURL() *common.URL {
return nil
}
@@ -181,6 +235,66 @@ func TestConnectionCallPassesOptions(t *testing.T) {
require.Same(t, &responseTrailer, trailerTarget)
}
+func TestNewGenericServicePropagatesGenericTypeToTypedInvoke(t *testing.T) {
+ const protocolName = "generic-client-test"
+ registerGenericResultProtocol(t, protocolName)
+
+ cli, err := NewClient()
+ require.NoError(t, err)
+
+ service, err := cli.NewGenericService(
+ "org.apache.dubbo.test.UserProvider",
+ WithProtocol(protocolName),
+ WithURL(protocolName+"://127.0.0.1:1"),
+ WithClusterAvailable(),
+ WithGenericType(constant.GenericSerializationGson),
+ )
+ require.NoError(t, err)
+ if service.GenericType() != constant.GenericSerializationGson {
+ t.Errorf("generic service mode = %q, want %q",
service.GenericType(), constant.GenericSerializationGson)
+ }
+
+ var user *genericClientTestUser
+ err = service.InvokeWithType(context.Background(), "getUser", nil, nil,
&user)
+ require.NoError(t, err)
+ require.NotNil(t, user)
+ require.Equal(t, "gsonUser", user.Name)
+ require.Equal(t, 42, user.Age)
+}
+
+func TestNewGenericServiceInvokeKeepsRawResult(t *testing.T) {
+ tests := []struct {
+ name string
+ protocolName string
+ generic string
+ }{
+ {name: "gson", protocolName: "generic-client-raw-gson-test",
generic: constant.GenericSerializationGson},
+ {name: "protobuf-json", protocolName:
"generic-client-raw-protobuf-test", generic:
constant.GenericSerializationProtobufJson},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ registerGenericResultProtocol(t, tt.protocolName)
+ cli, err := NewClient()
+ require.NoError(t, err)
+
+ service, err := cli.NewGenericService(
+ "org.apache.dubbo.test.UserProvider",
+ WithProtocol(tt.protocolName),
+ WithURL(tt.protocolName+"://127.0.0.1:1"),
+ WithClusterAvailable(),
+ WithGenericType(tt.generic),
+ )
+ require.NoError(t, err)
+
+ res, err := service.Invoke(context.Background(),
"getUser", nil, nil)
+
+ require.NoError(t, err)
+ require.JSONEq(t, `{"name":"gsonUser","age":42}`,
res.(string))
+ })
+ }
+}
+
func TestCallUnary(t *testing.T) {
t.Run("success", func(t *testing.T) {
invoker := &fakeInvoker{res: &result.RPCResult{}}
diff --git a/client/options_test.go b/client/options_test.go
index 9b8bf4505..ca2976366 100644
--- a/client/options_test.go
+++ b/client/options_test.go
@@ -726,6 +726,23 @@ func TestWithURL(t *testing.T) {
processReferenceOptionsInitCases(t, cases)
}
+func TestWithGenericType(t *testing.T) {
+ cases := []referenceOptionsInitCase{
+ {
+ desc: "empty generic overrides default generic option",
+ opts: []ReferenceOption{
+ WithGeneric(),
+ WithGenericType(""),
+ },
+ verify: func(t *testing.T, refOpts *ReferenceOptions,
err error) {
+ require.NoError(t, err)
+ assert.Empty(t, refOpts.Reference.Generic)
+ },
+ },
+ }
+ processReferenceOptionsInitCases(t, cases)
+}
+
func TestWithFilter(t *testing.T) {
cases := []referenceOptionsInitCase{
{
diff --git a/filter/generic/filter.go b/filter/generic/filter.go
index bfada008b..b151bf7b7 100644
--- a/filter/generic/filter.go
+++ b/filter/generic/filter.go
@@ -172,7 +172,7 @@ func (f *genericFilter) OnResponse(_ context.Context, res
result.Result, invoker
if isGenericDisabled(configuredGeneric) {
return res
}
- _, g, err := resolveGeneralizer(configuredGeneric, inv)
+ generic, g, err := resolveGeneralizer(configuredGeneric, inv)
if err != nil {
res.SetError(err)
res.SetResult(nil)
@@ -196,27 +196,37 @@ func (f *genericFilter) OnResponse(_ context.Context, res
result.Result, invoker
if data == nil {
return res
}
-
- // Check if data is a map type that needs to be deserialized
+ replyElem := replyValue.Elem()
dataValue := reflect.ValueOf(data)
- if dataValue.Kind() != reflect.Map && dataValue.Kind() != reflect.Slice
{
- // If data is not a map or slice, it's already a primitive
type, no need to deserialize
+ if replyElem.Kind() == reflect.Interface &&
dataValue.Type().AssignableTo(replyElem.Type()) {
+ if dataValue.Kind() == reflect.Pointer && dataValue.Pointer()
== replyValue.Pointer() {
+ return res
+ }
+ replyElem.Set(dataValue)
+ return res
+ }
+
+ if !shouldRealizeTypedResult(data, generic) {
return res
}
// Get the element type that the pointer points to
- replyElemType := replyValue.Elem().Type()
+ replyElemType := replyElem.Type()
// Realize the map/slice to the target struct using shared helper
realized, err := realizeResult(data, replyElemType, g)
if err != nil {
logger.Warnf("[Filter][Generic] failed to deserialize generic
result, err=%v", err)
+ res.SetError(err)
+ res.SetResult(nil)
return res
}
- // Set the realized value to reply
- if realized != nil {
- replyValue.Elem().Set(reflect.ValueOf(realized))
+ if err := setRealizedReply(replyValue, realized); err != nil {
+ logger.Warnf("[Filter][Generic] failed to set generic result
reply, err=%v", err)
+ res.SetError(err)
+ res.SetResult(nil)
+ return res
}
// Update the result with the deserialized reply
diff --git a/filter/generic/filter_test.go b/filter/generic/filter_test.go
index 5328b6ab5..081f15571 100644
--- a/filter/generic/filter_test.go
+++ b/filter/generic/filter_test.go
@@ -30,6 +30,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "google.golang.org/protobuf/types/known/structpb"
)
import (
@@ -338,6 +340,10 @@ type mockUser struct {
Address *mockAddress
}
+func (mockUser) JavaClassName() string {
+ return "org.apache.dubbo.test.MockUser"
+}
+
type mockAddress struct {
City string
Country string
@@ -520,6 +526,148 @@ func TestFilter_OnResponse_WithSliceDeserialization(t
*testing.T) {
assert.Equal(t, 25, users[1].Age)
}
+func TestFilter_OnResponse_WithTypedReplyByGenericMode(t *testing.T) {
+ filter := &genericFilter{}
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInvoker := mock.NewMockInvoker(ctrl)
+ mockInvoker.EXPECT().GetURL().DoAndReturn(func() *common.URL {
+ return common.NewURLWithOptions(
+ common.WithParams(url.Values{}),
+ common.WithParamsValue(constant.GenericKey,
constant.GenericSerializationDefault),
+ )
+ }).AnyTimes()
+
+ tests := []struct {
+ name string
+ mode string
+ result any
+ }{
+ {
+ name: constant.GenericSerializationDefault,
+ mode: constant.GenericSerializationDefault,
+ result: map[string]any{
+ "name": "mapUser",
+ "age": 31,
+ },
+ },
+ {
+ name: constant.GenericSerializationGson,
+ mode: constant.GenericSerializationGson,
+ result: `{"name":"gsonUser","age":32}`,
+ },
+ {
+ name: constant.GenericSerializationBean,
+ mode: constant.GenericSerializationBean,
+ result: mustGeneralize(t,
generalizer.GetBeanGeneralizer(), mockUser{
+ Name: "beanUser",
+ Age: 33,
+ }),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var user mockUser
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&user),
+
invocation.WithAttachments(map[string]any{constant.GenericKey: tt.mode}),
+ )
+ res := &result.RPCResult{Rest: tt.result}
+
+ newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
+
+ require.NoError(t, newRes.Error())
+ assert.NotEmpty(t, user.Name)
+ assert.NotZero(t, user.Age)
+ assert.Same(t, &user, newRes.Result())
+ })
+ }
+}
+
+func TestFilter_OnResponse_WithProtobufJsonTypedReply(t *testing.T) {
+ filter := &genericFilter{}
+ invokeURL := common.NewURLWithOptions(
+ common.WithParams(url.Values{}),
+ common.WithParamsValue(constant.GenericKey,
constant.GenericSerializationProtobufJson),
+ )
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInvoker := mock.NewMockInvoker(ctrl)
+ mockInvoker.EXPECT().GetURL().Return(invokeURL).AnyTimes()
+
+ var reply structpb.Struct
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&reply),
+ )
+ res := &result.RPCResult{Rest: `{"name":"protoUser"}`}
+
+ newRes := filter.OnResponse(context.Background(), res, mockInvoker, inv)
+
+ require.NoError(t, newRes.Error())
+ assert.Equal(t, "protoUser", reply.Fields["name"].GetStringValue())
+ assert.Same(t, &reply, newRes.Result())
+}
+
+func TestFilter_OnResponse_KeepsRawResultForAnyReply(t *testing.T) {
+ filter := &genericFilter{}
+ invokeURL := common.NewURLWithOptions(
+ common.WithParams(url.Values{}),
+ common.WithParamsValue(constant.GenericKey,
constant.GenericSerializationDefault),
+ )
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInvoker := mock.NewMockInvoker(ctrl)
+ mockInvoker.EXPECT().GetURL().Return(invokeURL).AnyTimes()
+
+ for _, genericType := range []string{
+ constant.GenericSerializationGson,
+ constant.GenericSerializationProtobufJson,
+ } {
+ t.Run(genericType, func(t *testing.T) {
+ const rawResult = `{"name":"rawUser"}`
+ t.Run("direct result", func(t *testing.T) {
+ var reply any
+ inv := invocation.NewRPCInvocationWithOptions(
+
invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&reply),
+
invocation.WithAttachments(map[string]any{constant.GenericKey: genericType}),
+ )
+
+ newRes :=
filter.OnResponse(context.Background(), &result.RPCResult{Rest: rawResult},
mockInvoker, inv)
+
+ require.NoError(t, newRes.Error())
+ assert.JSONEq(t, rawResult, reply.(string))
+ assert.JSONEq(t, rawResult,
newRes.Result().(string))
+ })
+
+ t.Run("protocol reply pointer", func(t *testing.T) {
+ reply := any(rawResult)
+ inv := invocation.NewRPCInvocationWithOptions(
+
invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&reply),
+
invocation.WithAttachments(map[string]any{constant.GenericKey: genericType}),
+ )
+ res := &result.RPCResult{Rest: &reply}
+
+ newRes :=
filter.OnResponse(context.Background(), res, mockInvoker, inv)
+
+ require.NoError(t, newRes.Error())
+ assert.JSONEq(t, rawResult, reply.(string))
+ assert.Same(t, &reply, newRes.Result())
+ })
+ })
+ }
+}
+
func TestFilter_OnResponse_WithUnsupportedGenericMode(t *testing.T) {
invokeURL := common.NewURLWithOptions(
common.WithParams(url.Values{}),
@@ -538,8 +686,8 @@ func TestFilter_OnResponse_WithUnsupportedGenericMode(t
*testing.T) {
assert.Nil(t, newRes.Result())
}
-// TestFilter_OnResponse_DeserializationError tests that OnResponse gracefully
handles
-// deserialization failures by logging a warning and returning the original
result.
+// TestFilter_OnResponse_DeserializationError tests that OnResponse returns an
explicit error
+// when typed result deserialization fails.
func TestFilter_OnResponse_DeserializationError(t *testing.T) {
invokeUrl := common.NewURLWithOptions(
common.WithParams(url.Values{}),
@@ -568,11 +716,10 @@ func TestFilter_OnResponse_DeserializationError(t
*testing.T) {
newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
- // OnResponse should return the original result when
deserialization fails
- // The user struct should remain unchanged (zero values)
+ // OnResponse should return an explicit error when typed
deserialization fails.
assert.Empty(t, user.Name)
assert.Equal(t, 0, user.Age)
- // The result should still be the original map
- assert.Equal(t, mapResult, newRes.Result())
+ require.Error(t, newRes.Error())
+ assert.Nil(t, newRes.Result())
})
}
diff --git a/filter/generic/generalizer/bean.go
b/filter/generic/generalizer/bean.go
index 8848f8966..a8f778d30 100644
--- a/filter/generic/generalizer/bean.go
+++ b/filter/generic/generalizer/bean.go
@@ -174,6 +174,9 @@ func (g *BeanGeneralizer) toDescriptor(obj any, visited
map[uintptr]bool) *JavaB
// fromDescriptor converts JavaBeanDescriptor to map for MapGeneralizer.Realize
func (g *BeanGeneralizer) fromDescriptor(obj any) any {
desc, ok := obj.(*JavaBeanDescriptor)
+ if ok && desc == nil {
+ return nil
+ }
if !ok {
if m, ok := obj.(map[any]any); ok {
desc = &JavaBeanDescriptor{Properties:
make(map[any]any)}
diff --git a/filter/generic/generalizer/gson.go
b/filter/generic/generalizer/gson.go
index 8be5f9da2..ae7135bf9 100644
--- a/filter/generic/generalizer/gson.go
+++ b/filter/generic/generalizer/gson.go
@@ -69,6 +69,10 @@ func (GsonGeneralizer) Realize(obj any, typ reflect.Type)
(any, error) {
return nil, perrors.Errorf("unexpected type of obj(=%T), wanted
is string", obj)
}
+ for typ.Kind() == reflect.Pointer {
+ typ = typ.Elem()
+ }
+
// create the target object
ret, ok := reflect.New(typ).Interface().(hessian.POJO)
if !ok {
diff --git a/filter/generic/generalizer/gson_test.go
b/filter/generic/generalizer/gson_test.go
index b1ec9a8fd..c25c88b79 100644
--- a/filter/generic/generalizer/gson_test.go
+++ b/filter/generic/generalizer/gson_test.go
@@ -87,9 +87,9 @@ func TestGsonPointer(t *testing.T) {
Name: "childName",
}
- m, err := mockMapGeneralizer.Generalize(c)
+ m, err := mockGsonGeneralizer.Generalize(c)
require.NoError(t, err)
- newC, err := mockMapGeneralizer.Realize(m,
reflect.TypeFor[*mockGsonChild]())
+ newC, err := mockGsonGeneralizer.Realize(m,
reflect.TypeFor[*mockGsonChild]())
require.NoError(t, err)
rMockChild, ok := newC.(*mockGsonChild)
assert.True(t, ok)
diff --git a/filter/generic/service.go b/filter/generic/service.go
index ba68fa235..514a39dde 100644
--- a/filter/generic/service.go
+++ b/filter/generic/service.go
@@ -19,7 +19,6 @@ package generic
import (
"context"
- "reflect"
)
import (
@@ -27,6 +26,7 @@ import (
)
import (
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/filter/generic/generalizer"
)
@@ -34,11 +34,12 @@ import (
type GenericService struct {
Invoke func(ctx context.Context, methodName string, types
[]string, args []hessian.Object) (any, error) `dubbo:"$invoke"`
referenceStr string
+ generic string
}
// NewGenericService returns a GenericService instance
func NewGenericService(referenceStr string) *GenericService {
- return &GenericService{referenceStr: referenceStr}
+ return &GenericService{referenceStr: referenceStr, generic:
constant.GenericSerializationDefault}
}
// Reference gets referenceStr from GenericService
@@ -46,13 +47,30 @@ func (s *GenericService) Reference() string {
return s.referenceStr
}
+// SetGenericType sets the generic mode used by InvokeWithType to realize
typed results.
+func (s *GenericService) SetGenericType(generic string) error {
+ if isGenericDisabled(generic) {
+ s.generic = generic
+ return nil
+ }
+ if _, err := getGeneralizer(generic); err != nil {
+ return err
+ }
+ s.generic = generic
+ return nil
+}
+
+// GenericType returns the generic mode used by InvokeWithType.
+func (s *GenericService) GenericType() string {
+ return s.generic
+}
+
// InvokeWithType invokes the remote method and deserializes the result into
the reply struct.
// The reply parameter must be a non-nil pointer to the target type.
//
-// Note: This method uses MapGeneralizer for deserialization, which means it
only supports
-// the default map-based generic serialization (generic=true). If you are
using other
-// serialization types like Gson or Protobuf-JSON, use the Invoke method
directly and
-// handle deserialization manually.
+// InvokeWithType uses the service generic mode to realize the result.
Supported modes are
+// true, gson, bean, protobuf-json, and the legacy protobuf mode (Map/Hessian
semantics).
+// generic=false disables generic result realization and returns an explicit
error.
//
// Example usage:
//
@@ -69,6 +87,11 @@ func (s *GenericService) InvokeWithType(ctx context.Context,
methodName string,
return err
}
+ g, err := s.getGeneralizer()
+ if err != nil {
+ return err
+ }
+
// Call the underlying Invoke method
result, err := s.Invoke(ctx, methodName, types, args)
if err != nil {
@@ -79,19 +102,19 @@ func (s *GenericService) InvokeWithType(ctx
context.Context, methodName string,
return nil
}
- // Get the element type that the pointer points to
replyType := replyValue.Elem().Type()
-
- // Use MapGeneralizer to realize the map result to the target struct
- g := generalizer.GetMapGeneralizer()
realized, err := realizeResult(result, replyType, g)
if err != nil {
return err
}
- // Set the realized value to reply
- if realized != nil {
- replyValue.Elem().Set(reflect.ValueOf(realized))
+ return setRealizedReply(replyValue, realized)
+}
+
+func (s *GenericService) getGeneralizer() (generalizer.Generalizer, error) {
+ generic := s.GenericType()
+ if isGenericDisabled(generic) {
+ return nil, unsupportedTypedResultModeError(generic)
}
- return nil
+ return getGeneralizer(generic)
}
diff --git a/filter/generic/service_test.go b/filter/generic/service_test.go
index 86ddbdc57..872a1ead3 100644
--- a/filter/generic/service_test.go
+++ b/filter/generic/service_test.go
@@ -27,6 +27,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "google.golang.org/protobuf/types/known/structpb"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
+ "dubbo.apache.org/dubbo-go/v3/filter/generic/generalizer"
)
type testUser struct {
@@ -36,6 +43,10 @@ type testUser struct {
Address *testAddress
}
+func (testUser) JavaClassName() string {
+ return "org.apache.dubbo.test.User"
+}
+
type testAddress struct {
City string
Country string
@@ -183,3 +194,113 @@ func TestGenericService_InvokeWithType(t *testing.T) {
assert.Contains(t, err.Error(), "failed to deserialize result")
})
}
+
+func TestGenericService_InvokeWithTypeUsesGenericMode(t *testing.T) {
+ tests := []struct {
+ name string
+ mode string
+ result any
+ }{
+ {
+ name: constant.GenericSerializationDefault,
+ mode: constant.GenericSerializationDefault,
+ result: map[string]any{
+ "name": "mapUser",
+ "age": 31,
+ },
+ },
+ {
+ name: constant.GenericSerializationGson,
+ mode: constant.GenericSerializationGson,
+ result: `{"name":"gsonUser","age":32}`,
+ },
+ {
+ name: constant.GenericSerializationBean,
+ mode: constant.GenericSerializationBean,
+ result: mustGeneralize(t,
generalizer.GetBeanGeneralizer(), testUser{
+ Name: "beanUser",
+ Age: 33,
+ }),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ service := NewGenericService("TestService")
+ require.NoError(t, service.SetGenericType(tt.mode))
+ service.Invoke = func(ctx context.Context, methodName
string, types []string, args []hessian.Object) (any, error) {
+ return tt.result, nil
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(),
"getUser", nil, nil, &user)
+
+ require.NoError(t, err)
+ assert.NotEmpty(t, user.Name)
+ assert.NotZero(t, user.Age)
+ })
+ }
+}
+
+func TestGenericService_InvokeWithTypeUsesProtobufJsonMode(t *testing.T) {
+ service := NewGenericService("TestService")
+ require.NoError(t,
service.SetGenericType(constant.GenericSerializationProtobufJson))
+ service.Invoke = func(ctx context.Context, methodName string, types
[]string, args []hessian.Object) (any, error) {
+ return `{"name":"protoUser"}`, nil
+ }
+
+ var reply structpb.Struct
+ err := service.InvokeWithType(context.Background(), "getUser", nil,
nil, &reply)
+
+ require.NoError(t, err)
+ assert.Equal(t, "protoUser", reply.Fields["name"].GetStringValue())
+}
+
+func TestGenericService_InvokeWithTypeUsesGsonPointerReply(t *testing.T) {
+ service := NewGenericService("TestService")
+ require.NoError(t,
service.SetGenericType(constant.GenericSerializationGson))
+ service.Invoke = func(ctx context.Context, methodName string, types
[]string, args []hessian.Object) (any, error) {
+ return `{"name":"gsonUser","age":32}`, nil
+ }
+
+ var reply *testUser
+ err := service.InvokeWithType(context.Background(), "getUser", nil,
nil, &reply)
+
+ require.NoError(t, err)
+ require.NotNil(t, reply)
+ assert.Equal(t, "gsonUser", reply.Name)
+ assert.Equal(t, 32, reply.Age)
+}
+
+func TestGenericService_InvokeWithTypeRejectsDisabledGenericMode(t *testing.T)
{
+ tests := []struct {
+ name string
+ mode string
+ }{
+ {name: "empty", mode: ""},
+ {name: "false", mode: "false"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ service := NewGenericService("TestService")
+ require.NoError(t, service.SetGenericType(tt.mode))
+ service.Invoke = func(ctx context.Context, methodName
string, types []string, args []hessian.Object) (any, error) {
+ t.Fatal("InvokeWithType should reject disabled
generic mode before invoking")
+ return nil, nil
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(),
"getUser", nil, nil, &user)
+
+ require.EqualError(t, err, `generic mode "`+tt.mode+`"
does not support typed result`)
+ })
+ }
+}
+
+func mustGeneralize(t *testing.T, g generalizer.Generalizer, obj any) any {
+ t.Helper()
+ result, err := g.Generalize(obj)
+ require.NoError(t, err)
+ return result
+}
diff --git a/filter/generic/util.go b/filter/generic/util.go
index cab2c2ede..78f9f53cb 100644
--- a/filter/generic/util.go
+++ b/filter/generic/util.go
@@ -142,6 +142,84 @@ func realizeResult(data any, targetType reflect.Type, g
generalizer.Generalizer)
return realized, nil
}
+func shouldRealizeTypedResult(data any, generic string) bool {
+ if data == nil {
+ return false
+ }
+ switch {
+ case strings.EqualFold(generic, constant.GenericSerializationGson),
+ strings.EqualFold(generic,
constant.GenericSerializationProtobufJson),
+ strings.EqualFold(generic, constant.GenericSerializationBean):
+ return true
+ default:
+ kind := reflect.ValueOf(data).Kind()
+ return kind == reflect.Map || kind == reflect.Slice
+ }
+}
+
+func unsupportedTypedResultModeError(generic string) error {
+ return perrors.Errorf("generic mode %q does not support typed result",
generic)
+}
+
+func setRealizedReply(replyValue reflect.Value, realized any) error {
+ if realized == nil {
+ return nil
+ }
+
+ target := replyValue.Elem()
+ value, ok := valueForAssignment(reflect.ValueOf(realized),
target.Type())
+ if !ok {
+ return perrors.Errorf("failed to assign realized result of type
%T to reply type %s", realized, target.Type())
+ }
+ target.Set(value)
+ return nil
+}
+
+func valueForAssignment(value reflect.Value, targetType reflect.Type)
(reflect.Value, bool) {
+ if !value.IsValid() {
+ if canBeNil(targetType) {
+ return reflect.Zero(targetType), true
+ }
+ return reflect.Value{}, false
+ }
+
+ if value.Type().AssignableTo(targetType) {
+ return value, true
+ }
+ if value.Type().ConvertibleTo(targetType) {
+ return value.Convert(targetType), true
+ }
+
+ if value.Kind() == reflect.Pointer {
+ if value.IsNil() {
+ return reflect.Value{}, false
+ }
+ elem := value.Elem()
+ if elem.Type().AssignableTo(targetType) {
+ return elem, true
+ }
+ if elem.Type().ConvertibleTo(targetType) {
+ return elem.Convert(targetType), true
+ }
+ }
+
+ if targetType.Kind() == reflect.Pointer {
+ elemType := targetType.Elem()
+ if value.Type().AssignableTo(elemType) {
+ ptr := reflect.New(elemType)
+ ptr.Elem().Set(value)
+ return ptr, true
+ }
+ if value.Type().ConvertibleTo(elemType) {
+ ptr := reflect.New(elemType)
+ ptr.Elem().Set(value.Convert(elemType))
+ return ptr, true
+ }
+ }
+
+ return reflect.Value{}, false
+}
+
// validateReplyPointer checks if the reply is a valid non-nil pointer.
//
// Parameters:
diff --git a/internal/config.go b/internal/config.go
index 24fccc101..a94e5e75f 100644
--- a/internal/config.go
+++ b/internal/config.go
@@ -233,14 +233,14 @@ func IsGenericMode(generic string) bool {
}
// ValidateGenericType validates the generic serialization type (generic mode).
-// An empty value means the call is not generic and is allowed. Unknown values
fail
-// fast instead of silently falling back to the Map generalizer.
+// An empty value or "false" means the call is not generic and is allowed.
+// Unknown values fail fast instead of silently falling back to the Map
generalizer.
//
// Valid values: "true" (Map, default), "gson", "protobuf-json", "bean".
// "protobuf" is kept as a legacy compatibility value and is not recommended.
func ValidateGenericType(generic string) error {
- if generic == "" || IsGenericMode(generic) {
+ if generic == "" || strings.EqualFold(generic, "false") ||
IsGenericMode(generic) {
return nil
}
- return fmt.Errorf("invalid generic type %q, valid values: true, gson,
protobuf-json, bean", generic)
+ return fmt.Errorf("invalid generic type %q, valid values: true, false,
gson, protobuf-json, bean, protobuf", generic)
}
diff --git a/internal/config_test.go b/internal/config_test.go
index 5753a761e..a33b8a6c6 100644
--- a/internal/config_test.go
+++ b/internal/config_test.go
@@ -339,6 +339,7 @@ func TestValidateGenericType(t *testing.T) {
{"protobuf-json", constant.GenericSerializationProtobufJson,
false},
{"bean", constant.GenericSerializationBean, false},
{"protobuf legacy compat",
constant.GenericSerializationProtobuf, false},
+ {"false disables generic", "false", false},
{"case insensitive", "TRUE", false},
{"unknown value", "bad-type", true},
}
@@ -349,6 +350,8 @@ func TestValidateGenericType(t *testing.T) {
if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.generic)
+ assert.Contains(t, err.Error(), "false")
+ assert.Regexp(t, `(?:^|,\s*)protobuf(?:,|$)`,
err.Error())
} else {
assert.NoError(t, err)
}