agent_patch
runs/2026-02-28__13-38-10__gpt-5-4/flux-pr-1155/flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/agent-logs/agent.patch
22628 bytes
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
index f876776..fe219cb 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
@@ -96,7 +96,7 @@ func (c *Configuration) FederationConfiguration() *FederationConfiguration {
}
func (c *Configuration) IsGRPC() bool {
- return c.grpc != nil
+ return c.grpc != nil && !c.grpc.Disabled
}
type SingleTypeField struct {
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
index 1d45769..4697264 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
@@ -317,7 +317,8 @@ func (p *Planner[T]) createInputForQuery() (input, operation []byte) {
}
func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
- if p.config.fetch == nil && p.config.grpc == nil {
+ grpcEnabled := p.config.grpc != nil && !p.config.grpc.Disabled
+ if p.config.fetch == nil && !grpcEnabled {
p.stopWithError(errors.WithStack(errors.New("ConfigureFetch: fetch and grpc configuration is empty")))
return resolve.FetchConfiguration{}
}
@@ -353,8 +354,13 @@ func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
dataSource = &Source{httpClient: p.fetchClient}
- if p.config.grpc != nil {
+ if grpcEnabled {
var err error
+ grpcClient, err := p.resolveGRPCClient()
+ if err != nil {
+ p.stopWithError(errors.WithStack(fmt.Errorf("failed to resolve gRPC client: %w", err)))
+ return resolve.FetchConfiguration{}
+ }
opDocument, opReport := astparser.ParseGraphqlDocumentBytes(operation)
if opReport.HasErrors() {
@@ -362,7 +368,7 @@ func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
return resolve.FetchConfiguration{}
}
- dataSource, err = grpcdatasource.NewDataSource(p.grpcClient, grpcdatasource.DataSourceConfig{
+ dataSource, err = grpcdatasource.NewDataSource(grpcClient, grpcdatasource.DataSourceConfig{
Operation: &opDocument,
Definition: p.config.schemaConfiguration.upstreamSchemaAst,
Mapping: p.config.grpc.Mapping,
@@ -389,6 +395,23 @@ func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
}
}
+func (p *Planner[T]) resolveGRPCClient() (grpc.ClientConnInterface, error) {
+ if p.config.grpc != nil {
+ if p.config.grpc.Client != nil {
+ return p.config.grpc.Client, nil
+ }
+ if p.config.grpc.ClientFactory != nil {
+ return p.config.grpc.ClientFactory(context.Background())
+ }
+ }
+
+ if p.grpcClient != nil {
+ return p.grpcClient, nil
+ }
+
+ return nil, fmt.Errorf("grpc client is required")
+}
+
func (p *Planner[T]) shouldSelectSingleEntity() bool {
return p.dataSourcePlannerConfig.HasRequiredFields() &&
p.dataSourcePlannerConfig.PathType == plan.PlannerPathObject
@@ -1736,10 +1759,6 @@ func NewFactoryGRPC(executionContext context.Context, grpcClient grpc.ClientConn
return nil, fmt.Errorf("execution context is required")
}
- if grpcClient == nil {
- return nil, fmt.Errorf("grpc client is required")
- }
-
return &Factory[Configuration]{
executionContext: executionContext,
grpcClient: grpcClient,
diff --git a/app/v2/pkg/engine/datasource/graphql_datasource/grpc_factory_test.go b/app/v2/pkg/engine/datasource/graphql_datasource/grpc_factory_test.go
new file mode 100644
index 0000000..f9979a3
--- /dev/null
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/grpc_factory_test.go
@@ -0,0 +1,57 @@
+package graphql_datasource
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ grpcdatasource "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/grpc_datasource"
+ "google.golang.org/grpc"
+)
+
+type testGRPCClient struct{}
+
+func (t *testGRPCClient) Invoke(context.Context, string, any, any, ...grpc.CallOption) error {
+ return nil
+}
+
+func (t *testGRPCClient) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) {
+ return nil, nil
+}
+
+func TestNewFactoryGRPC_AllowsDatasourceProvidedClient(t *testing.T) {
+ factory, err := NewFactoryGRPC(context.Background(), nil)
+ require.NoError(t, err)
+ require.NotNil(t, factory)
+}
+
+func TestPlannerResolveGRPCClient_UsesDatasourceFactory(t *testing.T) {
+ expectedClient := &testGRPCClient{}
+ planner := &Planner[Configuration]{
+ config: Configuration{
+ grpc: &grpcdatasource.GRPCConfiguration{
+ ClientFactory: func(context.Context) (grpc.ClientConnInterface, error) {
+ return expectedClient, nil
+ },
+ },
+ },
+ }
+
+ client, err := planner.resolveGRPCClient()
+ require.NoError(t, err)
+ require.Same(t, expectedClient, client)
+}
+
+func TestConfigurationIsGRPC_IgnoresDisabledGRPCDatasource(t *testing.T) {
+ schemaConfig, err := NewSchemaConfiguration("type Query { hello: String }", nil)
+ require.NoError(t, err)
+
+ cfg, err := NewConfiguration(ConfigurationInput{
+ GRPC: &grpcdatasource.GRPCConfiguration{
+ Disabled: true,
+ },
+ SchemaConfiguration: schemaConfig,
+ })
+ require.NoError(t, err)
+ require.False(t, cfg.IsGRPC())
+}
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go b/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go
index ed8b887..8defa41 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go
@@ -319,6 +319,7 @@ type Invocation struct {
// Compile processes an RPCExecutionPlan and builds protobuf messages from JSON data
// based on the compiled schema.
func (p *RPCCompiler) Compile(executionPlan *RPCExecutionPlan, inputData gjson.Result) ([]Invocation, error) {
+ p.report.Reset()
invocations := make([]Invocation, 0, len(executionPlan.Calls))
for _, call := range executionPlan.Calls {
@@ -397,21 +398,7 @@ func (p *RPCCompiler) buildProtoMessage(inputMessage Message, rpcMessage *RPCMes
// Handle repeated fields (arrays/lists)
if field.Repeated {
- // Get a mutable reference to the list field
- list := message.Mutable(fd.ByName(protoref.Name(field.Name))).List()
-
- // Extract the array elements from the JSON data
- elements := data.Get(rpcField.JSONPath).Array()
- if len(elements) == 0 {
- continue
- }
-
- // Process each element and append to the list
- for _, element := range elements {
- fieldMsg := p.buildProtoMessage(p.doc.Messages[field.MessageRef], rpcField.Message, element)
- list.Append(protoref.ValueOfMessage(fieldMsg))
- }
-
+ p.populateRepeatedField(message, inputMessage, field, rpcField, data)
continue
}
@@ -472,11 +459,63 @@ func (p *RPCCompiler) setValueForKind(kind DataType, data gjson.Result) protoref
return protoref.ValueOfFloat64(data.Float())
case DataTypeBool:
return protoref.ValueOfBool(data.Bool())
+ case DataTypeBytes:
+ return protoref.ValueOfBytes([]byte(data.String()))
}
return protoref.Value{}
}
+func (p *RPCCompiler) populateRepeatedField(message *dynamicpb.Message, inputMessage Message, field Field, rpcField *RPCField, data gjson.Result) {
+ list := message.Mutable(inputMessage.Desc.Fields().ByName(protoref.Name(field.Name))).List()
+ fieldData := data.Get(rpcField.JSONPath)
+ if !fieldData.Exists() || fieldData.Type == gjson.Null {
+ return
+ }
+
+ elements := fieldData.Array()
+ if len(elements) == 0 && (len(fieldData.Raw) == 0 || fieldData.Raw[0] != '[') {
+ elements = []gjson.Result{fieldData}
+ }
+
+ if field.MessageRef >= 0 {
+ for _, element := range elements {
+ fieldMsg := p.buildProtoMessage(p.doc.Messages[field.MessageRef], rpcField.Message, element)
+ list.Append(protoref.ValueOfMessage(fieldMsg))
+ }
+
+ return
+ }
+
+ if field.Type == DataTypeEnum {
+ enum, ok := p.doc.EnumByName(rpcField.EnumName)
+ if !ok {
+ p.report.AddInternalError(fmt.Errorf("enum %s not found in document", rpcField.EnumName))
+ return
+ }
+
+ for _, element := range elements {
+ matched := false
+ for _, enumValue := range enum.Values {
+ if enumValue.GraphqlValue == element.String() {
+ list.Append(protoref.ValueOfEnum(protoref.EnumNumber(enumValue.Number)))
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ p.report.AddInternalError(fmt.Errorf("enum value %q not found in document for enum %s", element.String(), rpcField.EnumName))
+ }
+ }
+
+ return
+ }
+
+ for _, element := range elements {
+ list.Append(p.setValueForKind(field.Type, element))
+ }
+}
+
// parseEnum extracts information from a protobuf enum descriptor.
func (p *RPCCompiler) parseEnum(e protoref.EnumDescriptor, mapping *GRPCMapping) Enum {
var enumValueMappings []EnumValueMapping
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go b/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go
index 5e098c3..fd251e1 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/wundergraph/graphql-go-tools/v2/pkg/grpctest"
+ protoref "google.golang.org/protobuf/reflect/protoreflect"
)
// Complete valid protobuf definition with service and message definitions
@@ -291,3 +292,69 @@ func TestBuildProtoMessage(t *testing.T) {
require.Equal(t, 1, len(invocations))
}
+
+func TestBuildProtoMessage_WithRepeatedScalarField(t *testing.T) {
+ compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), nil)
+ require.NoError(t, err)
+
+ request := RPCMessage{
+ Name: "QueryCalculateTotalsRequest",
+ Fields: []RPCField{
+ {
+ Name: "orders",
+ TypeName: string(DataTypeMessage),
+ JSONPath: "orders",
+ Repeated: true,
+ Message: &RPCMessage{
+ Name: "OrderInput",
+ Fields: []RPCField{
+ {Name: "order_id", TypeName: string(DataTypeString), JSONPath: "orderId"},
+ {Name: "customer_name", TypeName: string(DataTypeString), JSONPath: "customerName"},
+ {
+ Name: "lines",
+ TypeName: string(DataTypeMessage),
+ JSONPath: "lines",
+ Repeated: true,
+ Message: &RPCMessage{
+ Name: "OrderLineInput",
+ Fields: []RPCField{
+ {Name: "product_id", TypeName: string(DataTypeString), JSONPath: "productId"},
+ {Name: "quantity", TypeName: string(DataTypeInt32), JSONPath: "quantity"},
+ {Name: "modifiers", TypeName: string(DataTypeString), JSONPath: "modifiers", Repeated: true},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ data := gjson.Parse(`{
+ "orders": [{
+ "orderId": "order-1",
+ "customerName": "Jane Smith",
+ "lines": [{
+ "productId": "product-1",
+ "quantity": 2,
+ "modifiers": ["gift-wrap", "fragile"]
+ }]
+ }]
+ }`)
+
+ message := compiler.buildProtoMessage(compiler.doc.MessageByName("QueryCalculateTotalsRequest"), &request, data)
+ require.NotNil(t, message)
+
+ orders := message.Get(message.Descriptor().Fields().ByName(protoref.Name("orders"))).List()
+ require.Equal(t, 1, orders.Len())
+
+ order := orders.Get(0).Message()
+ lines := order.Get(order.Descriptor().Fields().ByName(protoref.Name("lines"))).List()
+ require.Equal(t, 1, lines.Len())
+
+ line := lines.Get(0).Message()
+ modifiers := line.Get(line.Descriptor().Fields().ByName(protoref.Name("modifiers"))).List()
+ require.Equal(t, 2, modifiers.Len())
+ require.Equal(t, "gift-wrap", modifiers.Get(0).String())
+ require.Equal(t, "fragile", modifiers.Get(1).String())
+}
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go b/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go
index 4dbd48b..3867ba6 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go
@@ -1,5 +1,11 @@
package grpcdatasource
+import (
+ "context"
+
+ "google.golang.org/grpc"
+)
+
type (
// RPCConfigMap is a map of RPC names to RPC configurations
RPCConfigMap map[string]RPCConfig
@@ -29,9 +35,14 @@ type EnumValueMapping struct {
TargetValue string
}
+type GRPCClientFactory func(context.Context) (grpc.ClientConnInterface, error)
+
type GRPCConfiguration struct {
- Mapping *GRPCMapping
- Compiler *RPCCompiler
+ Disabled bool
+ Mapping *GRPCMapping
+ Compiler *RPCCompiler
+ Client grpc.ClientConnInterface
+ ClientFactory GRPCClientFactory
}
// RPCConfig defines the configuration for a specific RPC operation
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go
index 940d501..710e37d 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go
@@ -17,7 +17,9 @@ import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
"google.golang.org/grpc"
+ "google.golang.org/grpc/status"
protoref "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/types/dynamicpb"
)
// Verify DataSource implements the resolve.DataSource interface
@@ -95,6 +97,9 @@ func (d *DataSource) Load(ctx context.Context, input []byte, out *bytes.Buffer)
if err != nil {
return err
}
+ if responseJSON == nil || responseJSON.Type() == astjson.TypeNull {
+ continue
+ }
root, _, err = astjson.MergeValues(root, responseJSON)
if err != nil {
@@ -123,7 +128,10 @@ func (d *DataSource) LoadWithFiles(ctx context.Context, input []byte, files []*h
func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessage, data protoref.Message) (*astjson.Value, error) {
if message == nil {
- return nil, nil
+ return arena.NewNull(), nil
+ }
+ if data == nil || !data.IsValid() {
+ return arena.NewNull(), nil
}
root := arena.NewObject()
@@ -162,12 +170,10 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa
root.Set(field.JSONPath, arr)
list := data.Get(fd).List()
for i := 0; i < list.Len(); i++ {
- message := list.Get(i).Message()
- value, err := d.marshalResponseJSON(arena, field.Message, message)
+ value, err := d.marshalListItemJSON(arena, &field, fd, list.Get(i))
if err != nil {
return nil, err
}
-
arr.SetArrayItem(i, value)
}
@@ -176,12 +182,18 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa
if fd.Kind() == protoref.MessageKind {
msg := data.Get(fd).Message()
+ if msg == nil || !msg.IsValid() {
+ msg = dynamicpb.NewMessage(fd.Message())
+ }
value, err := d.marshalResponseJSON(arena, field.Message, msg)
if err != nil {
return nil, err
}
if field.JSONPath == "" {
+ if value == nil || value.Type() == astjson.TypeNull {
+ continue
+ }
root, _, err = astjson.MergeValues(root, value)
if err != nil {
return nil, err
@@ -193,47 +205,58 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa
continue
}
- d.setJSONValue(arena, root, field.JSONPath, data, fd)
+ root.Set(field.JSONPath, d.protoValueToJSONValue(arena, fd, data.Get(fd)))
}
return root, nil
}
-func (d *DataSource) setJSONValue(arena *astjson.Arena, root *astjson.Value, name string, data protoref.Message, fd protoref.FieldDescriptor) {
+func (d *DataSource) marshalListItemJSON(arena *astjson.Arena, field *RPCField, fd protoref.FieldDescriptor, value protoref.Value) (*astjson.Value, error) {
+ if fd.Kind() == protoref.MessageKind {
+ msg := value.Message()
+ if msg == nil || !msg.IsValid() {
+ msg = dynamicpb.NewMessage(fd.Message())
+ }
+ return d.marshalResponseJSON(arena, field.Message, msg)
+ }
+
+ return d.protoValueToJSONValue(arena, fd, value), nil
+}
+
+func (d *DataSource) protoValueToJSONValue(arena *astjson.Arena, fd protoref.FieldDescriptor, value protoref.Value) *astjson.Value {
switch fd.Kind() {
case protoref.BoolKind:
- boolValue := data.Get(fd).Bool()
+ boolValue := value.Bool()
if boolValue {
- root.Set(name, arena.NewTrue())
- } else {
- root.Set(name, arena.NewFalse())
+ return arena.NewTrue()
}
+ return arena.NewFalse()
case protoref.StringKind:
- root.Set(name, arena.NewString(data.Get(fd).String()))
+ return arena.NewString(value.String())
case protoref.Int32Kind, protoref.Int64Kind:
- root.Set(name, arena.NewNumberInt(int(data.Get(fd).Int())))
+ return arena.NewNumberString(fmt.Sprintf("%d", value.Int()))
case protoref.Uint32Kind, protoref.Uint64Kind:
- root.Set(name, arena.NewNumberString(fmt.Sprintf("%d", data.Get(fd).Uint())))
+ return arena.NewNumberString(fmt.Sprintf("%d", value.Uint()))
case protoref.FloatKind, protoref.DoubleKind:
- root.Set(name, arena.NewNumberFloat64(data.Get(fd).Float()))
+ return arena.NewNumberFloat64(value.Float())
case protoref.BytesKind:
- root.Set(name, arena.NewStringBytes(data.Get(fd).Bytes()))
+ return arena.NewStringBytes(value.Bytes())
case protoref.EnumKind:
enumDesc := fd.Enum()
- enumValueDesc := enumDesc.Values().ByNumber(data.Get(fd).Enum())
+ enumValueDesc := enumDesc.Values().ByNumber(value.Enum())
if enumValueDesc == nil {
- root.Set(name, arena.NewNull())
- return
+ return arena.NewNull()
}
graphqlValue, ok := d.mapping.ResolveEnumValue(string(enumDesc.Name()), string(enumValueDesc.Name()))
if !ok {
- root.Set(name, arena.NewNull())
- return
+ return arena.NewNull()
}
- root.Set(name, arena.NewString(graphqlValue))
+ return arena.NewString(graphqlValue)
}
+
+ return arena.NewNull()
}
func writeErrorBytes(err error) []byte {
@@ -244,6 +267,12 @@ func writeErrorBytes(err error) []byte {
errorItem := a.NewObject()
errorItem.Set("message", a.NewString(err.Error()))
+ if st, ok := status.FromError(err); ok {
+ extensions := a.NewObject()
+ extensions.Set("grpcCode", a.NewString(st.Code().String()))
+ extensions.Set("grpcStatusCode", a.NewNumberInt(int(st.Code())))
+ errorItem.Set("extensions", extensions)
+ }
errorArray.SetArrayItem(0, errorItem)
return errorRoot.MarshalTo(nil)
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go
index a14f5a1..3b76ee5 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go
@@ -425,6 +425,8 @@ func Test_DataSource_Load_WithGrpcError(t *testing.T) {
// Verify the error message
require.Contains(t, response.Errors[0].Message, "user not found: error-user")
+ require.Contains(t, responseJson, `"grpcCode":"NotFound"`)
+ require.Contains(t, responseJson, `"grpcStatusCode":5`)
}
func TestMarshalResponseJSON(t *testing.T) {
@@ -493,6 +495,72 @@ func TestMarshalResponseJSON(t *testing.T) {
require.Equal(t, `{"_entities":[{"__typename":"Product","id":"123","name_different":"test","price_different":123.45}]}`, responseJSON.String())
}
+func TestMarshalResponseJSON_WithRepeatedScalarFields(t *testing.T) {
+ protoSchema := `
+syntax = "proto3";
+package product.v1;
+
+service ProductService {
+ rpc QueryScalars(QueryScalarsRequest) returns (QueryScalarsResponse) {}
+}
+
+message QueryScalarsRequest {}
+
+message QueryScalarsResponse {
+ repeated string tags = 1;
+ repeated int32 quantities = 2;
+ Nested nested = 3;
+}
+
+message Nested {
+ string name = 1;
+}
+`
+
+ compiler, err := NewProtoCompiler(protoSchema, nil)
+ require.NoError(t, err)
+
+ responseDesc := compiler.doc.MessageByName("QueryScalarsResponse").Desc
+ responseMessage := dynamicpb.NewMessage(responseDesc)
+ responseMessage.Mutable(responseDesc.Fields().ByName("tags")).List().Append(protoref.ValueOfString("a"))
+ responseMessage.Mutable(responseDesc.Fields().ByName("tags")).List().Append(protoref.ValueOfString("b"))
+ responseMessage.Mutable(responseDesc.Fields().ByName("quantities")).List().Append(protoref.ValueOfInt32(3))
+ responseMessage.Mutable(responseDesc.Fields().ByName("quantities")).List().Append(protoref.ValueOfInt32(5))
+
+ response := RPCMessage{
+ Name: "QueryScalarsResponse",
+ Fields: []RPCField{
+ {Name: "tags", TypeName: string(DataTypeString), JSONPath: "tags", Repeated: true},
+ {Name: "quantities", TypeName: string(DataTypeInt32), JSONPath: "quantities", Repeated: true},
+ {
+ Name: "nested",
+ TypeName: string(DataTypeMessage),
+ JSONPath: "nested",
+ Message: &RPCMessage{
+ Name: "Nested",
+ Fields: []RPCField{
+ {Name: "name", TypeName: string(DataTypeString), JSONPath: "name"},
+ },
+ },
+ },
+ },
+ }
+
+ ds := &DataSource{}
+ arena := astjson.Arena{}
+ responseJSON, err := ds.marshalResponseJSON(&arena, &response, responseMessage)
+ require.NoError(t, err)
+ require.Equal(t, `{"tags":["a","b"],"quantities":[3,5],"nested":{"name":""}}`, responseJSON.String())
+}
+
+func TestMarshalResponseJSON_WithNilMessage(t *testing.T) {
+ ds := &DataSource{}
+ arena := astjson.Arena{}
+ responseJSON, err := ds.marshalResponseJSON(&arena, &RPCMessage{Name: "Ignored"}, nil)
+ require.NoError(t, err)
+ require.Equal(t, astjson.TypeNull, responseJSON.Type())
+}
+
// TODO test interface types
// Test_DataSource_Load_WithAnimalInterface tests the datasource with Animal interface types (Cat/Dog)
// using a bufconn connection to the mock service