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

Add nullvalue #696

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
76 changes: 76 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,82 @@ func TestThreadsSourceCorrectly(t *testing.T) {
}
}

func TestCorrectlyListArgumentsWithNull(t *testing.T) {
query := `
query Example {
b(listStringArg: null, listBoolArg: [true,false,null],listIntArg:[123,null,12],listStringNonNullArg:[null])
}
`
var resolvedArgs map[string]interface{}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Type",
Fields: graphql.Fields{
"b": &graphql.Field{
Args: graphql.FieldConfigArgument{
"listStringArg": &graphql.ArgumentConfig{
Type: graphql.NewList(graphql.String),
},
"listStringNonNullArg": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.NewList(graphql.String)),
},
"listBoolArg": &graphql.ArgumentConfig{
Type: graphql.NewList(graphql.Boolean),
},
"listIntArg": &graphql.ArgumentConfig{
Type: graphql.NewList(graphql.Int),
},
},
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
resolvedArgs = p.Args
return resolvedArgs, nil
},
},
},
}),
})
if err != nil {
t.Fatalf("Error in schema %v", err.Error())
}
ast := testutil.TestParse(t, query)

ep := graphql.ExecuteParams{
Schema: schema,
AST: ast,
}
result := testutil.TestExecute(t, ep)
if len(result.Errors) > 0 {
t.Fatalf("wrong result, unexpected errors: %v", result.Errors)
}
tests := []struct {
key string
expected interface{}
}{
{
"listStringArg", nil,
},

{
"listStringNonNullArg", []interface{}{nil},
},

{
"listBoolArg", []interface{}{true, false, nil},
},

{
"listIntArg", []interface{}{123, nil, 12},
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("TestCorrectlyListArgumentsWithNull_%s", tt.key), func(t *testing.T) {
if !reflect.DeepEqual(resolvedArgs[tt.key], tt.expected) {
t.Fatalf("Expected args.%s to equal `%v`, got `%v`", tt.key, tt.expected, resolvedArgs[tt.key])
}
})
}
}
func TestCorrectlyThreadsArguments(t *testing.T) {

query := `
Expand Down
34 changes: 34 additions & 0 deletions language/ast/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var _ Value = (*BooleanValue)(nil)
var _ Value = (*EnumValue)(nil)
var _ Value = (*ListValue)(nil)
var _ Value = (*ObjectValue)(nil)
var _ Value = (*NullValue)(nil)

// Variable implements Node, Value
type Variable struct {
Expand Down Expand Up @@ -202,6 +203,39 @@ func (v *EnumValue) GetValue() interface{} {
return v.Value
}

// NullValue represents the GraphQL null value.
//
// It is used to support passing null as an input value.
//
// Reference: https://spec.graphql.org/October2021/#sec-Null-Value
type NullValue struct {
Kind string
Loc *Location
Value interface{}
}

func NewNullValue(v *NullValue) *NullValue {
if v == nil {
v = &NullValue{}
}
return &NullValue{
Kind: kinds.NullValue,
Loc: v.Loc,
Value: nil,
}
}
func (n *NullValue) GetKind() string {
return n.Kind
}

func (n *NullValue) GetLoc() *Location {
return n.Loc
}

func (n *NullValue) GetValue() interface{} {
return n.Value
}

// ListValue implements Node, Value
type ListValue struct {
Kind string
Expand Down
1 change: 1 addition & 0 deletions language/kinds/kinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
ListValue = "ListValue"
ObjectValue = "ObjectValue"
ObjectField = "ObjectField"
NullValue = "NullValue"

// Directives
Directive = "Directive"
Expand Down
2 changes: 2 additions & 0 deletions language/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
STRING
BLOCK_STRING
AMP
NULL
)

var tokenDescription = map[TokenKind]string{
Expand All @@ -57,6 +58,7 @@ var tokenDescription = map[TokenKind]string{
STRING: "String",
BLOCK_STRING: "BlockString",
AMP: "&",
NULL: "null",
}

func (kind TokenKind) String() string {
Expand Down
11 changes: 10 additions & 1 deletion language/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,14 @@ func parseValueLiteral(parser *Parser, isConst bool) (ast.Value, error) {
Value: token.Value,
Loc: loc(parser, token.Start),
}), nil
} else {
// If the value literal in the GraphQL input is `null`, converts it into a NullValue AST node.
if err := advance(parser); err != nil {
return nil, err
}
return ast.NewNullValue(&ast.NullValue{
Loc: loc(parser, token.Start),
}), nil
}
case lexer.DOLLAR:
if !isConst {
Expand Down Expand Up @@ -1562,7 +1570,8 @@ func unexpectedEmpty(parser *Parser, beginLoc int, openKind, closeKind lexer.Tok
return gqlerrors.NewSyntaxError(parser.Source, beginLoc, description)
}

// Returns list of parse nodes, determined by
// Returns list of parse nodes, determined by
//
// the parseFn. This list begins with a lex token of openKind
// and ends with a lex token of closeKind. Advances the parser
// to the next lex token after the closing token.
Expand Down
9 changes: 0 additions & 9 deletions language/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,6 @@ func TestDoesNotAcceptFragmentsSpreadOfOn(t *testing.T) {
testErrorMessage(t, test)
}

func TestDoesNotAllowNullAsValue(t *testing.T) {
test := errorMessageTest{
`{ fieldWithNullableStringInput(input: null) }'`,
`Syntax Error GraphQL (1:39) Unexpected Name "null"`,
false,
}
testErrorMessage(t, test)
}

func TestParsesMultiByteCharacters_Unicode(t *testing.T) {

doc := `
Expand Down
9 changes: 8 additions & 1 deletion language/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"reflect"

"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/lexer"
"github.com/graphql-go/graphql/language/visitor"
)

Expand Down Expand Up @@ -472,7 +473,13 @@ var printDocASTReducer = map[string]visitor.VisitFunc{
}
return visitor.ActionNoChange, nil
},

"NullValue": func(p visitor.VisitFuncParams) (string, interface{}) {
switch p.Node.(type) {
case *ast.NullValue:
return visitor.ActionUpdate, lexer.NULL.String()
}
return visitor.ActionNoChange, nil
},
// Type System Definitions
"SchemaDefinition": func(p visitor.VisitFuncParams) (string, interface{}) {
switch node := p.Node.(type) {
Expand Down
14 changes: 14 additions & 0 deletions language/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,17 @@ func TestPrinter_CorrectlyPrintsStringArgumentsWithProperQuoting(t *testing.T) {
t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, results))
}
}

func TestPrinter_CorrectlyPrintsNullArguments(t *testing.T) {
queryAst := `query { foo(nullArg: null) }`
expected := `{
foo(nullArg: null)
}
`
astDoc := parse(t, queryAst)
results := printer.Print(astDoc)

if !reflect.DeepEqual(expected, results) {
t.Fatalf("Unexpected result, Diff: %v", testutil.Diff(expected, results))
}
}
7 changes: 6 additions & 1 deletion rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -1735,14 +1735,19 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
if valueAST.GetKind() == kinds.Variable {
return true, nil
}
// Supplying a nullable variable type to a non-null input type is considered invalid.
// nullValue is only valid for nullable input types.
if valueAST.GetKind() == kinds.NullValue {
return true, nil
}
}
switch ttype := ttype.(type) {
case *NonNull:
// A value must be provided if the type is non-null.
if e := ttype.Error(); e != nil {
return false, []string{e.Error()}
}
if valueAST == nil {
if valueAST == nil || valueAST.GetKind() == kinds.NullValue {
if ttype.OfType.Name() != "" {
return false, []string{fmt.Sprintf(`Expected "%v!", found null.`, ttype.OfType.Name())}
}
Expand Down
35 changes: 35 additions & 0 deletions rules_arguments_of_correct_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,41 @@ import (
"github.com/graphql-go/graphql/testutil"
)

func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodNullValue(t *testing.T) {
testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, `
{
complicatedArgs {
intArgField(intArg: null)
}
}
`)
}

func TestValidator_NonNullArgsUsingNullValue(t *testing.T) {
testutil.ExpectFailsRule(t, graphql.ArgumentsOfCorrectTypeRule, `
{
complicatedArgs {
nonNullIntArgField(nonNullIntArg: null)
}
}
`, []gqlerrors.FormattedError{
testutil.RuleError(
"Argument \"nonNullIntArg\" has invalid value null.\nExpected \"Int!\", found null.",
4, 47,
),
})
}

func TestValidator_NullArgsUsingNullValue(t *testing.T) {
testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, `
{
complicatedArgs {
stringArgField(stringArg: null)
}
}
`)
}

func TestValidate_ArgValuesOfCorrectType_ValidValue_GoodIntValue(t *testing.T) {
testutil.ExpectPassesRule(t, graphql.ArgumentsOfCorrectTypeRule, `
{
Expand Down
4 changes: 4 additions & 0 deletions scalars.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ var Int = NewScalar(ScalarConfig{
return intValue
}
}

return nil
},
})
Expand Down Expand Up @@ -332,6 +333,9 @@ var String = NewScalar(ScalarConfig{
})

func coerceBool(value interface{}) interface{} {
if value == nil {
return nil
}
switch value := value.(type) {
case bool:
return value
Expand Down
12 changes: 12 additions & 0 deletions scalars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ func TestCoerceInt(t *testing.T) {
in: make(map[string]interface{}),
want: nil,
},
{
in: nil,
want: nil,
},
}

for i, tt := range tests {
Expand Down Expand Up @@ -438,6 +442,10 @@ func TestCoerceFloat(t *testing.T) {
in: make(map[string]interface{}),
want: nil,
},
{
in: nil,
want: nil,
},
}

for i, tt := range tests {
Expand Down Expand Up @@ -740,6 +748,10 @@ func TestCoerceBool(t *testing.T) {
in: make(map[string]interface{}),
want: false,
},
{
in: nil,
want: nil,
},
}

for i, tt := range tests {
Expand Down
1 change: 1 addition & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func TestValidator_SupportsFullValidation_ValidatesQueries(t *testing.T) {
`)
}


// NOTE: experimental
func TestValidator_SupportsFullValidation_ValidatesUsingACustomTypeInfo(t *testing.T) {

Expand Down
Loading