Repository: jensneuse/graphql-go-tools
Pull request #369 (2022-05-17 06:09:27)

fix plain renderer for string types


Patch:
diff --git a/pkg/engine/datasource/introspection_datasource/input.go b/pkg/engine/datasource/introspection_datasource/input.go
index 82108bba..c73aab77 100644
--- a/pkg/engine/datasource/introspection_datasource/input.go
+++ b/pkg/engine/datasource/introspection_datasource/input.go
@@ -33,7 +33,7 @@ var (
 	rBrace                 = []byte("}")
 	comma                  = []byte(",")
 	requestTypeField       = []byte(`"request_type":`)
-	onTypeField            = []byte(`"on_type_name":{{ .object.name }}`)
+	onTypeField            = []byte(`"on_type_name":"{{ .object.name }}"`)
 	typeNameField          = []byte(`"type_name":"{{ .arguments.name }}"`)
 	includeDeprecatedField = []byte(`"include_deprecated":{{ .arguments.includeDeprecated }}`)
 )
diff --git a/pkg/engine/resolve/inputtemplate.go b/pkg/engine/resolve/inputtemplate.go
index 75efe383..2253f0d2 100644
--- a/pkg/engine/resolve/inputtemplate.go
+++ b/pkg/engine/resolve/inputtemplate.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	"github.com/buger/jsonparser"
+
 	"github.com/jensneuse/graphql-go-tools/pkg/fastbuffer"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/literal"
 )
@@ -59,7 +60,13 @@ func (i *InputTemplate) renderObjectVariable(ctx context.Context, variables []by
 		return nil
 	}
 	if valueType == jsonparser.String {
-		value = variables[offset-len(value)-2:offset]
+		value = variables[offset-len(value)-2 : offset]
+		switch segment.Renderer.GetKind() {
+		case VariableRendererKindPlain:
+			if plainRenderer, ok := (segment.Renderer).(*PlainVariableRenderer); ok {
+				plainRenderer.rootValueType.Value = valueType
+			}
+		}
 	}
 	return segment.Renderer.RenderVariable(ctx, value, preparedInput)
 }
@@ -71,7 +78,7 @@ func (i *InputTemplate) renderContextVariable(ctx *Context, segment TemplateSegm
 		return nil
 	}
 	if valueType == jsonparser.String {
-		value = ctx.Variables[offset-len(value)-2:offset]
+		value = ctx.Variables[offset-len(value)-2 : offset]
 	}
 	return segment.Renderer.RenderVariable(ctx, value, preparedInput)
 }
diff --git a/pkg/engine/resolve/variable.go b/pkg/engine/resolve/variable.go
index 0b52a413..9f2f9642 100644
--- a/pkg/engine/resolve/variable.go
+++ b/pkg/engine/resolve/variable.go
@@ -22,11 +22,21 @@ const (
 	HeaderVariableKind
 )
 
+const (
+	VariableRendererKindPlain                 = "plain"
+	VariableRendererKindPlanWithValidation    = "plainWithValidation"
+	VariableRendererKindJson                  = "json"
+	VariableRendererKindJsonWithValidation    = "jsonWithValidation"
+	VariableRendererKindGraphqlWithValidation = "graphqlWithValidation"
+	VariableRendererKindCsv                   = "csv"
+)
+
 // VariableRenderer is the interface to allow custom implementations of rendering Variables
 // Depending on where a Variable is being used, a different method for rendering is required
 // E.g. a Variable needs to be rendered conforming to the GraphQL specification, when used within a GraphQL Query
 // If a Variable is used within a JSON Object, the contents need to be rendered as a JSON Object
 type VariableRenderer interface {
+	GetKind() string
 	RenderVariable(ctx context.Context, data []byte, out io.Writer) error
 }
 
@@ -40,6 +50,10 @@ type JSONVariableRenderer struct {
 	rootValueType JsonRootType
 }
 
+func (r *JSONVariableRenderer) GetKind() string {
+	return r.Kind
+}
+
 func (r *JSONVariableRenderer) RenderVariable(ctx context.Context, data []byte, out io.Writer) error {
 	if r.validator != nil {
 		err := r.validator.Validate(ctx, data)
@@ -53,14 +67,14 @@ func (r *JSONVariableRenderer) RenderVariable(ctx context.Context, data []byte,
 
 func NewJSONVariableRenderer() *JSONVariableRenderer {
 	return &JSONVariableRenderer{
-		Kind: "json",
+		Kind: VariableRendererKindJson,
 	}
 }
 
 func NewJSONVariableRendererWithValidation(jsonSchema string) *JSONVariableRenderer {
 	validator := graphqljsonschema.MustNewValidatorFromString(jsonSchema)
 	return &JSONVariableRenderer{
-		Kind:       "jsonWithValidation",
+		Kind:       VariableRendererKindJsonWithValidation,
 		JSONSchema: jsonSchema,
 		validator:  validator,
 	}
@@ -79,7 +93,7 @@ func NewJSONVariableRendererWithValidationFromTypeRef(operation, definition *ast
 		return nil, err
 	}
 	return &JSONVariableRenderer{
-		Kind:          "jsonWithValidation",
+		Kind:          VariableRendererKindJsonWithValidation,
 		JSONSchema:    string(schemaBytes),
 		validator:     validator,
 		rootValueType: getJSONRootType(operation, definition, variableTypeRef),
@@ -88,14 +102,14 @@ func NewJSONVariableRendererWithValidationFromTypeRef(operation, definition *ast
 
 func NewPlainVariableRenderer() *PlainVariableRenderer {
 	return &PlainVariableRenderer{
-		Kind: "plain",
+		Kind: VariableRendererKindPlain,
 	}
 }
 
 func NewPlainVariableRendererWithValidation(jsonSchema string) *PlainVariableRenderer {
 	validator := graphqljsonschema.MustNewValidatorFromString(jsonSchema)
 	return &PlainVariableRenderer{
-		Kind:       "plainWithValidation",
+		Kind:       VariableRendererKindPlanWithValidation,
 		JSONSchema: jsonSchema,
 		validator:  validator,
 	}
@@ -115,7 +129,7 @@ func NewPlainVariableRendererWithValidationFromTypeRef(operation, definition *as
 	}
 	rootValueType := getJSONRootType(operation, definition, variableTypeRef)
 	return &PlainVariableRenderer{
-		Kind:          "plainWithValidation",
+		Kind:          VariableRendererKindPlanWithValidation,
 		JSONSchema:    string(schemaBytes),
 		validator:     validator,
 		rootValueType: rootValueType,
@@ -134,6 +148,10 @@ type PlainVariableRenderer struct {
 	rootValueType JsonRootType
 }
 
+func (p *PlainVariableRenderer) GetKind() string {
+	return p.Kind
+}
+
 func (p *PlainVariableRenderer) RenderVariable(ctx context.Context, data []byte, out io.Writer) error {
 	if p.validator != nil {
 		err := p.validator.Validate(ctx, data)
@@ -161,7 +179,7 @@ func NewGraphQLVariableRendererFromTypeRef(operation, definition *ast.Document,
 		return nil, err
 	}
 	return &GraphQLVariableRenderer{
-		Kind:          "graphqlWithValidation",
+		Kind:          VariableRendererKindGraphqlWithValidation,
 		JSONSchema:    string(schemaBytes),
 		validator:     validator,
 		rootValueType: getJSONRootType(operation, definition, variableTypeRef),
@@ -179,7 +197,7 @@ func NewGraphQLVariableRendererFromTypeRefWithOverrides(operation, definition *a
 		return nil, err
 	}
 	return &GraphQLVariableRenderer{
-		Kind:          "graphqlWithValidation",
+		Kind:          VariableRendererKindGraphqlWithValidation,
 		JSONSchema:    string(schemaBytes),
 		validator:     validator,
 		rootValueType: getJSONRootType(operation, definition, variableTypeRef),
@@ -188,7 +206,7 @@ func NewGraphQLVariableRendererFromTypeRefWithOverrides(operation, definition *a
 
 func NewGraphQLVariableRendererFromTypeRefWithoutValidation(operation, definition *ast.Document, variableTypeRef int) (*GraphQLVariableRenderer, error) {
 	return &GraphQLVariableRenderer{
-		Kind:          "graphqlWithValidation",
+		Kind:          VariableRendererKindGraphqlWithValidation,
 		rootValueType: getJSONRootType(operation, definition, variableTypeRef),
 	}, nil
 }
@@ -201,7 +219,7 @@ func NewGraphQLVariableRenderer(jsonSchema string) *GraphQLVariableRenderer {
 		panic(err)
 	}
 	return &GraphQLVariableRenderer{
-		Kind:       "graphqlWithValidation",
+		Kind:       VariableRendererKindGraphqlWithValidation,
 		JSONSchema: jsonSchema,
 		validator:  validator,
 		rootValueType: JsonRootType{
@@ -316,6 +334,10 @@ type GraphQLVariableRenderer struct {
 	rootValueType JsonRootType
 }
 
+func (g *GraphQLVariableRenderer) GetKind() string {
+	return g.Kind
+}
+
 // add renderer that renders both variable name and variable value
 // before rendering, evaluate if the value contains null values
 // if an object contains only null values, set the object to null
@@ -400,14 +422,14 @@ func (g *GraphQLVariableRenderer) renderGraphQLValue(data []byte, valueType json
 
 func NewCSVVariableRenderer(arrayValueType JsonRootType) *CSVVariableRenderer {
 	return &CSVVariableRenderer{
-		Kind:           "csv",
+		Kind:           VariableRendererKindCsv,
 		arrayValueType: arrayValueType,
 	}
 }
 
 func NewCSVVariableRendererFromTypeRef(operation, definition *ast.Document, variableTypeRef int) *CSVVariableRenderer {
 	return &CSVVariableRenderer{
-		Kind:           "csv",
+		Kind:           VariableRendererKindCsv,
 		arrayValueType: getJSONRootType(operation, definition, variableTypeRef),
 	}
 }
@@ -419,6 +441,10 @@ type CSVVariableRenderer struct {
 	arrayValueType JsonRootType
 }
 
+func (c *CSVVariableRenderer) GetKind() string {
+	return c.Kind
+}
+
 func (c *CSVVariableRenderer) RenderVariable(_ context.Context, data []byte, out io.Writer) error {
 	isFirst := true
 	_, err := jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {


Test patch:
diff --git a/pkg/engine/datasource/introspection_datasource/input_test.go b/pkg/engine/datasource/introspection_datasource/input_test.go
index a6e3c4c9..edec94f4 100644
--- a/pkg/engine/datasource/introspection_datasource/input_test.go
+++ b/pkg/engine/datasource/introspection_datasource/input_test.go
@@ -19,8 +19,8 @@ func TestBuildInput(t *testing.T) {
 
 	t.Run("schema introspection", run(schemaFieldName, `{"request_type":1}`))
 	t.Run("type introspection", run(typeFieldName, `{"request_type":2,"type_name":"{{ .arguments.name }}"}`))
-	t.Run("type fields", run(fieldsFieldName, `{"request_type":3,"on_type_name":{{ .object.name }},"include_deprecated":{{ .arguments.includeDeprecated }}}`))
-	t.Run("type enum values", run(enumValuesFieldName, `{"request_type":4,"on_type_name":{{ .object.name }},"include_deprecated":{{ .arguments.includeDeprecated }}}`))
+	t.Run("type fields", run(fieldsFieldName, `{"request_type":3,"on_type_name":"{{ .object.name }}","include_deprecated":{{ .arguments.includeDeprecated }}}`))
+	t.Run("type enum values", run(enumValuesFieldName, `{"request_type":4,"on_type_name":"{{ .object.name }}","include_deprecated":{{ .arguments.includeDeprecated }}}`))
 }
 
 func TestUnmarshalIntrospectionInput(t *testing.T) {
diff --git a/pkg/engine/datasource/introspection_datasource/planner_test.go b/pkg/engine/datasource/introspection_datasource/planner_test.go
index d6c10e7d..14198331 100644
--- a/pkg/engine/datasource/introspection_datasource/planner_test.go
+++ b/pkg/engine/datasource/introspection_datasource/planner_test.go
@@ -208,7 +208,7 @@ func TestIntrospectionDataSourcePlanning(t *testing.T) {
 									Fetches: []resolve.Fetch{
 										&resolve.SingleFetch{
 											BufferId:   1,
-											Input:      `{"request_type":3,"on_type_name":$$0$$,"include_deprecated":$$1$$}`,
+											Input:      `{"request_type":3,"on_type_name":"$$0$$","include_deprecated":$$1$$}`,
 											DataSource: &Source{},
 											Variables: resolve.NewVariables(
 												&resolve.ObjectVariable{
@@ -224,7 +224,7 @@ func TestIntrospectionDataSourcePlanning(t *testing.T) {
 										},
 										&resolve.SingleFetch{
 											BufferId:   2,
-											Input:      `{"request_type":4,"on_type_name":$$0$$,"include_deprecated":$$1$$}`,
+											Input:      `{"request_type":4,"on_type_name":"$$0$$","include_deprecated":$$1$$}`,
 											DataSource: &Source{},
 											Variables: resolve.NewVariables(
 												&resolve.ObjectVariable{
diff --git a/pkg/graphql/execution_engine_v2_test.go b/pkg/graphql/execution_engine_v2_test.go
index 854c0ad9..50071a5a 100644
--- a/pkg/graphql/execution_engine_v2_test.go
+++ b/pkg/graphql/execution_engine_v2_test.go
@@ -418,6 +418,123 @@ func TestExecutionEngineV2_Execute(t *testing.T) {
 		},
 	))
 
+	t.Run("execute with .object placeholder", runWithoutError(
+		ExecutionEngineV2TestCase{
+			schema: func(t *testing.T) *Schema {
+				t.Helper()
+				schema := `
+					type Query {
+					  getPet(id: ID): Pet
+					  countries: [Country]
+					}
+					
+					type Country {
+					  name: String
+					}
+					
+					type Pet {
+					  id: Int
+					  name: String
+					  status: String
+					  country: Country
+					}
+					
+					type Mutation {
+					  createPet(id: Int, name: String, status: String): Pet
+					}`
+				parseSchema, err := NewSchemaFromString(schema)
+				assert.NoError(t, err)
+				return parseSchema
+			}(t),
+			operation: func(t *testing.T) Request {
+				t.Helper()
+				return Request{
+					OperationName: "MyQuery",
+					Query: `
+						query MyQuery {
+						  getPet(id: 1) {
+							id
+							name
+							country {
+							  name
+							}
+						  }
+						}
+					`,
+				}
+			},
+			dataSources: []plan.DataSourceConfiguration{
+				{
+					RootNodes: []plan.TypeField{
+						{TypeName: "Query", FieldNames: []string{"getPet"}},
+					},
+					ChildNodes: []plan.TypeField{
+						{TypeName: "Pet", FieldNames: []string{"id", "name", "status"}},
+					},
+					Factory: &rest_datasource.Factory{
+						Client: testNetHttpClient(t, roundTripperTestCase{
+							expectedHost:     "petstore.swagger.io",
+							expectedPath:     "/v2/pet/1",
+							expectedBody:     "",
+							sendResponseBody: `{"id":1,"category":{"id":1,"name":"string"},"name":"doggie"}`,
+							sendStatusCode:   200,
+						}),
+					},
+					Custom: rest_datasource.ConfigJSON(rest_datasource.Configuration{
+						Fetch: rest_datasource.FetchConfiguration{
+							URL:    "https://petstore.swagger.io/v2/pet/{{.arguments.id}}",
+							Method: "GET",
+						},
+					}),
+				},
+				{
+					RootNodes: []plan.TypeField{
+						{TypeName: "Pet", FieldNames: []string{"country"}},
+					},
+					ChildNodes: []plan.TypeField{
+						{TypeName: "Country", FieldNames: []string{"name"}},
+					},
+					Factory: &rest_datasource.Factory{
+						Client: testNetHttpClient(t, roundTripperTestCase{
+							expectedHost:     "rest-countries.example.com",
+							expectedPath:     "/name/doggie",
+							expectedBody:     "",
+							sendResponseBody: `{"name":"Germany"}`,
+							sendStatusCode:   200,
+						}),
+					},
+					Custom: rest_datasource.ConfigJSON(rest_datasource.Configuration{
+						Fetch: rest_datasource.FetchConfiguration{
+							URL:    "https://rest-countries.example.com/name/{{.object.name}}",
+							Method: "POST",
+						},
+					}),
+				},
+			},
+			fields: []plan.FieldConfiguration{
+				{
+					TypeName:              "Query",
+					FieldName:             "getPet",
+					DisableDefaultMapping: true,
+					Path:                  []string{""},
+					Arguments: []plan.ArgumentConfiguration{
+						{
+							Name:       "id",
+							SourceType: plan.FieldArgumentSource,
+						},
+					},
+				},
+				{
+					TypeName:              "Pet",
+					FieldName:             "country",
+					DisableDefaultMapping: true,
+					Path:                  []string{""},
+				},
+			},
+			expectedResponse: `{"data":{"getPet":{"id":1,"name":"doggie","country":{"name":"Germany"}}}}`,
+		},
+	))
+
 	t.Run("execute simple hero operation with graphql data source", runWithoutError(
 		ExecutionEngineV2TestCase{
 			schema:    starwarsSchema(t),