diff --git a/internal/pkg/unsafebytes/unsafebytes.go b/internal/pkg/unsafebytes/unsafebytes.go index c91cebedd7..a28207985f 100644 --- a/internal/pkg/unsafebytes/unsafebytes.go +++ b/internal/pkg/unsafebytes/unsafebytes.go @@ -51,6 +51,11 @@ func BytesIsValidInt64(byteSlice []byte) bool { return err == nil } +func BytesIsValidInt32(byteSlice []byte) bool { + _, err := strconv.ParseInt(*(*string)(unsafe.Pointer(&byteSlice)), 10, 32) + return err == nil +} + func BytesIsValidBool(byteSlice []byte) bool { _, err := strconv.ParseBool(*(*string)(unsafe.Pointer(&byteSlice))) return err == nil diff --git a/internal/pkg/unsafebytes/unsafebytes_test.go b/internal/pkg/unsafebytes/unsafebytes_test.go index e9e7799a5f..ca4e8c9116 100644 --- a/internal/pkg/unsafebytes/unsafebytes_test.go +++ b/internal/pkg/unsafebytes/unsafebytes_test.go @@ -79,9 +79,20 @@ func TestBytesIsValidFloat32(t *testing.T) { func TestBytesIsValidInt64(t *testing.T) { t.Run("valid int", testValidation(BytesIsValidInt64, []byte("123"), true)) - t.Run("invalid int", testValidation(BytesIsValidInt64, []byte("1.23"), false)) - t.Run("invalid int", testValidation(BytesIsValidInt64, []byte("true"), false)) - t.Run("invalid int", testValidation(BytesIsValidInt64, []byte("\"123\""), false)) + t.Run("valid big int", testValidation(BytesIsValidInt64, []byte("8293842938492834982"), true)) + t.Run("invalid very big int", testValidation(BytesIsValidInt64, []byte("8293842938492834982394"), false)) + t.Run("invalid float", testValidation(BytesIsValidInt64, []byte("1.23"), false)) + t.Run("invalid bool", testValidation(BytesIsValidInt64, []byte("true"), false)) + t.Run("invalid quoted int", testValidation(BytesIsValidInt64, []byte("\"123\""), false)) +} + +func TestBytesIsValidInt32(t *testing.T) { + t.Run("valid int", testValidation(BytesIsValidInt32, []byte("123"), true)) + t.Run("invalid valid big int", testValidation(BytesIsValidInt32, []byte("8293842938492834982"), false)) + t.Run("invalid very big int", testValidation(BytesIsValidInt32, []byte("829384293849283498239482938"), false)) + t.Run("invalid float", testValidation(BytesIsValidInt32, []byte("1.23"), false)) + t.Run("invalid bool", testValidation(BytesIsValidInt32, []byte("true"), false)) + t.Run("invalid quoted int", testValidation(BytesIsValidInt32, []byte("\"123\""), false)) } func TestBytesIsValidBool(t *testing.T) { diff --git a/pkg/ast/ast_argument.go b/pkg/ast/ast_argument.go index d5f7ea8e06..1b2dfe179f 100644 --- a/pkg/ast/ast_argument.go +++ b/pkg/ast/ast_argument.go @@ -16,9 +16,10 @@ type ArgumentList struct { } type Argument struct { - Name ByteSliceReference // e.g. foo - Colon position.Position // : - Value Value // e.g. 100 or "Bar" + Name ByteSliceReference // e.g. foo + Colon position.Position // : + Value Value // e.g. 100 or "Bar" + Position position.Position } func (d *Document) CopyArgument(ref int) int { diff --git a/pkg/ast/ast_object_field.go b/pkg/ast/ast_object_field.go index a628d6701f..b59c4f7c7a 100644 --- a/pkg/ast/ast_object_field.go +++ b/pkg/ast/ast_object_field.go @@ -11,9 +11,10 @@ import ( // example: // lon: 12.43 type ObjectField struct { - Name ByteSliceReference // e.g. lon - Colon position.Position // : - Value Value // e.g. 12.43 + Name ByteSliceReference // e.g. lon + Colon position.Position // : + Value Value // e.g. 12.43 + Position position.Position } func (d *Document) CopyObjectField(ref int) int { diff --git a/pkg/ast/ast_type.go b/pkg/ast/ast_type.go index 40438d407a..dfb89efa41 100644 --- a/pkg/ast/ast_type.go +++ b/pkg/ast/ast_type.go @@ -21,9 +21,10 @@ const ( type Type struct { TypeKind TypeKind // one of Named,List,NonNull Name ByteSliceReference // e.g. String (only on NamedType) - Open position.Position // [ (only on ListType) - Close position.Position // ] (only on ListType) - Bang position.Position // ! (only on NonNullType) + Position position.Position + Open position.Position // [ (only on ListType) + Close position.Position // ] (only on ListType) + Bang position.Position // ! (only on NonNullType) OfType int } @@ -76,17 +77,18 @@ func (d *Document) AddType(t Type) (ref int) { return len(d.Types) - 1 } -func (d *Document) AddNamedTypeByNameRef(nameRef ByteSliceReference) (ref int) { +func (d *Document) AddNamedTypeWithPosition(nameRef ByteSliceReference, position position.Position) (ref int) { return d.AddType(Type{ TypeKind: TypeKindNamed, Name: nameRef, OfType: -1, + Position: position, }) } func (d *Document) AddNamedType(name []byte) (ref int) { nameRef := d.Input.AppendInputBytes(name) - return d.AddNamedTypeByNameRef(nameRef) + return d.AddNamedTypeWithPosition(nameRef, position.Position{}) } func (d *Document) AddListType(ofType int) (ref int) { @@ -99,18 +101,20 @@ func (d *Document) AddListTypeWithPosition(ofType int, open position.Position, c Open: open, Close: close, OfType: ofType, + Position: open, }) } func (d *Document) AddNonNullType(ofType int) (ref int) { - return d.AddNonNullTypeWithPosition(ofType, position.Position{}) + return d.AddNonNullTypeWithBangPosition(ofType, position.Position{}) } -func (d *Document) AddNonNullTypeWithPosition(ofType int, bang position.Position) (ref int) { +func (d *Document) AddNonNullTypeWithBangPosition(ofType int, bang position.Position) (ref int) { return d.AddType(Type{ TypeKind: TypeKindNonNull, Bang: bang, OfType: ofType, + Position: d.Types[ofType].Position, }) } @@ -216,8 +220,7 @@ func (d *Document) TypesAreCompatibleDeep(left int, right int) bool { func (d *Document) ResolveTypeNameBytes(ref int) ByteSlice { resolvedTypeRef := d.ResolveUnderlyingType(ref) - graphqlType := d.Types[resolvedTypeRef] - return d.Input.ByteSlice(graphqlType.Name) + return d.TypeNameBytes(resolvedTypeRef) } func (d *Document) ResolveTypeNameString(ref int) string { diff --git a/pkg/ast/ast_val_int_value.go b/pkg/ast/ast_val_int_value.go index f20ca0fec2..7b88a761d7 100644 --- a/pkg/ast/ast_val_int_value.go +++ b/pkg/ast/ast_val_int_value.go @@ -41,6 +41,11 @@ func (d *Document) IntValueAsInt32(ref int) (out int32) { return } +func (d *Document) IntValueValidInt32(ref int) bool { + in := d.Input.ByteSlice(d.IntValues[ref].Raw) + return unsafebytes.BytesIsValidInt32(in) +} + func (d *Document) IntValue(ref int) IntValue { return d.IntValues[ref] } diff --git a/pkg/ast/ast_value.go b/pkg/ast/ast_value.go index 73bdfe3dab..8ac0298b4d 100644 --- a/pkg/ast/ast_value.go +++ b/pkg/ast/ast_value.go @@ -10,6 +10,7 @@ import ( "github.com/wundergraph/graphql-go-tools/internal/pkg/quotes" "github.com/wundergraph/graphql-go-tools/internal/pkg/unsafebytes" "github.com/wundergraph/graphql-go-tools/pkg/lexer/literal" + "github.com/wundergraph/graphql-go-tools/pkg/lexer/position" ) type ValueKind int @@ -28,8 +29,9 @@ const ( ) type Value struct { - Kind ValueKind // e.g. 100 or "Bar" - Ref int + Kind ValueKind // e.g. 100 or "Bar" + Ref int + Position position.Position } func (d *Document) CopyValue(ref int) int { diff --git a/pkg/astparser/parser.go b/pkg/astparser/parser.go index b0854c6c23..edbb24d0d4 100644 --- a/pkg/astparser/parser.go +++ b/pkg/astparser/parser.go @@ -280,7 +280,7 @@ func (p *Parser) parseRootOperationTypeDefinitionList(list *ast.RootOperationTyp NamedType: ast.Type{ TypeKind: ast.TypeKindNamed, Name: namedType.Literal, - OfType: -1, + OfType: ast.InvalidRef, }, } @@ -389,9 +389,10 @@ Loop: value := p.ParseValue() argument := ast.Argument{ - Name: name.Literal, - Colon: colon.TextPosition, - Value: value, + Name: name.Literal, + Colon: colon.TextPosition, + Value: value, + Position: name.TextPosition, } p.document.Arguments = append(p.document.Arguments, argument) @@ -423,29 +424,29 @@ func (p *Parser) ParseValue() (value ast.Value) { switch next { case keyword.STRING, keyword.BLOCKSTRING: value.Kind = ast.ValueKindString - value.Ref = p.parseStringValue() + value.Ref, value.Position = p.parseStringValue() case keyword.IDENT: key := p.identKeywordSliceRef(literal) switch key { case identkeyword.TRUE, identkeyword.FALSE: value.Kind = ast.ValueKindBoolean - value.Ref = p.parseBooleanValue() + value.Ref, value.Position = p.parseBooleanValue() case identkeyword.NULL: value.Kind = ast.ValueKindNull - p.read() + value.Position = p.read().TextPosition default: value.Kind = ast.ValueKindEnum - value.Ref = p.parseEnumValue() + value.Ref, value.Position = p.parseEnumValue() } case keyword.DOLLAR: value.Kind = ast.ValueKindVariable - value.Ref = p.parseVariableValue() + value.Ref, value.Position = p.parseVariableValue() case keyword.INTEGER: value.Kind = ast.ValueKindInteger - value.Ref = p.parseIntegerValue(nil) + value.Ref, value.Position = p.parseIntegerValue(nil) case keyword.FLOAT: value.Kind = ast.ValueKindFloat - value.Ref = p.parseFloatValue(nil) + value.Ref, value.Position = p.parseFloatValue(nil) case keyword.SUB: value = p.parseNegativeNumberValue() case keyword.LBRACK: @@ -453,7 +454,7 @@ func (p *Parser) ParseValue() (value ast.Value) { value.Ref = p.parseValueList() case keyword.LBRACE: value.Kind = ast.ValueKindObject - value.Ref = p.parseObjectValue() + value.Ref, value.Position = p.parseObjectValue() default: p.errUnexpectedToken(p.read()) } @@ -461,7 +462,7 @@ func (p *Parser) ParseValue() (value ast.Value) { return } -func (p *Parser) parseObjectValue() int { +func (p *Parser) parseObjectValue() (ref int, pos position.Position) { var objectValue ast.ObjectValue objectValue.LBRACE = p.mustRead(keyword.LBRACE).TextPosition @@ -470,8 +471,7 @@ func (p *Parser) parseObjectValue() int { switch next { case keyword.RBRACE: objectValue.RBRACE = p.read().TextPosition - p.document.ObjectValues = append(p.document.ObjectValues, objectValue) - return len(p.document.ObjectValues) - 1 + return p.document.AddObjectValue(objectValue), objectValue.LBRACE case keyword.IDENT: ref := p.parseObjectField() if cap(objectValue.Refs) == 0 { @@ -480,23 +480,26 @@ func (p *Parser) parseObjectValue() int { objectValue.Refs = append(objectValue.Refs, ref) default: p.errUnexpectedToken(p.read(), keyword.IDENT, keyword.RBRACE) - return -1 + return ast.InvalidRef, position.Position{} } if p.report.HasErrors() { - return -1 + return ast.InvalidRef, position.Position{} } } } func (p *Parser) parseObjectField() int { + nameToken := p.mustRead(keyword.IDENT) + objectField := ast.ObjectField{ - Name: p.mustRead(keyword.IDENT).Literal, - Colon: p.mustRead(keyword.COLON).TextPosition, - Value: p.ParseValue(), + Name: nameToken.Literal, + Colon: p.mustRead(keyword.COLON).TextPosition, + Value: p.ParseValue(), + Position: nameToken.TextPosition, } - p.document.ObjectFields = append(p.document.ObjectFields, objectField) - return len(p.document.ObjectFields) - 1 + + return p.document.AddObjectField(objectField) } func (p *Parser) parseValueList() int { @@ -521,7 +524,7 @@ func (p *Parser) parseValueList() int { } if p.report.HasErrors() { - return -1 + return ast.InvalidRef } } } @@ -531,10 +534,12 @@ func (p *Parser) parseNegativeNumberValue() (value ast.Value) { switch p.peek() { case keyword.INTEGER: value.Kind = ast.ValueKindInteger - value.Ref = p.parseIntegerValue(&negativeSign) + value.Ref, _ = p.parseIntegerValue(&negativeSign) + value.Position = negativeSign case keyword.FLOAT: value.Kind = ast.ValueKindFloat - value.Ref = p.parseFloatValue(&negativeSign) + value.Ref, _ = p.parseFloatValue(&negativeSign) + value.Position = negativeSign default: p.errUnexpectedToken(p.read(), keyword.INTEGER, keyword.FLOAT) } @@ -542,13 +547,13 @@ func (p *Parser) parseNegativeNumberValue() (value ast.Value) { return } -func (p *Parser) parseFloatValue(negativeSign *position.Position) int { +func (p *Parser) parseFloatValue(negativeSign *position.Position) (ref int, pos position.Position) { value := p.mustRead(keyword.FLOAT) if negativeSign != nil && negativeSign.CharEnd != value.TextPosition.CharStart { p.errUnexpectedToken(value) - return -1 + return ast.InvalidRef, position.Position{} } floatValue := ast.FloatValue{ @@ -559,17 +564,16 @@ func (p *Parser) parseFloatValue(negativeSign *position.Position) int { floatValue.NegativeSign = *negativeSign } - p.document.FloatValues = append(p.document.FloatValues, floatValue) - return len(p.document.FloatValues) - 1 + return p.document.AddFloatValue(floatValue), value.TextPosition } -func (p *Parser) parseIntegerValue(negativeSign *position.Position) int { +func (p *Parser) parseIntegerValue(negativeSign *position.Position) (ref int, pos position.Position) { value := p.mustRead(keyword.INTEGER) if negativeSign != nil && negativeSign.CharEnd != value.TextPosition.CharStart { p.errUnexpectedToken(value) - return -1 + return ast.InvalidRef, position.Position{} } intValue := ast.IntValue{ @@ -581,10 +585,10 @@ func (p *Parser) parseIntegerValue(negativeSign *position.Position) int { } p.document.IntValues = append(p.document.IntValues, intValue) - return len(p.document.IntValues) - 1 + return len(p.document.IntValues) - 1, value.TextPosition } -func (p *Parser) parseVariableValue() int { +func (p *Parser) parseVariableValue() (ref int, pos position.Position) { dollar := p.mustRead(keyword.DOLLAR) var value token.Token @@ -594,12 +598,12 @@ func (p *Parser) parseVariableValue() int { value = p.read() default: p.errUnexpectedToken(p.read(), keyword.IDENT) - return -1 + return ast.InvalidRef, position.Position{} } if dollar.TextPosition.CharEnd != value.TextPosition.CharStart { p.errUnexpectedToken(p.read(), keyword.IDENT) - return -1 + return ast.InvalidRef, position.Position{} } variable := ast.VariableValue{ @@ -608,43 +612,45 @@ func (p *Parser) parseVariableValue() int { } p.document.VariableValues = append(p.document.VariableValues, variable) - return len(p.document.VariableValues) - 1 + return len(p.document.VariableValues) - 1, dollar.TextPosition } -func (p *Parser) parseBooleanValue() int { +func (p *Parser) parseBooleanValue() (ref int, pos position.Position) { value := p.read() identKey := p.identKeywordToken(value) switch identKey { case identkeyword.FALSE: - return 0 + return 0, value.TextPosition case identkeyword.TRUE: - return 1 + return 1, value.TextPosition default: p.errUnexpectedIdentKey(value, identKey, identkeyword.TRUE, identkeyword.FALSE) - return -1 + return ast.InvalidRef, position.Position{} } } -func (p *Parser) parseEnumValue() int { +func (p *Parser) parseEnumValue() (ref int, pos position.Position) { + value := p.mustRead(keyword.IDENT) + enum := ast.EnumValue{ - Name: p.mustRead(keyword.IDENT).Literal, + Name: value.Literal, } - p.document.EnumValues = append(p.document.EnumValues, enum) - return len(p.document.EnumValues) - 1 + + return p.document.AddEnumValue(enum), value.TextPosition } -func (p *Parser) parseStringValue() int { +func (p *Parser) parseStringValue() (ref int, pos position.Position) { value := p.read() if value.Keyword != keyword.STRING && value.Keyword != keyword.BLOCKSTRING { p.errUnexpectedToken(value, keyword.STRING, keyword.BLOCKSTRING) - return -1 + return ast.InvalidRef, position.Position{} } stringValue := ast.StringValue{ Content: value.Literal, BlockString: value.Keyword == keyword.BLOCKSTRING, } - p.document.StringValues = append(p.document.StringValues, stringValue) - return len(p.document.StringValues) - 1 + + return p.document.AddStringValue(stringValue), value.TextPosition } func (p *Parser) parseObjectTypeDefinition(description *ast.Description) { @@ -738,7 +744,7 @@ func (p *Parser) parseImplementsInterfaces() (list ast.TypeList) { acceptIdent = false acceptAnd = true name := p.read() - ref := p.document.AddNamedTypeByNameRef(name.Literal) + ref := p.document.AddNamedTypeWithPosition(name.Literal, name.TextPosition) if cap(list.Refs) == 0 { list.Refs = p.document.Refs[p.document.NextRefIndex()][:0] } @@ -804,13 +810,13 @@ func (p *Parser) parseFieldDefinition() int { break default: p.errUnexpectedToken(p.read()) - return -1 + return ast.InvalidRef } nameToken := p.read() if nameToken.Keyword != keyword.IDENT { p.errUnexpectedToken(nameToken, keyword.IDENT) - return -1 + return ast.InvalidRef } fieldDefinition.Name = nameToken.Literal @@ -832,7 +838,7 @@ func (p *Parser) parseFieldDefinition() int { func (p *Parser) parseNamedType() (ref int) { ident := p.mustRead(keyword.IDENT) - return p.document.AddNamedTypeByNameRef(ident.Literal) + return p.document.AddNamedTypeWithPosition(ident.Literal, ident.TextPosition) } func (p *Parser) ParseType() (ref int) { @@ -840,7 +846,8 @@ func (p *Parser) ParseType() (ref int) { first := p.peek() if first == keyword.IDENT { - ref = p.document.AddNamedTypeByNameRef(p.read().Literal) + tok := p.read() + ref = p.document.AddNamedTypeWithPosition(tok.Literal, tok.TextPosition) } else if first == keyword.LBRACK { openList := p.read() @@ -861,7 +868,7 @@ func (p *Parser) ParseType() (ref int) { return } - ref = p.document.AddNonNullTypeWithPosition(ref, bangPosition) + ref = p.document.AddNonNullTypeWithBangPosition(ref, bangPosition) } return @@ -916,7 +923,7 @@ func (p *Parser) parseInputValueDefinition() int { break default: p.errUnexpectedToken(p.read()) - return -1 + return ast.InvalidRef } inputValueDefinition.Name = p.read().Literal @@ -1072,7 +1079,7 @@ func (p *Parser) parseUnionMemberTypes() (list ast.TypeList) { ident := p.read() - ref := p.document.AddNamedTypeByNameRef(ident.Literal) + ref := p.document.AddNamedTypeWithPosition(ident.Literal, ident.TextPosition) if cap(list.Refs) == 0 { list.Refs = p.document.Refs[p.document.NextRefIndex()][:0] @@ -1158,7 +1165,7 @@ func (p *Parser) parseEnumValueDefinition() int { break default: p.errUnexpectedToken(p.read()) - return -1 + return ast.InvalidRef } enumValueDefinition.EnumValue = p.mustRead(keyword.IDENT).Literal @@ -1293,7 +1300,7 @@ func (p *Parser) parseSelectionSet() (int, bool) { } if p.report.HasErrors() { - return -1, false + return ast.InvalidRef, false } } } @@ -1313,7 +1320,7 @@ func (p *Parser) parseSelection() int { default: nextToken := p.read() p.errUnexpectedToken(nextToken, keyword.IDENT, keyword.SPREAD) - return -1 + return ast.InvalidRef } } @@ -1397,7 +1404,7 @@ func (p *Parser) parseFragmentSpread(spread position.Position) int { func (p *Parser) parseInlineFragment(spread position.Position) int { fragment := ast.InlineFragment{ TypeCondition: ast.TypeCondition{ - Type: -1, + Type: ast.InvalidRef, }, } fragment.Spread = spread @@ -1517,7 +1524,7 @@ func (p *Parser) parseVariableDefinition() int { var variableDefinition ast.VariableDefinition variableDefinition.VariableValue.Kind = ast.ValueKindVariable - variableDefinition.VariableValue.Ref = p.parseVariableValue() + variableDefinition.VariableValue.Ref, variableDefinition.VariableValue.Position = p.parseVariableValue() variableDefinition.Colon = p.mustRead(keyword.COLON).TextPosition variableDefinition.Type = p.ParseType() diff --git a/pkg/astvalidation/operation_rule_fragments.go b/pkg/astvalidation/operation_rule_fragments.go index 5e2c5f07aa..791611727c 100644 --- a/pkg/astvalidation/operation_rule_fragments.go +++ b/pkg/astvalidation/operation_rule_fragments.go @@ -65,7 +65,9 @@ func (f *fragmentsVisitor) EnterInlineFragment(ref int) { node, exists := f.definition.Index.FirstNonExtensionNodeByNameBytes(typeName) if !exists { - f.StopWithExternalErr(operationreport.ErrTypeUndefined(typeName)) + typePosition := f.operation.Types[f.operation.InlineFragments[ref].TypeCondition.Type].Position + f.Report.AddExternalError(operationreport.ErrUnknownType(typeName, typePosition)) + f.SkipNode() // skipping node cause otherwise visitor will not be able to get enclosing type and will stop with error but error is already added here return } @@ -94,7 +96,8 @@ func (f *fragmentsVisitor) EnterFragmentDefinition(ref int) { node, exists := f.definition.Index.FirstNodeByNameBytes(typeName) if !exists { - f.StopWithExternalErr(operationreport.ErrTypeUndefined(typeName)) + typePosition := f.operation.Types[f.operation.FragmentDefinitions[ref].TypeCondition.Type].Position + f.StopWithExternalErr(operationreport.ErrUnknownType(typeName, typePosition)) return } diff --git a/pkg/astvalidation/operation_rule_known_arguments.go b/pkg/astvalidation/operation_rule_known_arguments.go new file mode 100644 index 0000000000..c80fce90aa --- /dev/null +++ b/pkg/astvalidation/operation_rule_known_arguments.go @@ -0,0 +1,62 @@ +package astvalidation + +import ( + "github.com/wundergraph/graphql-go-tools/pkg/ast" + "github.com/wundergraph/graphql-go-tools/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/pkg/operationreport" +) + +// KnownArguments validates if all arguments are known +func KnownArguments() Rule { + return func(walker *astvisitor.Walker) { + visitor := knownArgumentsVisitor{ + Walker: walker, + } + walker.RegisterEnterDocumentVisitor(&visitor) + walker.RegisterEnterArgumentVisitor(&visitor) + walker.RegisterEnterFieldVisitor(&visitor) + } +} + +type knownArgumentsVisitor struct { + *astvisitor.Walker + operation, definition *ast.Document + enclosingNode ast.Node +} + +func (v *knownArgumentsVisitor) EnterField(ref int) { + _, exists := v.FieldDefinition(ref) + if !exists { + v.SkipNode() // ignore arguments of not existing fields + return + } + + v.enclosingNode = v.EnclosingTypeDefinition +} + +func (v *knownArgumentsVisitor) EnterDocument(operation, definition *ast.Document) { + v.operation = operation + v.definition = definition +} + +func (v *knownArgumentsVisitor) EnterArgument(ref int) { + _, exists := v.ArgumentInputValueDefinition(ref) + if exists { + return + } + + ancestor := v.Ancestor() + ancestorName := v.AncestorNameBytes() + + argumentName := v.operation.ArgumentNameBytes(ref) + argumentPosition := v.operation.Arguments[ref].Position + + switch ancestor.Kind { + case ast.NodeKindField: + objectTypeDefName := v.definition.ObjectTypeDefinitionNameBytes(v.enclosingNode.Ref) + + v.Report.AddExternalError(operationreport.ErrArgumentNotDefinedOnField(argumentName, objectTypeDefName, ancestorName, argumentPosition)) + case ast.NodeKindDirective: + v.Report.AddExternalError(operationreport.ErrArgumentNotDefinedOnDirective(argumentName, ancestorName, argumentPosition)) + } +} diff --git a/pkg/astvalidation/operation_rule_valid_arguments.go b/pkg/astvalidation/operation_rule_valid_arguments.go index 0058a13d25..efdc91b975 100644 --- a/pkg/astvalidation/operation_rule_valid_arguments.go +++ b/pkg/astvalidation/operation_rule_valid_arguments.go @@ -6,11 +6,11 @@ import ( "github.com/wundergraph/graphql-go-tools/pkg/ast" "github.com/wundergraph/graphql-go-tools/pkg/astvisitor" - "github.com/wundergraph/graphql-go-tools/pkg/lexer/literal" "github.com/wundergraph/graphql-go-tools/pkg/operationreport" ) -// ValidArguments validates if arguments are valid +// ValidArguments validates if arguments are valid: values and variables has compatible types +// deep variables comparison is handled by Values func ValidArguments() Rule { return func(walker *astvisitor.Walker) { visitor := validArgumentsVisitor{ @@ -32,39 +32,35 @@ func (v *validArgumentsVisitor) EnterDocument(operation, definition *ast.Documen } func (v *validArgumentsVisitor) EnterArgument(ref int) { - definition, exists := v.ArgumentInputValueDefinition(ref) + definitionRef, exists := v.ArgumentInputValueDefinition(ref) if !exists { - argumentName := v.operation.ArgumentNameBytes(ref) - ancestorName := v.AncestorNameBytes() - v.StopWithExternalErr(operationreport.ErrArgumentNotDefinedOnNode(argumentName, ancestorName)) return } value := v.operation.ArgumentValue(ref) - v.validateIfValueSatisfiesInputFieldDefinition(value, definition) + v.validateIfValueSatisfiesInputFieldDefinition(value, definitionRef) } -func (v *validArgumentsVisitor) validateIfValueSatisfiesInputFieldDefinition(value ast.Value, inputValueDefinition int) { - var satisfied bool +func (v *validArgumentsVisitor) validateIfValueSatisfiesInputFieldDefinition(value ast.Value, inputValueDefinitionRef int) { + var ( + satisfied bool + operationTypeRef int + variableDefinitionRef int + ) switch value.Kind { case ast.ValueKindVariable: - satisfied = v.variableValueSatisfiesInputValueDefinition(value.Ref, inputValueDefinition) - case ast.ValueKindEnum: - satisfied = v.enumValueSatisfiesInputValueDefinition(value.Ref, inputValueDefinition) - case ast.ValueKindNull: - satisfied = v.nullValueSatisfiesInputValueDefinition(inputValueDefinition) - case ast.ValueKindBoolean: - satisfied = v.booleanValueSatisfiesInputValueDefinition(inputValueDefinition) - case ast.ValueKindInteger: - satisfied = v.intValueSatisfiesInputValueDefinition(value, inputValueDefinition) - case ast.ValueKindString: - satisfied = v.stringValueSatisfiesInputValueDefinition(value, inputValueDefinition) - case ast.ValueKindFloat: - satisfied = v.floatValueSatisfiesInputValueDefinition(value, inputValueDefinition) - case ast.ValueKindObject, ast.ValueKindList: - // object- and list values are covered by Values() / valuesVisitor + satisfied, operationTypeRef, variableDefinitionRef = v.variableValueSatisfiesInputValueDefinition(value.Ref, inputValueDefinitionRef) + case ast.ValueKindEnum, + ast.ValueKindNull, + ast.ValueKindBoolean, + ast.ValueKindInteger, + ast.ValueKindString, + ast.ValueKindFloat, + ast.ValueKindObject, + ast.ValueKindList: + // this types of values are covered by Values() / valuesVisitor return default: v.StopWithInternalErr(fmt.Errorf("validateIfValueSatisfiesInputFieldDefinition: not implemented for value.Kind: %s", value.Kind)) @@ -80,112 +76,59 @@ func (v *validArgumentsVisitor) validateIfValueSatisfiesInputFieldDefinition(val return } - typeRef := v.definition.InputValueDefinitionType(inputValueDefinition) + typeRef := v.definition.InputValueDefinitionType(inputValueDefinitionRef) + expectedTypeName, err := v.definition.PrintTypeBytes(typeRef, nil) + if v.HandleInternalErr(err) { + return + } - printedType, err := v.definition.PrintTypeBytes(typeRef, nil) + actualTypeName, err := v.operation.PrintTypeBytes(operationTypeRef, nil) if v.HandleInternalErr(err) { return } - v.StopWithExternalErr(operationreport.ErrValueDoesntSatisfyInputValueDefinition(printedValue, printedType)) + v.StopWithExternalErr(operationreport.ErrVariableTypeDoesntSatisfyInputValueDefinition(printedValue, actualTypeName, expectedTypeName, value.Position, v.operation.VariableDefinitions[variableDefinitionRef].VariableValue.Position)) } -func (v *validArgumentsVisitor) floatValueSatisfiesInputValueDefinition(value ast.Value, inputValueDefinition int) bool { - inputType := v.definition.Types[v.definition.InputValueDefinitionType(inputValueDefinition)] - if inputType.TypeKind == ast.TypeKindNonNull { - inputType = v.definition.Types[inputType.OfType] - } - if inputType.TypeKind != ast.TypeKindNamed { - return false - } - if !bytes.Equal(v.definition.Input.ByteSlice(inputType.Name), literal.FLOAT) { - return false +func (v *validArgumentsVisitor) variableValueSatisfiesInputValueDefinition(variableValue, inputValueDefinition int) (satisfies bool, operationTypeRef int, variableDefRef int) { + variableDefinitionRef, exists := v.variableDefinition(variableValue) + if !exists { + return false, ast.InvalidRef, variableDefinitionRef } - return true -} -func (v *validArgumentsVisitor) stringValueSatisfiesInputValueDefinition(value ast.Value, inputValueDefinition int) bool { - inputType := v.definition.Types[v.definition.InputValueDefinitionType(inputValueDefinition)] - if inputType.TypeKind == ast.TypeKindNonNull { - inputType = v.definition.Types[inputType.OfType] - } - if inputType.TypeKind != ast.TypeKindNamed { - return false - } + operationTypeRef = v.operation.VariableDefinitions[variableDefinitionRef].Type + definitionTypeRef := v.definition.InputValueDefinitions[inputValueDefinition].Type - inputTypeName := v.definition.Input.ByteSlice(inputType.Name) - if !bytes.Equal(inputTypeName, literal.STRING) && !bytes.Equal(inputTypeName, literal.ID) { - return false - } - return true -} + hasDefaultValue := v.validDefaultValue(v.operation.VariableDefinitions[variableDefinitionRef].DefaultValue) || + v.validDefaultValue(v.definition.InputValueDefinitions[inputValueDefinition].DefaultValue) -func (v *validArgumentsVisitor) intValueSatisfiesInputValueDefinition(value ast.Value, inputValueDefinition int) bool { - inputType := v.definition.Types[v.definition.InputValueDefinitionType(inputValueDefinition)] - if inputType.TypeKind == ast.TypeKindNonNull { - inputType = v.definition.Types[inputType.OfType] - } - if inputType.TypeKind != ast.TypeKindNamed { - return false - } - if !bytes.Equal(v.definition.Input.ByteSlice(inputType.Name), literal.INT) { - return false - } - return true + return v.operationTypeSatisfiesDefinitionType(operationTypeRef, definitionTypeRef, hasDefaultValue), operationTypeRef, variableDefinitionRef } -func (v *validArgumentsVisitor) booleanValueSatisfiesInputValueDefinition(inputValueDefinition int) bool { - inputType := v.definition.Types[v.definition.InputValueDefinitionType(inputValueDefinition)] - if inputType.TypeKind == ast.TypeKindNonNull { - inputType = v.definition.Types[inputType.OfType] - } - if inputType.TypeKind != ast.TypeKindNamed { - return false - } - if !bytes.Equal(v.definition.Input.ByteSlice(inputType.Name), literal.BOOLEAN) { - return false - } - return true -} +func (v *validArgumentsVisitor) variableDefinition(variableValueRef int) (ref int, exists bool) { + variableName := v.operation.VariableValueNameBytes(variableValueRef) -func (v *validArgumentsVisitor) nullValueSatisfiesInputValueDefinition(inputValueDefinition int) bool { - inputType := v.definition.Types[v.definition.InputValueDefinitionType(inputValueDefinition)] - return inputType.TypeKind != ast.TypeKindNonNull -} - -func (v *validArgumentsVisitor) enumValueSatisfiesInputValueDefinition(enumValue, inputValueDefinition int) bool { - definitionTypeName := v.definition.ResolveTypeNameBytes(v.definition.InputValueDefinitions[inputValueDefinition].Type) - node, exists := v.definition.Index.FirstNodeByNameBytes(definitionTypeName) - if !exists { - return false + if v.Ancestors[0].Kind == ast.NodeKindOperationDefinition { + return v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName) } - if node.Kind != ast.NodeKindEnumTypeDefinition { - return false + for opDefRef := 0; opDefRef < len(v.operation.OperationDefinitions); opDefRef++ { + ref, exists = v.operation.VariableDefinitionByNameAndOperation(opDefRef, variableName) + if exists { + return + } } - enumValueName := v.operation.Input.ByteSlice(v.operation.EnumValueName(enumValue)) - return v.definition.EnumTypeDefinitionContainsEnumValue(node.Ref, enumValueName) + return ast.InvalidRef, false } -func (v *validArgumentsVisitor) variableValueSatisfiesInputValueDefinition(variableValue, inputValueDefinition int) bool { - variableName := v.operation.VariableValueNameBytes(variableValue) - variableDefinition, exists := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName) - if !exists { - return false - } - - operationType := v.operation.VariableDefinitions[variableDefinition].Type - definitionType := v.definition.InputValueDefinitions[inputValueDefinition].Type - hasDefaultValue := v.operation.VariableDefinitions[variableDefinition].DefaultValue.IsDefined || - v.definition.InputValueDefinitions[inputValueDefinition].DefaultValue.IsDefined - - return v.operationTypeSatisfiesDefinitionType(operationType, definitionType, hasDefaultValue) +func (v *validArgumentsVisitor) validDefaultValue(value ast.DefaultValue) bool { + return value.IsDefined && value.Value.Kind != ast.ValueKindNull } -func (v *validArgumentsVisitor) operationTypeSatisfiesDefinitionType(operationType int, definitionType int, hasDefaultValue bool) bool { - opKind := v.operation.Types[operationType].TypeKind - defKind := v.definition.Types[definitionType].TypeKind +func (v *validArgumentsVisitor) operationTypeSatisfiesDefinitionType(operationTypeRef int, definitionTypeRef int, hasDefaultValue bool) bool { + opKind := v.operation.Types[operationTypeRef].TypeKind + defKind := v.definition.Types[definitionTypeRef].TypeKind // A nullable op type is compatible with a non-null def type if the def has // a default value. Strip the def non-null and continue comparing. This @@ -196,17 +139,17 @@ func (v *validArgumentsVisitor) operationTypeSatisfiesDefinitionType(operationTy // Op: someField(arg: Boolean): String // Def: someField(arg: Boolean! = false): String # Boolean! -> Boolean if opKind != ast.TypeKindNonNull && defKind == ast.TypeKindNonNull && hasDefaultValue { - definitionType = v.definition.Types[definitionType].OfType + definitionTypeRef = v.definition.Types[definitionTypeRef].OfType } // Unnest the op and def arg types until a named type is reached, // then compare. for { - if operationType == -1 || definitionType == -1 { + if operationTypeRef == -1 || definitionTypeRef == -1 { return false } - opKind = v.operation.Types[operationType].TypeKind - defKind = v.definition.Types[definitionType].TypeKind + opKind = v.operation.Types[operationTypeRef].TypeKind + defKind = v.definition.Types[definitionTypeRef].TypeKind // If the op arg type is stricter than the def arg type, that's okay. // Strip the op non-null and continue comparing. @@ -215,7 +158,7 @@ func (v *validArgumentsVisitor) operationTypeSatisfiesDefinitionType(operationTy // Op: someField(arg: Boolean!): String # Boolean! -> Boolean // Def: someField(arg: Boolean): String if opKind == ast.TypeKindNonNull && defKind != ast.TypeKindNonNull { - operationType = v.operation.Types[operationType].OfType + operationTypeRef = v.operation.Types[operationTypeRef].OfType continue } @@ -225,11 +168,12 @@ func (v *validArgumentsVisitor) operationTypeSatisfiesDefinitionType(operationTy if opKind == ast.TypeKindNamed { // defKind is also a named type because at this point both kinds // are the same! Compare the names. - return bytes.Equal(v.operation.Input.ByteSlice(v.operation.Types[operationType].Name), - v.definition.Input.ByteSlice(v.definition.Types[definitionType].Name)) + + return bytes.Equal(v.operation.Input.ByteSlice(v.operation.Types[operationTypeRef].Name), + v.definition.Input.ByteSlice(v.definition.Types[definitionTypeRef].Name)) } // Both types are non-null or list. Unnest and continue comparing. - operationType = v.operation.Types[operationType].OfType - definitionType = v.definition.Types[definitionType].OfType + operationTypeRef = v.operation.Types[operationTypeRef].OfType + definitionTypeRef = v.definition.Types[definitionTypeRef].OfType } } diff --git a/pkg/astvalidation/operation_rule_values.go b/pkg/astvalidation/operation_rule_values.go index d957523877..d17417913c 100644 --- a/pkg/astvalidation/operation_rule_values.go +++ b/pkg/astvalidation/operation_rule_values.go @@ -6,6 +6,7 @@ import ( "github.com/wundergraph/graphql-go-tools/pkg/ast" "github.com/wundergraph/graphql-go-tools/pkg/astimport" "github.com/wundergraph/graphql-go-tools/pkg/astvisitor" + "github.com/wundergraph/graphql-go-tools/pkg/lexer/literal" "github.com/wundergraph/graphql-go-tools/pkg/operationreport" ) @@ -17,6 +18,7 @@ func Values() Rule { } walker.RegisterEnterDocumentVisitor(&visitor) walker.RegisterEnterArgumentVisitor(&visitor) + walker.RegisterEnterVariableDefinitionVisitor(&visitor) } } @@ -31,14 +33,18 @@ func (v *valuesVisitor) EnterDocument(operation, definition *ast.Document) { v.definition = definition } +func (v *valuesVisitor) EnterVariableDefinition(ref int) { + if !v.operation.VariableDefinitionHasDefaultValue(ref) { + return // variable has no default value, deep type check not required + } + + v.valueSatisfiesOperationType(v.operation.VariableDefinitions[ref].DefaultValue.Value, v.operation.VariableDefinitions[ref].Type) +} + func (v *valuesVisitor) EnterArgument(ref int) { definition, exists := v.ArgumentInputValueDefinition(ref) - if !exists { - argName := v.operation.ArgumentNameBytes(ref) - nodeName := v.operation.NodeNameBytes(v.Ancestors[len(v.Ancestors)-1]) - v.StopWithExternalErr(operationreport.ErrArgumentNotDefinedOnNode(argName, nodeName)) return } @@ -57,183 +63,423 @@ func (v *valuesVisitor) EnterArgument(ref int) { value = v.operation.VariableDefinitions[variableDefinition].DefaultValue.Value } - if !v.valueSatisfiesInputValueDefinitionType(value, v.definition.InputValueDefinitions[definition].Type) { + v.valueSatisfiesInputValueDefinitionType(value, v.definition.InputValueDefinitions[definition].Type) +} - printedValue, err := v.operation.PrintValueBytes(value, nil) - if v.HandleInternalErr(err) { - return +func (v *valuesVisitor) valueSatisfiesOperationType(value ast.Value, operationTypeRef int) bool { + switch v.operation.Types[operationTypeRef].TypeKind { + case ast.TypeKindNonNull: + return v.valuesSatisfiesOperationNonNullType(value, operationTypeRef) + case ast.TypeKindNamed: + return v.valuesSatisfiesOperationNamedType(value, operationTypeRef) + case ast.TypeKindList: + return v.valueSatisfiesOperationListType(value, operationTypeRef, v.operation.Types[operationTypeRef].OfType) + default: + v.handleOperationTypeError(value, operationTypeRef) + return false + } +} + +func (v *valuesVisitor) valuesSatisfiesOperationNonNullType(value ast.Value, operationTypeRef int) bool { + if value.Kind == ast.ValueKindNull { + v.handleOperationUnexpectedNullError(value, operationTypeRef) + return false + } + return v.valueSatisfiesOperationType(value, v.operation.Types[operationTypeRef].OfType) +} + +func (v *valuesVisitor) valuesSatisfiesOperationNamedType(value ast.Value, operationTypeRef int) bool { + if value.Kind == ast.ValueKindNull { + // null always satisfies not required type + return true + } + + typeName := v.operation.ResolveTypeNameBytes(operationTypeRef) + node, exists := v.definition.Index.FirstNodeByNameBytes(typeName) + if !exists { + v.handleOperationTypeError(value, operationTypeRef) + return false + } + + definitionTypeRef := ast.InvalidRef + + for ref := 0; ref < len(v.definition.Types); ref++ { + if v.definition.Types[ref].TypeKind != ast.TypeKindNamed { + continue } - printedType, err := v.definition.PrintTypeBytes(v.definition.InputValueDefinitions[definition].Type, nil) - if v.HandleInternalErr(err) { - return + if bytes.Equal(v.definition.TypeNameBytes(ref), typeName) { + definitionTypeRef = ref + break } + } - v.StopWithExternalErr(operationreport.ErrValueDoesntSatisfyInputValueDefinition(printedValue, printedType)) - return + if definitionTypeRef == ast.InvalidRef { + // should not happen, as in case we have not found named type node we will report it earlier + return false } + + return v.valueSatisfiesTypeDefinitionNode(value, definitionTypeRef, node) } -func (v *valuesVisitor) valueSatisfiesInputValueDefinitionType(value ast.Value, definitionTypeRef int) bool { +func (v *valuesVisitor) valueSatisfiesOperationListType(value ast.Value, operationTypeRef int, listItemType int) bool { + if value.Kind == ast.ValueKindNull { + return true + } + + if value.Kind != ast.ValueKindList { + return v.valueSatisfiesOperationType(value, listItemType) + } + + if v.operation.Types[listItemType].TypeKind == ast.TypeKindNonNull { + if len(v.operation.ListValues[value.Ref].Refs) == 0 { + v.handleOperationTypeError(value, operationTypeRef) + return false + } + listItemType = v.operation.Types[listItemType].OfType + } + + valid := true + + for _, i := range v.operation.ListValues[value.Ref].Refs { + listValue := v.operation.Value(i) + if !v.valueSatisfiesOperationType(listValue, listItemType) { + valid = false + } + } + return valid +} + +func (v *valuesVisitor) valueSatisfiesInputValueDefinitionType(value ast.Value, definitionTypeRef int) bool { switch v.definition.Types[definitionTypeRef].TypeKind { case ast.TypeKindNonNull: - switch value.Kind { - case ast.ValueKindNull: + return v.valuesSatisfiesNonNullType(value, definitionTypeRef) + case ast.TypeKindNamed: + return v.valuesSatisfiesNamedType(value, definitionTypeRef) + case ast.TypeKindList: + return v.valueSatisfiesListType(value, definitionTypeRef, v.definition.Types[definitionTypeRef].OfType) + default: + v.handleTypeError(value, definitionTypeRef) + return false + } +} + +func (v *valuesVisitor) valuesSatisfiesNonNullType(value ast.Value, definitionTypeRef int) bool { + switch value.Kind { + case ast.ValueKindNull: + v.handleUnexpectedNullError(value, definitionTypeRef) + return false + case ast.ValueKindVariable: + variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(value.Ref) + if !ok { + v.handleTypeError(value, definitionTypeRef) return false - case ast.ValueKindVariable: - variableName := v.operation.VariableValueNameBytes(value.Ref) - variableDefinition, exists := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName) - if !exists { - return false - } - variableTypeRef := v.operation.VariableDefinitions[variableDefinition].Type - importedDefinitionType := v.importer.ImportType(definitionTypeRef, v.definition, v.operation) - if !v.operation.TypesAreEqualDeep(importedDefinitionType, variableTypeRef) { - return false - } } - return v.valueSatisfiesInputValueDefinitionType(value, v.definition.Types[definitionTypeRef].OfType) - case ast.TypeKindNamed: - typeName := v.definition.ResolveTypeNameBytes(definitionTypeRef) - node, exists := v.definition.Index.FirstNodeByNameBytes(typeName) - if !exists { + + if v.operation.VariableDefinitionHasDefaultValue(variableDefinitionRef) { + return v.valueSatisfiesInputValueDefinitionType(v.operation.VariableDefinitions[variableDefinitionRef].DefaultValue.Value, definitionTypeRef) + } + + importedDefinitionType := v.importer.ImportType(definitionTypeRef, v.definition, v.operation) + if !v.operation.TypesAreEqualDeep(importedDefinitionType, variableTypeRef) { + v.handleVariableHasIncompatibleTypeError(value, definitionTypeRef) return false } - return v.valueSatisfiesTypeDefinitionNode(value, node) - case ast.TypeKindList: - return v.valueSatisfiesListType(value, v.definition.Types[definitionTypeRef].OfType) - default: + return true + } + return v.valueSatisfiesInputValueDefinitionType(value, v.definition.Types[definitionTypeRef].OfType) +} + +func (v *valuesVisitor) valuesSatisfiesNamedType(value ast.Value, definitionTypeRef int) bool { + if value.Kind == ast.ValueKindNull { + // null always satisfies not required type + return true + } + + typeName := v.definition.ResolveTypeNameBytes(definitionTypeRef) + node, exists := v.definition.Index.FirstNodeByNameBytes(typeName) + if !exists { + v.handleTypeError(value, definitionTypeRef) return false } + + return v.valueSatisfiesTypeDefinitionNode(value, definitionTypeRef, node) } -func (v *valuesVisitor) valueSatisfiesListType(value ast.Value, listType int) bool { +func (v *valuesVisitor) valueSatisfiesListType(value ast.Value, definitionTypeRef int, listItemType int) bool { if value.Kind == ast.ValueKindVariable { - variableName := v.operation.VariableValueNameBytes(value.Ref) - variableDefinition, exists := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName) - if !exists { + variableDefinitionRef, actualType, _, ok := v.operationVariableType(value.Ref) + if !ok { + v.handleTypeError(value, definitionTypeRef) return false } - actualType := v.operation.VariableDefinitions[variableDefinition].Type - expectedType := v.importer.ImportType(listType, v.definition, v.operation) + + if v.operation.VariableDefinitionHasDefaultValue(variableDefinitionRef) { + return v.valueSatisfiesInputValueDefinitionType(v.operation.VariableDefinitions[variableDefinitionRef].DefaultValue.Value, definitionTypeRef) + } + + expectedType := v.importer.ImportType(listItemType, v.definition, v.operation) if v.operation.Types[actualType].TypeKind == ast.TypeKindNonNull { actualType = v.operation.Types[actualType].OfType } if v.operation.Types[actualType].TypeKind == ast.TypeKindList { actualType = v.operation.Types[actualType].OfType } - return v.operation.TypesAreEqualDeep(expectedType, actualType) + if !v.operation.TypesAreEqualDeep(expectedType, actualType) { + v.handleVariableHasIncompatibleTypeError(value, definitionTypeRef) + return false + } + return true + } + + if value.Kind == ast.ValueKindNull { + return true } if value.Kind != ast.ValueKindList { - return false + return v.valueSatisfiesInputValueDefinitionType(value, listItemType) } - if v.definition.Types[listType].TypeKind == ast.TypeKindNonNull { + if v.definition.Types[listItemType].TypeKind == ast.TypeKindNonNull { if len(v.operation.ListValues[value.Ref].Refs) == 0 { - return false + // [] empty list is a valid input for [item!] lists + return true } - listType = v.definition.Types[listType].OfType + listItemType = v.definition.Types[listItemType].OfType } + valid := true + for _, i := range v.operation.ListValues[value.Ref].Refs { listValue := v.operation.Value(i) - if !v.valueSatisfiesInputValueDefinitionType(listValue, listType) { - return false + if !v.valueSatisfiesInputValueDefinitionType(listValue, listItemType) { + valid = false } } - return true + return valid } -func (v *valuesVisitor) valueSatisfiesTypeDefinitionNode(value ast.Value, node ast.Node) bool { +func (v *valuesVisitor) valueSatisfiesTypeDefinitionNode(value ast.Value, definitionTypeRef int, node ast.Node) bool { switch node.Kind { case ast.NodeKindEnumTypeDefinition: - return v.valueSatisfiesEnum(value, node) + return v.valueSatisfiesEnum(value, definitionTypeRef, node) case ast.NodeKindScalarTypeDefinition: - return v.valueSatisfiesScalar(value, node.Ref) + return v.valueSatisfiesScalar(value, definitionTypeRef, node.Ref) case ast.NodeKindInputObjectTypeDefinition: - return v.valueSatisfiesInputObjectTypeDefinition(value, node.Ref) - default: - return false + return v.valueSatisfiesInputObjectTypeDefinition(value, definitionTypeRef, node.Ref) } + return false } -func (v *valuesVisitor) valueSatisfiesEnum(value ast.Value, node ast.Node) bool { +func (v *valuesVisitor) valueSatisfiesEnum(value ast.Value, definitionTypeRef int, node ast.Node) bool { if value.Kind == ast.ValueKindVariable { - name := v.operation.VariableValueNameBytes(value.Ref) - if v.Ancestors[0].Kind != ast.NodeKindOperationDefinition { - return false - } - definition, ok := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, name) - if !ok { - return false - } - variableType := v.operation.VariableDefinitions[definition].Type - actualTypeName := v.operation.ResolveTypeNameBytes(variableType) expectedTypeName := node.NameBytes(v.definition) - return bytes.Equal(actualTypeName, expectedTypeName) + return v.variableValueHasMatchingTypeName(value, definitionTypeRef, expectedTypeName) } + if value.Kind != ast.ValueKindEnum { + v.handleUnexpectedEnumValueError(value, definitionTypeRef) return false } enumValue := v.operation.EnumValueNameBytes(value.Ref) - return v.definition.EnumTypeDefinitionContainsEnumValue(node.Ref, enumValue) + + if !v.definition.EnumTypeDefinitionContainsEnumValue(node.Ref, enumValue) { + v.handleNotExistingEnumValueError(value, definitionTypeRef) + return false + } + + return true } -func (v *valuesVisitor) valueSatisfiesInputObjectTypeDefinition(value ast.Value, inputObjectTypeDefinition int) bool { +func (v *valuesVisitor) valueSatisfiesScalar(value ast.Value, definitionTypeRef int, scalar int) bool { + scalarName := v.definition.ScalarTypeDefinitionNameBytes(scalar) if value.Kind == ast.ValueKindVariable { - name := v.operation.VariableValueNameBytes(value.Ref) - if v.Ancestors[0].Kind != ast.NodeKindOperationDefinition { - return false - } - definition, ok := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, name) - if !ok { - return false - } - variableType := v.operation.VariableDefinitions[definition].Type - actualTypeName := v.operation.ResolveTypeNameBytes(variableType) + return v.variableValueHasMatchingTypeName(value, definitionTypeRef, scalarName) + } + + switch { + case bytes.Equal(scalarName, literal.ID): + return v.valueSatisfiesScalarID(value, definitionTypeRef) + case bytes.Equal(scalarName, literal.BOOLEAN): + return v.valueSatisfiesScalarBoolean(value, definitionTypeRef) + case bytes.Equal(scalarName, literal.INT): + return v.valueSatisfiesScalarInt(value, definitionTypeRef) + case bytes.Equal(scalarName, literal.FLOAT): + return v.valueSatisfiesScalarFloat(value, definitionTypeRef) + case bytes.Equal(scalarName, literal.STRING): + return v.valueSatisfiesScalarString(value, definitionTypeRef, true) + default: + return v.valueSatisfiesScalarString(value, definitionTypeRef, false) + } +} + +func (v *valuesVisitor) valueSatisfiesScalarID(value ast.Value, definitionTypeRef int) bool { + if value.Kind == ast.ValueKindString || value.Kind == ast.ValueKindInteger { + return true + } + + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return false + } + + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyID(printedValue, printedType, value.Position)) + + return false +} + +func (v *valuesVisitor) valueSatisfiesScalarBoolean(value ast.Value, definitionTypeRef int) bool { + if value.Kind == ast.ValueKindBoolean { + return true + } + + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return false + } + + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyBoolean(printedValue, printedType, value.Position)) + + return false +} + +func (v *valuesVisitor) valueSatisfiesScalarInt(value ast.Value, definitionTypeRef int) bool { + var isValidInt32 bool + isInt := value.Kind == ast.ValueKindInteger + + if isInt { + isValidInt32 = v.operation.IntValueValidInt32(value.Ref) + } + + if isInt && isValidInt32 { + return true + } + + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return false + } + + if !isInt { + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyInt(printedValue, printedType, value.Position)) + return false + } + + v.Report.AddExternalError(operationreport.ErrBigIntValueDoesntSatisfyInt(printedValue, printedType, value.Position)) + return false +} + +func (v *valuesVisitor) valueSatisfiesScalarFloat(value ast.Value, definitionTypeRef int) bool { + if value.Kind == ast.ValueKindFloat || value.Kind == ast.ValueKindInteger { + return true + } + + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return false + } + + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyFloat(printedValue, printedType, value.Position)) + + return false +} + +func (v *valuesVisitor) valueSatisfiesScalarString(value ast.Value, definitionTypeRef int, builtInStringScalar bool) bool { + if value.Kind == ast.ValueKindString { + return true + } + + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return false + } + + if builtInStringScalar { + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyString(printedValue, printedType, value.Position)) + } else { + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyType(printedValue, printedType, value.Position)) + } + + return false +} + +func (v *valuesVisitor) valueSatisfiesInputObjectTypeDefinition(value ast.Value, definitionTypeRef int, inputObjectTypeDefinition int) bool { + if value.Kind == ast.ValueKindVariable { expectedTypeName := v.definition.InputObjectTypeDefinitionNameBytes(inputObjectTypeDefinition) - return bytes.Equal(actualTypeName, expectedTypeName) + return v.variableValueHasMatchingTypeName(value, definitionTypeRef, expectedTypeName) } if value.Kind != ast.ValueKindObject { + v.handleNotObjectTypeError(value, definitionTypeRef) return false } + valid := true + for _, i := range v.definition.InputObjectTypeDefinitions[inputObjectTypeDefinition].InputFieldsDefinition.Refs { - if !v.objectValueSatisfiesInputValueDefinition(value.Ref, i) { - return false + if !v.objectValueSatisfiesInputValueDefinition(value, inputObjectTypeDefinition, i) { + valid = false } } + if !valid { + return false + } + for _, i := range v.operation.ObjectValues[value.Ref].Refs { if !v.objectFieldDefined(i, inputObjectTypeDefinition) { - objectFieldName := string(v.operation.ObjectFieldNameBytes(i)) - def := string(v.definition.Input.ByteSlice(v.definition.InputObjectTypeDefinitions[inputObjectTypeDefinition].Name)) - _, _ = objectFieldName, def - return false + objectFieldName := v.operation.ObjectFieldNameBytes(i) + def := v.definition.Input.ByteSlice(v.definition.InputObjectTypeDefinitions[inputObjectTypeDefinition].Name) + + v.Report.AddExternalError(operationreport.ErrUnknownFieldOfInputObject(objectFieldName, def, v.operation.ObjectField(i).Position)) + valid = false } } - return !v.objectValueHasDuplicateFields(value.Ref) + if !valid { + return false + } + + if v.objectValueHasDuplicateFields(value.Ref) { + return false + } + + return true } func (v *valuesVisitor) objectValueHasDuplicateFields(objectValue int) bool { + hasDuplicates := false + + reportedFieldRefs := make(map[int]struct{}) for i, j := range v.operation.ObjectValues[objectValue].Refs { for k, l := range v.operation.ObjectValues[objectValue].Refs { if i == k || i > k { continue } - if bytes.Equal(v.operation.ObjectFieldNameBytes(j), v.operation.ObjectFieldNameBytes(l)) { - return true + + if _, ok := reportedFieldRefs[l]; ok { + continue + } + + fieldName := v.operation.ObjectFieldNameBytes(j) + otherFieldName := v.operation.ObjectFieldNameBytes(l) + + if bytes.Equal(fieldName, otherFieldName) { + v.Report.AddExternalError(operationreport.ErrDuplicatedFieldInputObject( + fieldName, + v.operation.ObjectField(j).Position, + v.operation.ObjectField(l).Position)) + hasDuplicates = true + reportedFieldRefs[l] = struct{}{} } } } - return false + + return hasDuplicates } func (v *valuesVisitor) objectFieldDefined(objectField, inputObjectTypeDefinition int) bool { @@ -246,42 +492,226 @@ func (v *valuesVisitor) objectFieldDefined(objectField, inputObjectTypeDefinitio return false } -func (v *valuesVisitor) objectValueSatisfiesInputValueDefinition(objectValue, inputValueDefinition int) bool { +func (v *valuesVisitor) objectValueSatisfiesInputValueDefinition(objectValue ast.Value, inputObjectDefinition, inputValueDefinition int) bool { name := v.definition.InputValueDefinitionNameBytes(inputValueDefinition) - definitionType := v.definition.InputValueDefinitionType(inputValueDefinition) + definitionTypeRef := v.definition.InputValueDefinitionType(inputValueDefinition) - for _, i := range v.operation.ObjectValues[objectValue].Refs { + for _, i := range v.operation.ObjectValues[objectValue.Ref].Refs { if bytes.Equal(name, v.operation.ObjectFieldNameBytes(i)) { value := v.operation.ObjectFieldValue(i) - return v.valueSatisfiesInputValueDefinitionType(value, definitionType) + return v.valueSatisfiesInputValueDefinitionType(value, definitionTypeRef) } } // argument is not present on object value, if arg is optional it's still ok, otherwise not satisfied - return v.definition.InputValueDefinitionArgumentIsOptional(inputValueDefinition) + if !v.definition.InputValueDefinitionArgumentIsOptional(inputValueDefinition) { + v.handleMissingRequiredFieldOfInputObjectError(objectValue, name, inputObjectDefinition, inputValueDefinition) + return false + } + + return true } -func (v *valuesVisitor) valueSatisfiesScalar(value ast.Value, scalar int) bool { - scalarName := v.definition.ScalarTypeDefinitionNameString(scalar) - if value.Kind == ast.ValueKindVariable { - variableName := v.operation.VariableValueNameBytes(value.Ref) - variableDefinition, exists := v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName) - if !exists { - return false +func (v *valuesVisitor) variableValueHasMatchingTypeName(value ast.Value, definitionTypeRef int, expectedTypeName []byte) bool { + variableDefinitionRef, _, actualTypeName, ok := v.operationVariableType(value.Ref) + if !ok { + v.handleVariableHasIncompatibleTypeError(value, definitionTypeRef) + return false + } + + if v.operation.VariableDefinitionHasDefaultValue(variableDefinitionRef) { + return v.valueSatisfiesInputValueDefinitionType(v.operation.VariableDefinitions[variableDefinitionRef].DefaultValue.Value, definitionTypeRef) + } + + if !bytes.Equal(actualTypeName, expectedTypeName) { + v.handleVariableHasIncompatibleTypeError(value, definitionTypeRef) + return false + } + + return true +} + +func (v *valuesVisitor) handleTypeError(value ast.Value, definitionTypeRef int) { + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return + } + + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyType(printedValue, printedType, value.Position)) +} + +func (v *valuesVisitor) handleNotObjectTypeError(value ast.Value, definitionTypeRef int) { + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return + } + + v.Report.AddExternalError(operationreport.ErrValueIsNotAnInputObjectType(printedValue, printedType, value.Position)) +} + +func (v *valuesVisitor) handleUnexpectedNullError(value ast.Value, definitionTypeRef int) { + printedType, err := v.definition.PrintTypeBytes(definitionTypeRef, nil) + if v.HandleInternalErr(err) { + return + } + + v.Report.AddExternalError(operationreport.ErrNullValueDoesntSatisfyInputValueDefinition(printedType, value.Position)) +} + +func (v *valuesVisitor) handleUnexpectedEnumValueError(value ast.Value, definitionTypeRef int) { + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return + } + + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyEnum(printedValue, printedType, value.Position)) +} + +func (v *valuesVisitor) handleNotExistingEnumValueError(value ast.Value, definitionTypeRef int) { + printedValue, printedType, ok := v.printValueAndUnderlyingType(value, definitionTypeRef) + if !ok { + return + } + + v.Report.AddExternalError(operationreport.ErrValueDoesntExistsInEnum(printedValue, printedType, value.Position)) +} + +func (v *valuesVisitor) handleVariableHasIncompatibleTypeError(value ast.Value, definitionTypeRef int) { + printedValue, ok := v.printOperationValue(value) + if !ok { + return + } + + expectedTypeName, err := v.definition.PrintTypeBytes(definitionTypeRef, nil) + if v.HandleInternalErr(err) { + return + } + + variableDefinitionRef, _, actualTypeName, ok := v.operationVariableType(value.Ref) + if !ok { + return + } + + v.Report.AddExternalError(operationreport.ErrVariableTypeDoesntSatisfyInputValueDefinition( + printedValue, + actualTypeName, + expectedTypeName, + value.Position, + v.operation.VariableDefinitions[variableDefinitionRef].VariableValue.Position, + )) +} + +func (v *valuesVisitor) handleMissingRequiredFieldOfInputObjectError(value ast.Value, fieldName ast.ByteSlice, inputObjectDefinition, inputValueDefinition int) { + printedType, err := v.definition.PrintTypeBytes(v.definition.InputValueDefinitions[inputValueDefinition].Type, nil) + if v.HandleInternalErr(err) { + return + } + + v.Report.AddExternalError(operationreport.ErrMissingRequiredFieldOfInputObject( + v.definition.InputObjectTypeDefinitionNameBytes(inputObjectDefinition), + fieldName, + printedType, + value.Position, + )) +} + +func (v *valuesVisitor) handleOperationTypeError(value ast.Value, operationTypeRef int) { + printedValue, printedType, ok := v.printOperationValueAndUnderlyingType(value, operationTypeRef) + if !ok { + return + } + + v.Report.AddExternalError(operationreport.ErrValueDoesntSatisfyType(printedValue, printedType, value.Position)) +} + +func (v *valuesVisitor) handleOperationUnexpectedNullError(value ast.Value, operationTypeRef int) { + printedType, err := v.operation.PrintTypeBytes(operationTypeRef, nil) + if v.HandleInternalErr(err) { + return + } + + v.Report.AddExternalError(operationreport.ErrNullValueDoesntSatisfyInputValueDefinition(printedType, value.Position)) +} + +func (v *valuesVisitor) printValueAndUnderlyingType(value ast.Value, definitionTypeRef int) (printedValue, printedType []byte, ok bool) { + var err error + + printedValue, ok = v.printOperationValue(value) + if !ok { + return nil, nil, false + } + + underlyingType := v.definition.ResolveUnderlyingType(definitionTypeRef) + printedType, err = v.definition.PrintTypeBytes(underlyingType, nil) + if v.HandleInternalErr(err) { + return nil, nil, false + } + + return printedValue, printedType, true +} + +func (v *valuesVisitor) printOperationValueAndUnderlyingType(value ast.Value, operationTypeRef int) (printedValue, printedType []byte, ok bool) { + printedValue, ok = v.printOperationValue(value) + if !ok { + return nil, nil, false + } + + printedType, ok = v.printUnderlyingOperationType(operationTypeRef) + if !ok { + return nil, nil, false + } + + return printedValue, printedType, true +} + +func (v *valuesVisitor) printUnderlyingOperationType(operationTypeRef int) (printedType []byte, ok bool) { + var err error + + underlyingType := v.operation.ResolveUnderlyingType(operationTypeRef) + printedType, err = v.operation.PrintTypeBytes(underlyingType, nil) + if v.HandleInternalErr(err) { + return nil, false + } + + return printedType, true +} + +func (v *valuesVisitor) printOperationValue(value ast.Value) (printedValue []byte, ok bool) { + var err error + printedValue, err = v.operation.PrintValueBytes(value, nil) + if v.HandleInternalErr(err) { + return nil, false + } + + return printedValue, true +} + +func (v *valuesVisitor) operationVariableDefinition(variableValueRef int) (ref int, exists bool) { + variableName := v.operation.VariableValueNameBytes(variableValueRef) + + if v.Ancestors[0].Kind == ast.NodeKindOperationDefinition { + return v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName) + } + + for opDefRef := 0; opDefRef < len(v.operation.OperationDefinitions); opDefRef++ { + ref, exists = v.operation.VariableDefinitionByNameAndOperation(opDefRef, variableName) + if exists { + return } - variableTypeRef := v.operation.VariableDefinitions[variableDefinition].Type - typeName := v.operation.ResolveTypeNameString(variableTypeRef) - return scalarName == typeName - } - switch scalarName { - case "Boolean": - return value.Kind == ast.ValueKindBoolean - case "Int": - return value.Kind == ast.ValueKindInteger - case "Float": - return value.Kind == ast.ValueKindFloat || value.Kind == ast.ValueKindInteger - default: - return value.Kind == ast.ValueKindString } + + return ast.InvalidRef, false +} + +func (v *valuesVisitor) operationVariableType(variableValueRef int) (variableDefinitionRef int, variableTypeRef int, typeName ast.ByteSlice, ok bool) { + variableDefRef, exists := v.operationVariableDefinition(variableValueRef) + if !exists { + return ast.InvalidRef, ast.InvalidRef, nil, false + } + + variableTypeRef = v.operation.VariableDefinitions[variableDefRef].Type + typeName = v.operation.ResolveTypeNameBytes(variableTypeRef) + + return variableDefRef, variableTypeRef, typeName, true } diff --git a/pkg/astvalidation/operation_rule_variables_are_input_types.go b/pkg/astvalidation/operation_rule_variables_are_input_types.go index bb62c1cfa1..6ef7372fac 100644 --- a/pkg/astvalidation/operation_rule_variables_are_input_types.go +++ b/pkg/astvalidation/operation_rule_variables_are_input_types.go @@ -30,13 +30,25 @@ func (v *variablesAreInputTypesVisitor) EnterDocument(operation, definition *ast func (v *variablesAreInputTypesVisitor) EnterVariableDefinition(ref int) { typeName := v.operation.ResolveTypeNameBytes(v.operation.VariableDefinitions[ref].Type) - typeDefinitionNode, _ := v.definition.Index.FirstNodeByNameBytes(typeName) + typeDefinitionNode, ok := v.definition.Index.FirstNodeByNameBytes(typeName) + if !ok { + v.Report.AddExternalError(operationreport.ErrUnknownType(typeName, v.operation.Types[v.operation.VariableDefinitions[ref].Type].Position)) + return + } + switch typeDefinitionNode.Kind { case ast.NodeKindInputObjectTypeDefinition, ast.NodeKindScalarTypeDefinition, ast.NodeKindEnumTypeDefinition: return default: variableName := v.operation.VariableDefinitionNameBytes(ref) - v.StopWithExternalErr(operationreport.ErrVariableOfTypeIsNoValidInputValue(variableName, typeName)) + variableTypePos := v.operation.Types[v.operation.VariableDefinitions[ref].Type].Position + + printedType, err := v.operation.PrintTypeBytes(v.operation.VariableDefinitions[ref].Type, nil) + if v.HandleInternalErr(err) { + return + } + + v.Report.AddExternalError(operationreport.ErrVariableOfTypeIsNoValidInputValue(variableName, printedType, variableTypePos)) return } } diff --git a/pkg/astvalidation/operation_validation.go b/pkg/astvalidation/operation_validation.go index 9e6ff7b685..21921243b5 100644 --- a/pkg/astvalidation/operation_validation.go +++ b/pkg/astvalidation/operation_validation.go @@ -20,6 +20,7 @@ func DefaultOperationValidator() *OperationValidator { validator.RegisterRule(SubscriptionSingleRootField()) validator.RegisterRule(FieldSelections()) validator.RegisterRule(FieldSelectionMerging()) + validator.RegisterRule(KnownArguments()) validator.RegisterRule(ValidArguments()) validator.RegisterRule(Values()) validator.RegisterRule(ArgumentUniqueness()) diff --git a/pkg/astvalidation/operation_validation_test.go b/pkg/astvalidation/operation_validation_test.go index 81e977553a..252712c5cf 100644 --- a/pkg/astvalidation/operation_validation_test.go +++ b/pkg/astvalidation/operation_validation_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wundergraph/graphql-go-tools/internal/pkg/unsafeparser" @@ -15,8 +16,10 @@ import ( ) type options struct { - disableNormalization bool - expectNormalizationError bool + disableNormalization bool + expectNormalizationError bool + expectValidationErrors bool + expectedValidationErrorMsgs []string } type option func(options *options) @@ -34,6 +37,13 @@ func withExpectNormalizationError() option { } } +func withValidationErrors(errMsgs ...string) option { + return func(options *options) { + options.expectValidationErrors = true + options.expectedValidationErrorMsgs = errMsgs + } +} + func TestExecutionValidation(t *testing.T) { must := func(err error) { if report, ok := err.(operationreport.Report); ok { @@ -85,6 +95,12 @@ func TestExecutionValidation(t *testing.T) { printedOperation := mustString(astprinter.PrintString(&operation, &definition)) + if options.expectValidationErrors { + for _, msg := range options.expectedValidationErrorMsgs { + assert.Contains(t, report.Error(), msg) + } + } + require.Equal(t, expectation, result, "wrong validation result expected: %v got: %v\nreason: %v\noperation:\n%s\n", expectation, result, report.Error(), printedOperation) } @@ -96,6 +112,14 @@ func TestExecutionValidation(t *testing.T) { } _ = runManyRulesWithDefinition + runManyRules := func(t *testing.T, operationInput string, expectation ValidationState, rules ...Rule) { + t.Helper() + for _, rule := range rules { + runWithDefinition(t, testDefinition, operationInput, rule, expectation) + } + } + _ = runManyRules + run := func(t *testing.T, operationInput string, rule Rule, expectation ValidationState, opts ...option) { t.Helper() runWithDefinition(t, testDefinition, operationInput, rule, expectation, opts...) @@ -2009,7 +2033,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: true) @include(if: true) }`, - ValidArguments(), Valid) + KnownArguments(), Valid) }) t.Run("117 variant", func(t *testing.T) { run(t, ` query argOnRequiredArg { @@ -2021,7 +2045,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: true) @include(if: true) }`, - ValidArguments(), Valid) + KnownArguments(), Valid) }) t.Run("117 variant", func(t *testing.T) { run(t, ` query argOnRequiredArg($dogCommand: DogCommand!) { @@ -2033,7 +2057,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: true) @include(if: true) }`, - ValidArguments(), Valid) + KnownArguments(), Valid) }) t.Run("117 variant", func(t *testing.T) { run(t, ` @@ -2046,7 +2070,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: true) @include(if: true) }`, - ValidArguments(), Valid, withDisableNormalization()) + KnownArguments(), Valid, withDisableNormalization()) }) t.Run("117 variant", func(t *testing.T) { run(t, ` @@ -2055,7 +2079,7 @@ func TestExecutionValidation(t *testing.T) { doesKnowCommand(dogCommand: $catCommand) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$catCommand" of type "CatCommand" used in position expecting type "DogCommand!".`)) }) t.Run("117 variant", func(t *testing.T) { run(t, `query argOnRequiredArg($dogCommand: CatCommand) { @@ -2065,7 +2089,7 @@ func TestExecutionValidation(t *testing.T) { } } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$dogCommand" of type "CatCommand" used in position expecting type "DogCommand!".`)) }) t.Run("117 variant", func(t *testing.T) { run(t, ` query argOnRequiredArg($booleanArg: Boolean) { @@ -2078,54 +2102,6 @@ func TestExecutionValidation(t *testing.T) { }`, ValidArguments(), Valid) }) - t.Run("required String", func(t *testing.T) { - run(t, ` query requiredString { - args { - requiredString(s: "foo") - } - }`, - ValidArguments(), Valid) - }) - t.Run("required String", func(t *testing.T) { - run(t, ` query requiredString { - args { - requiredString(s: foo) - } - }`, - ValidArguments(), Invalid) - }) - t.Run("required String", func(t *testing.T) { - run(t, ` query requiredString { - args { - requiredString(s: null) - } - }`, - ValidArguments(), Invalid) - }) - t.Run("required String", func(t *testing.T) { - run(t, ` query requiredFloat { - args { - requiredFloat(f: 1.1) - } - }`, - ValidArguments(), Valid) - }) - t.Run("required String", func(t *testing.T) { - run(t, ` query requiredFloat { - args { - requiredFloat(f: "1.1") - } - }`, - ValidArguments(), Invalid) - }) - t.Run("required String", func(t *testing.T) { - run(t, ` query requiredFloat { - args { - requiredFloat(f: null) - } - }`, - ValidArguments(), Invalid) - }) t.Run("117 variant", func(t *testing.T) { run(t, ` query argOnRequiredArg($booleanArg: Boolean!) { @@ -2148,7 +2124,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: $booleanArg) @include(if: $booleanArg) }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$booleanArg" of type "Boolean" used in position expecting type "Boolean!".`)) }) t.Run("117 variant", func(t *testing.T) { run(t, ` query argOnRequiredArg($booleanArg: Boolean!) { @@ -2172,7 +2148,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: $intArg) @include(if: true) }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$intArg" of type "Integer" used in position expecting type "Boolean".`)) }) t.Run("117 variant", func(t *testing.T) { run(t, ` query argOnRequiredArg($intArg: Integer) { @@ -2183,7 +2159,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: $intArg) @include(if: true) }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$intArg" of type "Integer" used in position expecting type "Boolean".`)) }) t.Run("117 variant", func(t *testing.T) { run(t, ` query argOnRequiredArg($intArg: Integer) { @@ -2196,7 +2172,7 @@ func TestExecutionValidation(t *testing.T) { fragment argOnOptional on Dog { isHousetrained(atOtherHomes: $intArg) @include(if: true) }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$intArg" of type "Integer" used in position expecting type "Boolean".`)) }) t.Run("118", func(t *testing.T) { run(t, ` @@ -2206,7 +2182,7 @@ func TestExecutionValidation(t *testing.T) { fragment invalidArgName on Dog { doesKnowCommand(command: CLEAN_UP_HOUSE) }`, - ValidArguments(), Invalid) + KnownArguments(), Invalid, withValidationErrors(`Unknown argument "command" on field "Dog.doesKnowCommand"`)) }) t.Run("118 variant", func(t *testing.T) { run(t, ` @@ -2216,7 +2192,9 @@ func TestExecutionValidation(t *testing.T) { fragment invalidArgName on Dog { doesKnowCommand(dogCommand: CLEAN_UP_HOUSE) }`, - ValidArguments(), Invalid) + Values(), + Invalid, + withValidationErrors(`Value "CLEAN_UP_HOUSE" does not exist in "DogCommand" enum.`)) }) t.Run("119", func(t *testing.T) { run(t, ` { @@ -2225,9 +2203,9 @@ func TestExecutionValidation(t *testing.T) { fragment invalidArgName on Dog { isHousetrained(atOtherHomes: true) @include(unless: false) }`, - ValidArguments(), Invalid) + KnownArguments(), Invalid, withValidationErrors(`Unknown argument "unless" on directive "@include".`)) }) - t.Run("121", func(t *testing.T) { + t.Run("121 args in reversed order", func(t *testing.T) { run(t, ` fragment multipleArgs on ValidArguments { multipleReqs(x: 1, y: 2) } @@ -2242,16 +2220,7 @@ func TestExecutionValidation(t *testing.T) { name } }`, - ValidArguments(), Invalid) - }) - t.Run("ID as arg given as string", func(t *testing.T) { - runManyRulesWithDefinition(t, countriesDefinition, `{ - country(code: "DE") { - code - name - } - }`, - Valid, ValidArguments(), Values()) + KnownArguments(), Invalid, withValidationErrors(`Unknown argument "name" on field "Query.dog".`)) }) }) t.Run("5.4.2 Argument Uniqueness", func(t *testing.T) { @@ -2274,9 +2243,64 @@ func TestExecutionValidation(t *testing.T) { }`, ArgumentUniqueness(), Valid) }) - t.Run("5.4.2.1 Required ValidArguments", func(t *testing.T) { - t.Run("122", func(t *testing.T) { - run(t, ` { + }) + + t.Run("Required Invalid Arguments", func(t *testing.T) { + t.Run("required String", func(t *testing.T) { + run(t, ` query requiredString { + args { + requiredString(s: foo) + } + }`, + Values(), Invalid, withValidationErrors(`String cannot represent a non string value: foo`)) + }) + t.Run("required String", func(t *testing.T) { + run(t, ` query requiredString { + args { + requiredString(s: null) + } + }`, + Values(), Invalid, withValidationErrors(`Expected value of type "String!", found null`)) + }) + + t.Run("required Float", func(t *testing.T) { + run(t, ` query requiredFloat { + args { + requiredFloat(f: "1.1") + } + }`, + Values(), Invalid, withValidationErrors(`Float cannot represent non numeric value: "1.1"`)) + }) + t.Run("required Float", func(t *testing.T) { + run(t, ` query requiredFloat { + args { + requiredFloat(f: null) + } + }`, + Values(), Invalid, withValidationErrors(`Expected value of type "Float!", found null`)) + }) + }) + + t.Run("5.4.2.1 Required ValidArguments", func(t *testing.T) { + t.Run("required String", func(t *testing.T) { + run(t, ` query requiredString { + args { + requiredString(s: "foo") + } + }`, + Values(), Valid) + }) + + t.Run("required Float", func(t *testing.T) { + run(t, ` query requiredFloat { + args { + requiredFloat(f: 1.1) + } + }`, + Values(), Valid) + }) + t.Run("122", func(t *testing.T) { + run(t, ` { arguments { ...goodBooleanArg ...goodNonNullArg @@ -2288,10 +2312,10 @@ func TestExecutionValidation(t *testing.T) { fragment goodNonNullArg on ValidArguments { nonNullBooleanArgField(nonNullBooleanArg: true) }`, - ValidArguments(), Valid) - }) - t.Run("123", func(t *testing.T) { - run(t, ` { + RequiredArguments(), Valid) + }) + t.Run("123", func(t *testing.T) { + run(t, ` { arguments { ...goodBooleanArgDefault } @@ -2299,10 +2323,10 @@ func TestExecutionValidation(t *testing.T) { fragment goodBooleanArgDefault on ValidArguments { booleanArgField }`, - ValidArguments(), Valid) - }) - t.Run("124", func(t *testing.T) { - run(t, ` + RequiredArguments(), Valid) + }) + t.Run("124", func(t *testing.T) { + run(t, ` { arguments { ...missingRequiredArg @@ -2311,10 +2335,10 @@ func TestExecutionValidation(t *testing.T) { fragment missingRequiredArg on ValidArguments { nonNullBooleanArgField }`, - RequiredArguments(), Invalid) - }) - t.Run("125", func(t *testing.T) { - run(t, ` { + RequiredArguments(), Invalid) + }) + t.Run("125", func(t *testing.T) { + run(t, ` { arguments { ...missingRequiredArg } @@ -2322,10 +2346,10 @@ func TestExecutionValidation(t *testing.T) { fragment missingRequiredArg on ValidArguments { nonNullBooleanArgField(nonNullBooleanArg: null) }`, - ValidArguments(), Invalid) - }) - t.Run("125 variant", func(t *testing.T) { - run(t, ` { + RequiredArguments(), Invalid) + }) + t.Run("125 variant", func(t *testing.T) { + run(t, ` { arguments { ...missingRequiredArg } @@ -2333,14 +2357,13 @@ func TestExecutionValidation(t *testing.T) { fragment missingRequiredArg on ValidArguments { nonNullBooleanArgField(nonNullBooleanArg: true) }`, - RequiredArguments(), Valid) - }) - t.Run("125 variant", func(t *testing.T) { - run(t, ` { + RequiredArguments(), Valid) + }) + t.Run("125 variant", func(t *testing.T) { + run(t, ` { booleanList (booleanListArg: [true]) }`, - RequiredArguments(), Valid) - }) + RequiredArguments(), Valid) }) }) }) @@ -2774,6 +2797,27 @@ func TestExecutionValidation(t *testing.T) { }) t.Run("5.6 Values", func(t *testing.T) { t.Run("5.6.1 Values of Correct Type", func(t *testing.T) { + t.Run("valid ID arguments", func(t *testing.T) { + t.Run("ID as arg given as string", func(t *testing.T) { + runWithDefinition(t, countriesDefinition, `{ + country(code: "DE") { + code + name + } + }`, + Values(), Valid) + }) + t.Run("ID as arg given as integer", func(t *testing.T) { + runWithDefinition(t, countriesDefinition, `{ + country(code: 11) { + code + name + } + }`, + Values(), Valid) + }) + }) + t.Run("145", func(t *testing.T) { run(t, ` query goodComplexDefaultValue($search: ComplexInput = { name: "Fido" }) { @@ -2815,19 +2859,19 @@ func TestExecutionValidation(t *testing.T) { }`, Values(), Valid) }) - t.Run("145 variant variable non null", func(t *testing.T) { + t.Run("145 variant variable non null into required field", func(t *testing.T) { run(t, ` query goodComplexDefaultValue($name: String ) { findDogNonOptional(complex: { name: $name }) }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Variable "$name" of type "String" used in position expecting type "String!"`)) }) t.Run("145 variant", func(t *testing.T) { run(t, ` query goodComplexDefaultValue($search: ComplexInput = { name: 123 }) { findDog(complex: $search) }`, - Values(), Invalid, withDisableNormalization()) + Values(), Invalid, withDisableNormalization(), withValidationErrors(`String cannot represent a non string value: 123`)) }) t.Run("145 variant", func(t *testing.T) { run(t, `query goodComplexDefaultValue($search: ComplexInput = { name: "123" }) { @@ -2839,7 +2883,7 @@ func TestExecutionValidation(t *testing.T) { run(t, ` query goodComplexDefaultValue { findDog(complex: { name: 123 }) }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`String cannot represent a non string value: 123`)) }) t.Run("145 variant", func(t *testing.T) { run(t, ` query goodComplexDefaultValue { @@ -2861,7 +2905,7 @@ func TestExecutionValidation(t *testing.T) { doesKnowCommand(dogCommand: MEOW) } }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Value "MEOW" does not exist in "DogCommand" enum`)) }) t.Run("145 variant", func(t *testing.T) { run(t, ` { @@ -2869,7 +2913,7 @@ func TestExecutionValidation(t *testing.T) { doesKnowCommand(dogCommand: [true]) } }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Enum "DogCommand" cannot represent non-enum value: [true]`)) }) t.Run("145 variant", func(t *testing.T) { run(t, ` { @@ -2877,7 +2921,7 @@ func TestExecutionValidation(t *testing.T) { doesKnowCommand(dogCommand: {foo: "bar"}) } }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Enum "DogCommand" cannot represent non-enum value: {foo: "bar"}`)) }) t.Run("146", func(t *testing.T) { run(t, ` @@ -2887,12 +2931,12 @@ func TestExecutionValidation(t *testing.T) { fragment stringIntoInt on ValidArguments { intArgField(intArg: "123") }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Int cannot represent non-integer value: "123"`)) run(t, ` query badComplexValue { findDog(complex: { name: 123 }) }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`String cannot represent a non string value: 123`)) }) t.Run("146 variant", func(t *testing.T) { run(t, ` @@ -2913,7 +2957,7 @@ func TestExecutionValidation(t *testing.T) { run(t, `{ findDog(complex: { favoriteCookieFlavor: "Bacon" }) }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Field "favoriteCookieFlavor" is not defined by type "ComplexInput"`)) }) }) t.Run("5.6.3 Input Object Field Uniqueness", func(t *testing.T) { @@ -2921,7 +2965,7 @@ func TestExecutionValidation(t *testing.T) { run(t, `{ findDog(complex: { name: "Fido", name: "Goofy"}) }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`There can be only one input field named "name"`)) }) }) t.Run("5.6.4 Input Object Required Fields", func(t *testing.T) { @@ -2935,19 +2979,19 @@ func TestExecutionValidation(t *testing.T) { run(t, `query goodComplexDefaultValue($search: ComplexNonOptionalInput = { name: null }) { findDogNonOptional(complex: $search) }`, - Values(), Invalid, withDisableNormalization()) + Values(), Invalid, withDisableNormalization(), withValidationErrors(`Expected value of type "String!", found null`)) }) t.Run("145 variant", func(t *testing.T) { run(t, `query goodComplexDefaultValue($search: ComplexNonOptionalInput = {}) { findDogNonOptional(complex: $search) }`, - Values(), Invalid, withDisableNormalization()) + Values(), Invalid, withDisableNormalization(), withValidationErrors(`Field "ComplexNonOptionalInput.name" of required type "String!" was not provided.`)) }) t.Run("145 variant", func(t *testing.T) { run(t, `query goodComplexDefaultValue { findDogNonOptional(complex: {}) }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Field "ComplexNonOptionalInput.name" of required type "String!" was not provided.`)) }) t.Run("145 variant", func(t *testing.T) { run(t, `query goodComplexDefaultValue { @@ -2971,7 +3015,7 @@ func TestExecutionValidation(t *testing.T) { fragment viaFragment on Query { findDogNonOptional(complex: { name: 123 }) }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`String cannot represent a non string value: 123`)) }) }) t.Run("complex nested validation", func(t *testing.T) { @@ -2980,7 +3024,7 @@ func TestExecutionValidation(t *testing.T) { { nested(input: {}) } - `, Values(), Invalid) + `, Values(), Invalid, withValidationErrors(`Field "NestedInput.requiredString" of required type "String!" was not provided.`)) }) t.Run("complex nested ok", func(t *testing.T) { run(t, ` @@ -2993,7 +3037,7 @@ func TestExecutionValidation(t *testing.T) { } `, Values(), Valid) }) - t.Run("complex nested 'notList' is not list of Strings", func(t *testing.T) { + t.Run("complex nested 'notList' is not list of Strings should be ok with coersion", func(t *testing.T) { run(t, ` { nested(input: { @@ -3002,7 +3046,7 @@ func TestExecutionValidation(t *testing.T) { requiredListOfRequiredStrings: ["str"] }) } - `, Values(), Invalid) + `, Values(), Valid) }) t.Run("complex nested ok 3", func(t *testing.T) { run(t, ` @@ -3056,7 +3100,7 @@ func TestExecutionValidation(t *testing.T) { ] }) } - `, Values(), Invalid) + `, Values(), Invalid, withValidationErrors(`Field "NestedInput.requiredString" of required type "String!" was not provided.`)) }) t.Run("complex nested 'str' is not String", func(t *testing.T) { run(t, ` @@ -3068,9 +3112,9 @@ func TestExecutionValidation(t *testing.T) { requiredListOfOptionalStringsWithDefault: ["more strings"] }) } - `, Values(), Invalid) + `, Values(), Invalid, withValidationErrors(`String cannot represent a non string value: str`)) }) - t.Run("complex nested requiredListOfRequiredStrings must not be empty", func(t *testing.T) { + t.Run("complex nested requiredListOfRequiredStrings could be empty but not `null` or `[null]`", func(t *testing.T) { run(t, ` { nested(input: { @@ -3079,7 +3123,7 @@ func TestExecutionValidation(t *testing.T) { requiredListOfRequiredStrings: [] }) } - `, Values(), Invalid) + `, Values(), Valid) }) t.Run("complex 2x nested", func(t *testing.T) { run(t, ` @@ -3110,7 +3154,7 @@ func TestExecutionValidation(t *testing.T) { } }) } - `, Values(), Invalid) + `, Values(), Invalid, withValidationErrors(`Field "NestedInput.requiredString" of required type "String!" was not provided.`)) }) t.Run("complex 2x nested '123' is no String", func(t *testing.T) { run(t, ` @@ -3126,7 +3170,7 @@ func TestExecutionValidation(t *testing.T) { } }) } - `, Values(), Invalid) + `, Values(), Invalid, withValidationErrors(`String cannot represent a non string value: 123`)) }) }) }) @@ -3579,7 +3623,7 @@ func TestExecutionValidation(t *testing.T) { booleanArgField(booleanArg: $intArg) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$intArg" of type "Int" used in position expecting type "Boolean".`)) }) t.Run("170", func(t *testing.T) { run(t, `query booleanListCannotGoIntoBoolean($booleanListArg: [Boolean]) { @@ -3587,7 +3631,7 @@ func TestExecutionValidation(t *testing.T) { booleanArgField(booleanArg: $booleanListArg) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$booleanListArg" of type "[Boolean]" used in position expecting type "Boolean".`)) }) t.Run("171", func(t *testing.T) { run(t, `query booleanArgQuery($booleanArg: Boolean) { @@ -3595,7 +3639,7 @@ func TestExecutionValidation(t *testing.T) { nonNullBooleanArgField(nonNullBooleanArg: $booleanArg) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$booleanArg" of type "Boolean" used in position expecting type "Boolean!".`)) }) // Non-null types are compatible with nullable types. t.Run("172", func(t *testing.T) { @@ -3645,7 +3689,7 @@ func TestExecutionValidation(t *testing.T) { nonNullListOfBooleanArgField(nonNullListOfBooleanArg: [true,false,"123"]) } }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Boolean cannot represent a non boolean value: "123"`)) }) t.Run("172 variant", func(t *testing.T) { run(t, `query listContainingIncorrectType { @@ -3653,7 +3697,7 @@ func TestExecutionValidation(t *testing.T) { nonNullListOfBooleanArgField(nonNullListOfBooleanArg: [true,false,123]) } }`, - Values(), Invalid) + Values(), Invalid, withValidationErrors(`Boolean cannot represent a non boolean value: 123`)) }) // Nullable types are NOT compatible with non-null types. t.Run("172 variant", func(t *testing.T) { @@ -3662,7 +3706,7 @@ func TestExecutionValidation(t *testing.T) { listOfNonNullBooleanArgField(listOfNonNullBooleanArg: $listOfBoolean) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$listOfBoolean" of type "[Boolean]" used in position expecting type "[Boolean!]"`)) }) t.Run("172 variant", func(t *testing.T) { run(t, `query nonNullListToListOfNonNull($nonNullListOfBoolean: [Boolean]!) { @@ -3670,7 +3714,7 @@ func TestExecutionValidation(t *testing.T) { listOfNonNullBooleanArgField(listOfNonNullBooleanArg: $nonNullListOfBoolean) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$nonNullListOfBoolean" of type "[Boolean]!" used in position expecting type "[Boolean!]"`)) }) t.Run("172 variant", func(t *testing.T) { run(t, `query listOfNonNullToNonNullList($listOfNonNullBoolean: [Boolean!]) { @@ -3678,7 +3722,7 @@ func TestExecutionValidation(t *testing.T) { nonNullListOfBooleanArgField(nonNullListOfBooleanArg: $listOfNonNullBoolean) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$listOfNonNullBoolean" of type "[Boolean!]" used in position expecting type "[Boolean]!"`)) }) t.Run("173", func(t *testing.T) { run(t, `query listToNonNullList($listOfBoolean: [Boolean]) { @@ -3686,7 +3730,7 @@ func TestExecutionValidation(t *testing.T) { nonNullListOfBooleanArgField(nonNullListOfBooleanArg: $listOfBoolean) } }`, - ValidArguments(), Invalid) + ValidArguments(), Invalid, withValidationErrors(`Variable "$listOfBoolean" of type "[Boolean]" used in position expecting type "[Boolean]!"`)) }) t.Run("174", func(t *testing.T) { run(t, `query booleanArgQueryWithDefault($booleanArg: Boolean) { @@ -3748,6 +3792,56 @@ func TestExecutionValidation(t *testing.T) { } `, Values(), Valid) }) + t.Run("with boolean input", func(t *testing.T) { + runWithDefinition(t, wundergraphSchema, ` + query QueryWithBooleanInput($a: Boolean) { + findFirstnodepool( + where: { shared: { equals: $a } } + ) { + id + } + } + `, Values(), Valid) + }) + t.Run("with nested boolean where clause", func(t *testing.T) { + runWithDefinition(t, wundergraphSchema, ` + query QueryWithNestedBooleanClause($a: String) { + findFirstnodepool( + where: { id: { equals: $a }, AND: { shared: { equals: true } } } + ) { + id + } + } + `, Values(), Valid) + }) + t.Run("with variables inside an input object", func(t *testing.T) { + runWithDefinition(t, wundergraphSchema, ` + query QueryWithNestedBooleanClause($a: String, $b: Boolean) { + findFirstnodepool( + where: { id: { equals: $b }, AND: { shared: { equals: $a } } } + ) { + id + } + } + `, Values(), Invalid, + withValidationErrors( + `Variable "$a" of type "String" used in position expecting type "Boolean"`, + `Variable "$b" of type "Boolean" used in position expecting type "String"`, + )) + }) + + t.Run("with variables inside an input object", func(t *testing.T) { + run(t, ` + query booleanIntoStringList($a: Boolean) { + findDog(complex: {optionalListOfOptionalStrings: $a}) { + id + } + } + `, Values(), Invalid, + withValidationErrors( + `Variable "$a" of type "Boolean" used in position expecting type "[String]"`, + )) + }) }) }) } @@ -4317,7 +4411,7 @@ type Mutation { mutateDog: Dog } -input ComplexInput { name: String, owner: String } +input ComplexInput { name: String, owner: String, optionalListOfOptionalStrings: [String]} input ComplexNestedInput { complex: ComplexInput } input ComplexNonOptionalInput { name: String! } @@ -5283,6 +5377,7 @@ schema { } scalar String +scalar Boolean enum QueryMode { default diff --git a/pkg/astvalidation/reference/testsgo/KnownArgumentNamesRule_test.go b/pkg/astvalidation/reference/testsgo/KnownArgumentNamesRule_test.go index 2ec3f24189..d3421c8736 100755 --- a/pkg/astvalidation/reference/testsgo/KnownArgumentNamesRule_test.go +++ b/pkg/astvalidation/reference/testsgo/KnownArgumentNamesRule_test.go @@ -5,8 +5,6 @@ import ( ) func TestKnownArgumentNamesRule(t *testing.T) { - t.Skip() - ExpectErrors := func(t *testing.T, queryStr string) ResultCompare { return ExpectValidationErrors(t, KnownArgumentNamesRule, queryStr) } @@ -79,7 +77,7 @@ func TestKnownArgumentNamesRule(t *testing.T) { doesKnowCommand(dogCommand: SIT) } human { - pet { + pets { # we are using pets instead of pet: to not fail in visitor with unknown field name on a type ... on Dog { doesKnowCommand(dogCommand: SIT) } @@ -136,6 +134,21 @@ func TestKnownArgumentNamesRule(t *testing.T) { { dog @skip(iff: true) } + `)([]Err{ + { + message: `Unknown argument "iff" on directive "@skip".`, + locations: []Loc{{line: 3, column: 19}}, + }, + }) + }) + + t.Run("misspelled directive args are reported with suggestions", func(t *testing.T) { + t.Skip(NotSupportedSuggestionsSkipMsg) + + ExpectErrors(t, ` + { + dog @skip(iff: true) + } `)([]Err{ { message: `Unknown argument "iff" on directive "@skip". Did you mean "if"?`, @@ -162,6 +175,21 @@ func TestKnownArgumentNamesRule(t *testing.T) { fragment invalidArgName on Dog { doesKnowCommand(DogCommand: true) } + `)([]Err{ + { + message: `Unknown argument "DogCommand" on field "Dog.doesKnowCommand".`, + locations: []Loc{{line: 3, column: 25}}, + }, + }) + }) + + t.Run("misspelled arg name is reported with suggestions", func(t *testing.T) { + t.Skip(NotSupportedSuggestionsSkipMsg) + + ExpectErrors(t, ` + fragment invalidArgName on Dog { + doesKnowCommand(DogCommand: true) + } `)([]Err{ { message: `Unknown argument "DogCommand" on field "Dog.doesKnowCommand". Did you mean "dogCommand"?`, @@ -194,7 +222,7 @@ func TestKnownArgumentNamesRule(t *testing.T) { doesKnowCommand(unknown: true) } human { - pet { + pets { # we are using pets instead of pet: to not fail in visitor with unknown field name on a type ... on Dog { doesKnowCommand(unknown: true) } @@ -214,6 +242,8 @@ func TestKnownArgumentNamesRule(t *testing.T) { }) t.Run("within SDL", func(t *testing.T) { + t.Skip("Definition directive args validation is not supported yet") + t.Run("known arg on directive defined inside SDL", func(t *testing.T) { ExpectValidSDL(t, ` type Query { diff --git a/pkg/astvalidation/reference/testsgo/KnownTypeNamesRule_test.go b/pkg/astvalidation/reference/testsgo/KnownTypeNamesRule_test.go index 08f653600b..d891d525bd 100755 --- a/pkg/astvalidation/reference/testsgo/KnownTypeNamesRule_test.go +++ b/pkg/astvalidation/reference/testsgo/KnownTypeNamesRule_test.go @@ -5,14 +5,12 @@ import ( ) func TestKnownTypeNamesRule(t *testing.T) { - t.Skip() - ExpectErrors := func(t *testing.T, queryStr string) ResultCompare { - return ExpectValidationErrors(t, KnownTypeNamesRule, queryStr) + return ExpectValidationErrors(t, KnownTypeNamesOperationRule, queryStr) } ExpectErrorsWithSchema := func(t *testing.T, schema string, queryStr string) ResultCompare { - return ExpectValidationErrorsWithSchema(t, schema, KnownTypeNamesRule, queryStr) + return ExpectValidationErrorsWithSchema(t, schema, KnownTypeNamesOperationRule, queryStr) } ExpectValid := func(t *testing.T, queryStr string) { @@ -37,25 +35,50 @@ func TestKnownTypeNamesRule(t *testing.T) { query Foo( $var: String $required: [Int!]! - $introspectionType: __EnumValue + # $introspectionType: __EnumValue - disabled cause it is not valid input type ) { - user(id: 4) { - pets { ... on Pet { name }, ...PetFields, ... { name } } + human(id: 4) { + pets { ... on Pet { name }, ... { name } } } } + `) + }) - fragment PetFields on Pet { + t.Run("unknown type names are invalid", func(t *testing.T) { + ExpectErrors(t, ` + query Foo($var: JumbledUpLetters) { + human(id: 4) { + name + pets { ... on Badger { name } } + } + } + fragment PetFields on Peat { name } - `) + `)([]Err{ + { + message: `Unknown type "JumbledUpLetters".`, + locations: []Loc{{line: 2, column: 23}}, + }, + { + message: `Unknown type "Badger".`, + locations: []Loc{{line: 5, column: 25}}, + }, + { + message: `Unknown type "Peat".`, + locations: []Loc{{line: 8, column: 29}}, + }, + }) }) t.Run("unknown type names are invalid", func(t *testing.T) { + t.Skip("1. Suggestions is not supported. 2. Fragment cycle is reported incorrectly for PetFields fragment") + ExpectErrors(t, ` query Foo($var: JumbledUpLetters) { - user(id: 4) { + human(id: 4) { name - pets { ... on Badger { name }, ...PetFields } + pets { ... on Badger { name } ...PetFields } } } fragment PetFields on Peat { @@ -78,6 +101,8 @@ func TestKnownTypeNamesRule(t *testing.T) { }) t.Run("references to standard scalars that are missing in schema", func(t *testing.T) { + t.Skip("think fix or remove - currently harness helpers are always merging base schema with schema") + schema := BuildSchema("type Query { foo: String }") query := ` query ($id: ID, $float: Float, $int: Int) { @@ -101,6 +126,8 @@ func TestKnownTypeNamesRule(t *testing.T) { }) t.Run("within SDL", func(t *testing.T) { + t.Skip() + t.Run("use standard types", func(t *testing.T) { ExpectValidSDL(t, ` type Query { diff --git a/pkg/astvalidation/reference/testsgo/UniqueInputFieldNamesRule_test.go b/pkg/astvalidation/reference/testsgo/UniqueInputFieldNamesRule_test.go index 1464ebdf91..b61b691544 100755 --- a/pkg/astvalidation/reference/testsgo/UniqueInputFieldNamesRule_test.go +++ b/pkg/astvalidation/reference/testsgo/UniqueInputFieldNamesRule_test.go @@ -5,19 +5,24 @@ import ( ) func TestUniqueInputFieldNamesRule(t *testing.T) { - t.Skip() - - ExpectErrors := func(t *testing.T, queryStr string) ResultCompare { - return ExpectValidationErrors(t, UniqueInputFieldNamesRule, queryStr) + ExpectErrors := func(t *testing.T, schema, queryStr string) ResultCompare { + return ExpectValidationErrorsWithSchema(t, schema, UniqueInputFieldNamesRule, queryStr) } - ExpectValid := func(t *testing.T, queryStr string) { - ExpectErrors(t, queryStr)([]Err{}) + ExpectValid := func(t *testing.T, schemaStr, queryStr string) { + ExpectErrors(t, schemaStr, queryStr)([]Err{}) } t.Run("Validate: Unique input field names", func(t *testing.T) { t.Run("input object with fields", func(t *testing.T) { ExpectValid(t, ` + input Input { + f: Boolean + } + + type Query { + field(arg: Input): String + }`, ` { field(arg: { f: true }) } @@ -26,6 +31,13 @@ func TestUniqueInputFieldNamesRule(t *testing.T) { t.Run("same input object within two args", func(t *testing.T) { ExpectValid(t, ` + input Input { + f: Boolean + } + + type Query { + field(arg1: Input, arg2: Input): String + }`, ` { field(arg1: { f: true }, arg2: { f: true }) } @@ -34,6 +46,15 @@ func TestUniqueInputFieldNamesRule(t *testing.T) { t.Run("multiple input object fields", func(t *testing.T) { ExpectValid(t, ` + input Input { + f1: String + f2: String + f3: String + } + + type Query { + field(arg: Input): String + }`, ` { field(arg: { f1: "value", f2: "value", f3: "value" }) } @@ -42,6 +63,23 @@ func TestUniqueInputFieldNamesRule(t *testing.T) { t.Run("allows for nested input objects with similar fields", func(t *testing.T) { ExpectValid(t, ` + input Nested1 { + id: ID + deep: Nested2 + } + + input Nested2 { + id: ID + } + + input Input { + id: ID + deep: Nested1 + } + + type Query { + field(arg: Input): String + }`, ` { field(arg: { deep: { @@ -58,6 +96,13 @@ func TestUniqueInputFieldNamesRule(t *testing.T) { t.Run("duplicate input object fields", func(t *testing.T) { ExpectErrors(t, ` + input Input { + f1: String + } + + type Query { + field(arg: Input): String + }`, ` { field(arg: { f1: "value", f1: "value" }) } @@ -74,6 +119,13 @@ func TestUniqueInputFieldNamesRule(t *testing.T) { t.Run("many duplicate input object fields", func(t *testing.T) { ExpectErrors(t, ` + input Input { + f1: String + } + + type Query { + field(arg: Input): String + }`, ` { field(arg: { f1: "value", f1: "value", f1: "value" }) } @@ -97,6 +149,17 @@ func TestUniqueInputFieldNamesRule(t *testing.T) { t.Run("nested duplicate input object fields", func(t *testing.T) { ExpectErrors(t, ` + input Nested { + f2: String + } + + input Input { + f1: Nested + } + + type Query { + field(arg: Input): String + }`, ` { field(arg: { f1: {f2: "value", f2: "value" }}) } diff --git a/pkg/astvalidation/reference/testsgo/ValuesOfCorrectTypeRule_test.go b/pkg/astvalidation/reference/testsgo/ValuesOfCorrectTypeRule_test.go index e92a90a5c5..350011c965 100755 --- a/pkg/astvalidation/reference/testsgo/ValuesOfCorrectTypeRule_test.go +++ b/pkg/astvalidation/reference/testsgo/ValuesOfCorrectTypeRule_test.go @@ -5,7 +5,6 @@ import ( ) func TestValuesOfCorrectTypeRule(t *testing.T) { - t.Skip() ExpectErrors := func(t *testing.T, queryStr string) ResultCompare { return ExpectValidationErrors(t, ValuesOfCorrectTypeRule, queryStr) @@ -489,6 +488,23 @@ func TestValuesOfCorrectTypeRule(t *testing.T) { doesKnowCommand(dogCommand: "SIT") } } + `)([]Err{ + { + message: `Enum "DogCommand" cannot represent non-enum value: "SIT".`, + locations: []Loc{{line: 4, column: 41}}, + }, + }) + }) + + t.Run("String into Enum with suggestions", func(t *testing.T) { + t.Skip(NotSupportedSuggestionsSkipMsg) + + ExpectErrors(t, ` + { + dog { + doesKnowCommand(dogCommand: "SIT") + } + } `)([]Err{ { message: `Enum "DogCommand" cannot represent non-enum value: "SIT". Did you mean the enum value "SIT"?`, @@ -534,6 +550,23 @@ func TestValuesOfCorrectTypeRule(t *testing.T) { doesKnowCommand(dogCommand: sit) } } + `)([]Err{ + { + message: `Value "sit" does not exist in "DogCommand" enum.`, + locations: []Loc{{line: 4, column: 41}}, + }, + }) + }) + + t.Run("Different case Enum Value into Enum with suggestions", func(t *testing.T) { + t.Skip(NotSupportedSuggestionsSkipMsg) + + ExpectErrors(t, ` + { + dog { + doesKnowCommand(dogCommand: sit) + } + } `)([]Err{ { message: `Value "sit" does not exist in "DogCommand" enum. Did you mean the enum value "SIT"?`, @@ -906,6 +939,26 @@ func TestValuesOfCorrectTypeRule(t *testing.T) { }) } } + `)([]Err{ + { + message: `Field "invalidField" is not defined by type "ComplexInput".`, + locations: []Loc{{line: 6, column: 15}}, + }, + }) + }) + + t.Run("Partial object, unknown field arg with suggestions", func(t *testing.T) { + t.Skip(NotSupportedSuggestionsSkipMsg) + + ExpectErrors(t, ` + { + complicatedArgs { + complexArgField(complexArg: { + requiredField: true, + invalidField: "value" + }) + } + } `)([]Err{ { message: `Field "invalidField" is not defined by type "ComplexInput". Did you mean "intField"?`, diff --git a/pkg/astvalidation/reference/testsgo/VariablesAreInputTypesRule_test.go b/pkg/astvalidation/reference/testsgo/VariablesAreInputTypesRule_test.go index 460797d292..b0b24b9bd4 100755 --- a/pkg/astvalidation/reference/testsgo/VariablesAreInputTypesRule_test.go +++ b/pkg/astvalidation/reference/testsgo/VariablesAreInputTypesRule_test.go @@ -5,7 +5,6 @@ import ( ) func TestVariablesAreInputTypesRule(t *testing.T) { - t.Skip() ExpectErrors := func(t *testing.T, queryStr string) ResultCompare { return ExpectValidationErrors(t, VariablesAreInputTypesRule, queryStr) diff --git a/pkg/astvalidation/reference/testsgo/VariablesInAllowedPositionRule_test.go b/pkg/astvalidation/reference/testsgo/VariablesInAllowedPositionRule_test.go index 5020652b49..c5415c9580 100755 --- a/pkg/astvalidation/reference/testsgo/VariablesInAllowedPositionRule_test.go +++ b/pkg/astvalidation/reference/testsgo/VariablesInAllowedPositionRule_test.go @@ -5,7 +5,6 @@ import ( ) func TestVariablesInAllowedPositionRule(t *testing.T) { - t.Skip() ExpectErrors := func(t *testing.T, queryStr string) ResultCompare { return ExpectValidationErrors(t, VariablesInAllowedPositionRule, queryStr) @@ -139,7 +138,7 @@ func TestVariablesInAllowedPositionRule(t *testing.T) { query Query($boolVar: Boolean = false) { complicatedArgs { - complexArgField(complexArg: {requiredArg: $boolVar}) + complexArgField(complexArg: {requiredField: $boolVar}) # requiredArg is not exists in complex input } } `) @@ -195,8 +194,6 @@ func TestVariablesInAllowedPositionRule(t *testing.T) { }) t.Run("Int => Int! within nested fragment", func(t *testing.T) { - t.Skip("Panic: possibly unsupported case") - ExpectErrors(t, ` fragment outerFrag on ComplicatedArgs { ...nonNullIntArgFieldFrag diff --git a/pkg/astvalidation/reference/testsgo/harness_test.go b/pkg/astvalidation/reference/testsgo/harness_test.go index 6ffcdc834d..7c8222de6c 100644 --- a/pkg/astvalidation/reference/testsgo/harness_test.go +++ b/pkg/astvalidation/reference/testsgo/harness_test.go @@ -15,7 +15,7 @@ import ( ) const ( - // NotSupportedSuggestionsSkipMsg = "Suggestions is not supported" + NotSupportedSuggestionsSkipMsg = "Suggestions is not supported" RuleHasNoMapping = `Validation rule: "%s" has no mapped rule` ) @@ -27,6 +27,7 @@ const ( KnownArgumentNamesOnDirectivesRule = "KnownArgumentNamesOnDirectivesRule" KnownDirectivesRule = "KnownDirectivesRule" KnownTypeNamesRule = "KnownTypeNamesRule" + KnownTypeNamesOperationRule = "KnownTypeNamesOperationRule" LoneAnonymousOperationRule = "LoneAnonymousOperationRule" NoUndefinedVariablesRule = "NoUndefinedVariablesRule" NoUnusedVariablesRule = "NoUnusedVariablesRule" @@ -63,7 +64,7 @@ const ( var rulesMap = map[string][]astvalidation.Rule{ ExecutableDefinitionsRule: {astvalidation.DocumentContainsExecutableOperation()}, FieldsOnCorrectTypeRule: {astvalidation.FieldSelections()}, - KnownArgumentNamesRule: {}, // {astvalidation.KnownArguments()}, + KnownArgumentNamesRule: {astvalidation.KnownArguments()}, KnownArgumentNamesOnDirectivesRule: {}, KnownDirectivesRule: {astvalidation.DirectivesAreDefined()}, KnownTypeNamesRule: {astvalidation.KnownTypeNames()}, @@ -84,7 +85,8 @@ var rulesMap = map[string][]astvalidation.Rule{ UniqueVariableNamesRule: {astvalidation.VariableUniqueness()}, ValuesOfCorrectTypeRule: {astvalidation.Values()}, VariablesAreInputTypesRule: {astvalidation.VariablesAreInputTypes()}, - VariablesInAllowedPositionRule: {astvalidation.ValidArguments()}, + KnownTypeNamesOperationRule: {astvalidation.VariablesAreInputTypes(), astvalidation.Fragments()}, + VariablesInAllowedPositionRule: {astvalidation.ValidArguments(), astvalidation.Values()}, // fragments rules FragmentsOnCompositeTypesRule: {astvalidation.Fragments()}, diff --git a/pkg/astvalidation/reference/testsgo/test_schema.graphql b/pkg/astvalidation/reference/testsgo/test_schema.graphql index c2c4bfd2e8..308893229c 100644 --- a/pkg/astvalidation/reference/testsgo/test_schema.graphql +++ b/pkg/astvalidation/reference/testsgo/test_schema.graphql @@ -114,6 +114,7 @@ type QueryRoot { dogOrHuman: DogOrHuman humanOrAlien: HumanOrAlien complicatedArgs: ComplicatedArgs + field(strArg: String, boolListArg: [Boolean!]!, inputArg: ComplexInput): String # need to be here to not have an error of not existing field in visitor in TestVariablesAreInputTypesRule } schema { diff --git a/pkg/astvisitor/visitor.go b/pkg/astvisitor/visitor.go index 955b760296..50b89b4b75 100644 --- a/pkg/astvisitor/visitor.go +++ b/pkg/astvisitor/visitor.go @@ -3614,6 +3614,13 @@ func (w *Walker) FieldDefinition(field int) (definition int, exists bool) { return w.definition.NodeFieldDefinitionByName(w.EnclosingTypeDefinition, fieldName) } +func (w *Walker) Ancestor() ast.Node { + if len(w.Ancestors) == 0 { + return ast.InvalidNode + } + return w.Ancestors[len(w.Ancestors)-1] +} + func (w *Walker) AncestorNameBytes() ast.ByteSlice { if len(w.Ancestors) == 0 { return nil diff --git a/pkg/operationreport/externalerror.go b/pkg/operationreport/externalerror.go index 44a3a9aff1..78c92e782c 100644 --- a/pkg/operationreport/externalerror.go +++ b/pkg/operationreport/externalerror.go @@ -5,6 +5,28 @@ import ( "github.com/wundergraph/graphql-go-tools/pkg/ast" "github.com/wundergraph/graphql-go-tools/pkg/graphqlerrors" + "github.com/wundergraph/graphql-go-tools/pkg/lexer/position" +) + +const ( + NotCompatibleTypeErrMsg = "%s cannot represent value: %s" + NotStringErrMsg = "%s cannot represent a non string value: %s" + NotIntegerErrMsg = "%s cannot represent non-integer value: %s" + BigIntegerErrMsg = "%s cannot represent non 32-bit signed integer value: %s" + NotFloatErrMsg = "%s cannot represent non numeric value: %s" + NotBooleanErrMsg = "%s cannot represent a non boolean value: %s" + NotIDErrMsg = "%s cannot represent a non-string and non-integer value: %s" + NotEnumErrMsg = `Enum "%s" cannot represent non-enum value: %s.` + NotAnEnumMemberErrMsg = `Value "%s" does not exist in "%s" enum.` + NullValueErrMsg = `Expected value of type "%s", found null.` + UnknownArgumentOnDirectiveErrMsg = `Unknown argument "%s" on directive "@%s".` + UnknownArgumentOnFieldErrMsg = `Unknown argument "%s" on field "%s.%s".` + UnknownTypeErrMsg = `Unknown type "%s".` + VariableIsNotInputTypeErrMsg = `Variable "$%s" cannot be non-input type "%s".` + MissingRequiredFieldOfInputObjectErrMsg = `Field "%s.%s" of required type "%s" was not provided.` + UnknownFieldOfInputObjectErrMsg = `Field "%s" is not defined by type "%s".` + DuplicatedFieldInputObjectErrMsg = `There can be only one input field named "%s".` + ValueIsNotAnInputObjectTypeErrMsg = `Expected value of type "%s", found %s.` ) type ExternalError struct { @@ -13,6 +35,15 @@ type ExternalError struct { Locations []graphqlerrors.Location `json:"locations"` } +func LocationsFromPosition(position position.Position) []graphqlerrors.Location { + return []graphqlerrors.Location{ + { + Line: position.LineStart, + Column: position.CharStart, + }, + } +} + func ErrDocumentDoesntContainExecutableOperation() (err ExternalError) { err.Message = "document doesn't contain any executable operation" return @@ -29,7 +60,7 @@ func ErrFieldNameMustBeUniqueOnType(fieldName, typeName ast.ByteSlice) (err Exte } func ErrTypeUndefined(typeName ast.ByteSlice) (err ExternalError) { - err.Message = fmt.Sprintf("type not defined: %s", typeName) + err.Message = fmt.Sprintf(UnknownTypeErrMsg, typeName) return err } @@ -124,13 +155,147 @@ func ErrMissingFieldSelectionOnNonScalar(fieldName, enclosingTypeName ast.ByteSl return err } -func ErrArgumentNotDefinedOnNode(argName, node ast.ByteSlice) (err ExternalError) { - err.Message = fmt.Sprintf("argument: %s not defined on node: %s", argName, node) +func ErrArgumentNotDefinedOnDirective(argName, directiveName ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(UnknownArgumentOnDirectiveErrMsg, argName, directiveName) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrUnknownType(typeName ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(UnknownTypeErrMsg, typeName) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrMissingRequiredFieldOfInputObject(objName, fieldName, typeName ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(MissingRequiredFieldOfInputObjectErrMsg, objName, fieldName, typeName) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrUnknownFieldOfInputObject(objName, fieldName ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(UnknownFieldOfInputObjectErrMsg, objName, fieldName) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrDuplicatedFieldInputObject(fieldName ast.ByteSlice, first, duplicated position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(DuplicatedFieldInputObjectErrMsg, fieldName) + + err.Locations = []graphqlerrors.Location{ + { + Line: first.LineStart, + Column: first.CharStart, + }, + { + Line: duplicated.LineStart, + Column: duplicated.CharStart, + }, + } + + return err +} + +func ErrArgumentNotDefinedOnField(argName, typeName, fieldName ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(UnknownArgumentOnFieldErrMsg, argName, typeName, fieldName) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrNullValueDoesntSatisfyInputValueDefinition(inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NullValueErrMsg, inputType) + err.Locations = LocationsFromPosition(position) + return err } -func ErrValueDoesntSatisfyInputValueDefinition(value, inputType ast.ByteSlice) (err ExternalError) { - err.Message = fmt.Sprintf("value: %s doesn't satisfy inputType: %s", value, inputType) +func ErrValueDoesntSatisfyEnum(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotEnumErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueDoesntExistsInEnum(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotAnEnumMemberErrMsg, value, inputType) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueDoesntSatisfyType(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotCompatibleTypeErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueIsNotAnInputObjectType(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(ValueIsNotAnInputObjectTypeErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueDoesntSatisfyString(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotStringErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueDoesntSatisfyInt(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotIntegerErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrBigIntValueDoesntSatisfyInt(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(BigIntegerErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueDoesntSatisfyFloat(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotFloatErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueDoesntSatisfyBoolean(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotBooleanErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrValueDoesntSatisfyID(value, inputType ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(NotIDErrMsg, inputType, value) + err.Locations = LocationsFromPosition(position) + + return err +} + +func ErrVariableTypeDoesntSatisfyInputValueDefinition(value, inputType, expectedType ast.ByteSlice, valuePos, variableDefinitionPos position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(`Variable "%v" of type "%v" used in position expecting type "%v".`, value, inputType, expectedType) + err.Locations = []graphqlerrors.Location{ + { + Line: variableDefinitionPos.LineStart, + Column: variableDefinitionPos.CharStart, + }, + { + Line: valuePos.LineStart, + Column: valuePos.CharStart, + }, + } return err } @@ -154,8 +319,10 @@ func ErrVariableNotDefinedOnArgument(variableName, argumentName ast.ByteSlice) ( return err } -func ErrVariableOfTypeIsNoValidInputValue(variableName, ofTypeName ast.ByteSlice) (err ExternalError) { - err.Message = fmt.Sprintf("variable: %s of type: %s is no valid input value type", variableName, ofTypeName) +func ErrVariableOfTypeIsNoValidInputValue(variableName, ofTypeName ast.ByteSlice, position position.Position) (err ExternalError) { + err.Message = fmt.Sprintf(VariableIsNotInputTypeErrMsg, variableName, ofTypeName) + err.Locations = LocationsFromPosition(position) + return err }