Skip to content

Commit e28541e

Browse files
AndrewSisleyfredcarle
authored andcommitted
feat: Add null support (#6)
Code is mostly copy-pasted from the PR graphql-go#536 - main difference is that I haven't copied over a couple of new tests, and that I permitted null within arrays (unanswered question in original PR, and I see no reason for it too).
1 parent c25738a commit e28541e

File tree

9 files changed

+106
-32
lines changed

9 files changed

+106
-32
lines changed

language/ast/node.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var _ Node = (*IntValue)(nil)
2222
var _ Node = (*FloatValue)(nil)
2323
var _ Node = (*StringValue)(nil)
2424
var _ Node = (*BooleanValue)(nil)
25+
var _ Node = (*NullValue)(nil)
2526
var _ Node = (*EnumValue)(nil)
2627
var _ Node = (*ListValue)(nil)
2728
var _ Node = (*ObjectValue)(nil)

language/ast/values.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var _ Value = (*IntValue)(nil)
1616
var _ Value = (*FloatValue)(nil)
1717
var _ Value = (*StringValue)(nil)
1818
var _ Value = (*BooleanValue)(nil)
19+
var _ Value = (*NullValue)(nil)
1920
var _ Value = (*EnumValue)(nil)
2021
var _ Value = (*ListValue)(nil)
2122
var _ Value = (*ObjectValue)(nil)
@@ -172,6 +173,33 @@ func (v *BooleanValue) GetValue() interface{} {
172173
return v.Value
173174
}
174175

176+
type NullValue struct {
177+
Kind string
178+
Loc *Location
179+
Value interface{}
180+
}
181+
182+
func NewNullValue(v *NullValue) *NullValue {
183+
184+
return &NullValue{
185+
Kind: kinds.NullValue,
186+
Loc: v.Loc,
187+
Value: v.Value,
188+
}
189+
}
190+
191+
func (v *NullValue) GetKind() string {
192+
return v.Kind
193+
}
194+
195+
func (v *NullValue) GetLoc() *Location {
196+
return v.Loc
197+
}
198+
199+
func (v *NullValue) GetValue() interface{} {
200+
return nil
201+
}
202+
175203
// EnumValue implements Node, Value
176204
type EnumValue struct {
177205
Kind string

language/kinds/kinds.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
FloatValue = "FloatValue"
2424
StringValue = "StringValue"
2525
BooleanValue = "BooleanValue"
26+
NullValue = "NullValue"
2627
EnumValue = "EnumValue"
2728
ListValue = "ListValue"
2829
ObjectValue = "ObjectValue"

language/parser/parser.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,15 +610,23 @@ func parseValueLiteral(parser *Parser, isConst bool) (ast.Value, error) {
610610
Value: value,
611611
Loc: loc(parser, token.Start),
612612
}), nil
613-
} else if token.Value != "null" {
613+
} else if token.Value == "null" {
614614
if err := advance(parser); err != nil {
615615
return nil, err
616616
}
617-
return ast.NewEnumValue(&ast.EnumValue{
618-
Value: token.Value,
617+
return ast.NewNullValue(&ast.NullValue{
618+
Value: nil,
619619
Loc: loc(parser, token.Start),
620620
}), nil
621621
}
622+
623+
if err := advance(parser); err != nil {
624+
return nil, err
625+
}
626+
return ast.NewEnumValue(&ast.EnumValue{
627+
Value: token.Value,
628+
Loc: loc(parser, token.Start),
629+
}), nil
622630
case lexer.DOLLAR:
623631
if !isConst {
624632
return parseVariable(parser)

language/parser/parser_test.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,6 @@ func TestDoesNotAcceptFragmentsSpreadOfOn(t *testing.T) {
183183
testErrorMessage(t, test)
184184
}
185185

186-
func TestDoesNotAllowNullAsValue(t *testing.T) {
187-
test := errorMessageTest{
188-
`{ fieldWithNullableStringInput(input: null) }'`,
189-
`Syntax Error GraphQL (1:39) Unexpected Name "null"`,
190-
false,
191-
}
192-
testErrorMessage(t, test)
193-
}
194-
195186
func TestParsesMultiByteCharacters_Unicode(t *testing.T) {
196187

197188
doc := `
@@ -367,6 +358,7 @@ func TestAllowsNonKeywordsAnywhereNameIsAllowed(t *testing.T) {
367358
"subscription",
368359
"true",
369360
"false",
361+
"null",
370362
}
371363
for _, keyword := range nonKeywords {
372364
fragmentName := keyword

language/printer/printer.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,15 @@ var printDocASTReducer = map[string]visitor.VisitFunc{
388388
}
389389
return visitor.ActionNoChange, nil
390390
},
391+
"NullValue": func(p visitor.VisitFuncParams) (string, interface{}) {
392+
switch node := p.Node.(type) {
393+
case *ast.NullValue:
394+
return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value)
395+
case map[string]interface{}:
396+
return visitor.ActionUpdate, getMapValueString(node, "Value")
397+
}
398+
return visitor.ActionNoChange, nil
399+
},
391400
"EnumValue": func(p visitor.VisitFuncParams) (string, interface{}) {
392401
switch node := p.Node.(type) {
393402
case *ast.EnumValue:

rules.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,10 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
17301730
return true, nil
17311731
}
17321732

1733+
if valueAST.GetKind() == kinds.NullValue {
1734+
return true, nil
1735+
}
1736+
17331737
// This function only tests literals, and assumes variables will provide
17341738
// values of the correct type.
17351739
if valueAST.GetKind() == kinds.Variable {
@@ -1742,7 +1746,7 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
17421746
if e := ttype.Error(); e != nil {
17431747
return false, []string{e.Error()}
17441748
}
1745-
if valueAST == nil {
1749+
if valueAST == nil || valueAST.GetKind() == kinds.NullValue {
17461750
if ttype.OfType.Name() != "" {
17471751
return false, []string{fmt.Sprintf(`Expected "%v!", found null.`, ttype.OfType.Name())}
17481752
}

values.go

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import (
1414
"github.com/graphql-go/graphql/language/printer"
1515
)
1616

17+
// Used to detect the difference between a "null" literal and not present
18+
type nullValue struct{}
19+
1720
// Prepares an object map of variableValues of the correct type based on the
1821
// provided variable definitions and arbitrary input. If the input cannot be
1922
// parsed to match the variable definitions, a GraphQLError will be returned.
@@ -27,7 +30,7 @@ func getVariableValues(
2730
continue
2831
}
2932
varName := defAST.Variable.Name.Value
30-
if varValue, err := getVariableValue(schema, defAST, inputs[varName]); err != nil {
33+
if varValue, err := getVariableValue(schema, defAST, getValueOrNull(inputs, varName)); err != nil {
3134
return values, err
3235
} else {
3336
values[varName] = varValue
@@ -36,6 +39,25 @@ func getVariableValues(
3639
return values, nil
3740
}
3841

42+
func getValueOrNull(values map[string]interface{}, name string) interface{} {
43+
if tmp, ok := values[name]; ok { // Is present
44+
if tmp == nil {
45+
return nullValue{} // Null value
46+
} else {
47+
return tmp
48+
}
49+
}
50+
return nil // Not present
51+
}
52+
53+
func addValueOrNull(values map[string]interface{}, name string, value interface{}) {
54+
if _, ok := value.(nullValue); ok { // Null value
55+
values[name] = nil
56+
} else if !isNullish(value) { // Not present
57+
values[name] = value
58+
}
59+
}
60+
3961
// Prepares an object map of argument values given a list of argument
4062
// definitions and list of argument AST nodes.
4163
func getArgumentValues(
@@ -60,9 +82,7 @@ func getArgumentValues(
6082
if tmp = valueFromAST(value, argDef.Type, variableValues); isNullish(tmp) {
6183
tmp = argDef.DefaultValue
6284
}
63-
if !isNullish(tmp) {
64-
results[argDef.PrivateName] = tmp
65-
}
85+
addValueOrNull(results, argDef.PrivateName, tmp)
6686
}
6787
return results
6888
}
@@ -97,7 +117,7 @@ func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, inpu
97117
}
98118
return coerceValue(ttype, input), nil
99119
}
100-
if isNullish(input) {
120+
if _, ok := input.(nullValue); ok || isNullish(input) {
101121
return "", gqlerrors.NewError(
102122
fmt.Sprintf(`Variable "$%v" of required type `+
103123
`"%v" was not provided.`, variable.Name.Value, printer.Print(definitionAST.Type)),
@@ -134,6 +154,11 @@ func coerceValue(ttype Input, value interface{}) interface{} {
134154
if isNullish(value) {
135155
return nil
136156
}
157+
158+
if _, ok := value.(nullValue); ok {
159+
return nullValue{}
160+
}
161+
137162
switch ttype := ttype.(type) {
138163
case *NonNull:
139164
return coerceValue(ttype.OfType, value)
@@ -156,13 +181,11 @@ func coerceValue(ttype Input, value interface{}) interface{} {
156181
}
157182

158183
for name, field := range ttype.Fields() {
159-
fieldValue := coerceValue(field.Type, valueMap[name])
184+
fieldValue := coerceValue(field.Type, getValueOrNull(valueMap, name))
160185
if isNullish(fieldValue) {
161186
fieldValue = field.DefaultValue
162187
}
163-
if !isNullish(fieldValue) {
164-
obj[name] = fieldValue
165-
}
188+
addValueOrNull(obj, name, fieldValue)
166189
}
167190
return obj
168191
case *Scalar:
@@ -212,7 +235,7 @@ func typeFromAST(schema Schema, inputTypeAST ast.Type) (Type, error) {
212235
// accepted for that type. This is primarily useful for validating the
213236
// runtime values of query variables.
214237
func isValidInputValue(value interface{}, ttype Input) (bool, []string) {
215-
if isNullish(value) {
238+
if _, ok := value.(nullValue); ok || isNullish(value) {
216239
if ttype, ok := ttype.(*NonNull); ok {
217240
if ttype.OfType.Name() != "" {
218241
return false, []string{fmt.Sprintf(`Expected "%v!", found null.`, ttype.OfType.Name())}
@@ -233,9 +256,14 @@ func isValidInputValue(value interface{}, ttype Input) (bool, []string) {
233256
messagesReduce := []string{}
234257
for i := 0; i < valType.Len(); i++ {
235258
val := valType.Index(i).Interface()
236-
_, messages := isValidInputValue(val, ttype.OfType)
237-
for idx, message := range messages {
238-
messagesReduce = append(messagesReduce, fmt.Sprintf(`In element #%v: %v`, idx+1, message))
259+
var messages []string
260+
if _, ok := val.(nullValue); ok {
261+
messages = []string{"Unexpected null value."}
262+
} else {
263+
_, messages = isValidInputValue(val, ttype.OfType)
264+
}
265+
for _, message := range messages {
266+
messagesReduce = append(messagesReduce, fmt.Sprintf(`In element #%v: %v`, i+1, message))
239267
}
240268
}
241269
return (len(messagesReduce) == 0), messagesReduce
@@ -352,6 +380,11 @@ func valueFromAST(valueAST ast.Value, ttype Input, variables map[string]interfac
352380
if valueAST == nil {
353381
return nil
354382
}
383+
384+
if valueAST.GetKind() == kinds.NullValue {
385+
return nullValue{}
386+
}
387+
355388
// precedence: value > type
356389
if valueAST, ok := valueAST.(*ast.Variable); ok {
357390
if valueAST.Name == nil || variables == nil {
@@ -398,9 +431,7 @@ func valueFromAST(valueAST ast.Value, ttype Input, variables map[string]interfac
398431
} else {
399432
value = field.DefaultValue
400433
}
401-
if !isNullish(value) {
402-
obj[name] = value
403-
}
434+
addValueOrNull(obj, name, value)
404435
}
405436
return obj
406437
case *Scalar:

variables_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ var testNestedInputObject *graphql.InputObject = graphql.NewInputObject(graphql.
6767

6868
func inputResolved(p graphql.ResolveParams) (interface{}, error) {
6969
input, ok := p.Args["input"]
70-
if !ok {
70+
if !ok || input == nil {
7171
return nil, nil
7272
}
7373
b, err := json.Marshal(input)
@@ -1188,7 +1188,7 @@ func TestVariables_ListsAndNullability_DoesNotAllowListOfNonNullsToContainNull(t
11881188
{
11891189
Message: `Variable "$input" got invalid value ` +
11901190
`["A",null,"B"].` +
1191-
"\nIn element #1: Expected \"String!\", found null.",
1191+
"\nIn element #2: Expected \"String!\", found null.",
11921192
Locations: []location.SourceLocation{
11931193
{
11941194
Line: 2, Column: 17,
@@ -1290,7 +1290,7 @@ func TestVariables_ListsAndNullability_DoesNotAllowNonNullListOfNonNullsToContai
12901290
{
12911291
Message: `Variable "$input" got invalid value ` +
12921292
`["A",null,"B"].` +
1293-
"\nIn element #1: Expected \"String!\", found null.",
1293+
"\nIn element #2: Expected \"String!\", found null.",
12941294
Locations: []location.SourceLocation{
12951295
{
12961296
Line: 2, Column: 17,

0 commit comments

Comments
 (0)