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

feat: Add null support #6

Merged
merged 1 commit into from
Sep 13, 2022
Merged
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
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
28 changes: 28 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,33 @@ func (v *BooleanValue) GetValue() interface{} {
return v.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
14 changes: 11 additions & 3 deletions language/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,15 +614,23 @@ 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.NewEnumValue(&ast.EnumValue{
Value: token.Value,
return ast.NewNullValue(&ast.NullValue{
Value: nil,
Loc: loc(parser, token.Start),
}), nil
}

if err := advance(parser); err != nil {
return nil, err
}
return ast.NewEnumValue(&ast.EnumValue{
Value: token.Value,
Loc: loc(parser, token.Start),
}), nil
case lexer.DOLLAR:
if !isConst {
return parseVariable(parser)
Expand Down
10 changes: 1 addition & 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 Expand Up @@ -367,6 +358,7 @@ func TestAllowsNonKeywordsAnywhereNameIsAllowed(t *testing.T) {
"subscription",
"true",
"false",
"null",
}
for _, keyword := range nonKeywords {
fragmentName := keyword
Expand Down
9 changes: 9 additions & 0 deletions language/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,15 @@ var printDocASTReducer = map[string]visitor.VisitFunc{
}
return visitor.ActionNoChange, nil
},
"NullValue": func(p visitor.VisitFuncParams) (string, interface{}) {
switch node := p.Node.(type) {
case *ast.NullValue:
return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value)
case map[string]interface{}:
return visitor.ActionUpdate, getMapValueString(node, "Value")
}
return visitor.ActionNoChange, nil
},
"EnumValue": func(p visitor.VisitFuncParams) (string, interface{}) {
switch node := p.Node.(type) {
case *ast.EnumValue:
Expand Down
6 changes: 5 additions & 1 deletion rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,10 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
return true, nil
}

if valueAST.GetKind() == kinds.NullValue {
return true, nil
}

// This function only tests literals, and assumes variables will provide
// values of the correct type.
if valueAST.GetKind() == kinds.Variable {
Expand All @@ -1742,7 +1746,7 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) {
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
63 changes: 47 additions & 16 deletions values.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import (
"github.com/graphql-go/graphql/language/printer"
)

// Used to detect the difference between a "null" literal and not present
type nullValue struct{}

// Prepares an object map of variableValues of the correct type based on the
// provided variable definitions and arbitrary input. If the input cannot be
// parsed to match the variable definitions, a GraphQLError will be returned.
Expand All @@ -27,7 +30,7 @@ func getVariableValues(
continue
}
varName := defAST.Variable.Name.Value
if varValue, err := getVariableValue(schema, defAST, inputs[varName]); err != nil {
if varValue, err := getVariableValue(schema, defAST, getValueOrNull(inputs, varName)); err != nil {
return values, err
} else {
values[varName] = varValue
Expand All @@ -36,6 +39,25 @@ func getVariableValues(
return values, nil
}

func getValueOrNull(values map[string]interface{}, name string) interface{} {
if tmp, ok := values[name]; ok { // Is present
if tmp == nil {
return nullValue{} // Null value
} else {
return tmp
}
Comment on lines +44 to +48

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: No else needed here since if ends with a return.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be the norm in this repo, will leave as is unless we decide to take full ownership of the project

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's copy pasted code I'll agree but if it's our code I would change it to the idiomatic form.

}
return nil // Not present
}

func addValueOrNull(values map[string]interface{}, name string, value interface{}) {
fredcarle marked this conversation as resolved.
Show resolved Hide resolved
if _, ok := value.(nullValue); ok { // Null value
values[name] = nil
} else if !isNullish(value) { // Not present
values[name] = value
}
}

// Prepares an object map of argument values given a list of argument
// definitions and list of argument AST nodes.
func getArgumentValues(
Expand All @@ -60,9 +82,7 @@ func getArgumentValues(
if tmp = valueFromAST(value, argDef.Type, variableValues); isNullish(tmp) {
tmp = argDef.DefaultValue
}
if !isNullish(tmp) {
results[argDef.PrivateName] = tmp
}
addValueOrNull(results, argDef.PrivateName, tmp)
}
return results
}
Expand Down Expand Up @@ -97,7 +117,7 @@ func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, inpu
}
return coerceValue(ttype, input), nil
}
if isNullish(input) {
if _, ok := input.(nullValue); ok || isNullish(input) {
return "", gqlerrors.NewError(
fmt.Sprintf(`Variable "$%v" of required type `+
`"%v" was not provided.`, variable.Name.Value, printer.Print(definitionAST.Type)),
Expand Down Expand Up @@ -134,6 +154,11 @@ func coerceValue(ttype Input, value interface{}) interface{} {
if isNullish(value) {
return nil
}

if _, ok := value.(nullValue); ok {
return nullValue{}
}

switch ttype := ttype.(type) {
case *NonNull:
return coerceValue(ttype.OfType, value)
Expand All @@ -156,13 +181,11 @@ func coerceValue(ttype Input, value interface{}) interface{} {
}

for name, field := range ttype.Fields() {
fieldValue := coerceValue(field.Type, valueMap[name])
fieldValue := coerceValue(field.Type, getValueOrNull(valueMap, name))
if isNullish(fieldValue) {
fieldValue = field.DefaultValue
}
if !isNullish(fieldValue) {
obj[name] = fieldValue
}
addValueOrNull(obj, name, fieldValue)
}
return obj
case *Scalar:
Expand Down Expand Up @@ -212,7 +235,7 @@ func typeFromAST(schema Schema, inputTypeAST ast.Type) (Type, error) {
// accepted for that type. This is primarily useful for validating the
// runtime values of query variables.
func isValidInputValue(value interface{}, ttype Input) (bool, []string) {
if isNullish(value) {
if _, ok := value.(nullValue); ok || isNullish(value) {
if ttype, ok := ttype.(*NonNull); ok {
if ttype.OfType.Name() != "" {
return false, []string{fmt.Sprintf(`Expected "%v!", found null.`, ttype.OfType.Name())}
Expand All @@ -233,9 +256,14 @@ func isValidInputValue(value interface{}, ttype Input) (bool, []string) {
messagesReduce := []string{}
for i := 0; i < valType.Len(); i++ {
val := valType.Index(i).Interface()
_, messages := isValidInputValue(val, ttype.OfType)
for idx, message := range messages {
messagesReduce = append(messagesReduce, fmt.Sprintf(`In element #%v: %v`, idx+1, message))
var messages []string
if _, ok := val.(nullValue); ok {
messages = []string{"Unexpected null value."}
fredcarle marked this conversation as resolved.
Show resolved Hide resolved
} else {
_, messages = isValidInputValue(val, ttype.OfType)
}
for _, message := range messages {
messagesReduce = append(messagesReduce, fmt.Sprintf(`In element #%v: %v`, i+1, message))
}
}
return (len(messagesReduce) == 0), messagesReduce
Expand Down Expand Up @@ -352,6 +380,11 @@ func valueFromAST(valueAST ast.Value, ttype Input, variables map[string]interfac
if valueAST == nil {
return nil
}

if valueAST.GetKind() == kinds.NullValue {
return nullValue{}
}

// precedence: value > type
if valueAST, ok := valueAST.(*ast.Variable); ok {
if valueAST.Name == nil || variables == nil {
Expand Down Expand Up @@ -398,9 +431,7 @@ func valueFromAST(valueAST ast.Value, ttype Input, variables map[string]interfac
} else {
value = field.DefaultValue
}
if !isNullish(value) {
obj[name] = value
}
addValueOrNull(obj, name, value)
}
return obj
case *Scalar:
Expand Down
6 changes: 3 additions & 3 deletions variables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ var testNestedInputObject *graphql.InputObject = graphql.NewInputObject(graphql.

func inputResolved(p graphql.ResolveParams) (interface{}, error) {
input, ok := p.Args["input"]
if !ok {
if !ok || input == nil {
return nil, nil
}
b, err := json.Marshal(input)
Expand Down Expand Up @@ -1188,7 +1188,7 @@ func TestVariables_ListsAndNullability_DoesNotAllowListOfNonNullsToContainNull(t
{
Message: `Variable "$input" got invalid value ` +
`["A",null,"B"].` +
"\nIn element #1: Expected \"String!\", found null.",
"\nIn element #2: Expected \"String!\", found null.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do you know why was this saying element #1 was null before?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no idea

Locations: []location.SourceLocation{
{
Line: 2, Column: 17,
Expand Down Expand Up @@ -1290,7 +1290,7 @@ func TestVariables_ListsAndNullability_DoesNotAllowNonNullListOfNonNullsToContai
{
Message: `Variable "$input" got invalid value ` +
`["A",null,"B"].` +
"\nIn element #1: Expected \"String!\", found null.",
"\nIn element #2: Expected \"String!\", found null.",
Locations: []location.SourceLocation{
{
Line: 2, Column: 17,
Expand Down