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 8807b9317 feat(generic): add InvokeWithType and move GenericService to
filter/generic (#3174)
8807b9317 is described below
commit 8807b93177f78f1458232aa1c0d1c2e6d5dbb351
Author: CAICAII <[email protected]>
AuthorDate: Tue Jan 27 10:18:56 2026 +0800
feat(generic): add InvokeWithType and move GenericService to filter/generic
(#3174)
* feat(generic): add InvokeWithType and move GenericService to
filter/generic
- Add InvokeWithType method to GenericService for automatic deserialization
- Move GenericService from config/generic to filter/generic
- Enhance OnResponse in genericFilter to support auto deserialization
- Add comprehensive tests for InvokeWithType functionality
Breaking change: import path changed from
dubbo.apache.org/dubbo-go/v3/config/generic to
dubbo.apache.org/dubbo-go/v3/filter/generic
* style: fix import formatting in filter/generic/filter_test.go
Reorganize imports into three separate blocks per project guidelines:
1. Standard library
2. Third-party packages
3. Internal packages
* refactor(generic): shared deserialization logic & enhance tests
* fix(generic): add nil check before reflect.Set to prevent panic
When realizeResult returns nil, calling reflect.ValueOf(nil).Set()
would panic. Add nil check before Set operation.
Signed-off-by: CAICAIIs <[email protected]>
---------
Signed-off-by: CAICAIIs <[email protected]>
---
client/client.go | 2 +-
config/generic/generic_service.go | 28 ++--
config/generic/generic_service_test.go | 32 -----
config/reference_config.go | 2 +-
filter/generic/filter.go | 68 +++++++++-
filter/generic/filter_test.go | 230 +++++++++++++++++++++++++++++++++
filter/generic/service.go | 97 ++++++++++++++
filter/generic/service_test.go | 185 ++++++++++++++++++++++++++
filter/generic/util.go | 52 ++++++++
9 files changed, 639 insertions(+), 57 deletions(-)
diff --git a/client/client.go b/client/client.go
index 074e5a1e1..0820961b6 100644
--- a/client/client.go
+++ b/client/client.go
@@ -26,7 +26,7 @@ import (
import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
- "dubbo.apache.org/dubbo-go/v3/config/generic"
+ "dubbo.apache.org/dubbo-go/v3/filter/generic"
"dubbo.apache.org/dubbo-go/v3/metadata"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
"dubbo.apache.org/dubbo-go/v3/protocol/invocation"
diff --git a/config/generic/generic_service.go
b/config/generic/generic_service.go
index 37fd8b747..bcadb130d 100644
--- a/config/generic/generic_service.go
+++ b/config/generic/generic_service.go
@@ -15,28 +15,18 @@
* limitations under the License.
*/
+// Package generic provides type aliases for backward compatibility.
+// Deprecated: Use dubbo.apache.org/dubbo-go/v3/filter/generic instead.
package generic
import (
- "context"
+ "dubbo.apache.org/dubbo-go/v3/filter/generic"
)
-import (
- hessian "github.com/apache/dubbo-go-hessian2"
-)
-
-// GenericService uses for generic invoke for service call
-type GenericService struct {
- Invoke func(ctx context.Context, methodName string, types
[]string, args []hessian.Object) (any, error) `dubbo:"$invoke"`
- referenceStr string
-}
-
-// NewGenericService returns a GenericService instance
-func NewGenericService(referenceStr string) *GenericService {
- return &GenericService{referenceStr: referenceStr}
-}
+// GenericService is an alias for backward compatibility.
+// Deprecated: Use dubbo.apache.org/dubbo-go/v3/filter/generic.GenericService
instead.
+type GenericService = generic.GenericService
-// Reference gets referenceStr from GenericService
-func (u *GenericService) Reference() string {
- return u.referenceStr
-}
+// NewGenericService is an alias for backward compatibility.
+// Deprecated: Use
dubbo.apache.org/dubbo-go/v3/filter/generic.NewGenericService instead.
+var NewGenericService = generic.NewGenericService
diff --git a/config/generic/generic_service_test.go
b/config/generic/generic_service_test.go
deleted file mode 100644
index e66145b81..000000000
--- a/config/generic/generic_service_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package generic
-
-import (
- "testing"
-)
-
-import (
- "github.com/stretchr/testify/assert"
-)
-
-func TestGenericService(t *testing.T) {
- service := NewGenericService("HelloService")
- reference := service.Reference()
- assert.Equal(t, "HelloService", reference)
-}
diff --git a/config/reference_config.go b/config/reference_config.go
index 24cc0dd23..1c980b529 100644
--- a/config/reference_config.go
+++ b/config/reference_config.go
@@ -39,7 +39,7 @@ import (
"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/config/generic"
+ "dubbo.apache.org/dubbo-go/v3/filter/generic"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
"dubbo.apache.org/dubbo-go/v3/protocol/protocolwrapper"
"dubbo.apache.org/dubbo-go/v3/proxy"
diff --git a/filter/generic/filter.go b/filter/generic/filter.go
index 977f450c9..67422ed12 100644
--- a/filter/generic/filter.go
+++ b/filter/generic/filter.go
@@ -20,6 +20,7 @@ package generic
import (
"context"
+ "reflect"
"sync"
)
@@ -152,8 +153,67 @@ func (f *genericFilter) Invoke(ctx context.Context,
invoker base.Invoker, inv ba
return invoker.Invoke(ctx, inv)
}
-// OnResponse dummy process, returns the result directly
-func (f *genericFilter) OnResponse(_ context.Context, result result.Result, _
base.Invoker,
- _ base.Invocation) result.Result {
- return result
+// OnResponse deserializes the map result to the target struct if reply is
provided.
+// If inv.Reply() is a non-nil pointer to a struct, the map result will be
automatically
+// deserialized into it using the appropriate generalizer.
+func (f *genericFilter) OnResponse(_ context.Context, res result.Result,
invoker base.Invoker,
+ inv base.Invocation) result.Result {
+ // Only process if this is a generic call and there's no error
+ if res.Error() != nil {
+ return res
+ }
+
+ // Check if this is a generic invocation
+ if !isGeneric(invoker.GetURL().GetParam(constant.GenericKey, "")) {
+ return res
+ }
+
+ // Get the reply from invocation
+ reply := inv.Reply()
+ if reply == nil {
+ return res
+ }
+
+ // Check if reply is a valid pointer
+ replyValue := reflect.ValueOf(reply)
+ if replyValue.Kind() != reflect.Ptr || replyValue.IsNil() {
+ return res
+ }
+
+ // Get the result data
+ data := res.Result()
+ if data == nil {
+ return res
+ }
+
+ // Check if data is a map type that needs to be deserialized
+ 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
+ return res
+ }
+
+ // Get the element type that the pointer points to
+ replyElemType := replyValue.Elem().Type()
+
+ // Get the generalizer based on the generic serialization type
+ generic := invoker.GetURL().GetParam(constant.GenericKey,
constant.GenericSerializationDefault)
+ g := getGeneralizer(generic)
+
+ // Realize the map/slice to the target struct using shared helper
+ realized, err := realizeResult(data, replyElemType, g)
+ if err != nil {
+ logger.Warnf("failed to deserialize generic result: %v", err)
+ return res
+ }
+
+ // Set the realized value to reply
+ if realized != nil {
+ replyValue.Elem().Set(reflect.ValueOf(realized))
+ }
+
+ // Update the result with the deserialized reply
+ res.SetResult(reply)
+
+ return res
}
diff --git a/filter/generic/filter_test.go b/filter/generic/filter_test.go
index a301f08a4..57dbdc440 100644
--- a/filter/generic/filter_test.go
+++ b/filter/generic/filter_test.go
@@ -29,6 +29,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
import (
@@ -101,3 +102,232 @@ func TestFilter_InvokeWithGenericCall(t *testing.T) {
r := filter.Invoke(context.Background(), mockInvoker, genericInvocation)
assert.NotNil(t, r)
}
+
+// mockUser is a test struct for OnResponse deserialization
+type mockUser struct {
+ Name string
+ Age int
+ Email string
+ Address *mockAddress
+}
+
+type mockAddress struct {
+ City string
+ Country string
+}
+
+// test OnResponse with struct deserialization
+func TestFilter_OnResponse_WithStructDeserialization(t *testing.T) {
+ invokeUrl := common.NewURLWithOptions(
+ common.WithParams(url.Values{}),
+ common.WithParamsValue(constant.GenericKey,
constant.GenericSerializationDefault))
+ filter := &genericFilter{}
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInvoker := mock.NewMockInvoker(ctrl)
+ mockInvoker.EXPECT().GetURL().Return(invokeUrl).AnyTimes()
+
+ t.Run("simple struct deserialization", func(t *testing.T) {
+ // Create a reply pointer for the target struct
+ var user mockUser
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&user),
+ )
+
+ // Simulate a map result from generic call
+ mapResult := map[string]any{
+ "name": "testUser",
+ "age": 25,
+ "email": "[email protected]",
+ }
+ res := &result.RPCResult{Rest: mapResult}
+
+ // Call OnResponse
+ newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
+
+ // Verify the result is deserialized into user struct
+ require.NoError(t, newRes.Error())
+ assert.Equal(t, "testUser", user.Name)
+ assert.Equal(t, 25, user.Age)
+ assert.Equal(t, "[email protected]", user.Email)
+ })
+
+ t.Run("nested struct deserialization", func(t *testing.T) {
+ var user mockUser
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&user),
+ )
+
+ // Simulate a nested map result
+ mapResult := map[string]any{
+ "name": "nestedUser",
+ "age": 30,
+ "email": "[email protected]",
+ "address": map[string]any{
+ "city": "Beijing",
+ "country": "China",
+ },
+ }
+ res := &result.RPCResult{Rest: mapResult}
+
+ newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
+
+ require.NoError(t, newRes.Error())
+ assert.Equal(t, "nestedUser", user.Name)
+ assert.Equal(t, 30, user.Age)
+ require.NotNil(t, user.Address)
+ assert.Equal(t, "Beijing", user.Address.City)
+ assert.Equal(t, "China", user.Address.Country)
+ })
+
+ t.Run("nil reply - no deserialization", func(t *testing.T) {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(nil),
+ )
+
+ mapResult := map[string]any{"name": "test"}
+ res := &result.RPCResult{Rest: mapResult}
+
+ newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
+
+ // Result should remain as map since no reply pointer provided
+ assert.Equal(t, mapResult, newRes.Result())
+ })
+
+ t.Run("nil result - no deserialization", func(t *testing.T) {
+ var user mockUser
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&user),
+ )
+
+ res := &result.RPCResult{Rest: nil}
+
+ newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
+
+ // User should remain zero value
+ assert.Empty(t, user.Name)
+ assert.Equal(t, 0, user.Age)
+ assert.Nil(t, newRes.Result())
+ })
+
+ t.Run("primitive result - no deserialization", func(t *testing.T) {
+ var count int
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&count),
+ )
+
+ // Primitive result should pass through without deserialization
+ res := &result.RPCResult{Rest: 42}
+
+ newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
+
+ // Result should remain as primitive
+ assert.Equal(t, 42, newRes.Result())
+ })
+
+ t.Run("error result - no deserialization", func(t *testing.T) {
+ var user mockUser
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&user),
+ )
+
+ res := &result.RPCResult{
+ Rest: map[string]any{"name": "test"},
+ Err: assert.AnError,
+ }
+
+ newRes := filter.OnResponse(context.Background(), res,
mockInvoker, inv)
+
+ // Should return immediately without deserialization when
there's an error
+ require.Error(t, newRes.Error())
+ assert.Empty(t, user.Name)
+ })
+}
+
+func TestFilter_OnResponse_WithSliceDeserialization(t *testing.T) {
+ invokeUrl := common.NewURLWithOptions(
+ common.WithParams(url.Values{}),
+ common.WithParamsValue(constant.GenericKey,
constant.GenericSerializationDefault))
+ filter := &genericFilter{}
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInvoker := mock.NewMockInvoker(ctrl)
+ mockInvoker.EXPECT().GetURL().Return(invokeUrl).AnyTimes()
+
+ var users []mockUser
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&users),
+ )
+
+ // Simulate a slice of maps result
+ sliceResult := []any{
+ map[string]any{
+ "name": "user1",
+ "age": 20,
+ },
+ map[string]any{
+ "name": "user2",
+ "age": 25,
+ },
+ }
+ res := &result.RPCResult{Rest: sliceResult}
+
+ newRes := filter.OnResponse(context.Background(), res, mockInvoker, inv)
+
+ require.NoError(t, newRes.Error())
+ require.Len(t, users, 2)
+ assert.Equal(t, "user1", users[0].Name)
+ assert.Equal(t, 20, users[0].Age)
+ assert.Equal(t, "user2", users[1].Name)
+ assert.Equal(t, 25, users[1].Age)
+}
+
+// TestFilter_OnResponse_DeserializationError tests that OnResponse gracefully
handles
+// deserialization failures by logging a warning and returning the original
result.
+func TestFilter_OnResponse_DeserializationError(t *testing.T) {
+ invokeUrl := common.NewURLWithOptions(
+ common.WithParams(url.Values{}),
+ common.WithParamsValue(constant.GenericKey,
constant.GenericSerializationDefault))
+ filter := &genericFilter{}
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInvoker := mock.NewMockInvoker(ctrl)
+ mockInvoker.EXPECT().GetURL().Return(invokeUrl).AnyTimes()
+
+ t.Run("type mismatch - string to int", func(t *testing.T) {
+ var user mockUser
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName(constant.Generic),
+ invocation.WithReply(&user),
+ )
+
+ // Return a map with incompatible type (string instead of int
for age)
+ mapResult := map[string]any{
+ "name": "testUser",
+ "age": "not_an_int", // This should cause
deserialization to fail
+ }
+ res := &result.RPCResult{Rest: mapResult}
+
+ 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)
+ 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())
+ })
+}
diff --git a/filter/generic/service.go b/filter/generic/service.go
new file mode 100644
index 000000000..ba68fa235
--- /dev/null
+++ b/filter/generic/service.go
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package generic
+
+import (
+ "context"
+ "reflect"
+)
+
+import (
+ hessian "github.com/apache/dubbo-go-hessian2"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/filter/generic/generalizer"
+)
+
+// GenericService uses for generic invoke for service call
+type GenericService struct {
+ Invoke func(ctx context.Context, methodName string, types
[]string, args []hessian.Object) (any, error) `dubbo:"$invoke"`
+ referenceStr string
+}
+
+// NewGenericService returns a GenericService instance
+func NewGenericService(referenceStr string) *GenericService {
+ return &GenericService{referenceStr: referenceStr}
+}
+
+// Reference gets referenceStr from GenericService
+func (s *GenericService) Reference() string {
+ return s.referenceStr
+}
+
+// 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.
+//
+// Example usage:
+//
+// var user User
+// err := genericService.InvokeWithType(ctx, "getUser",
[]string{"java.lang.String"}, []hessian.Object{"123"}, &user)
+// if err != nil {
+// return err
+// }
+// fmt.Println(user.Name, user.Age)
+func (s *GenericService) InvokeWithType(ctx context.Context, methodName
string, types []string, args []hessian.Object, reply any) error {
+ // Validate the reply pointer
+ replyValue, err := validateReplyPointer(reply)
+ if err != nil {
+ return err
+ }
+
+ // Call the underlying Invoke method
+ result, err := s.Invoke(ctx, methodName, types, args)
+ if err != nil {
+ return err
+ }
+
+ if result == nil {
+ 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 nil
+}
diff --git a/filter/generic/service_test.go b/filter/generic/service_test.go
new file mode 100644
index 000000000..86ddbdc57
--- /dev/null
+++ b/filter/generic/service_test.go
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package generic
+
+import (
+ "context"
+ "testing"
+)
+
+import (
+ hessian "github.com/apache/dubbo-go-hessian2"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testUser struct {
+ Name string
+ Age int
+ Email string
+ Address *testAddress
+}
+
+type testAddress struct {
+ City string
+ Country string
+}
+
+func TestGenericService(t *testing.T) {
+ service := NewGenericService("HelloService")
+ reference := service.Reference()
+ assert.Equal(t, "HelloService", reference)
+}
+
+func TestGenericService_InvokeWithType(t *testing.T) {
+ t.Run("simple struct", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ assert.Equal(t, "getUser", methodName)
+ return map[string]any{
+ "name": "testUser",
+ "age": 25,
+ "email": "[email protected]",
+ }, nil
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(), "getUser",
[]string{"java.lang.String"}, []hessian.Object{"123"}, &user)
+
+ require.NoError(t, err)
+ assert.Equal(t, "testUser", user.Name)
+ assert.Equal(t, 25, user.Age)
+ assert.Equal(t, "[email protected]", user.Email)
+ })
+
+ t.Run("nested struct", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ return map[string]any{
+ "name": "nestedUser",
+ "age": 30,
+ "email": "[email protected]",
+ "address": map[string]any{
+ "city": "Beijing",
+ "country": "China",
+ },
+ }, nil
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(), "getUser",
[]string{"java.lang.String"}, []hessian.Object{"456"}, &user)
+
+ require.NoError(t, err)
+ assert.Equal(t, "nestedUser", user.Name)
+ assert.Equal(t, 30, user.Age)
+ require.NotNil(t, user.Address)
+ assert.Equal(t, "Beijing", user.Address.City)
+ assert.Equal(t, "China", user.Address.Country)
+ })
+
+ t.Run("slice result", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ return []any{
+ map[string]any{"name": "user1", "age": 20},
+ map[string]any{"name": "user2", "age": 25},
+ }, nil
+ }
+
+ var users []testUser
+ err := service.InvokeWithType(context.Background(),
"listUsers", []string{}, []hessian.Object{}, &users)
+
+ require.NoError(t, err)
+ require.Len(t, users, 2)
+ assert.Equal(t, "user1", users[0].Name)
+ assert.Equal(t, 20, users[0].Age)
+ assert.Equal(t, "user2", users[1].Name)
+ assert.Equal(t, 25, users[1].Age)
+ })
+
+ t.Run("nil result", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ return nil, nil
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(), "getUser",
[]string{"java.lang.String"}, []hessian.Object{"789"}, &user)
+
+ require.NoError(t, err)
+ assert.Empty(t, user.Name)
+ assert.Zero(t, user.Age)
+ })
+
+ t.Run("nil reply error", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ // Add dummy invoke to prevent nil pointer dereference
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ return nil, nil
+ }
+
+ err := service.InvokeWithType(context.Background(), "getUser",
[]string{"java.lang.String"}, []hessian.Object{"123"}, nil)
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "reply cannot be nil")
+ })
+
+ t.Run("non-pointer reply error", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ // Add dummy invoke to prevent nil pointer dereference
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ return nil, nil
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(), "getUser",
[]string{"java.lang.String"}, []hessian.Object{"123"}, user)
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "reply must be a pointer")
+ })
+
+ t.Run("invoke error", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ return nil, assert.AnError
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(), "getUser",
[]string{"java.lang.String"}, []hessian.Object{"123"}, &user)
+
+ require.Error(t, err)
+ assert.Equal(t, assert.AnError, err)
+ })
+
+ t.Run("deserialization error", func(t *testing.T) {
+ service := NewGenericService("TestService")
+ service.Invoke = func(ctx context.Context, methodName string,
types []string, args []hessian.Object) (any, error) {
+ // Return a type that mismatches the target struct
+ return map[string]any{
+ "age": "invalid_age_type", // string cannot be
unmarshaled to int
+ }, nil
+ }
+
+ var user testUser
+ err := service.InvokeWithType(context.Background(), "getUser",
[]string{"java.lang.String"}, []hessian.Object{"123"}, &user)
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to deserialize result")
+ })
+}
diff --git a/filter/generic/util.go b/filter/generic/util.go
index 5ed6cd321..17c1766c9 100644
--- a/filter/generic/util.go
+++ b/filter/generic/util.go
@@ -18,11 +18,14 @@
package generic
import (
+ "reflect"
"strings"
)
import (
"github.com/dubbogo/gost/log/logger"
+
+ perrors "github.com/pkg/errors"
)
import (
@@ -68,3 +71,52 @@ func getGeneralizer(generic string) (g
generalizer.Generalizer) {
}
return
}
+
+// realizeResult deserializes the data into the target type using the provided
generalizer.
+// It returns the realized value and any error that occurred during
deserialization.
+//
+// Parameters:
+// - data: the source data to deserialize (typically map[string]any or []any)
+// - targetType: the reflect.Type of the target struct
+// - g: the generalizer to use for deserialization
+//
+// Returns:
+// - the realized value matching targetType
+// - error if deserialization fails
+func realizeResult(data any, targetType reflect.Type, g
generalizer.Generalizer) (any, error) {
+ if data == nil {
+ return nil, nil
+ }
+
+ realized, err := g.Realize(data, targetType)
+ if err != nil {
+ return nil, perrors.Errorf("failed to deserialize result to %s:
%v", targetType.String(), err)
+ }
+
+ return realized, nil
+}
+
+// validateReplyPointer checks if the reply is a valid non-nil pointer.
+//
+// Parameters:
+// - reply: the value to validate
+//
+// Returns:
+// - the reflect.Value of the reply
+// - error if validation fails
+func validateReplyPointer(reply any) (reflect.Value, error) {
+ if reply == nil {
+ return reflect.Value{}, perrors.New("reply cannot be nil")
+ }
+
+ replyValue := reflect.ValueOf(reply)
+ if replyValue.Kind() != reflect.Ptr {
+ return reflect.Value{}, perrors.New("reply must be a pointer")
+ }
+
+ if replyValue.IsNil() {
+ return reflect.Value{}, perrors.New("reply cannot be a nil
pointer")
+ }
+
+ return replyValue, nil
+}