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
+}

Reply via email to