Skip to content

Commit

Permalink
Fix missing argument default values (#42)
Browse files Browse the repository at this point in the history
* Add failing test for directive default value
* Fix missing argument default values
  • Loading branch information
JohnStarich authored Jun 28, 2024
1 parent c68b248 commit 0cce251
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 10 deletions.
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"
}
]
}
]
}
}

0 comments on commit 0cce251

Please sign in to comment.