Skip to content

Commit

Permalink
Merge pull request #1 from nytimes/nullish
Browse files Browse the repository at this point in the history
Pull in support for null literals
  • Loading branch information
chrisfrank authored Apr 1, 2024
2 parents f2b39ca + dba344b commit 7eaf583
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 36 deletions.
297 changes: 297 additions & 0 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,300 @@ func TestEmptyStringIsNotNull(t *testing.T) {
t.Errorf("wrong result, query: %v, graphql result diff: %v", query, testutil.Diff(expected, result))
}
}

func TestNullLiteralArguments(t *testing.T) {
checkForNull := func(p graphql.ResolveParams) (interface{}, error) {
arg, ok := p.Args["arg"]
if !ok || arg != nil {
t.Errorf("expected null for input arg, got %#v", arg)
}
return "yay", nil
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"checkNullStringArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.String},
},
Resolve: checkForNull,
},
"checkNullIntArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.Int},
},
Resolve: checkForNull,
},
"checkNullBooleanArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.Boolean},
},
Resolve: checkForNull,
},
"checkNullListArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.NewList(graphql.String)},
},
Resolve: checkForNull,
},
"checkNullInputObjectArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.NewInputObject(
graphql.InputObjectConfig{
Name: "InputType",
Fields: graphql.InputObjectConfigFieldMap{
"field1": {Type: graphql.String},
"field2": {Type: graphql.Int},
},
})},
},
Resolve: checkForNull,
},
},
}),
})
if err != nil {
t.Fatalf("wrong result, unexpected errors: %v", err.Error())
}
query := `{ checkNullStringArg(arg:null) checkNullIntArg(arg:null) checkNullBooleanArg(arg:null) checkNullListArg(arg:null) checkNullInputObjectArg(arg:null) }`

result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})
if len(result.Errors) > 0 {
t.Fatalf("wrong result, unexpected errors: %v", result.Errors)
}
expected := map[string]interface{}{
"checkNullStringArg": "yay", "checkNullIntArg": "yay",
"checkNullBooleanArg": "yay", "checkNullListArg": "yay",
"checkNullInputObjectArg": "yay"}
if !reflect.DeepEqual(result.Data, expected) {
t.Errorf("wrong result, query: %v, graphql result diff: %v", query, testutil.Diff(expected, result))
}
}

func TestNullLiteralDefaultVariableValue(t *testing.T) {
checkForNull := func(p graphql.ResolveParams) (interface{}, error) {
arg, ok := p.Args["arg"]
if !ok || arg != nil {
t.Errorf("expected null for input arg, got %#v", arg)
}
return "yay", nil
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"checkNullStringArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.String},
},
Resolve: checkForNull,
},
},
}),
})
if err != nil {
t.Fatalf("wrong result, unexpected errors: %v", err.Error())
}
query := `query Test($value: String = null) { checkNullStringArg(arg: $value) }`

result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
VariableValues: map[string]interface{}{"value2": nil},
})
if len(result.Errors) > 0 {
t.Fatalf("wrong result, unexpected errors: %v", result.Errors)
}
expected := map[string]interface{}{ "checkNullStringArg": "yay", }
if !reflect.DeepEqual(result.Data, expected) {
t.Errorf("wrong result, query: %v, graphql result diff: %v", query, testutil.Diff(expected, result))
}
}

func TestNullLiteralVariables(t *testing.T) {
checkForNull := func(p graphql.ResolveParams) (interface{}, error) {
arg, ok := p.Args["arg"]
if !ok || arg != nil {
t.Errorf("expected null for input arg, got %#v", arg)
}
return "yay", nil
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"checkNullStringArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.String},
},
Resolve: checkForNull,
},
},
}),
})
if err != nil {
t.Fatalf("wrong result, unexpected errors: %v", err.Error())
}
query := `query Test($value: String) { checkNullStringArg(arg: $value) }`

result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
VariableValues: map[string]interface{}{"value": nil},
})
if len(result.Errors) > 0 {
t.Fatalf("wrong result, unexpected errors: %v", result.Errors)
}
expected := map[string]interface{}{ "checkNullStringArg": "yay", }
if !reflect.DeepEqual(result.Data, expected) {
t.Errorf("wrong result, query: %v, graphql result diff: %v", query, testutil.Diff(expected, result))
}
}

func TestErrorNullLiteralForNotNullArgument(t *testing.T) {
checkNotCalled := func(p graphql.ResolveParams) (interface{}, error) {
t.Error("shouldn't have been called")
return nil, nil
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"checkNotNullArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String) },
},
Resolve: checkNotCalled,
},
},
}),
})
if err != nil {
t.Fatalf("wrong result, unexpected errors: %v", err.Error())
}
query := `{ checkNotNullArg(arg:null) }`

result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})

if len(result.Errors) == 0 {
t.Fatalf("expected errors, got: %v", result)
}

expectedMessage := `Argument "arg" has invalid value <nil>.
Expected "String!", found null.`;

if result.Errors[0].Message != expectedMessage {
t.Fatalf("unexpected error.\nexpected:\n%s\ngot:\n%s\n", expectedMessage, result.Errors[0].Message)
}
}

func TestNullInputObjectFields(t *testing.T) {
checkForNull := func(p graphql.ResolveParams) (interface{}, error) {
arg := p.Args["arg"]
expectedValue := map[string]interface{}{ "field1": nil, "field2": nil, "field3": nil, "field4" : "abc", "field5": 42, "field6": true}
if value, ok := arg.(map[string]interface{}); !ok {
t.Errorf("expected map[string]interface{} for input arg, got %#v", arg)
} else if !reflect.DeepEqual(expectedValue, value) {
t.Errorf("unexpected input object, diff: %v", testutil.Diff(expectedValue, value))
}
return "yay", nil
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"checkNullInputObjectFields": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.NewInputObject(
graphql.InputObjectConfig{
Name: "InputType",
Fields: graphql.InputObjectConfigFieldMap{
"field1": {Type: graphql.String},
"field2": {Type: graphql.Int},
"field3": {Type: graphql.Boolean},
"field4": {Type: graphql.String},
"field5": {Type: graphql.Int},
"field6": {Type: graphql.Boolean},
},
})},
},
Resolve: checkForNull,
},
},
}),
})
if err != nil {
t.Fatalf("wrong result, unexpected errors: %v", err.Error())
}
query := `{ checkNullInputObjectFields(arg: {field1: null, field2: null, field3: null, field4: "abc", field5: 42, field6: true }) }`

result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})
if len(result.Errors) > 0 {
t.Fatalf("wrong result, unexpected errors: %v", result.Errors)
}
expected := map[string]interface{}{ "checkNullInputObjectFields": "yay" }
if !reflect.DeepEqual(result.Data, expected) {
t.Errorf("wrong result, query: %v, graphql result diff: %v", query, testutil.Diff(expected, result))
}
}

func TestErrorNullInList(t *testing.T) {
checkNotCalled := func(p graphql.ResolveParams) (interface{}, error) {
t.Error("shouldn't have been called")
return nil, nil
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"checkNotNullInListArg": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{Type: graphql.NewList(graphql.String) },
},
Resolve: checkNotCalled,
},
},
}),
})
if err != nil {
t.Fatalf("wrong result, unexpected errors: %v", err.Error())
}
query := `{ checkNotNullInListArg(arg: [null, null]) }`

result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})

if len(result.Errors) == 0 {
t.Fatalf("expected errors, got: %v", result)
}

expectedMessage := `Argument "arg" has invalid value [<nil>, <nil>].
In element #1: Unexpected null literal.
In element #2: Unexpected null literal.`

if result.Errors[0].Message != expectedMessage {
t.Fatalf("unexpected error.\nexpected:\n%s\ngot:\n%s\n", expectedMessage, result.Errors[0].Message)
}
}
1 change: 1 addition & 0 deletions language/ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var _ Node = (*IntValue)(nil)
var _ Node = (*FloatValue)(nil)
var _ Node = (*StringValue)(nil)
var _ Node = (*BooleanValue)(nil)
var _ Node = (*NullValue)(nil)
var _ Node = (*EnumValue)(nil)
var _ Node = (*ListValue)(nil)
var _ Node = (*ObjectValue)(nil)
Expand Down
29 changes: 29 additions & 0 deletions language/ast/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var _ Value = (*IntValue)(nil)
var _ Value = (*FloatValue)(nil)
var _ Value = (*StringValue)(nil)
var _ Value = (*BooleanValue)(nil)
var _ Value = (*NullValue)(nil)
var _ Value = (*EnumValue)(nil)
var _ Value = (*ListValue)(nil)
var _ Value = (*ObjectValue)(nil)
Expand Down Expand Up @@ -172,6 +173,34 @@ func (v *BooleanValue) GetValue() interface{} {
return v.Value
}

// NullValue implements Node, Value
type NullValue struct {
Kind string
Loc *Location
Value interface{}
}

func NewNullValue(v *NullValue) *NullValue {

return &NullValue{
Kind: kinds.NullValue,
Loc: v.Loc,
Value: v.Value,
}
}

func (v *NullValue) GetKind() string {
return v.Kind
}

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

func (v *NullValue) GetValue() interface{} {
return nil
}

// EnumValue implements Node, Value
type EnumValue 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 @@ -23,6 +23,7 @@ const (
FloatValue = "FloatValue"
StringValue = "StringValue"
BooleanValue = "BooleanValue"
NullValue = "NullValue"
EnumValue = "EnumValue"
ListValue = "ListValue"
ObjectValue = "ObjectValue"
Expand Down
10 changes: 9 additions & 1 deletion language/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,15 @@ func parseValueLiteral(parser *Parser, isConst bool) (ast.Value, error) {
Value: value,
Loc: loc(parser, token.Start),
}), nil
} else if token.Value != "null" {
} else if token.Value == "null" {
if err := advance(parser); err != nil {
return nil, err
}
return ast.NewNullValue(&ast.NullValue{
Value: nil,
Loc: loc(parser, token.Start),
}), nil
} else {
if err := advance(parser); err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 7eaf583

Please sign in to comment.