diff --git a/language/ast/node.go b/language/ast/node.go index cd63a0fc..058c35f6 100644 --- a/language/ast/node.go +++ b/language/ast/node.go @@ -26,6 +26,7 @@ var _ Node = (*EnumValue)(nil) var _ Node = (*ListValue)(nil) var _ Node = (*ObjectValue)(nil) var _ Node = (*ObjectField)(nil) +var _ Node = (*NullValue)(nil) var _ Node = (*Directive)(nil) var _ Node = (*Named)(nil) var _ Node = (*List)(nil) diff --git a/language/ast/values.go b/language/ast/values.go index 6c3c8864..1d9045d0 100644 --- a/language/ast/values.go +++ b/language/ast/values.go @@ -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 { @@ -300,3 +301,31 @@ func (f *ObjectField) GetLoc() *Location { func (f *ObjectField) GetValue() interface{} { return f.Value } + +// NullValue implements Node, Value +type NullValue struct { + Kind string + Loc *Location +} + +func (n *NullValue) GetKind() string { + return n.Kind +} + +func (n *NullValue) GetLoc() *Location { + return n.Loc +} + +func (n *NullValue) GetValue() interface{} { + return nil +} + +func NewNullValue(v *NullValue) *NullValue { + if v == nil { + v = &NullValue{} + } + return &NullValue{ + Kind: kinds.NullValue, + Loc: v.Loc, + } +} diff --git a/language/kinds/kinds.go b/language/kinds/kinds.go index 40bc994e..d5be9ed8 100644 --- a/language/kinds/kinds.go +++ b/language/kinds/kinds.go @@ -27,6 +27,7 @@ const ( ListValue = "ListValue" ObjectValue = "ObjectValue" ObjectField = "ObjectField" + NullValue = "NullValue" // Directives Directive = "Directive" diff --git a/language/parser/parser.go b/language/parser/parser.go index 4ae3dc33..b2f90208 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -606,7 +606,14 @@ 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{ + Loc: loc(parser, token.Start), + }), nil + } else { if err := advance(parser); err != nil { return nil, err } @@ -1562,7 +1569,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. diff --git a/language/parser/parser_test.go b/language/parser/parser_test.go index 8f0e0715..ba8372b6 100644 --- a/language/parser/parser_test.go +++ b/language/parser/parser_test.go @@ -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 := ` @@ -498,6 +489,14 @@ func TestParsesEnumValueDefinitionWithDescription(t *testing.T) { } } +func TestNullIsAllowedAsValue(t *testing.T) { + source := `{ fieldWithNullableStringInput(input: null) }` + _, err := Parse(ParseParams{Source: source}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + func TestDefinitionsWithDescriptions(t *testing.T) { testCases := []struct { name string diff --git a/language/printer/printer.go b/language/printer/printer.go index ac771ba6..6af23e2a 100644 --- a/language/printer/printer.go +++ b/language/printer/printer.go @@ -428,6 +428,16 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ } return visitor.ActionNoChange, nil }, + "NullValue": func(p visitor.VisitFuncParams) (string, interface{}) { + const nullStr = "null" + switch p.Node.(type) { + case *ast.NullValue: + return visitor.ActionUpdate, nullStr + case map[string]interface{}: //TODO: not sure if this is necessary + return visitor.ActionUpdate, nullStr + } + return visitor.ActionNoChange, nil + }, // Directive "Directive": func(p visitor.VisitFuncParams) (string, interface{}) { diff --git a/scalars.go b/scalars.go index 45479b54..fb97ae89 100644 --- a/scalars.go +++ b/scalars.go @@ -15,6 +15,9 @@ import ( // n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because // they are internally represented as IEEE 754 doubles. func coerceInt(value interface{}) interface{} { + if value == nil { + return nil + } switch value := value.(type) { case bool: if value == true { @@ -162,12 +165,17 @@ var Int = NewScalar(ScalarConfig{ if intValue, err := strconv.Atoi(valueAST.Value); err == nil { return intValue } + case *ast.NullValue: + return nil } return nil }, }) func coerceFloat(value interface{}) interface{} { + if value == nil { + return nil + } switch value := value.(type) { case bool: if value == true { @@ -299,12 +307,17 @@ var Float = NewScalar(ScalarConfig{ if floatValue, err := strconv.ParseFloat(valueAST.Value, 64); err == nil { return floatValue } + case *ast.NullValue: + return nil } return nil }, }) func coerceString(value interface{}) interface{} { + if value == nil { + return nil + } if v, ok := value.(*string); ok { if v == nil { return nil @@ -326,12 +339,17 @@ var String = NewScalar(ScalarConfig{ switch valueAST := valueAST.(type) { case *ast.StringValue: return valueAST.Value + case *ast.NullValue: + return nil } return nil }, }) func coerceBool(value interface{}) interface{} { + if value == nil { + return nil + } switch value := value.(type) { case bool: return value @@ -485,6 +503,8 @@ var Boolean = NewScalar(ScalarConfig{ switch valueAST := valueAST.(type) { case *ast.BooleanValue: return valueAST.Value + case *ast.NullValue: + return nil } return nil }, @@ -506,12 +526,17 @@ var ID = NewScalar(ScalarConfig{ return valueAST.Value case *ast.StringValue: return valueAST.Value + case *ast.NullValue: + return nil } return nil }, }) func serializeDateTime(value interface{}) interface{} { + if value == nil { + return nil + } switch value := value.(type) { case time.Time: buff, err := value.MarshalText() @@ -531,6 +556,9 @@ func serializeDateTime(value interface{}) interface{} { } func unserializeDateTime(value interface{}) interface{} { + if value == nil { + return nil + } switch value := value.(type) { case []byte: t := time.Time{} @@ -564,6 +592,8 @@ var DateTime = NewScalar(ScalarConfig{ switch valueAST := valueAST.(type) { case *ast.StringValue: return unserializeDateTime(valueAST.Value) + case *ast.NullValue: + return nil } return nil },