Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix missing argument default values #42

Merged
merged 2 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 44 additions & 9 deletions introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"

"github.com/pkg/errors"
"github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast"
)

Expand Down Expand Up @@ -91,7 +92,6 @@ func IntrospectWithRetrier(retrier Retrier) *IntrospectOptions {
// IntrospectRemoteSchema is used to build a RemoteSchema by firing the introspection query
// at a remote service and reconstructing the schema object from the response
func IntrospectRemoteSchema(url string, opts ...*IntrospectOptions) (*RemoteSchema, error) {

// introspect the schema at the designated url
schema, err := IntrospectAPI(NewSingleRequestQueryer(url), opts...)
if err != nil {
Expand Down Expand Up @@ -244,7 +244,6 @@ func IntrospectAPI(queryer Queryer, opts ...*IntrospectOptions) (*ast.Schema, er
}

if len(remoteType.Interfaces) > 0 {

// each interface value needs to be added to the list
for _, iFace := range remoteType.Interfaces {
// if there is no name
Expand All @@ -271,11 +270,15 @@ func IntrospectAPI(queryer Queryer, opts ...*IntrospectOptions) (*ast.Schema, er

for _, field := range remoteType.Fields {
// add the field to the list
args, err := introspectionConvertArgList(field.Args)
if err != nil {
return nil, err
}
fields = append(fields, &ast.FieldDefinition{
Name: field.Name,
Type: introspectionUnmarshalTypeRef(&field.Type),
Description: field.Description,
Arguments: introspectionConvertArgList(field.Args),
Arguments: args,
})
}

Expand Down Expand Up @@ -306,11 +309,15 @@ func IntrospectAPI(queryer Queryer, opts ...*IntrospectOptions) (*ast.Schema, er
}

// save the directive definition to the schema
args, err := introspectionConvertArgList(directive.Args)
if err != nil {
return nil, err
}
schema.Directives[directive.Name] = &ast.DirectiveDefinition{
Position: &ast.Position{Src: &ast.Source{}},
Name: directive.Name,
Description: directive.Description,
Arguments: introspectionConvertArgList(directive.Args),
Arguments: args,
Locations: locations,
}
switch directive.Name {
Expand All @@ -332,19 +339,24 @@ func addPossibleTypeOnce(schema *ast.Schema, name string, definition *ast.Defini
schema.AddPossibleType(name, definition)
}

func introspectionConvertArgList(args []IntrospectionInputValue) ast.ArgumentDefinitionList {
func introspectionConvertArgList(args []IntrospectionInputValue) (ast.ArgumentDefinitionList, error) {
result := ast.ArgumentDefinitionList{}

// we need to add each argument to the field
for _, argument := range args {
defaultValue, err := introspectionUnmarshalArgumentDefaultValue(argument)
if err != nil {
return nil, err
}
result = append(result, &ast.ArgumentDefinition{
Name: argument.Name,
Description: argument.Description,
Type: introspectionUnmarshalTypeRef(&argument.Type),
Name: argument.Name,
Description: argument.Description,
Type: introspectionUnmarshalTypeRef(&argument.Type),
DefaultValue: defaultValue,
})
}

return result
return result, nil
}

func introspectionUnmarshalType(schemaType IntrospectionQueryFullType) *ast.Definition {
Expand Down Expand Up @@ -387,6 +399,29 @@ func introspectionUnmarshalType(schemaType IntrospectionQueryFullType) *ast.Defi
return definition
}

// introspectionUnmarshalArgumentDefaultValue returns the *ast.Value form of an argument's default value.
//
// The tricky part here is the default value comes in as a string, so it's non-trivial to unmarshal.
// This takes advantage of gqlparser's loose validation when parsing an argument's default value even if the type is wrong (to avoid including full definitions of custom types).
// This validation will likely become stricter with time -- hopefully the library will provide some additional tools to parse the default value separately.
func introspectionUnmarshalArgumentDefaultValue(argument IntrospectionInputValue) (*ast.Value, error) {
if argument.DefaultValue == "" {
return nil, nil
}
const inputValueQuery = `
type Query {
field(input: String = %s): String
}
`
schema, err := gqlparser.LoadSchema(&ast.Source{
Input: fmt.Sprintf(inputValueQuery, argument.DefaultValue),
})
if err != nil {
return nil, err
}
return schema.Query.Fields.ForName("field").Arguments.ForName("input").DefaultValue, nil
}

// a mapping of marshaled directive locations to their parsed equivalent
var directiveLocationMap map[string]ast.DirectiveLocation

Expand Down
44 changes: 43 additions & 1 deletion introspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package graphql
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -1033,7 +1034,6 @@ func TestIntrospectWithHTTPClient(t *testing.T) {
opt := IntrospectWithHTTPClient(row.Client)
queryer = opt.Apply(queryer).(*SingleRequestQueryer)
assert.Equal(t, row.Client, queryer.queryer.Client)

})
}
}
Expand Down Expand Up @@ -1518,3 +1518,45 @@ query {
`)
assert.Nil(t, err, "Spreading object fragment on matching interface should be allowed")
}

//go:embed testdata/introspect_default_values.json
var introspectionDefaultValuesJSON string

// TestIntrospectDirectivesDefaultValue verifies fix for https://github.com/nautilus/graphql/issues/40
func TestIntrospectDirectivesDefaultValue(t *testing.T) {
t.Parallel()

schema, err := IntrospectAPI(&mockJSONQueryer{
JSONResult: introspectionDefaultValuesJSON,
})
require.NoError(t, err)

var schemaBuffer bytes.Buffer
formatter.NewFormatter(&schemaBuffer).FormatSchema(schema)

expectedSchema, err := gqlparser.LoadSchema(&ast.Source{Input: `
directive @hello(
foo: String! = "foo"
bar: Int = 1
baz: Boolean = true
biff: Float = 1.23
boo: [String] = ["boo"]
bah: HelloInput = {humbug: "humbug"}
blah: HelloEnum = HELLO_1
) on FIELD_DEFINITION

input HelloInput {
humbug: String
}

enum HelloEnum {
HELLO_1
HELLO_2
HELLO_3
}
`})
require.NoError(t, err)
var expectedBuffer bytes.Buffer
formatter.NewFormatter(&expectedBuffer).FormatSchema(expectedSchema)
assert.Equal(t, expectedBuffer.String(), schemaBuffer.String())
}
200 changes: 200 additions & 0 deletions testdata/introspect_default_values.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
{
"__schema": {
"queryType": {
"name": "Query"
},
"mutationType": null,
"subscriptionType": null,
"types": [
{
"kind": "SCALAR",
"name": "Boolean",
"description": "The `Boolean` scalar type represents `true` or `false`.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Float",
"description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "HelloEnum",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "HELLO_1",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "HELLO_2",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "HELLO_3",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "HelloInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "humbug",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ID",
"description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Int",
"description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "String",
"description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
}
],
"directives": [
{
"name": "hello",
"description": null,
"locations": [
"FIELD_DEFINITION"
],
"args": [
{
"name": "foo",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": "\"foo\""
},
{
"name": "bar",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": "1"
},
{
"name": "baz",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "true"
},
{
"name": "biff",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
},
"defaultValue": "1.23"
},
{
"name": "boo",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": "[\"boo\"]"
},
{
"name": "bah",
"description": null,
"type": {
"kind": "INPUT_OBJECT",
"name": "HelloInput",
"ofType": null
},
"defaultValue": "{humbug: \"humbug\"}"
},
{
"name": "blah",
"description": null,
"type": {
"kind": "ENUM",
"name": "HelloEnum",
"ofType": null
},
"defaultValue": "HELLO_1"
}
]
}
]
}
}
Loading