diff --git a/README.md b/README.md index d0b755bf1..dca1d6d62 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Tanka GraphQL library ## Features * Execute queries, mutations and subscriptions -* Validation (Dirty port from graphql-dotnet). Validation is the slowest part of the execution and needs to be redone. +* Validation (new implementation in v0.3.0) * SignalR hub for streaming queries, mutations and subscriptions * ApolloLink for the provided SignalR hub diff --git a/docs/1-execution/01-validation.md b/docs/1-execution/01-validation.md index 26f39a38f..0b22a482b 100644 --- a/docs/1-execution/01-validation.md +++ b/docs/1-execution/01-validation.md @@ -5,14 +5,22 @@ ### Execution -[{tanka.graphql.tests.validation.ValidatorFacts.ValidateAsync}] +[{tanka.graphql.tests.validation.ValidatorFacts.Validate}] ### Rules -5.1.1 Executable Definitions +Execution + +[{tanka.graphql.validation.ExecutionRules.All}] + +Rules not implemented +[{tanka.graphql.tests.validation.ValidatorFacts.Rule_532_Field_Selection_Merging}] +[{tanka.graphql.tests.validation.ValidatorFacts.Rule_572_DirectivesAreInValidLocations_valid1}] +[{tanka.graphql.tests.validation.ValidatorFacts.Rule_583_AllVariableUsesDefined}] +[{tanka.graphql.tests.validation.ValidatorFacts.Rule_584_AllVariablesUsed_valid1}] +[{tanka.graphql.tests.validation.ValidatorFacts.Rule_585_AllVariableUsagesAreAllowed_valid1}] + -[{tanka.graphql.tests.validation.ValidatorFacts.Rule_511_Executable_Definitions}] ->TODO: Rest of the rules. Issue [#16](https://github.com/pekkah/tanka-graphql/issues/16) diff --git a/docs/_template.html b/docs/_template.html index 973f1adc7..4eef87e14 100644 --- a/docs/_template.html +++ b/docs/_template.html @@ -61,7 +61,7 @@ Tanka GraphQL diff --git a/src/graphql.benchmarks/Benchmarks.cs b/src/graphql.benchmarks/Benchmarks.cs index 2a0f64058..4c1025935 100644 --- a/src/graphql.benchmarks/Benchmarks.cs +++ b/src/graphql.benchmarks/Benchmarks.cs @@ -20,6 +20,7 @@ public class Benchmarks private ISchema _schema; private GraphQLDocument _mutation; private GraphQLDocument _subscription; + private IEnumerable _defaultRulesMap; [GlobalSetup] public async Task Setup() @@ -28,8 +29,9 @@ public async Task Setup() _query = Utils.InitializeQuery(); _mutation = Utils.InitializeMutation(); _subscription = Utils.InitializeSubscription(); + _defaultRulesMap = ExecutionRules.All; } - + [Benchmark] public async Task Query_with_defaults() { @@ -135,11 +137,12 @@ public async Task Subscribe_without_validation_and_get_value() var value = result.Source.Receive(); AssertResult(value.Errors); } - + [Benchmark] - public async Task Validate_query_with_defaults() + public void Validate_query_with_defaults() { - var result = await Validator.ValidateAsync( + var result = Validator.Validate( + _defaultRulesMap, _schema, _query); diff --git a/src/graphql/Error.cs b/src/graphql/Error.cs index 163543406..6e7cfd604 100644 --- a/src/graphql/Error.cs +++ b/src/graphql/Error.cs @@ -14,7 +14,7 @@ public Error(string message) public string Message { get; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public List Locations { get; set; } + public List Locations { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public List Path { get; set; } diff --git a/src/graphql/Executor.cs b/src/graphql/Executor.cs index 90b31aeb7..6930b429a 100644 --- a/src/graphql/Executor.cs +++ b/src/graphql/Executor.cs @@ -41,7 +41,7 @@ public static async Task ExecuteAsync( if (!validationResult.IsValid) return new ExecutionResult { - Errors = validationResult.Errors.Select(e => new Error(e.Message)) + Errors = validationResult.Errors.Select(e =>e.ToError()) }; ExecutionResult executionResult; @@ -141,10 +141,11 @@ public static async Task SubscribeAsync( if (options.Validate) { await extensions.BeginValidationAsync(); - validationResult = await Validator.ValidateAsync( + validationResult = Validator.Validate( + ExecutionRules.All, options.Schema, document, - coercedVariableValues).ConfigureAwait(false); + coercedVariableValues); logger.ValidationResult(validationResult); diff --git a/src/graphql/graphql.csproj b/src/graphql/graphql.csproj index d7529ba10..2da33982c 100644 --- a/src/graphql/graphql.csproj +++ b/src/graphql/graphql.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -20,5 +20,4 @@ - diff --git a/src/graphql/language/Visitor.cs b/src/graphql/language/Visitor.cs new file mode 100644 index 000000000..cda119152 --- /dev/null +++ b/src/graphql/language/Visitor.cs @@ -0,0 +1,294 @@ +using System.Collections.Generic; +using GraphQLParser.AST; + +namespace tanka.graphql.language +{ + public class Visitor + { + private readonly List _fragments = + new List(); + + public IEnumerable Fragments => _fragments; + + public virtual GraphQLName BeginVisitAlias(GraphQLName alias) + { + return alias; + } + + public virtual GraphQLArgument BeginVisitArgument(GraphQLArgument argument) + { + if (argument.Name != null) + BeginVisitNode(argument.Name); + if (argument.Value != null) + BeginVisitNode(argument.Value); + return EndVisitArgument(argument); + } + + public virtual IEnumerable BeginVisitArguments( + IEnumerable arguments) + { + foreach (ASTNode node in arguments) + BeginVisitNode(node); + return arguments; + } + + public virtual GraphQLScalarValue BeginVisitBooleanValue( + GraphQLScalarValue value) + { + return value; + } + + public virtual GraphQLDirective BeginVisitDirective(GraphQLDirective directive) + { + if (directive.Name != null) + BeginVisitNode(directive.Name); + if (directive.Arguments != null) + BeginVisitArguments(directive.Arguments); + return directive; + } + + public virtual IEnumerable BeginVisitDirectives( + IEnumerable directives) + { + foreach (ASTNode directive in directives) + BeginVisitNode(directive); + return directives; + } + + public virtual GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) + { + return value; + } + + public virtual GraphQLFieldSelection BeginVisitFieldSelection( + GraphQLFieldSelection selection) + { + BeginVisitNode(selection.Name); + if (selection.Alias != null) + BeginVisitAlias((GraphQLName) BeginVisitNode(selection.Alias)); + if (selection.Arguments != null) + BeginVisitArguments(selection.Arguments); + if (selection.SelectionSet != null) + BeginVisitNode(selection.SelectionSet); + if (selection.Directives != null) + BeginVisitDirectives(selection.Directives); + return EndVisitFieldSelection(selection); + } + + public virtual GraphQLScalarValue BeginVisitFloatValue( + GraphQLScalarValue value) + { + return value; + } + + public virtual GraphQLFragmentDefinition BeginVisitFragmentDefinition( + GraphQLFragmentDefinition node) + { + BeginVisitNode(node.TypeCondition); + BeginVisitNode(node.Name); + if (node.SelectionSet != null) + BeginVisitNode(node.SelectionSet); + + _fragments.Add(node); + return node; + } + + public virtual GraphQLFragmentSpread BeginVisitFragmentSpread( + GraphQLFragmentSpread fragmentSpread) + { + BeginVisitNode(fragmentSpread.Name); + return fragmentSpread; + } + + public virtual GraphQLInlineFragment BeginVisitInlineFragment( + GraphQLInlineFragment inlineFragment) + { + if (inlineFragment.TypeCondition != null) + BeginVisitNode(inlineFragment.TypeCondition); + if (inlineFragment.Directives != null) + BeginVisitDirectives(inlineFragment.Directives); + if (inlineFragment.SelectionSet != null) + BeginVisitSelectionSet(inlineFragment.SelectionSet); + return inlineFragment; + } + + public virtual GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) + { + return value; + } + + public virtual GraphQLName BeginVisitName(GraphQLName name) + { + return name; + } + + public virtual GraphQLNamedType BeginVisitNamedType( + GraphQLNamedType typeCondition) + { + return typeCondition; + } + + public virtual ASTNode BeginVisitNode(ASTNode node) + { + switch (node.Kind) + { + case ASTNodeKind.Name: + return BeginVisitName((GraphQLName) node); + case ASTNodeKind.OperationDefinition: + return BeginVisitOperationDefinition((GraphQLOperationDefinition) node); + case ASTNodeKind.VariableDefinition: + return BeginVisitVariableDefinition((GraphQLVariableDefinition) node); + case ASTNodeKind.Variable: + return BeginVisitVariable((GraphQLVariable) node); + case ASTNodeKind.SelectionSet: + return BeginVisitSelectionSet((GraphQLSelectionSet) node); + case ASTNodeKind.Field: + return BeginVisitNonIntrospectionFieldSelection((GraphQLFieldSelection) node); + case ASTNodeKind.Argument: + return BeginVisitArgument((GraphQLArgument) node); + case ASTNodeKind.FragmentSpread: + return BeginVisitFragmentSpread((GraphQLFragmentSpread) node); + case ASTNodeKind.InlineFragment: + return BeginVisitInlineFragment((GraphQLInlineFragment) node); + case ASTNodeKind.FragmentDefinition: + return BeginVisitFragmentDefinition((GraphQLFragmentDefinition) node); + case ASTNodeKind.IntValue: + return BeginVisitIntValue((GraphQLScalarValue) node); + case ASTNodeKind.FloatValue: + return BeginVisitFloatValue((GraphQLScalarValue) node); + case ASTNodeKind.StringValue: + return BeginVisitStringValue((GraphQLScalarValue) node); + case ASTNodeKind.BooleanValue: + return BeginVisitBooleanValue((GraphQLScalarValue) node); + case ASTNodeKind.EnumValue: + return BeginVisitEnumValue((GraphQLScalarValue) node); + case ASTNodeKind.ListValue: + return BeginVisitListValue((GraphQLListValue) node); + case ASTNodeKind.ObjectValue: + return BeginVisitObjectValue((GraphQLObjectValue) node); + case ASTNodeKind.ObjectField: + return BeginVisitObjectField((GraphQLObjectField) node); + case ASTNodeKind.Directive: + return BeginVisitDirective((GraphQLDirective) node); + case ASTNodeKind.NamedType: + return BeginVisitNamedType((GraphQLNamedType) node); + default: + return null; + } + } + + public virtual GraphQLOperationDefinition BeginVisitOperationDefinition( + GraphQLOperationDefinition definition) + { + if (definition.Name != null) + BeginVisitNode(definition.Name); + if (definition.VariableDefinitions != null) + BeginVisitVariableDefinitions(definition.VariableDefinitions); + BeginVisitNode(definition.SelectionSet); + return EndVisitOperationDefinition(definition); + } + + public virtual GraphQLOperationDefinition EndVisitOperationDefinition( + GraphQLOperationDefinition definition) + { + return definition; + } + + public virtual GraphQLSelectionSet BeginVisitSelectionSet( + GraphQLSelectionSet selectionSet) + { + foreach (var selection in selectionSet.Selections) + BeginVisitNode(selection); + return selectionSet; + } + + public virtual GraphQLScalarValue BeginVisitStringValue( + GraphQLScalarValue value) + { + return value; + } + + public virtual GraphQLVariable BeginVisitVariable(GraphQLVariable variable) + { + if (variable.Name != null) + BeginVisitNode(variable.Name); + return EndVisitVariable(variable); + } + + public virtual GraphQLVariableDefinition BeginVisitVariableDefinition( + GraphQLVariableDefinition node) + { + BeginVisitNode(node.Type); + return node; + } + + public virtual IEnumerable BeginVisitVariableDefinitions( + IEnumerable variableDefinitions) + { + foreach (ASTNode variableDefinition in variableDefinitions) + BeginVisitNode(variableDefinition); + return variableDefinitions; + } + + public virtual GraphQLArgument EndVisitArgument(GraphQLArgument argument) + { + return argument; + } + + public virtual GraphQLFieldSelection EndVisitFieldSelection( + GraphQLFieldSelection selection) + { + return selection; + } + + public virtual GraphQLVariable EndVisitVariable(GraphQLVariable variable) + { + return variable; + } + + public virtual void Visit(GraphQLDocument ast) + { + foreach (var definition in ast.Definitions) + BeginVisitNode(definition); + } + + public virtual GraphQLObjectField BeginVisitObjectField( + GraphQLObjectField node) + { + BeginVisitNode(node.Name); + BeginVisitNode(node.Value); + return node; + } + + public virtual GraphQLObjectValue BeginVisitObjectValue( + GraphQLObjectValue node) + { + foreach (ASTNode field in node.Fields) + BeginVisitNode(field); + return EndVisitObjectValue(node); + } + + public virtual GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) + { + return node; + } + + public virtual GraphQLListValue EndVisitListValue(GraphQLListValue node) + { + return node; + } + + public virtual GraphQLListValue BeginVisitListValue(GraphQLListValue node) + { + foreach (ASTNode node1 in node.Values) + BeginVisitNode(node1); + + return EndVisitListValue(node); + } + + private ASTNode BeginVisitNonIntrospectionFieldSelection(GraphQLFieldSelection selection) + { + return BeginVisitFieldSelection(selection); + } + } +} \ No newline at end of file diff --git a/src/graphql/sdl/SdlReader.cs b/src/graphql/sdl/SdlReader.cs index 63f5e1d38..91662152a 100644 --- a/src/graphql/sdl/SdlReader.cs +++ b/src/graphql/sdl/SdlReader.cs @@ -21,27 +21,32 @@ public SdlReader(GraphQLDocument document, SchemaBuilder builder = null) public SchemaBuilder Read() { - var definitions = _document.Definitions; + var definitions = _document.Definitions.ToList(); foreach (var definition in definitions.OfType()) Scalar(definition); - foreach (var directiveDefinition in _document.Definitions.OfType()) + foreach (var directiveDefinition in definitions.OfType()) DirectiveType(directiveDefinition); - foreach (var definition in _document.Definitions.OfType()) + foreach (var definition in definitions.OfType()) InputObject(definition); - foreach (var definition in _document.Definitions.OfType()) + foreach (var definition in definitions.OfType()) Enum(definition); - foreach (var definition in _document.Definitions.OfType()) + foreach (var definition in definitions.OfType()) Interface(definition); - foreach (var definition in _document.Definitions.OfType()) + foreach (var definition in definitions.OfType()) Object(definition); - foreach (var definition in _document.Definitions.OfType()) + foreach (var definition in definitions.OfType()) + { + Union(definition); + } + + foreach (var definition in definitions.OfType()) Extend(definition); return _builder; diff --git a/src/graphql/type/Ast.cs b/src/graphql/type/Ast.cs index d1ba09965..5cdfe6e7f 100644 --- a/src/graphql/type/Ast.cs +++ b/src/graphql/type/Ast.cs @@ -7,6 +7,9 @@ public static class Ast { public static IType TypeFromAst(ISchema schema, GraphQLType type) { + if (type == null) + return null; + if (type.Kind == ASTNodeKind.NonNullType) { var innerType = TypeFromAst(schema, ((GraphQLNonNullType)type).Type); diff --git a/src/graphql/type/IAbstractType.cs b/src/graphql/type/IAbstractType.cs new file mode 100644 index 000000000..e27beffd7 --- /dev/null +++ b/src/graphql/type/IAbstractType.cs @@ -0,0 +1,7 @@ +namespace tanka.graphql.type +{ + public interface IAbstractType + { + bool IsPossible(ObjectType type); + } +} \ No newline at end of file diff --git a/src/graphql/type/IField.cs b/src/graphql/type/IField.cs index 8c372a3c3..050d91d85 100644 --- a/src/graphql/type/IField.cs +++ b/src/graphql/type/IField.cs @@ -14,5 +14,8 @@ public interface IField : IDirectives, IDeprecable, IDescribable Resolver Resolve { get; set; } Subscriber Subscribe {get; set; } + DirectiveInstance GetDirective(string name); + Argument GetArgument(string name); + bool HasArgument(string name); } } \ No newline at end of file diff --git a/src/graphql/type/ISchema.cs b/src/graphql/type/ISchema.cs index 597a5c0f1..159feb56d 100644 --- a/src/graphql/type/ISchema.cs +++ b/src/graphql/type/ISchema.cs @@ -29,5 +29,7 @@ public interface ISchema IEnumerable> GetInputFields(string type); InputObjectField GetInputField(string type, string name); + + IEnumerable GetPossibleTypes(IAbstractType abstractType); } } \ No newline at end of file diff --git a/src/graphql/type/InterfaceType.cs b/src/graphql/type/InterfaceType.cs index 9d22024fe..153e42f41 100644 --- a/src/graphql/type/InterfaceType.cs +++ b/src/graphql/type/InterfaceType.cs @@ -2,7 +2,7 @@ namespace tanka.graphql.type { - public class InterfaceType : ComplexType, IDirectives, IDescribable + public class InterfaceType : ComplexType, IDirectives, IDescribable, IAbstractType { public InterfaceType(string name, Meta meta = null) : base(name) @@ -25,5 +25,10 @@ public override string ToString() { return $"{Name}"; } + + public bool IsPossible(ObjectType type) + { + return type.Implements(this); + } } } \ No newline at end of file diff --git a/src/graphql/type/SchemaGraph.cs b/src/graphql/type/SchemaGraph.cs index 12249ae7e..cf6ea1bd3 100644 --- a/src/graphql/type/SchemaGraph.cs +++ b/src/graphql/type/SchemaGraph.cs @@ -80,7 +80,12 @@ public IQueryable QueryTypes(Predicate filter = null) where T : INamedT public DirectiveType GetDirective(string name) { - return _directiveTypes[name]; + if (_directiveTypes.TryGetValue(name, out var directive)) + { + return directive; + } + + return null; } public IQueryable QueryDirectives(Predicate filter = null) @@ -100,7 +105,16 @@ public IEnumerable> GetInputFields(string public InputObjectField GetInputField(string type, string name) { - return _inputFields[type][name]; + if (_inputFields.TryGetValue(type, out var fields)) + if (fields.TryGetValue(name, out var field)) + return field; + + return null; + } + + public IEnumerable GetPossibleTypes(IAbstractType abstractType) + { + return QueryTypes(abstractType.IsPossible); } public T GetNamedType(string name) where T : INamedType diff --git a/src/graphql/type/UnionType.cs b/src/graphql/type/UnionType.cs index c75fa11d7..8664c669a 100644 --- a/src/graphql/type/UnionType.cs +++ b/src/graphql/type/UnionType.cs @@ -3,11 +3,11 @@ namespace tanka.graphql.type { - public class UnionType : INamedType, IDescribable + public class UnionType : ComplexType, INamedType, IDescribable, IAbstractType { - public UnionType(string name, IEnumerable possibleTypes, Meta meta = null) + public UnionType(string name, IEnumerable possibleTypes, Meta meta = null) :base(name) { - Name = name; + //Name = name; Meta = meta ?? new Meta(); foreach (var possibleType in possibleTypes) @@ -25,7 +25,7 @@ public UnionType(string name, IEnumerable possibleTypes, Meta meta = public Meta Meta { get; } public string Description => Meta.Description; - public string Name { get; } + //public string Name { get; } public bool IsPossible(ObjectType type) { diff --git a/src/graphql/type/converters/BooleanConverter.cs b/src/graphql/type/converters/BooleanConverter.cs index 87e98047f..11b8c339f 100644 --- a/src/graphql/type/converters/BooleanConverter.cs +++ b/src/graphql/type/converters/BooleanConverter.cs @@ -33,8 +33,12 @@ public object ParseValue(object input) public object ParseLiteral(GraphQLScalarValue input) { - if (input.Kind == ASTNodeKind.BooleanValue || input.Kind == ASTNodeKind.StringValue || - input.Kind == ASTNodeKind.IntValue) + if (input.Kind == ASTNodeKind.NullValue) + { + return null; + } + + if (input.Kind == ASTNodeKind.BooleanValue) { var value = input.Value; @@ -50,7 +54,8 @@ public object ParseLiteral(GraphQLScalarValue input) return Convert.ToBoolean(input.Value, NumberFormatInfo.InvariantInfo); } - return null; + throw new FormatException( + $"Cannot coerce Bool value from '{input.Kind}'"); } } } \ No newline at end of file diff --git a/src/graphql/type/converters/DoubleConverter.cs b/src/graphql/type/converters/DoubleConverter.cs index 120efac9c..be761784c 100644 --- a/src/graphql/type/converters/DoubleConverter.cs +++ b/src/graphql/type/converters/DoubleConverter.cs @@ -24,6 +24,11 @@ public object ParseValue(object input) public object ParseLiteral(GraphQLScalarValue input) { + if (input.Kind == ASTNodeKind.NullValue) + { + return null; + } + if (input.Kind == ASTNodeKind.FloatValue || input.Kind == ASTNodeKind.IntValue) { if (input.Value == null) @@ -32,7 +37,8 @@ public object ParseLiteral(GraphQLScalarValue input) return Convert.ToDouble(input.Value, NumberFormatInfo.InvariantInfo); } - return null; + throw new FormatException( + $"Cannot coerce Long value from '{input.Kind}'"); } } } \ No newline at end of file diff --git a/src/graphql/type/converters/IdConverter.cs b/src/graphql/type/converters/IdConverter.cs index 7ee5a781e..d6193a93a 100644 --- a/src/graphql/type/converters/IdConverter.cs +++ b/src/graphql/type/converters/IdConverter.cs @@ -24,10 +24,16 @@ public object ParseValue(object input) public object ParseLiteral(GraphQLScalarValue input) { - if (input.Kind == ASTNodeKind.StringValue || input.Kind == ASTNodeKind.IntValue) + if (input.Kind == ASTNodeKind.NullValue) + { + return null; + } + + if (input.Kind == ASTNodeKind.StringValue) return input.Value; - return null; + throw new FormatException( + $"Cannot coerce Long value from '{input.Kind}'"); } } } \ No newline at end of file diff --git a/src/graphql/type/converters/LongConverter.cs b/src/graphql/type/converters/LongConverter.cs index 6a3c10adb..b74cb36c7 100644 --- a/src/graphql/type/converters/LongConverter.cs +++ b/src/graphql/type/converters/LongConverter.cs @@ -24,6 +24,11 @@ public object ParseValue(object input) public object ParseLiteral(GraphQLScalarValue input) { + if (input.Kind == ASTNodeKind.NullValue) + { + return null; + } + if (input.Kind == ASTNodeKind.IntValue) { if (input.Value == null) @@ -32,7 +37,8 @@ public object ParseLiteral(GraphQLScalarValue input) return Convert.ToInt64(input.Value); } - return null; + throw new FormatException( + $"Cannot coerce Long value from '{input.Kind}'"); } } } \ No newline at end of file diff --git a/src/graphql/type/converters/StringConverter.cs b/src/graphql/type/converters/StringConverter.cs index ca429870d..fbd0d1431 100644 --- a/src/graphql/type/converters/StringConverter.cs +++ b/src/graphql/type/converters/StringConverter.cs @@ -24,10 +24,16 @@ public object ParseValue(object input) public object ParseLiteral(GraphQLScalarValue input) { + if (input.Kind == ASTNodeKind.NullValue) + { + return null; + } + if (input.Kind == ASTNodeKind.StringValue) return input.Value; - return null; + throw new FormatException( + $"Cannot coerce Long value from '{input.Kind}'"); } } } \ No newline at end of file diff --git a/src/graphql/validation/BasicVisitor.cs b/src/graphql/validation/BasicVisitor.cs deleted file mode 100644 index 48b0110b7..000000000 --- a/src/graphql/validation/BasicVisitor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser; -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public class BasicVisitor : GraphQLAstVisitor - { - private readonly IEnumerable _visitors; - - public BasicVisitor(params INodeVisitor[] visitors) - { - _visitors = visitors; - } - - public override void Visit(GraphQLDocument ast) - { - //foreach (var visitor in _visitors) visitor.Enter(ast); - base.Visit(ast); - //foreach (var visitor in _visitors.Reverse()) visitor.Leave(ast); - } - - public override ASTNode BeginVisitNode(ASTNode node) - { - foreach (var visitor in _visitors) visitor.Enter(node); - - var result = base.BeginVisitNode(node); - - foreach (var visitor in _visitors.Reverse()) visitor.Leave(node); - - return result; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/CombineRule.cs b/src/graphql/validation/CombineRule.cs new file mode 100644 index 000000000..fb533f602 --- /dev/null +++ b/src/graphql/validation/CombineRule.cs @@ -0,0 +1,4 @@ +namespace tanka.graphql.validation +{ + public delegate void CombineRule(IRuleVisitorContext context, RuleVisitor rule); +} \ No newline at end of file diff --git a/src/graphql/validation/DebugNodeVisitor.cs b/src/graphql/validation/DebugNodeVisitor.cs deleted file mode 100644 index 6f724b27e..000000000 --- a/src/graphql/validation/DebugNodeVisitor.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Diagnostics; -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public class DebugNodeVisitor : INodeVisitor - { - public void Enter(ASTNode node) - { - Debug.WriteLine($"Entering {node}"); - } - - public void Leave(ASTNode node) - { - Debug.WriteLine($"Leaving {node}"); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/EnterLeaveListener.cs b/src/graphql/validation/EnterLeaveListener.cs deleted file mode 100644 index a2274f686..000000000 --- a/src/graphql/validation/EnterLeaveListener.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public class MatchingNodeListener - { - public Func Matches { get; set; } - - public Action Enter { get; set; } - - public Action Leave { get; set; } - } - - public class EnterLeaveListener : INodeVisitor - { - private readonly List _listeners = - new List(); - - public EnterLeaveListener(Action configure) - { - configure(this); - } - - public void Leave(ASTNode node) - { - foreach (var listener in _listeners.Where(l => l.Leave != null) - .Where(l => l.Matches(node))) - listener.Leave(node); - } - - public void Enter(ASTNode node) - { - try - { - foreach (var listener in _listeners.Where(l => l.Enter != null) - .Where(l => l.Matches(node))) - listener.Enter(node); - } - catch (InvalidCastException e) - { - throw; - } - } - - public void Match( - Action enter = null, - Action leave = null) where T : ASTNode - { - if (enter == null && leave == null) - throw new InvalidOperationException("Must provide an enter or leave function."); - - bool Matches(ASTNode n) - { - return n.GetType().IsAssignableFrom(typeof(T)); - } - - var listener = new MatchingNodeListener - { - Matches = Matches - }; - - if (enter != null) listener.Enter = n => enter((T) n); - if (leave != null) listener.Leave = n => leave((T) n); - - _listeners.Add(listener); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/Errors.cs deleted file mode 100644 index 37fc200b3..000000000 --- a/src/graphql/validation/Errors.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace tanka.graphql.validation -{ - public static class Errors - { - public const int R511ExecutableDefinitions = 1; - - public const int UserErrorCode = 1000; - } -} \ No newline at end of file diff --git a/src/graphql/validation/ExecutionRules.cs b/src/graphql/validation/ExecutionRules.cs new file mode 100644 index 000000000..17fc851b2 --- /dev/null +++ b/src/graphql/validation/ExecutionRules.cs @@ -0,0 +1,1077 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.execution; +using tanka.graphql.type; +using tanka.graphql.type.converters; + +namespace tanka.graphql.validation +{ + public static class ExecutionRules + { + public static IEnumerable All = new[] + { + R511ExecutableDefinitions(), + R5211OperationNameUniqueness(), + R5221LoneAnonymousOperation(), + R5511FragmentNameUniqueness(), + R5512FragmentSpreadTypeExistence(), + R5513FragmentsOnCompositeTypes(), + R5514FragmentsMustBeUsed(), + R5522FragmentSpreadsMustNotFormCycles(), + R5523FragmentSpreadIsPossible(), + R5231SingleRootField(), + R531FieldSelections(), + R533LeafFieldSelections(), + R541ArgumentNames(), + R542ArgumentUniqueness(), + R5421RequiredArguments(), + R561ValuesOfCorrectType(), + R562InputObjectFieldNames(), + R563InputObjectFieldUniqueness(), + R564InputObjectRequiredFields(), + R57Directives(), + R58Variables() + }; + + + /// + /// Formal Specification + /// For each definition definition in the document. + /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). + /// + public static CombineRule R511ExecutableDefinitions() + { + return (context, rule) => + { + rule.EnterDocument += document => + { + foreach (var definition in document.Definitions) + { + var valid = definition.Kind == ASTNodeKind.OperationDefinition + || definition.Kind == ASTNodeKind.FragmentDefinition; + + if (!valid) + context.Error( + ValidationErrorCodes.R511ExecutableDefinitions, + "GraphQL execution will only consider the " + + "executable definitions Operation and Fragment. " + + "Type system definitions and extensions are not " + + "executable, and are not considered during execution.", + definition); + } + }; + }; + } + + /// + /// Formal Specification + /// For each operation definition operation in the document. + /// Let operationName be the name of operation. + /// If operationName exists + /// Let operations be all operation definitions in the document named operationName. + /// operations must be a set of one. + /// + public static CombineRule R5211OperationNameUniqueness() + { + return (context, rule) => + { + var known = new List(); + rule.EnterOperationDefinition += definition => + { + var operationName = definition.Name?.Value; + + if (string.IsNullOrWhiteSpace(operationName)) + return; + + if (known.Contains(operationName)) + context.Error(ValidationErrorCodes.R5211OperationNameUniqueness, + "Each named operation definition must be unique within a " + + "document when referred to by its name.", + definition); + + known.Add(operationName); + }; + }; + } + + /// + /// Let operations be all operation definitions in the document. + /// Let anonymous be all anonymous operation definitions in the document. + /// If operations is a set of more than 1: + /// anonymous must be empty. + /// + public static CombineRule R5221LoneAnonymousOperation() + { + return (context, rule) => + { + rule.EnterDocument += document => + { + var operations = document.Definitions + .OfType() + .ToList(); + + var anonymous = operations + .Count(op => string.IsNullOrEmpty(op.Name?.Value)); + + if (operations.Count() > 1) + if (anonymous > 0) + context.Error( + ValidationErrorCodes.R5221LoneAnonymousOperation, + "GraphQL allows a short‐hand form for defining " + + "query operations when only that one operation exists in " + + "the document.", + operations); + }; + }; + } + + /// + /// For each subscription operation definition subscription in the document + /// Let subscriptionType be the root Subscription type in schema. + /// Let selectionSet be the top level selection set on subscription. + /// Let variableValues be the empty set. + /// Let groupedFieldSet be the result of CollectFields(subscriptionType, selectionSet, variableValues). + /// groupedFieldSet must have exactly one entry. + /// + public static CombineRule R5231SingleRootField() + { + return (context, rule) => + { + rule.EnterDocument += document => + { + var subscriptions = document.Definitions + .OfType() + .Where(op => op.Operation == OperationType.Subscription) + .ToList(); + + if (!subscriptions.Any()) + return; + + var schema = context.Schema; + //todo(pekka): should this report error? + if (schema.Subscription == null) + return; + + var subscriptionType = schema.Subscription; + foreach (var subscription in subscriptions) + { + var selectionSet = subscription.SelectionSet; + var variableValues = new Dictionary(); + + var groupedFieldSet = SelectionSets.CollectFields( + schema, + context.Document, + subscriptionType, + selectionSet, + variableValues); + + if (groupedFieldSet.Count != 1) + context.Error( + ValidationErrorCodes.R5231SingleRootField, + "Subscription operations must have exactly one root field.", + subscription); + } + }; + }; + } + + /// + /// For each selection in the document. + /// Let fieldName be the target field of selection + /// fieldName must be defined on type in scope + /// + public static CombineRule R531FieldSelections() + { + return (context, rule) => + { + rule.EnterFieldSelection += selection => + { + var fieldName = selection.Name.Value; + + if (fieldName == "__typename") + return; + + if (context.Tracker.GetFieldDef() == null) + context.Error( + ValidationErrorCodes.R531FieldSelections, + "The target field of a field selection must be defined " + + "on the scoped type of the selection set. There are no " + + "limitations on alias names.", + selection); + }; + }; + } + + /// + /// For each selection in the document + /// Let selectionType be the result type of selection + /// If selectionType is a scalar or enum: + /// The subselection set of that selection must be empty + /// If selectionType is an interface, union, or object + /// The subselection set of that selection must NOT BE empty + /// + public static CombineRule R533LeafFieldSelections() + { + return (context, rule) => + { + rule.EnterFieldSelection += selection => + { + var fieldName = selection.Name.Value; + + if (fieldName == "__typename") + return; + + var field = context.Tracker.GetFieldDef(); + + if (field != null) + { + var selectionType = field.Value.Field.Type.Unwrap(); + var hasSubSelection = selection.SelectionSet?.Selections?.Any(); + + if (selectionType is ScalarType && hasSubSelection == true) + context.Error( + ValidationErrorCodes.R533LeafFieldSelections, + "Field selections on scalars or enums are never " + + "allowed, because they are the leaf nodes of any GraphQL query.", + selection); + + if (selectionType is EnumType && hasSubSelection == true) + context.Error( + ValidationErrorCodes.R533LeafFieldSelections, + "Field selections on scalars or enums are never " + + "allowed, because they are the leaf nodes of any GraphQL query.", + selection); + + if (selectionType is ObjectType && hasSubSelection == null) + context.Error( + ValidationErrorCodes.R533LeafFieldSelections, + "Leaf selections on objects, interfaces, and unions " + + "without subfields are disallowed.", + selection); + + if (selectionType is InterfaceType && hasSubSelection == null) + context.Error( + ValidationErrorCodes.R533LeafFieldSelections, + "Leaf selections on objects, interfaces, and unions " + + "without subfields are disallowed.", + selection); + + if (selectionType is UnionType && hasSubSelection == null) + context.Error( + ValidationErrorCodes.R533LeafFieldSelections, + "Leaf selections on objects, interfaces, and unions " + + "without subfields are disallowed.", + selection); + } + }; + }; + } + + /// + /// For each argument in the document + /// Let argumentName be the Name of argument. + /// Let argumentDefinition be the argument definition provided by the parent field or definition named argumentName. + /// argumentDefinition must exist. + /// + public static CombineRule R541ArgumentNames() + { + return (context, rule) => + { + rule.EnterArgument += argument => + { + if (context.Tracker.GetArgument() == null) + context.Error( + ValidationErrorCodes.R541ArgumentNames, + "Every argument provided to a field or directive " + + "must be defined in the set of possible arguments of that " + + "field or directive.", + argument); + }; + }; + } + + /// + /// For each Field or Directive in the document. + /// Let arguments be the arguments provided by the Field or Directive. + /// Let argumentDefinitions be the set of argument definitions of that Field or Directive. + /// For each argumentDefinition in argumentDefinitions: + /// - Let type be the expected type of argumentDefinition. + /// - Let defaultValue be the default value of argumentDefinition. + /// - If type is Non‐Null and defaultValue does not exist: + /// - Let argumentName be the name of argumentDefinition. + /// - Let argument be the argument in arguments named argumentName + /// argument must exist. + /// - Let value be the value of argument. + /// value must not be the null literal. + /// + public static CombineRule R5421RequiredArguments() + { + IEnumerable> GetArgumentDefinitions(IRuleVisitorContext context) + { + var definitions = context.Tracker.GetDirective()?.Arguments + ?? context.Tracker.GetFieldDef()?.Field.Arguments; + + return definitions; + } + + void ValidateArguments(IEnumerable> keyValuePairs, + List graphQLArguments, IRuleVisitorContext ruleVisitorContext) + { + foreach (var argumentDefinition in keyValuePairs) + { + var type = argumentDefinition.Value.Type; + var defaultValue = argumentDefinition.Value.DefaultValue; + + if (!(type is NonNull nonNull) || defaultValue != null) + continue; + + var argumentName = argumentDefinition.Key; + var argument = graphQLArguments.SingleOrDefault(a => a.Name.Value == argumentName); + + if (argument == null) + { + ruleVisitorContext.Error( + ValidationErrorCodes.R5421RequiredArguments, + "Arguments is required. An argument is required " + + "if the argument type is non‐null and does not have a default " + + "value. Otherwise, the argument is optional. " + + $"Argument {argumentName} not given", + graphQLArguments); + + return; + } + + // We don't want to throw error here due to non-null so we use the WrappedType directly + var argumentValue = + Values.CoerceValue(ruleVisitorContext.Schema, argument.Value, nonNull.WrappedType); + if (argumentValue == null) + ruleVisitorContext.Error( + ValidationErrorCodes.R5421RequiredArguments, + "Arguments is required. An argument is required " + + "if the argument type is non‐null and does not have a default " + + "value. Otherwise, the argument is optional. " + + $"Value of argument {argumentName} cannot be null", + graphQLArguments); + } + } + + return (context, rule) => + { + rule.EnterFieldSelection += field => + { + var args = field.Arguments.ToList(); + var argumentDefinitions = GetArgumentDefinitions(context); + + //todo: should this produce error? + if (argumentDefinitions == null) + return; + + ValidateArguments(argumentDefinitions, args, context); + }; + rule.EnterDirective += directive => + { + var args = directive.Arguments.ToList(); + var argumentDefinitions = GetArgumentDefinitions(context); + + //todo: should this produce error? + if (argumentDefinitions == null) + return; + + ValidateArguments(argumentDefinitions, args, context); + }; + }; + } + + /// + /// For each argument in the Document. + /// Let argumentName be the Name of argument. + /// Let arguments be all Arguments named argumentName in the Argument Set which contains argument. + /// arguments must be the set containing only argument. + /// + public static CombineRule R542ArgumentUniqueness() + { + return (context, rule) => + { + var knownArgs = new List(); + rule.EnterFieldSelection += _ => knownArgs = new List(); + rule.EnterDirective += _ => knownArgs = new List(); + rule.EnterArgument += argument => + { + if (knownArgs.Contains(argument.Name.Value)) + context.Error( + ValidationErrorCodes.R542ArgumentUniqueness, + "Fields and directives treat arguments as a mapping of " + + "argument name to value. More than one argument with the same " + + "name in an argument set is ambiguous and invalid.", + argument); + + knownArgs.Add(argument.Name.Value); + }; + }; + } + + /// + /// For each fragment definition fragment in the document + /// Let fragmentName be the name of fragment. + /// Let fragments be all fragment definitions in the document named fragmentName. + /// fragments must be a set of one. + /// + public static CombineRule R5511FragmentNameUniqueness() + { + return (context, rule) => + { + var knownFragments = new List(); + rule.EnterFragmentDefinition += fragment => + { + if (knownFragments.Contains(fragment.Name.Value)) + context.Error( + ValidationErrorCodes.R5511FragmentNameUniqueness, + "Fragment definitions are referenced in fragment spreads by name. To avoid " + + "ambiguity, each fragment’s name must be unique within a document.", + fragment); + + knownFragments.Add(fragment.Name.Value); + }; + }; + } + + /// + /// For each named spread namedSpread in the document + /// Let fragment be the target of namedSpread + /// The target type of fragment must be defined in the schema + /// + public static CombineRule R5512FragmentSpreadTypeExistence() + { + return (context, rule) => + { + rule.EnterFragmentDefinition += node => + { + var type = context.Tracker.GetCurrentType(); + + if (type == null) + context.Error( + ValidationErrorCodes.R5512FragmentSpreadTypeExistence, + "Fragments must be specified on types that exist in the schema. This " + + "applies for both named and inline fragments. ", + node); + }; + rule.EnterInlineFragment += node => + { + var type = context.Tracker.GetCurrentType(); + + if (type == null) + context.Error( + ValidationErrorCodes.R5512FragmentSpreadTypeExistence, + "Fragments must be specified on types that exist in the schema. This " + + "applies for both named and inline fragments. ", + node); + }; + }; + } + + /// + /// For each fragment defined in the document. + /// The target type of fragment must have kind UNION, INTERFACE, or OBJECT. + /// + public static CombineRule R5513FragmentsOnCompositeTypes() + { + return (context, rule) => + { + rule.EnterFragmentDefinition += node => + { + var type = context.Tracker.GetCurrentType(); + + if (type is UnionType) + return; + + if (type is ComplexType) + return; + + context.Error( + ValidationErrorCodes.R5513FragmentsOnCompositeTypes, + "Fragments can only be declared on unions, interfaces, and objects", + node); + }; + rule.EnterInlineFragment += node => + { + var type = context.Tracker.GetCurrentType(); + + if (type is UnionType) + return; + + if (type is ComplexType) + return; + + context.Error( + ValidationErrorCodes.R5513FragmentsOnCompositeTypes, + "Fragments can only be declared on unions, interfaces, and objects", + node); + }; + }; + } + + /// + /// For each fragment defined in the document. + /// fragment must be the target of at least one spread in the document + /// + public static CombineRule R5514FragmentsMustBeUsed() + { + return (context, rule) => + { + var fragments = new Dictionary(); + var fragmentSpreads = new List(); + + rule.EnterFragmentDefinition += fragment => { fragments.Add(fragment.Name.Value, fragment); }; + rule.EnterFragmentSpread += spread => { fragmentSpreads.Add(spread.Name.Value); }; + rule.LeaveDocument += document => + { + foreach (var fragment in fragments) + { + var name = fragment.Key; + if (!fragmentSpreads.Contains(name)) + context.Error( + ValidationErrorCodes.R5514FragmentsMustBeUsed, + "Defined fragments must be used within a document.", + fragment.Value); + } + }; + }; + } + + public static CombineRule R5522FragmentSpreadsMustNotFormCycles() + { + return (context, rule) => + { + var visitedFrags = new List(); + var spreadPath = new Stack(); + + // Position in the spread path + var spreadPathIndexByName = new Dictionary(); + + var fragments = context.Document.Definitions.OfType() + .ToList(); + + rule.EnterFragmentDefinition += node => + { + DetectCycleRecursive( + node, + spreadPath, + visitedFrags, + spreadPathIndexByName, + context, + fragments); + }; + }; + + string CycleErrorMessage(string fragName, string[] spreadNames) + { + var via = spreadNames.Any() ? " via " + string.Join(", ", spreadNames) : ""; + return "The graph of fragment spreads must not form any cycles including spreading itself. " + + "Otherwise an operation could infinitely spread or infinitely execute on cycles in the " + + "underlying data. " + + $"Cannot spread fragment \"{fragName}\" within itself {via}."; + } + + IEnumerable GetFragmentSpreads(GraphQLSelectionSet node) + { + var spreads = new List(); + + var setsToVisit = new Stack(new[] {node}); + + while (setsToVisit.Any()) + { + var set = setsToVisit.Pop(); + + foreach (var selection in set.Selections) + if (selection is GraphQLFragmentSpread spread) + spreads.Add(spread); + else if (selection is GraphQLFieldSelection fieldSelection) + if (fieldSelection.SelectionSet != null) + setsToVisit.Push(fieldSelection.SelectionSet); + } + + return spreads; + } + + void DetectCycleRecursive( + GraphQLFragmentDefinition fragment, + Stack spreadPath, + List visitedFrags, + Dictionary spreadPathIndexByName, + IRuleVisitorContext context, + List fragments) + { + var fragmentName = fragment.Name.Value; + if (visitedFrags.Contains(fragmentName)) + return; + + var spreadNodes = GetFragmentSpreads(fragment.SelectionSet) + .ToArray(); + + if (!spreadNodes.Any()) + return; + + spreadPathIndexByName[fragmentName] = spreadPath.Count; + + for (var i = 0; i < spreadNodes.Length; i++) + { + var spreadNode = spreadNodes[i]; + var spreadName = spreadNode.Name.Value; + var cycleIndex = spreadPathIndexByName.ContainsKey(spreadName) + ? spreadPathIndexByName[spreadName] + : default; + + spreadPath.Push(spreadNode); + + if (cycleIndex == null) + { + var spreadFragment = fragments.SingleOrDefault(f => f.Name.Value == spreadName); + + if (spreadFragment != null) + DetectCycleRecursive( + spreadFragment, + spreadPath, + visitedFrags, + spreadPathIndexByName, + context, + fragments); + } + else + { + var cyclePath = spreadPath.Skip(cycleIndex.Value).ToList(); + var fragmentNames = cyclePath.Take(cyclePath.Count() - 1) + .Select(s => s.Name.Value) + .ToArray(); + + context.Error( + ValidationErrorCodes.R5522FragmentSpreadsMustNotFormCycles, + CycleErrorMessage(spreadName, fragmentNames), + cyclePath); + } + + spreadPath.Pop(); + } + + spreadPathIndexByName[fragmentName] = null; + } + } + + /// + /// For each spread (named or inline) defined in the document. + /// Let fragment be the target of spread + /// Let fragmentType be the type condition of fragment + /// Let parentType be the type of the selection set containing spread + /// Let applicableTypes be the intersection of GetPossibleTypes(fragmentType) and GetPossibleTypes(parentType) + /// applicableTypes must not be empty. + /// + /// + public static CombineRule R5523FragmentSpreadIsPossible() + { + return (context, rule) => + { + var fragments = context.Document.Definitions.OfType() + .ToDictionary(f => f.Name.Value); + + rule.EnterFragmentSpread += node => + { + var fragment = fragments[node.Name.Value]; + var fragmentType = Ast.TypeFromAst(context.Schema, fragment.TypeCondition); + var parentType = context.Tracker.GetParentType(); + var applicableTypes = GetPossibleTypes(fragmentType, context.Schema) + .Intersect(GetPossibleTypes(parentType, context.Schema)); + + if (!applicableTypes.Any()) + context.Error( + ValidationErrorCodes.R5523FragmentSpreadIsPossible, + "Fragments are declared on a type and will only apply " + + "when the runtime object type matches the type condition. They " + + "also are spread within the context of a parent type. A fragment " + + "spread is only valid if its type condition could ever apply within " + + "the parent type.", + node); + }; + + rule.EnterInlineFragment += node => + { + var fragmentType = Ast.TypeFromAst(context.Schema, node.TypeCondition); + var parentType = context.Tracker.GetParentType(); + var applicableTypes = GetPossibleTypes(fragmentType, context.Schema) + .Intersect(GetPossibleTypes(parentType, context.Schema)); + + if (!applicableTypes.Any()) + context.Error( + ValidationErrorCodes.R5523FragmentSpreadIsPossible, + "Fragments are declared on a type and will only apply " + + "when the runtime object type matches the type condition. They " + + "also are spread within the context of a parent type. A fragment " + + "spread is only valid if its type condition could ever apply within " + + "the parent type.", + node); + }; + }; + + ObjectType[] GetPossibleTypes(IType type, ISchema schema) + { + switch (type) + { + case ObjectType objectType: + return new[] {objectType}; + case InterfaceType interfaceType: + return schema.GetPossibleTypes(interfaceType).ToArray(); + case UnionType unionType: + return schema.GetPossibleTypes(unionType).ToArray(); + default: return new ObjectType[] { }; + } + } + } + + public static CombineRule R561ValuesOfCorrectType() + { + return (context, rule) => + { + //todo: there's an astnodekind for nullvalue but no type + //rule.EnterNullValue += node => { }; + + rule.EnterListValue += node => + { + var type = context.Tracker.GetNullableType( + context.Tracker.GetParentInputType()); + + if (!(type is List)) IsValidScalar(context, node); + }; + rule.EnterObjectValue += node => + { + var type = context.Tracker.GetNamedType( + context.Tracker.GetInputType()); + + if (!(type is InputObjectType inputType)) + { + IsValidScalar(context, node); + // return false; + return; + } + + var fieldNodeMap = node.Fields.ToDictionary( + f => f.Name.Value); + + foreach (var fieldDef in context.Schema.GetInputFields( + inputType.Name)) + { + var fieldNode = fieldNodeMap.ContainsKey(fieldDef.Key); + if (!fieldNode && fieldDef.Value.Type is NonNull nonNull) + context.Error( + ValidationErrorCodes.R561ValuesOfCorrectType, + RequiredFieldMessage( + type.ToString(), + fieldDef.Key, + nonNull.ToString()), + node); + } + }; + rule.EnterObjectField += node => + { + var parentType = context.Tracker + .GetNamedType(context.Tracker.GetParentInputType()); + + var fieldType = context.Tracker.GetInputType(); + if (fieldType == null && parentType is InputObjectType) + context.Error( + ValidationErrorCodes.R561ValuesOfCorrectType, + UnknownFieldMessage( + parentType.ToString(), + node.Name.Value, + string.Empty), + node); + }; + rule.EnterEnumValue += node => + { + var maybeEnumType = context.Tracker.GetNamedType( + context.Tracker.GetInputType()); + + if (!(maybeEnumType is EnumType type)) + IsValidScalar(context, node); + else if (type.ParseValue(node.Value) == null) + context.Error( + ValidationErrorCodes.R561ValuesOfCorrectType, + BadValueMessage( + type.Name, + node.ToString(), + string.Empty)); + }; + rule.EnterIntValue += node => IsValidScalar(context, node); + rule.EnterFloatValue += node => IsValidScalar(context, node); + rule.EnterStringValue += node => IsValidScalar(context, node); + rule.EnterBooleanValue += node => IsValidScalar(context, node); + }; + + string BadValueMessage( + string typeName, + string valueName, + string message + ) + { + return $"Expected type {typeName}, found {valueName} " + + message; + } + + string RequiredFieldMessage( + string typeName, + string fieldName, + string fieldTypeName + ) + { + return $"Field {typeName}.{fieldName} of required type " + + $"{fieldTypeName} was not provided."; + } + + string UnknownFieldMessage( + string typeName, + string fieldName, + string message + ) + { + return $"Field {fieldName} is not defined by type {typeName} " + + message; + } + + void IsValidScalar( + IRuleVisitorContext context, + GraphQLValue node) + { + var locationType = context.Tracker.GetInputType(); + + if (locationType == null) + return; + + var maybeScalarType = context + .Tracker + .GetNamedType(locationType); + + if (!(maybeScalarType is IValueConverter type)) + { + context.Error( + ValidationErrorCodes.R561ValuesOfCorrectType, + BadValueMessage( + maybeScalarType?.ToString(), + node.ToString(), + string.Empty), + node); + + return; + } + + try + { + type.ParseLiteral((GraphQLScalarValue) node); + } + catch (Exception e) + { + context.Error( + ValidationErrorCodes.R561ValuesOfCorrectType, + BadValueMessage(locationType?.ToString(), + node.ToString(), + e.ToString()), + node); + } + } + } + + public static CombineRule R562InputObjectFieldNames() + { + return (context, rule) => + { + rule.EnterObjectField += inputField => + { + var inputFieldName = inputField.Name.Value; + + if (!(context.Tracker + .GetParentInputType() is InputObjectType parentType)) + return; + + var inputFieldDefinition = context.Schema + .GetInputField(parentType.Name, inputFieldName); + + if (inputFieldDefinition == null) + context.Error( + ValidationErrorCodes.R562InputObjectFieldNames, + "Every input field provided in an input object " + + "value must be defined in the set of possible fields of " + + "that input object’s expected type.", + inputField); + }; + }; + } + + public static CombineRule R563InputObjectFieldUniqueness() + { + return (context, rule) => + { + rule.EnterObjectValue += node => + { + var fields = node.Fields.ToList(); + + foreach (var inputField in fields) + { + var name = inputField.Name.Value; + if (fields.Count(f => f.Name.Value == name) > 1) + context.Error( + ValidationErrorCodes.R563InputObjectFieldUniqueness, + "Input objects must not contain more than one field " + + "of the same name, otherwise an ambiguity would exist which " + + "includes an ignored portion of syntax.", + fields.Where(f => f.Name.Value == name)); + } + }; + }; + } + + public static CombineRule R564InputObjectRequiredFields() + { + return (context, rule) => + { + rule.EnterObjectValue += node => + { + var inputObject = context.Tracker.GetInputType() as InputObjectType; + + if (inputObject == null) + return; + + var fields = node.Fields.ToDictionary(f => f.Name.Value); + var fieldDefinitions = context.Schema.GetInputFields(inputObject.Name); + + foreach (var fieldDefinition in fieldDefinitions) + { + var type = fieldDefinition.Value.Type; + var defaultValue = fieldDefinition.Value.DefaultValue; + + if (type is NonNull nonNull && defaultValue == null) + { + var fieldName = fieldDefinition.Key; + if (!fields.TryGetValue(fieldName, out var field)) + { + context.Error( + ValidationErrorCodes.R564InputObjectRequiredFields, + "Input object fields may be required. Much like a field " + + "may have required arguments, an input object may have required " + + "fields. An input field is required if it has a non‐null type and " + + "does not have a default value. Otherwise, the input object field " + + "is optional. " + + $"Field '{nonNull}.{fieldName}' is required.", + node); + + return; + } + + if (field.Value.Kind == ASTNodeKind.NullValue) + context.Error( + ValidationErrorCodes.R564InputObjectRequiredFields, + "Input object fields may be required. Much like a field " + + "may have required arguments, an input object may have required " + + "fields. An input field is required if it has a non‐null type and " + + "does not have a default value. Otherwise, the input object field " + + "is optional. " + + $"Field '{nonNull}.{field}' value cannot be null.", + node, field); + } + } + }; + }; + } + + /// + /// 5.7.1, 5.73 + /// + /// + public static CombineRule R57Directives() + { + return (context, rule) => + { + rule.EnterDirective += directive => + { + var directiveName = directive.Name.Value; + var directiveDefinition = context.Schema.GetDirective(directiveName); + + if (directiveDefinition == null) + context.Error( + ValidationErrorCodes.R57Directives, + "GraphQL servers define what directives they support. " + + "For each usage of a directive, the directive must be available " + + "on that server.", + directive); + }; + + rule.EnterOperationDefinition += node => CheckDirectives(context, node.Directives); + rule.EnterFieldSelection += node => CheckDirectives(context, node.Directives); + rule.EnterFragmentDefinition += node => CheckDirectives(context, node.Directives); + rule.EnterFragmentSpread += node => CheckDirectives(context, node.Directives); + rule.EnterInlineFragment += node => CheckDirectives(context, node.Directives); + }; + + // 5.7.3 + void CheckDirectives(IRuleVisitorContext context, IEnumerable directives) + { + var knownDirectives = new List(); + + foreach (var directive in directives) + { + if (knownDirectives.Contains(directive.Name.Value)) + context.Error( + ValidationErrorCodes.R57Directives, + "For each usage of a directive, the directive must be used in a " + + "location that the server has declared support for. " + + $"Directive '{directive.Name.Value}' is used multiple times on same location", + directive); + + knownDirectives.Add(directive.Name.Value); + } + } + } + + /// + /// 5.8.1, 5.8.2 + /// + /// + public static CombineRule R58Variables() + { + return (context, rule) => + { + rule.EnterOperationDefinition += node => + { + var knownVariables = new List(); + if (node.VariableDefinitions == null) + return; + + foreach (var variableUsage in node.VariableDefinitions) + { + var variable = variableUsage.Variable; + var variableName = variable.Name.Value; + + // 5.8.1 Variable Uniqueness + if (knownVariables.Contains(variableName)) + context.Error( + ValidationErrorCodes.R58Variables, + "If any operation defines more than one " + + "variable with the same name, it is ambiguous and " + + "invalid. It is invalid even if the type of the " + + "duplicate variable is the same.", + node); + + knownVariables.Add(variableName); + + // 5.8.2 + var variableType = Ast.TypeFromAst(context.Schema, variableUsage.Type); + if (!TypeIs.IsInputType(variableType)) + context.Error( + ValidationErrorCodes.R58Variables, + "Variables can only be input types. Objects, unions, " + + "and interfaces cannot be used as inputs.." + + $"Given variable type is '{variableType}'", + node); + } + }; + }; + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/INodeVisitor.cs b/src/graphql/validation/INodeVisitor.cs deleted file mode 100644 index db73f5d36..000000000 --- a/src/graphql/validation/INodeVisitor.cs +++ /dev/null @@ -1,11 +0,0 @@ -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public interface INodeVisitor - { - void Enter(ASTNode node); - - void Leave(ASTNode node); - } -} diff --git a/src/graphql/validation/IRuleVisitorContext.cs b/src/graphql/validation/IRuleVisitorContext.cs new file mode 100644 index 000000000..3306e32f0 --- /dev/null +++ b/src/graphql/validation/IRuleVisitorContext.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using GraphQLParser.AST; +using tanka.graphql.type; + +namespace tanka.graphql.validation +{ + public interface IRuleVisitorContext + { + ISchema Schema { get; } + + GraphQLDocument Document { get; } + + IDictionary VariableValues { get; } + + TypeTracker Tracker { get; } + + void Error(string code, string message, params ASTNode[] nodes); + + void Error(string code, string message, ASTNode node); + + void Error(string code, string message, IEnumerable nodes); + } +} \ No newline at end of file diff --git a/src/graphql/validation/IValidationRule.cs b/src/graphql/validation/IValidationRule.cs deleted file mode 100644 index d40e0a10a..000000000 --- a/src/graphql/validation/IValidationRule.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace tanka.graphql.validation -{ - public interface IValidationRule - { - INodeVisitor CreateVisitor(ValidationContext context); - } -} \ No newline at end of file diff --git a/src/graphql/validation/NodeVisitor.cs b/src/graphql/validation/NodeVisitor.cs new file mode 100644 index 000000000..f6ad10c4c --- /dev/null +++ b/src/graphql/validation/NodeVisitor.cs @@ -0,0 +1,6 @@ +using GraphQLParser.AST; + +namespace tanka.graphql.validation +{ + public delegate void NodeVisitor(T node) where T : ASTNode; +} \ No newline at end of file diff --git a/src/graphql/validation/RuleVisitor.cs b/src/graphql/validation/RuleVisitor.cs new file mode 100644 index 000000000..f2f6942ea --- /dev/null +++ b/src/graphql/validation/RuleVisitor.cs @@ -0,0 +1,83 @@ +using GraphQLParser.AST; + +namespace tanka.graphql.validation +{ + public class RuleVisitor + { + public NodeVisitor EnterAlias { get; set; } + + public NodeVisitor EnterArgument { get; set; } + + public NodeVisitor EnterBooleanValue { get; set; } + + public NodeVisitor EnterDirective { get; set; } + + public NodeVisitor EnterDirectives { get; set; } + + public NodeVisitor EnterDocument { get; set; } + + public NodeVisitor EnterEnumValue { get; set; } + + public NodeVisitor EnterFieldSelection { get; set; } + + public NodeVisitor EnterFloatValue { get; set; } + + public NodeVisitor EnterFragmentDefinition { get; set; } + + public NodeVisitor EnterFragmentSpread { get; set; } + + public NodeVisitor EnterInlineFragment { get; set; } + + public NodeVisitor EnterIntValue { get; set; } + + public NodeVisitor EnterListValue { get; set; } + + public NodeVisitor EnterName { get; set; } + + public NodeVisitor EnterNamedType{ get; set; } + + public NodeVisitor EnterNode{ get; set; } + + public NodeVisitor EnterObjectField{ get; set; } + + public NodeVisitor EnterObjectValue{ get; set; } + + public NodeVisitor EnterOperationDefinition{ get; set; } + + public NodeVisitor EnterSelectionSet{ get; set; } + + public NodeVisitor EnterStringValue{ get; set; } + + public NodeVisitor EnterVariable{ get; set; } + + public NodeVisitor EnterVariableDefinition{ get; set; } + + public NodeVisitor LeaveArgument{ get; set; } + + public NodeVisitor LeaveDirective{ get; set; } + + public NodeVisitor LeaveDocument{ get; set; } + + public NodeVisitor LeaveEnumValue{ get; set; } + + public NodeVisitor LeaveFieldSelection{ get; set; } + + public NodeVisitor LeaveFragmentDefinition{ get; set; } + + public NodeVisitor LeaveInlineFragment{ get; set; } + + public NodeVisitor LeaveListValue{ get; set; } + + public NodeVisitor LeaveObjectField{ get; set; } + + public NodeVisitor LeaveObjectValue{ get; set; } + + public NodeVisitor LeaveOperationDefinition{ get; set; } + + public NodeVisitor LeaveSelectionSet{ get; set; } + + public NodeVisitor LeaveVariable{ get; set; } + + public NodeVisitor LeaveVariableDefinition{ get; set; } + } +} \ No newline at end of file diff --git a/src/graphql/validation/RulesWalker.cs b/src/graphql/validation/RulesWalker.cs new file mode 100644 index 000000000..56f83a345 --- /dev/null +++ b/src/graphql/validation/RulesWalker.cs @@ -0,0 +1,399 @@ +using System.Collections.Generic; +using GraphQLParser.AST; +using tanka.graphql.language; +using tanka.graphql.type; + +namespace tanka.graphql.validation +{ + public class RulesWalker : Visitor, IRuleVisitorContext + { + private readonly List _errors = + new List(); + + public RulesWalker( + IEnumerable rules, + ISchema schema, + GraphQLDocument document, + Dictionary variableValues = null) + { + Schema = schema; + Document = document; + VariableValues = variableValues; + CreateVisitors(rules); + } + + public GraphQLDocument Document { get; } + + public IDictionary VariableValues { get; } + + public TypeTracker Tracker { get; protected set; } + + public ISchema Schema { get; } + + public void Error(string code, string message, params ASTNode[] nodes) + { + _errors.Add(new ValidationError(code, message, nodes)); + } + + public void Error(string code, string message, ASTNode node) + { + _errors.Add(new ValidationError(code, message, node)); + } + + public void Error(string code, string message, IEnumerable nodes) + { + _errors.Add(new ValidationError(code, message, nodes)); + } + + public ValidationResult Validate() + { + Visit(Document); + return BuildResult(); + } + + public override void Visit(GraphQLDocument document) + { + { + Tracker.EnterDocument?.Invoke(document); + } + + base.Visit(document); + + + { + Tracker.LeaveDocument?.Invoke(document); + } + } + + public override GraphQLName BeginVisitAlias(GraphQLName alias) + { + { + Tracker.EnterAlias?.Invoke(alias); + } + + return base.BeginVisitAlias(alias); + } + + public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) + { + { + Tracker.EnterArgument?.Invoke(argument); + } + + return base.BeginVisitArgument(argument); + } + + public override GraphQLScalarValue BeginVisitBooleanValue( + GraphQLScalarValue value) + { + { + Tracker.EnterBooleanValue?.Invoke(value); + } + + return base.BeginVisitBooleanValue(value); + } + + public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) + { + { + Tracker.EnterDirective?.Invoke(directive); + } + + var _ = base.BeginVisitDirective(directive); + + + { + Tracker.LeaveDirective?.Invoke(directive); + } + + return _; + } + + public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) + { + { + Tracker.EnterEnumValue?.Invoke(value); + } + + var _ = base.BeginVisitEnumValue(value); + + + { + Tracker.LeaveEnumValue?.Invoke(value); + } + + return _; + } + + public override GraphQLFieldSelection BeginVisitFieldSelection( + GraphQLFieldSelection selection) + { + { + Tracker.EnterFieldSelection?.Invoke(selection); + } + + return base.BeginVisitFieldSelection(selection); + } + + public override GraphQLScalarValue BeginVisitFloatValue( + GraphQLScalarValue value) + { + { + Tracker.EnterFloatValue?.Invoke(value); + } + + return base.BeginVisitFloatValue(value); + } + + public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( + GraphQLFragmentDefinition node) + { + { + Tracker.EnterFragmentDefinition?.Invoke(node); + } + + var result = base.BeginVisitFragmentDefinition(node); + + + { + Tracker.LeaveFragmentDefinition?.Invoke(node); + } + + return result; + } + + public override GraphQLFragmentSpread BeginVisitFragmentSpread( + GraphQLFragmentSpread fragmentSpread) + { + { + Tracker.EnterFragmentSpread?.Invoke(fragmentSpread); + } + + return base.BeginVisitFragmentSpread(fragmentSpread); + } + + public override GraphQLInlineFragment BeginVisitInlineFragment( + GraphQLInlineFragment inlineFragment) + { + { + Tracker.EnterInlineFragment?.Invoke(inlineFragment); + } + + var _ = base.BeginVisitInlineFragment(inlineFragment); + + + { + Tracker.LeaveInlineFragment?.Invoke(inlineFragment); + } + + return _; + } + + public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) + { + { + Tracker.EnterIntValue?.Invoke(value); + } + + return base.BeginVisitIntValue(value); + } + + public override GraphQLName BeginVisitName(GraphQLName name) + { + { + Tracker.EnterName?.Invoke(name); + } + + return base.BeginVisitName(name); + } + + public override GraphQLNamedType BeginVisitNamedType( + GraphQLNamedType typeCondition) + { + { + Tracker.EnterNamedType?.Invoke(typeCondition); + } + + return base.BeginVisitNamedType(typeCondition); + } + + public override GraphQLOperationDefinition BeginVisitOperationDefinition( + GraphQLOperationDefinition definition) + { + { + Tracker.EnterOperationDefinition?.Invoke(definition); + } + + return base.BeginVisitOperationDefinition(definition); + } + + public override GraphQLOperationDefinition EndVisitOperationDefinition( + GraphQLOperationDefinition definition) + { + { + Tracker.LeaveOperationDefinition?.Invoke(definition); + } + + return base.EndVisitOperationDefinition(definition); + } + + public override GraphQLSelectionSet BeginVisitSelectionSet( + GraphQLSelectionSet selectionSet) + { + { + Tracker.EnterSelectionSet?.Invoke(selectionSet); + } + + var _ = base.BeginVisitSelectionSet(selectionSet); + + + { + Tracker.LeaveSelectionSet?.Invoke(selectionSet); + } + + return _; + } + + public override GraphQLScalarValue BeginVisitStringValue( + GraphQLScalarValue value) + { + { + Tracker.EnterStringValue?.Invoke(value); + } + + return base.BeginVisitStringValue(value); + } + + public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) + { + { + Tracker.EnterVariable?.Invoke(variable); + } + + return base.BeginVisitVariable(variable); + } + + public override GraphQLVariableDefinition BeginVisitVariableDefinition( + GraphQLVariableDefinition node) + { + { + Tracker.EnterVariableDefinition?.Invoke(node); + } + + var _ = base.BeginVisitVariableDefinition(node); + + + { + Tracker.LeaveVariableDefinition?.Invoke(node); + } + + return _; + } + + public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) + { + { + Tracker.LeaveArgument?.Invoke(argument); + } + + return base.EndVisitArgument(argument); + } + + public override GraphQLFieldSelection EndVisitFieldSelection( + GraphQLFieldSelection selection) + { + { + Tracker.LeaveFieldSelection?.Invoke(selection); + } + + return base.EndVisitFieldSelection(selection); + } + + public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) + { + { + Tracker.EnterVariable?.Invoke(variable); + } + + return base.EndVisitVariable(variable); + } + + public override GraphQLObjectField BeginVisitObjectField( + GraphQLObjectField node) + { + { + Tracker.EnterObjectField?.Invoke(node); + } + + var _ = base.BeginVisitObjectField(node); + + + { + Tracker.LeaveObjectField?.Invoke(node); + } + + return _; + } + + public override GraphQLObjectValue BeginVisitObjectValue( + GraphQLObjectValue node) + { + { + Tracker.EnterObjectValue?.Invoke(node); + } + + return base.BeginVisitObjectValue(node); + } + + public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) + { + { + Tracker.LeaveObjectValue?.Invoke(node); + } + + return base.EndVisitObjectValue(node); + } + + public override ASTNode BeginVisitNode(ASTNode node) + { + { + Tracker.EnterNode?.Invoke(node); + } + + return base.BeginVisitNode(node); + } + + public override GraphQLListValue BeginVisitListValue(GraphQLListValue node) + { + { + Tracker.EnterListValue?.Invoke(node); + } + + return base.BeginVisitListValue(node); + } + + public override GraphQLListValue EndVisitListValue(GraphQLListValue node) + { + { + Tracker.LeaveListValue?.Invoke(node); + } + + return base.EndVisitListValue(node); + } + + protected void CreateVisitors(IEnumerable rules) + { + Tracker = new TypeTracker(Schema); + + foreach (var createRule in rules) createRule(this, Tracker); + } + + private ValidationResult BuildResult() + { + return new ValidationResult + { + Errors = _errors + }; + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/TypeInfo.cs b/src/graphql/validation/TypeInfo.cs deleted file mode 100644 index 9bcc43278..000000000 --- a/src/graphql/validation/TypeInfo.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using tanka.graphql.type; -using GraphQLParser.AST; -using static tanka.graphql.type.Ast; - -namespace tanka.graphql.validation -{ - public class TypeInfo : INodeVisitor - { - private readonly Stack _ancestorStack = new Stack(); - private readonly Stack _fieldDefStack = new Stack(); - private readonly Stack _inputTypeStack = new Stack(); - private readonly Stack _parentTypeStack = new Stack(); - private readonly ISchema _schema; - - private readonly Stack _typeStack = new Stack(); - - //private DirectiveType _directive; - private Argument _argument; - private DirectiveType _directive; - - public TypeInfo(ISchema schema) - { - _schema = schema; - } - - public DirectiveType GetDirective() - { - return _directive; - } - - public void Enter(ASTNode node) - { - _ancestorStack.Push(node); - - if (node is GraphQLSelectionSet) - { - _parentTypeStack.Push(GetLastType()); - return; - } - - if (node is GraphQLFieldSelection fieldSelection) - { - var parentType = GetParentType().Unwrap(); - var field = GetFieldDef(_schema, parentType, fieldSelection); - - _fieldDefStack.Push(field); - var targetType = field?.Type; - _typeStack.Push(targetType); - return; - } - - if (node is GraphQLDirective directive) - { - _directive = _schema.GetDirective(directive.Name.Value); - } - - if (node is GraphQLOperationDefinition op) - { - INamedType type = null; - if (op.Operation == OperationType.Query) - type = _schema.Query; - else if (op.Operation == OperationType.Mutation) - type = _schema.Mutation; - else if (op.Operation == OperationType.Subscription) type = _schema.Subscription; - _typeStack.Push(type); - return; - } - - if (node is GraphQLFragmentDefinition fragmentDefinition) - { - var type = _schema.GetNamedType(fragmentDefinition.TypeCondition.Name.Value); - _typeStack.Push(type); - return; - } - - if (node is GraphQLInlineFragment inlineFragment) - { - var type = inlineFragment.TypeCondition != null - ? _schema.GetNamedType(inlineFragment.TypeCondition.Name.Value) - : GetLastType(); - - _typeStack.Push(type); - return; - } - - if (node is GraphQLVariableDefinition varDef) - { - var inputType = TypeFromAst(_schema, varDef.Type); - _inputTypeStack.Push(inputType); - return; - } - - if (node is GraphQLArgument argAst) - { - Argument argDef = null; - IType argType = null; - - var args = GetDirective() != null ? GetDirective()?.Arguments : GetFieldDef()?.Arguments; - - if (args != null) - { - argDef = args.SingleOrDefault(a => a.Key == argAst.Name.Value).Value; - argType = argDef?.Type; - } - - _argument = argDef; - _inputTypeStack.Push(argType); - } - - if (node is GraphQLListValue) - { - var type = GetInputType().Unwrap(); - _inputTypeStack.Push(type); - } - - if (node is GraphQLObjectField objectField) - { - var objectType = GetInputType().Unwrap(); - IType fieldType = null; - - if (objectType is InputObjectType inputObjectType) - { - var inputField = _schema.GetInputField(inputObjectType.Name, objectField.Name.Value); - fieldType = inputField?.Type; - } - - _inputTypeStack.Push(fieldType); - } - } - - public void Leave(ASTNode node) - { - _ancestorStack.Pop(); - - if (node is GraphQLSelectionSet) - { - _parentTypeStack.Pop(); - return; - } - - if (node is GraphQLFieldSelection) - { - _fieldDefStack.Pop(); - _typeStack.Pop(); - return; - } - - if (node is GraphQLDirective) - { - _directive = null; - return; - }; - - if (node is GraphQLOperationDefinition - || node is GraphQLFragmentDefinition - || node is GraphQLInlineFragment) - { - _typeStack.Pop(); - return; - } - - if (node is GraphQLVariableDefinition) - { - _inputTypeStack.Pop(); - return; - } - - if (node is GraphQLArgument) - { - _argument = null; - _inputTypeStack.Pop(); - return; - } - - if (node is GraphQLListValue || node is GraphQLObjectField) - { - _inputTypeStack.Pop(); - } - } - - public ASTNode[] GetAncestors() - { - return _ancestorStack.Select(x => x).Skip(1).Reverse().ToArray(); - } - - public INamedType GetLastType() - { - var type = _typeStack.Any() ? _typeStack.Peek() : null; - - return ResolveNamedReference(type); - } - - private INamedType ResolveNamedReference(IType type) - { - if (type == null) - return null; - - if (type is NamedTypeReference typeRef) - { - return ResolveNamedReference(typeRef.TypeName); - } - - return type as INamedType; - } - - private INamedType ResolveNamedReference(string typeName) - { - var type = _schema.GetNamedType(typeName); - return type; - } - - public IType GetInputType() - { - return _inputTypeStack.Any() ? _inputTypeStack.Peek() : null; - } - - public INamedType GetParentType() - { - var type = _parentTypeStack.Any() ? _parentTypeStack.Peek() : null; - - return ResolveNamedReference(type.Unwrap()); - } - - public IField GetFieldDef() - { - return _fieldDefStack.Any() ? _fieldDefStack.Peek() : null; - } - - /*public DirectiveType GetDirective() - { - return _directive; - }*/ - - public Argument GetArgument() - { - return _argument; - } - - private IField GetFieldDef(ISchema schema, IType parentType, GraphQLFieldSelection fieldSelection) - { - var name = fieldSelection.Name.Value; - - if (parentType is ComplexType complexType) - { - return schema.GetField(complexType.Name, name); - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/TypeTracker.cs b/src/graphql/validation/TypeTracker.cs new file mode 100644 index 000000000..f4cfb4738 --- /dev/null +++ b/src/graphql/validation/TypeTracker.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.execution; +using tanka.graphql.type; + +namespace tanka.graphql.validation +{ + public class TypeTracker : RuleVisitor + { + private readonly Stack _defaultValueStack = new Stack(); + + private readonly Stack<(string Name, IField Field)?> _fieldDefStack = new Stack<(string Name, IField Field)?>(); + + private readonly Stack _inputTypeStack = new Stack(); + + private readonly Stack _parentTypeStack = new Stack(); + + private readonly Stack _typeStack = new Stack(); + private Argument _argument; + + private DirectiveType _directive; + + private object _enumValue; + + public TypeTracker(ISchema schema) + { + EnterSelectionSet = selectionSet => + { + var namedType = GetNamedType(GetCurrentType()); + var complexType = namedType as ComplexType; + _parentTypeStack.Push(complexType); + }; + + EnterFieldSelection = selection => + { + var parentType = GetParentType(); + (string Name, IField Field)? fieldDef = null; + IType fieldType = null; + + if (parentType != null) + { + fieldDef = GetFieldDef(schema, parentType, selection); + + if (fieldDef != null) fieldType = fieldDef.Value.Field.Type; + } + + _fieldDefStack.Push(fieldDef); + _typeStack.Push(TypeIs.IsOutputType(fieldType) ? fieldType : null); + }; + + EnterDirective = directive => { _directive = schema.GetDirective(directive.Name.Value); }; + + EnterOperationDefinition = definition => + { + ObjectType type = null; + switch (definition.Operation) + { + case OperationType.Query: + type = schema.Query; + break; + case OperationType.Mutation: + type = schema.Mutation; + break; + case OperationType.Subscription: + type = schema.Subscription; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + _typeStack.Push(type); + }; + + EnterInlineFragment = inlineFragment => + { + var typeConditionAst = inlineFragment.TypeCondition; + + IType outputType; + if (typeConditionAst != null) + outputType = Ast.TypeFromAst(schema, typeConditionAst); + else + outputType = GetNamedType(GetCurrentType()); + + _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); + }; + + EnterFragmentDefinition = node => + { + var typeConditionAst = node.TypeCondition; + + IType outputType; + if (typeConditionAst != null) + outputType = Ast.TypeFromAst(schema, typeConditionAst); + else + outputType = GetNamedType(GetCurrentType()); + + _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); + }; + + EnterVariableDefinition = node => + { + var inputType = Ast.TypeFromAst(schema, node.Type); + _inputTypeStack.Push(TypeIs.IsInputType(inputType) ? inputType : null); + }; + + EnterArgument = argument => + { + Argument argDef = null; + IType argType = null; + + if (GetDirective() != null) + { + argDef = GetDirective()?.GetArgument(argument.Name.Value); + argType = argDef?.Type; + } + else if (GetFieldDef() != null) + { + argDef = GetFieldDef()?.Field.GetArgument(argument.Name.Value); + argType = argDef?.Type; + } + + _argument = argDef; + _defaultValueStack.Push(argDef?.DefaultValue); + _inputTypeStack.Push(TypeIs.IsInputType(argType) ? argType : null); + }; + + EnterListValue = node => + { + var listType = GetNullableType(GetInputType()); + var itemType = listType is List list ? list.WrappedType : listType; + + // List positions never have a default value + _defaultValueStack.Push(null); + _inputTypeStack.Push(TypeIs.IsInputType(itemType) ? itemType : null); + }; + + EnterObjectField = node => + { + var objectType = GetNamedType(GetInputType()); + IType inputFieldType = null; + InputObjectField inputField = null; + + if (objectType is InputObjectType inputObjectType) + { + inputField = schema.GetInputField( + inputObjectType.Name, + node.Name.Value); + + if (inputField != null) + inputFieldType = inputField.Type; + } + + _defaultValueStack.Push(inputField?.DefaultValue); + _inputTypeStack.Push(TypeIs.IsInputType(inputFieldType) ? inputFieldType : null); + }; + + EnterEnumValue = value => + { + var maybeEnumType = GetNamedType(GetInputType()); + object enumValue = null; + + if (maybeEnumType is EnumType enumType) + enumValue = enumType.ParseLiteral(value); + + _enumValue = enumValue; + }; + + LeaveSelectionSet = _ => _parentTypeStack.Pop(); + + LeaveFieldSelection = _ => + { + _fieldDefStack.Pop(); + _typeStack.Pop(); + }; + + LeaveDirective = _ => _directive = null; + + LeaveOperationDefinition = _ => _typeStack.Pop(); + + LeaveInlineFragment = _ => _typeStack.Pop(); + + LeaveFragmentDefinition = _ => _typeStack.Pop(); + + LeaveVariableDefinition = _ => _inputTypeStack.Pop(); + + LeaveArgument = _ => + { + _argument = null; + _defaultValueStack.Pop(); + _inputTypeStack.Pop(); + }; + + LeaveListValue = _ => + { + _defaultValueStack.Pop(); + _inputTypeStack.Pop(); + }; + + LeaveObjectField = _ => + { + _defaultValueStack.Pop(); + _inputTypeStack.Pop(); + }; + + LeaveEnumValue = _ => _enumValue = null; + } + + public IType GetCurrentType() + { + if (_typeStack.Count == 0) + return null; + + return _typeStack.Peek(); + } + + public ComplexType GetParentType() + { + if (_typeStack.Count == 0) + return null; + + return _parentTypeStack.Peek(); + } + + //todo: originally returns an input type + public IType GetInputType() + { + if (_typeStack.Count == 0) + return null; + + return _inputTypeStack.Peek(); + } + + public IType GetParentInputType() + { + //todo: probably a bad idea + return _inputTypeStack.ElementAtOrDefault(_inputTypeStack.Count - 1); + } + + public (string Name, IField Field)? GetFieldDef() + { + if (_fieldDefStack.Count == 0) + return null; + + return _fieldDefStack.Peek(); + } + + public object GetDefaultValue() + { + if (_defaultValueStack.Count == 0) + return null; + + return _defaultValueStack.Peek(); + } + + public DirectiveType GetDirective() + { + return _directive; + } + + public Argument GetArgument() + { + return _argument; + } + + public object GetEnumValue() + { + return _enumValue; + } + + public IType GetNamedType(IType type) + { + return type?.Unwrap(); + } + + public IType GetNullableType(IType type) + { + if (type is NonNull nonNull) + return nonNull.WrappedType; + + return null; + } + + public (string Name, IField Field)? GetFieldDef( + ISchema schema, + IType parentType, + GraphQLFieldSelection fieldNode) + { + var name = fieldNode.Name.Value; + /*if (name == SchemaMetaFieldDef.name + && schema.getQueryType() == parentType) + { + return SchemaMetaFieldDef; + } + + if (name == TypeMetaFieldDef.name + && schema.getQueryType() == parentType) + { + return TypeMetaFieldDef; + } + + if (name == TypeNameMetaFieldDef.name + && isCompositeType(parentType)) + { + return TypeNameMetaFieldDef; + }*/ + + if (parentType is ComplexType complexType) + { + var field = schema.GetField(complexType.Name, name); + + if (field == null) + return null; + + return (name, field); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/ValidationContext.cs b/src/graphql/validation/ValidationContext.cs deleted file mode 100644 index c966b1c88..000000000 --- a/src/graphql/validation/ValidationContext.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public class ValidationContext - { - private readonly List _errors = new List(); - - private readonly Dictionary> _fragments - = new Dictionary>(); - - private readonly Dictionary> _variables = - new Dictionary>(); - - public ISchema Schema { get; set; } - - public GraphQLDocument Document { get; set; } - - public TypeInfo TypeInfo { get; set; } - - public IEnumerable Errors => _errors; - - public IDictionary Variables { get; set; } - - public void ReportError(ValidationError error) - { - if (error == null) throw new ArgumentNullException(nameof(error)); - - _errors.Add(error); - } - - public IEnumerable GetVariables(ASTNode node) - { - var usages = new List(); - var info = new TypeInfo(Schema); - - var listener = new EnterLeaveListener(_ => - { - _.Match( - varRef => usages.Add(new VariableUsage {Node = varRef, Type = info.GetInputType()}) - ); - }); - - var visitor = new BasicVisitor(info, listener); - visitor.BeginVisitNode(node); - - return usages; - } - - public IEnumerable GetRecursiveVariables(GraphQLOperationDefinition graphQLOperationDefinition) - { - if (_variables.TryGetValue(graphQLOperationDefinition, out var results)) - { - return results; - } - - var usages = GetVariables(graphQLOperationDefinition).ToList(); - var fragments = GetRecursivelyReferencedFragments(graphQLOperationDefinition); - - foreach (var fragment in fragments) - { - usages.AddRange(GetVariables(fragment)); - } - - _variables[graphQLOperationDefinition] = usages; - - return usages; - } - - public GraphQLFragmentDefinition GetFragment(string name) - { - return Document.Definitions.OfType().SingleOrDefault(f => f.Name.Value == name); - } - - public IEnumerable GetFragmentSpreads(GraphQLSelectionSet node) - { - var spreads = new List(); - - var setsToVisit = new Stack(new[] {node}); - - while (setsToVisit.Any()) - { - var set = setsToVisit.Pop(); - - foreach (var selection in set.Selections) - { - if (selection is GraphQLFragmentSpread spread) - { - spreads.Add(spread); - } - else - { - if (selection is GraphQLSelectionSet hasSet) - { - setsToVisit.Push(hasSet); - } - } - } - } - - return spreads; - } - - public IEnumerable GetRecursivelyReferencedFragments(GraphQLOperationDefinition graphQLOperationDefinition) - { - if (_fragments.TryGetValue(graphQLOperationDefinition, out var results)) - { - return results; - } - - var fragments = new List(); - var nodesToVisit = new Stack(new[] - { - graphQLOperationDefinition.SelectionSet - }); - - var collectedNames = new Dictionary(); - - while (nodesToVisit.Any()) - { - var node = nodesToVisit.Pop(); - var spreads = GetFragmentSpreads(node); - - foreach (var spread in spreads) - { - var fragName = spread.Name.Value; - if (collectedNames.ContainsKey(fragName)) - continue; - - collectedNames[fragName] = true; - - var fragment = GetFragment(fragName); - if (fragment != null) - { - fragments.Add(fragment); - nodesToVisit.Push(fragment.SelectionSet); - } - } - } - - _fragments[graphQLOperationDefinition] = fragments; - - return fragments; - } - - } -} diff --git a/src/graphql/validation/ValidationError.cs b/src/graphql/validation/ValidationError.cs index 3032cfb2e..a870d7b05 100644 --- a/src/graphql/validation/ValidationError.cs +++ b/src/graphql/validation/ValidationError.cs @@ -15,17 +15,22 @@ public ValidationError(string message, params ASTNode[] nodes) _nodes.AddRange(nodes); } - public ValidationError(int code, string message, params ASTNode[] nodes) - : this(message, nodes) + public ValidationError(string code, string message, IEnumerable nodes) + : this(message, nodes.ToArray()) { Code = code; } + public ValidationError(string code, string message, ASTNode node) + : this(code, message, new[] {node}) + { + } + public string Message { get; set; } public IEnumerable Nodes => _nodes; - public int Code { get; set; } = -1; + public string Code { get; set; } public override string ToString() { @@ -45,5 +50,22 @@ public override string ToString() return builder.ToString().TrimEnd(','); } + + public Error ToError() + { + return new Error(ToString()) + { + Locations = Nodes.Select(n => n.Location).ToList(), + Extensions = new Dictionary + { + { + "doc", new + { + section = Code + } + } + } + }; + } } } \ No newline at end of file diff --git a/src/graphql/validation/ValidationErrorCodes.cs b/src/graphql/validation/ValidationErrorCodes.cs new file mode 100644 index 000000000..f701ce268 --- /dev/null +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -0,0 +1,47 @@ +namespace tanka.graphql.validation +{ + public static class ValidationErrorCodes + { + public const string R511ExecutableDefinitions = "5.1.1 Executable Definitions"; + + public const string R5211OperationNameUniqueness = "5.2.1.1 Operation Name Uniqueness"; + + public const string R5221LoneAnonymousOperation = "5.2.2.1 Lone Anonymous Operation"; + + public const string R5231SingleRootField = "5.2.3.1 Single root field"; + + public const string R531FieldSelections = "5.3.1 Field Selections on Objects, Interfaces, and Unions Types"; + + public const string R533LeafFieldSelections = "5.3.3 Leaf Field Selections"; + + public const string R541ArgumentNames = "5.4.1 Argument Names"; + + public const string R5421RequiredArguments = "5.4.2.1 Required Arguments"; + + public const string R542ArgumentUniqueness = "5.4.2 Argument Uniqueness"; + + public const string R5511FragmentNameUniqueness = "5.5.1.1 Fragment Name Uniqueness"; + + public const string R5512FragmentSpreadTypeExistence = "5.5.1.2 Fragment Spread Type Existence"; + + public const string R5513FragmentsOnCompositeTypes = "5.5.1.3 Fragments On Composite Types"; + + public const string R5514FragmentsMustBeUsed = "5.5.1.4 Fragments Must Be Used"; + + public const string R5522FragmentSpreadsMustNotFormCycles = "5.5.2.2 Fragment spreads must not form cycles"; + + public const string R5523FragmentSpreadIsPossible = "5.5.2.3 Fragment spread is possible"; + + public const string R561ValuesOfCorrectType = "5.6.1 Values of Correct Type"; + + public const string R562InputObjectFieldNames = "5.6.2 Input Object Field Names"; + + public const string R563InputObjectFieldUniqueness = "5.6.3 Input Object Field Uniqueness"; + + public const string R564InputObjectRequiredFields = "5.6.4 Input Object Required Fields"; + + public const string R57Directives = "5.7 Directives"; + + public const string R58Variables = "5.8 Variables"; + } +} \ No newline at end of file diff --git a/src/graphql/validation/Validator.cs b/src/graphql/validation/Validator.cs index 5ce334dc0..1d333f948 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -1,76 +1,24 @@ using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using tanka.graphql.type; -using tanka.graphql.validation.rules; using GraphQLParser.AST; +using tanka.graphql.type; namespace tanka.graphql.validation { public static class Validator { - public static async Task ValidateAsync( + public static ValidationResult Validate( + IEnumerable rules, ISchema schema, GraphQLDocument document, - IDictionary variables = null, - IEnumerable rules = null) + Dictionary variableValues = null) { - var context = new ValidationContext - { - Schema = schema, - Document = document, - TypeInfo = new TypeInfo(schema), - Variables = variables ?? new Dictionary() - }; - - if (rules == null) rules = CoreRules(); - - var visitors = rules.Select(x => x.CreateVisitor(context)).ToList(); - - visitors.Insert(0, context.TypeInfo); -// #if DEBUG -// visitors.Insert(1, new DebugNodeVisitor()); -// #endif + var visitor = new RulesWalker( + rules, + schema, + document, + variableValues); - var basic = new BasicVisitor(visitors.ToArray()); - basic.Visit(document); - - var result = new ValidationResult {Errors = context.Errors}; - return result; - } - - public static List CoreRules() - { - var rules = new List - { - new R511ExecutableDefinitions(), - new UniqueOperationNames(), - new LoneAnonymousOperation(), - new KnownTypeNames(), - new FragmentsOnCompositeTypes(), - new VariablesAreInputTypes(), - new ScalarLeafs(), - new FieldsOnCorrectType(), - new UniqueFragmentNames(), - new KnownFragmentNames(), - new NoUnusedFragments(), - new PossibleFragmentSpreads(), - new NoFragmentCycles(), - new NoUndefinedVariables(), - new NoUnusedVariables(), - new UniqueVariableNames(), - new KnownDirectives(), - new UniqueDirectivesPerLocation(), - new KnownArgumentNames(), - new UniqueArgumentNames(), - new ArgumentsOfCorrectType(), - new ProvidedNonNullArguments(), - new DefaultValuesOfCorrectType(), - new VariablesInAllowedPosition(), - new UniqueInputFieldNames(), - new SubscriptionHasSingleRootField() - }; - return rules; + return visitor.Validate(); } } } \ No newline at end of file diff --git a/src/graphql/validation/rules/ArgumentsOfCorrectType.cs b/src/graphql/validation/rules/ArgumentsOfCorrectType.cs deleted file mode 100644 index a9a7bdf1e..000000000 --- a/src/graphql/validation/rules/ArgumentsOfCorrectType.cs +++ /dev/null @@ -1,90 +0,0 @@ -using tanka.graphql.type; -using tanka.graphql.type.converters; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Argument values of correct type - /// A GraphQL document is only valid if all field argument literal values are - /// of the type expected by their position. - /// - public class ArgumentsOfCorrectType : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var argDef = context.TypeInfo.GetArgument(); - if (argDef == null) - return; - - var type = argDef.Type; - - // variables should be of the expected type - if (node.Value is GraphQLVariable) return; - - ValidateValue(context, node, node.Value, type); - }); - }); - } - - private void ValidateValue(ValidationContext context, GraphQLArgument node, GraphQLValue nodeValue, IType type) - { - if (type is NonNull nonNull) ValidateValue(context, node, nodeValue, nonNull.WrappedType); - - if (type is List list) - { - if (nodeValue is GraphQLListValue listValue) - foreach (var listValueValue in listValue.Values) - ValidateValue(context, node, listValueValue, list.WrappedType); - else - context.ReportError(new ValidationError( - BadValueMessage( - "Expected type is list but value is not list value", - node.Name.Value, - type, - null))); - } - - if (type is IValueConverter leafType) - { - if (nodeValue is GraphQLScalarValue scalarValue) - { - var value = leafType.ParseLiteral(scalarValue); - if (value == null) - context.ReportError(new ValidationError( - BadValueMessage( - "Expected non-null value but null was parsed", - node.Name.Value, - type, - null), node)); - } - else if (nodeValue is GraphQLVariable variableValue) - { - //variables are expected to be ok - } - else - { - context.ReportError(new ValidationError( - BadValueMessage( - $"Expected leaf type value but was {nodeValue.Kind}", - node.Name.Value, - type, - null), node)); - } - } - } - - public string BadValueMessage( - string mesage, - string argName, - IType type, - string value) - { - return $"Argument \"{argName}\" has invalid value {value}. {mesage}."; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/DefaultValuesOfCorrectType.cs b/src/graphql/validation/rules/DefaultValuesOfCorrectType.cs deleted file mode 100644 index 6b6d2e869..000000000 --- a/src/graphql/validation/rules/DefaultValuesOfCorrectType.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using tanka.graphql.type; -using tanka.graphql.type.converters; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Variable default values of correct type - /// A GraphQL document is only valid if all variable default values are of the - /// type expected by their definition. - /// - public class DefaultValuesOfCorrectType : IValidationRule - { - public Func BadValueForDefaultArgMessage = - (message, varName, type, value) => - $"Variable \"{varName}\" of type \"{type}\" has invalid default value {value}. {message}"; - - public Func BadValueForNonNullArgMessage = - (varName, type, guessType) => $"Variable \"{varName}\" of type \"{type}\" is required and" + - " will not use default value. " + - $"Perhaps you mean to use type \"{guessType}\"?"; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var name = node.Variable.Name.Value; - var defaultValue = node.DefaultValue; - var inputType = context.TypeInfo.GetInputType(); - - if (inputType is NonNull nonNull && defaultValue != null) - context.ReportError(new ValidationError( - BadValueForNonNullArgMessage( - name, - nonNull.ToString(), - nonNull.WrappedType.ToString()), - node)); - - if (inputType != null && defaultValue != null) - ValidateValue(context, node, defaultValue, inputType); - }); - }); - } - - private void ValidateValue(ValidationContext context, GraphQLVariableDefinition node, object nodeValue, - IType type) - { - if (type is NonNull nonNull) ValidateValue(context, node, nodeValue, nonNull.WrappedType); - - if (type is List list) - { - if (nodeValue is GraphQLListValue listValue) - foreach (var listValueValue in listValue.Values) - ValidateValue(context, node, listValueValue, list.WrappedType); - else - context.ReportError(new ValidationError( - BadValueForDefaultArgMessage( - "Expected type is list but value is not list value", - node.Variable.Name.Value, - type, - null))); - } - - if (type is IValueConverter leafType) - { - if (nodeValue is GraphQLScalarValue scalarValue) - { - var value = leafType.ParseLiteral(scalarValue); - if (value == null) - context.ReportError(new ValidationError( - BadValueForNonNullArgMessage( - "Expected non-null value but null was parsed", - node.Variable.Name.Value, - type.ToString()), node)); - } - else if (nodeValue is GraphQLVariable variableValue) - { - //variables are expected to be ok - } - else - { - context.ReportError(new ValidationError( - BadValueForDefaultArgMessage( - $"Expected leaf type value but was {nodeValue.GetType()}", - node.Variable.Name.Value, - type, - nodeValue?.ToString()), node)); - } - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/FieldsOnCorrectType.cs b/src/graphql/validation/rules/FieldsOnCorrectType.cs deleted file mode 100644 index 6ab53dc87..000000000 --- a/src/graphql/validation/rules/FieldsOnCorrectType.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Fields on correct type - /// A GraphQL document is only valid if all Fields selected are defined by the - /// parent type, or are an allowed meta Fields such as __typename - /// - public class FieldsOnCorrectType : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var type = context.TypeInfo.GetParentType().Unwrap(); - - if (type != null) - { - var field = context.TypeInfo.GetFieldDef(); - if (field == null && node.Name.Value != "__typename") - { - // This Fields doesn't exist, lets look for suggestions. - var fieldName = node.Name.Value; - - // First determine if there are any suggested types to condition on. - var suggestedTypeNames = GetSuggestedTypeNames( - context.Schema, - type, - fieldName).ToList(); - - // If there are no suggested types, then perhaps this was a typo? - var suggestedGraphQLFieldSelectionNames = suggestedTypeNames.Any() - ? new string[] { } - : GetSuggestedGraphQLFieldSelectionNames(type, fieldName); - - // Report an error, including helpful suggestions. - context.ReportError(new ValidationError( - UndefinedGraphQLFieldSelectionMessage(fieldName, type, - suggestedTypeNames, suggestedGraphQLFieldSelectionNames), - node - )); - } - } - }); - }); - } - - public string UndefinedGraphQLFieldSelectionMessage( - string field, - IType type, - IEnumerable suggestedTypeNames, - IEnumerable suggestedGraphQLFieldSelectionNames) - { - var message = $"Cannot query Fields \"{field}\" on type \"{type}\"."; - - if (suggestedTypeNames != null && suggestedTypeNames.Any()) - { - var suggestions = string.Join(",", suggestedTypeNames); - message += $" Did you mean to use an inline fragment on {suggestions}?"; - } - else if (suggestedGraphQLFieldSelectionNames != null && suggestedGraphQLFieldSelectionNames.Any()) - { - message += $" Did you mean {string.Join(",", suggestedGraphQLFieldSelectionNames)}?"; - } - - return message; - } - - /// - /// Go through all of the implementations of type, as well as the interfaces - /// that they implement. If any of those types include the provided GraphQLFieldSelection, - /// suggest them, sorted by how often the type is referenced, starting - /// with Interfaces. - /// - private IEnumerable GetSuggestedTypeNames( - ISchema schema, - IType type, - string graphQLFieldSelectionName) - { - /* - if (type is InterfaceType) - { - var suggestedObjectTypes = new List(); - var interfaceUsageCount = new LightweightCache(key => 0); - - var absType = type as IAbstractGraphType; - absType.PossibleTypes.Apply(possibleType => - { - if (!possibleType.HasGraphQLFieldSelection(graphQLFieldSelectionName)) - { - return; - } - - // This object defines this GraphQLFieldSelection. - suggestedObjectTypes.Add(possibleType.Name); - - possibleType.ResolvedInterfaces.Apply(possibleInterface => - { - if (possibleInterface.HasGraphQLFieldSelection(graphQLFieldSelectionName)) - { - // This interface type defines this GraphQLFieldSelection. - interfaceUsageCount[possibleInterface.Name] = interfaceUsageCount[possibleInterface.Name] + 1; - } - }); - }); - - var suggestedInterfaceTypes = interfaceUsageCount.Keys.OrderBy(x => interfaceUsageCount[x]); - return suggestedInterfaceTypes.Concat(suggestedObjectTypes); - }*/ - - return Enumerable.Empty(); - } - - /// - /// For the GraphQLFieldSelection name provided, determine if there are any similar GraphQLFieldSelection names - /// that may be the result of a typo. - /// - private IEnumerable GetSuggestedGraphQLFieldSelectionNames( - IType type, - string graphQLFieldSelectionName) - { - /* - if (type is InterfaceType) - { - var complexType = type as IComplexGraphType; - return StringUtils.SuggestionList(graphQLFieldSelectionName, complexType.GraphQLFieldSelections.Select(x => x.Name)); - }*/ - - return Enumerable.Empty(); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/FragmentsOnCompositeTypes.cs b/src/graphql/validation/rules/FragmentsOnCompositeTypes.cs deleted file mode 100644 index c36cb6855..000000000 --- a/src/graphql/validation/rules/FragmentsOnCompositeTypes.cs +++ /dev/null @@ -1,48 +0,0 @@ -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Fragments on composite type - /// Fragments use a type condition to determine if they apply, since fragments - /// can only be spread into a composite type (object, interface, or union), the - /// type condition must also be a composite type. - /// - public class FragmentsOnCompositeTypes : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var type = context.TypeInfo.GetLastType(); - if (node.TypeCondition != null && type != null && !(type is ComplexType)) - context.ReportError(new ValidationError( - GraphQLInlineFragmentOnNonCompositeErrorMessage(type.ToString()), - node)); - }); - - _.Match(node => - { - var type = context.TypeInfo.GetLastType(); - if (type != null && !(type is ComplexType)) - context.ReportError(new ValidationError( - FragmentOnNonCompositeErrorMessage(node.Name.Value, type.ToString()), - node)); - }); - }); - } - - public string GraphQLInlineFragmentOnNonCompositeErrorMessage(string type) - { - return $"Fragment cannot condition on non composite type \"{type}\"."; - } - - public string FragmentOnNonCompositeErrorMessage(string fragName, string type) - { - return $"Fragment \"{fragName}\" cannot condition on non composite type \"{type}\"."; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/KnownArgumentNames.cs b/src/graphql/validation/rules/KnownArgumentNames.cs deleted file mode 100644 index a2e854dd9..000000000 --- a/src/graphql/validation/rules/KnownArgumentNames.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Known argument names - /// A GraphQL field is only valid if all supplied arguments are defined by - /// that field. - /// - public class KnownArgumentNames : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var ancestors = context.TypeInfo.GetAncestors(); - var argumentOf = ancestors[ancestors.Length - 2]; - if (argumentOf is GraphQLFieldSelection) - { - var fieldDef = context.TypeInfo.GetFieldDef(); - if (fieldDef != null) - { - var fieldArgDef = fieldDef.Arguments?.SingleOrDefault(a => a.Key == node.Name.Value); - if (fieldArgDef == null) - { - var parentType = context.TypeInfo.GetParentType() - ?? throw new ArgumentNullException( - nameof(context.TypeInfo.GetParentType)); - - context.ReportError(new ValidationError( - UnknownArgMessage( - node.Name.Value, - fieldDef.ToString(), - parentType.Name, - null), - node)); - } - } - } - - /*else if (argumentOf is Directive) - { - var directive = context.TypeInfo.GetDirective(); - if (directive != null) - { - var directiveArgDef = directive.Arguments?.Find(node.Name); - if (directiveArgDef == null) - { - context.ReportError(new ValidationError( - context.OriginalQuery, - "5.3.1", - UnknownDirectiveArgMessage( - node.Name, - directive.Name, - StringUtils.SuggestionList(node.Name, directive.Arguments?.Select(q => q.Name))), - node)); - } - } - }*/ - }); - }); - } - - public string UnknownArgMessage(string argName, string fieldName, string type, string[] suggestedArgs) - { - var message = $"Unknown argument \"{argName}\" on field \"{fieldName}\" of type \"{type}\"."; - if (suggestedArgs != null && suggestedArgs.Length > 0) - message += $"Did you mean {string.Join(",", suggestedArgs)}"; - return message; - } - - public string UnknownDirectiveArgMessage(string argName, string directiveName, string[] suggestedArgs) - { - var message = $"Unknown argument \"{argName}\" on directive \"{directiveName}\"."; - if (suggestedArgs != null && suggestedArgs.Length > 0) - message += $"Did you mean {string.Join(",", suggestedArgs)}"; - return message; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/KnownDirectives.cs b/src/graphql/validation/rules/KnownDirectives.cs deleted file mode 100644 index aa827d7d3..000000000 --- a/src/graphql/validation/rules/KnownDirectives.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Linq; -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Known directives - /// A GraphQL document is only valid if all `@directives` are known by the - /// schema and legally positioned. - /// - public class KnownDirectives : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var name = node.Name.Value; - var directiveDef = context.Schema.GetDirective(name); - if (directiveDef == null) - { - context.ReportError(new ValidationError( - UnknownDirectiveMessage(name), node)); - return; - } - - var candidateLocation = GetDirectiveLocationForAstPath(context.TypeInfo.GetAncestors(), context); - if (directiveDef.Locations.All(x => x != candidateLocation)) - context.ReportError(new ValidationError( - MisplacedDirectiveMessage(name, candidateLocation.ToString()), - node)); - }); - }); - } - - public string UnknownDirectiveMessage(string directiveName) - { - return $"Unknown directive \"{directiveName}\"."; - } - - public string MisplacedDirectiveMessage(string directiveName, string location) - { - return $"Directive \"{directiveName}\" may not be used on {location}."; - } - - - private DirectiveLocation GetDirectiveLocationForAstPath(ASTNode[] ancestors, ValidationContext context) - { - var appliedTo = ancestors[ancestors.Length - 1]; - /* - if (appliedTo is Directives || appliedTo is GraphQLArguments) - { - appliedTo = ancestors[ancestors.Length - 2]; - }*/ - - switch (appliedTo) - { - case GraphQLOperationDefinition op: - switch (op.Operation) - { - case OperationType.Query: return DirectiveLocation.QUERY; - case OperationType.Mutation: return DirectiveLocation.MUTATION; - case OperationType.Subscription: return DirectiveLocation.SUBSCRIPTION; - default: - throw new ArgumentOutOfRangeException(); - } - case GraphQLFieldSelection _: - return DirectiveLocation.FIELD; - case GraphQLFragmentSpread _: - return DirectiveLocation.FRAGMENT_SPREAD; - case GraphQLFragmentDefinition _: - return DirectiveLocation.FRAGMENT_DEFINITION; - case GraphQLInlineFragment _: - return DirectiveLocation.INLINE_FRAGMENT; - default: - throw new ArgumentOutOfRangeException(nameof(appliedTo)); - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/KnownFragmentNames.cs b/src/graphql/validation/rules/KnownFragmentNames.cs deleted file mode 100644 index f3a00ff75..000000000 --- a/src/graphql/validation/rules/KnownFragmentNames.cs +++ /dev/null @@ -1,34 +0,0 @@ -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Known fragment names - /// A GraphQL document is only valid if all ...Fragment fragment spreads refer - /// to fragments defined in the same document. - /// - public class KnownFragmentNames : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var fragmentName = node.Name.Value; - var fragment = context.GetFragment(fragmentName); - if (fragment == null) - { - var error = new ValidationError(UnknownFragmentMessage(fragmentName), node); - context.ReportError(error); - } - }); - }); - } - - public string UnknownFragmentMessage(string fragName) - { - return $"Unknown fragment \"{fragName}\"."; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/KnownTypeNames.cs b/src/graphql/validation/rules/KnownTypeNames.cs deleted file mode 100644 index b11cf3c9a..000000000 --- a/src/graphql/validation/rules/KnownTypeNames.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Linq; -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Known type names - /// A GraphQL document is only valid if referenced types (specifically - /// variable definitions and fragment conditions) are defined by the type schema. - /// - public class KnownTypeNames : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(leave: node => - { - var type = context.Schema.GetNamedType(node.Name.Value); - if (type == null) - { - var typeNames = context.Schema.QueryTypes().Select(x => x.Name).ToArray(); - var suggestionList = Enumerable.Empty().ToArray(); - context.ReportError(new ValidationError(UnknownTypeMessage(node.Name.Value, suggestionList), - node)); - } - }); - }); - } - - public string UnknownTypeMessage(string type, string[] suggestedTypes) - { - var message = $"Unknown type {type}."; - if (suggestedTypes != null && suggestedTypes.Length > 0) - message += $" Did you mean {string.Join(",", suggestedTypes)}?"; - return message; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/LoneAnonymousOperation.cs b/src/graphql/validation/rules/LoneAnonymousOperation.cs deleted file mode 100644 index a5589633d..000000000 --- a/src/graphql/validation/rules/LoneAnonymousOperation.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Lone anonymous operation - /// A GraphQL document is only valid if when it contains an anonymous operation - /// (the query short-hand) that it contains only that one operation definition. - /// - public class LoneAnonymousOperation : IValidationRule - { - public Func AnonOperationNotAloneMessage => () => - "This anonymous operation must be the only defined operation."; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - var operationCount = context.Document.Definitions.OfType().Count(); - - return new EnterLeaveListener(_ => - { - _.Match(op => - { - if (string.IsNullOrWhiteSpace(op.Name?.Value) - && operationCount > 1) - { - var error = new ValidationError( - AnonOperationNotAloneMessage(), - op); - context.ReportError(error); - } - }); - }); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/NoFragmentCycles.cs b/src/graphql/validation/rules/NoFragmentCycles.cs deleted file mode 100644 index 4d5b8e695..000000000 --- a/src/graphql/validation/rules/NoFragmentCycles.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// No fragment cycles - /// - public class NoFragmentCycles : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - // Tracks already visited fragments to maintain O(N) and to ensure that cycles - // are not redundantly reported. - var visitedFrags = new Dictionary(); - - // Array of AST nodes used to produce meaningful errors - var spreadPath = new Stack(); - - // Position in the spread path - var spreadPathIndexByName = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match(node => - { - if (!visitedFrags.ContainsKey(node.Name.Value)) - DetectCycleRecursive(node, spreadPath, visitedFrags, spreadPathIndexByName, context); - }); - }); - } - - public string CycleErrorMessage(string fragName, string[] spreadNames) - { - var via = spreadNames.Any() ? " via " + string.Join(", ", spreadNames) : ""; - return $"Cannot spread fragment \"{fragName}\" within itself{via}."; - } - - private void DetectCycleRecursive( - GraphQLFragmentDefinition fragment, - Stack spreadPath, - Dictionary visitedFrags, - Dictionary spreadPathIndexByName, - ValidationContext context) - { - var fragmentName = fragment.Name.Value; - visitedFrags[fragmentName] = true; - - var spreadNodes = context.GetFragmentSpreads(fragment.SelectionSet).ToArray(); - if (!spreadNodes.Any()) return; - - spreadPathIndexByName[fragmentName] = spreadPath.Count; - - foreach (var spreadNode in spreadNodes) - { - var spreadName = spreadNode.Name.Value; - var cycleIndex = spreadPathIndexByName[spreadName]; - - if (cycleIndex == -1) - { - spreadPath.Push(spreadNode); - - if (!visitedFrags[spreadName]) - { - var spreadFragment = context.GetFragment(spreadName); - if (spreadFragment != null) - DetectCycleRecursive( - spreadFragment, - spreadPath, - visitedFrags, - spreadPathIndexByName, - context); - } - - spreadPath.Pop(); - } - else - { - var cyclePath = spreadPath.Reverse().Skip(cycleIndex).ToArray(); - var nodes = cyclePath.OfType().Concat(new[] {spreadNode}).ToArray(); - - context.ReportError(new ValidationError( - CycleErrorMessage(spreadName, cyclePath.Select(x => x.Name.Value).ToArray()), - nodes)); - } - } - - spreadPathIndexByName[fragmentName] = -1; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/NoUndefinedVariables.cs b/src/graphql/validation/rules/NoUndefinedVariables.cs deleted file mode 100644 index 70c180f3e..000000000 --- a/src/graphql/validation/rules/NoUndefinedVariables.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// No undefined variables - /// A GraphQL operation is only valid if all variables encountered, both directly - /// and via fragment spreads, are defined by that operation. - /// - public class NoUndefinedVariables : IValidationRule - { - public Func UndefinedVarMessage = (varName, opName) => - !string.IsNullOrWhiteSpace(opName) - ? $"Variable \"${varName}\" is not defined by operation \"{opName}\"." - : $"Variable \"${varName}\" is not defined."; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - var variableNameDefined = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match(varDef => variableNameDefined[varDef.Variable.Name.Value] = true); - - _.Match( - op => variableNameDefined = new Dictionary(), - op => - { - var usages = context.GetRecursiveVariables(op); - - foreach (var usage in usages) - { - var varName = usage.Node.Name.Value; - if (!variableNameDefined.TryGetValue(varName, out var _)) - { - var error = new ValidationError( - UndefinedVarMessage(varName, op.Name.Value), - usage.Node, - op); - context.ReportError(error); - } - } - }); - }); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/NoUnusedFragments.cs b/src/graphql/validation/rules/NoUnusedFragments.cs deleted file mode 100644 index 4743c4d98..000000000 --- a/src/graphql/validation/rules/NoUnusedFragments.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// No unused fragments - /// A GraphQL document is only valid if all fragment definitions are spread - /// within operations, or spread within other fragments spread within operations. - /// - public class NoUnusedFragments : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - var operationDefs = new List(); - var fragmentDefs = new List(); - - return new EnterLeaveListener(_ => - { - _.Match(node => operationDefs.Add(node)); - _.Match(node => fragmentDefs.Add(node)); - _.Match( - leave: document => - { - var fragmentNameUsed = new List(); - operationDefs.ForEach(operation => - { - context.GetRecursivelyReferencedFragments(operation).ToList().ForEach(fragment => - { - fragmentNameUsed.Add(fragment.Name.Value); - }); - }); - - fragmentDefs.ForEach(fragmentDef => - { - var fragName = fragmentDef.Name.Value; - if (!fragmentNameUsed.Contains(fragName)) - { - var error = new ValidationError(UnusedFragMessage(fragName), fragmentDef); - context.ReportError(error); - } - }); - }); - }); - } - - public string UnusedFragMessage(string fragName) - { - return $"Fragment \"{fragName}\" is never used."; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/NoUnusedVariables.cs b/src/graphql/validation/rules/NoUnusedVariables.cs deleted file mode 100644 index eb94e9797..000000000 --- a/src/graphql/validation/rules/NoUnusedVariables.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// No unused variables - /// A GraphQL operation is only valid if all variables defined by that operation - /// are used in that operation or a fragment transitively included by that - /// operation. - /// - public class NoUnusedVariables : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - var variableDefs = new List(); - - return new EnterLeaveListener(_ => - { - _.Match(def => variableDefs.Add(def)); - - _.Match( - op => variableDefs = new List(), - op => - { - var usages = context.GetRecursiveVariables(op).Select(usage => usage.Node.Name.Value); - variableDefs.ForEach(variableDef => - { - var variableName = variableDef.Variable.Name.Value; - if (!usages.Contains(variableName)) - { - var error = new ValidationError(UnusedVariableMessage(variableName, op.Name.Value), - variableDef); - context.ReportError(error); - } - }); - }); - }); - } - - public string UnusedVariableMessage(string varName, string opName) - { - return !string.IsNullOrWhiteSpace(opName) - ? $"Variable \"${varName}\" is never used in operation \"${opName}\"." - : $"Variable \"${varName}\" is never used."; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/PossibleFragmentSpreads.cs b/src/graphql/validation/rules/PossibleFragmentSpreads.cs deleted file mode 100644 index 4eec6b755..000000000 --- a/src/graphql/validation/rules/PossibleFragmentSpreads.cs +++ /dev/null @@ -1,64 +0,0 @@ -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Possible fragment spread - /// A fragment spread is only valid if the type condition could ever possibly - /// be true: if there is a non-empty intersection of the possible parent types, - /// and possible types which pass the type condition. - /// - public class PossibleFragmentSpreads : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(node => - { - var fragType = context.TypeInfo.GetLastType(); - var parentType = context.TypeInfo.GetParentType().Unwrap(); - - /*if (fragType != null && parentType != null && !context.Schema.DoTypesOverlap(fragType, parentType)) - context.ReportError(new ValidationError( - TypeIncompatibleAnonSpreadMessage(context.Print(parentType), context.Print(fragType)), - node));*/ - }); - - _.Match(node => - { - var fragName = node.Name.Value; - var fragType = GetFragmentType(context, fragName); - var parentType = context.TypeInfo.GetParentType().Unwrap(); - - /*if (fragType != null && parentType != null && !context.Schema.DoTypesOverlap(fragType, parentType)) - context.ReportError(new ValidationError( - TypeIncompatibleSpreadMessage(fragName, context.Print(parentType), context.Print(fragType)), - node));*/ - }); - }); - } - - public string TypeIncompatibleSpreadMessage(string fragName, string parentType, string fragType) - { - return - $"Fragment \"{fragName}\" cannot be spread here as objects of type \"{parentType}\" can never be of type \"{fragType}\"."; - } - - public string TypeIncompatibleAnonSpreadMessage(string parentType, string fragType) - { - return - $"Fragment cannot be spread here as objects of type \"{parentType}\" can never be of type \"{fragType}\"."; - } - - private static IType GetFragmentType(ValidationContext context, string name) - { - var frag = context.GetFragment(name); - if (frag == null) - return null; - - return Ast.TypeFromAst(context.Schema, frag.TypeCondition); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/ProvidedNonNullArguments.cs b/src/graphql/validation/rules/ProvidedNonNullArguments.cs deleted file mode 100644 index 71aec338a..000000000 --- a/src/graphql/validation/rules/ProvidedNonNullArguments.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Linq; -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Provided required arguments - /// - /// A field or directive is only valid if all required (non-null) field arguments - /// have been provided. - /// - public class ProvidedNonNullArguments : IValidationRule - { - public string MissingFieldArgMessage(string fieldName, string argName, string type) - { - return $"Field \"{fieldName}\" argument \"{argName}\" of type \"{type}\" is required but not provided."; - } - - public string MissingDirectiveArgMessage(string directiveName, string argName, string type) - { - return $"Directive \"{directiveName}\" argument \"{argName}\" of type \"{type}\" is required but not provided."; - } - - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(leave: node => - { - var fieldDef = context.TypeInfo.GetFieldDef(); - - if (fieldDef == null) - { - return; - } - - fieldDef.Arguments?.ToList().ForEach(arg => - { - var type = arg.Value.Type; - var argAst = node.Arguments?.SingleOrDefault(a => a.Name.Value == arg.Key); - - if (argAst == null && type is NonNull) - { - context.ReportError( - new ValidationError( - MissingFieldArgMessage(node.Name.Value, arg.Key, type?.ToString()), - node)); - } - }); - }); - - _.Match(leave: node => - { - var directive = context.TypeInfo.GetDirective(); - - if (directive == null) - { - return; - } - - directive.Arguments?.ToList().ForEach(arg => - { - var type = arg.Value.Type; - var argAst = node.Arguments?.SingleOrDefault(a => a.Name.Value == arg.Key); - - if (argAst == null && type is NonNull) - { - context.ReportError( - new ValidationError( - MissingDirectiveArgMessage(node.Name.Value, arg.Key, type?.ToString()), - node)); - } - }); - }); - }); - } - } -} diff --git a/src/graphql/validation/rules/R511ExecutableDefinitions.cs b/src/graphql/validation/rules/R511ExecutableDefinitions.cs deleted file mode 100644 index 645245171..000000000 --- a/src/graphql/validation/rules/R511ExecutableDefinitions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - public class R511ExecutableDefinitions : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match( - document => - { - foreach (var definition in document.Definitions) - { - var valid = definition.Kind == ASTNodeKind.OperationDefinition - || definition.Kind == ASTNodeKind.FragmentDefinition; - - if (!valid) - context.ReportError(new ValidationError( - Errors.R511ExecutableDefinitions, - "GraphQL execution will only consider the " + - "executable definitions Operation and Fragment. " + - "Type system definitions and extensions are not " + - "executable, and are not considered during execution.", - definition)); - } - }); - }); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/ScalarLeafs.cs b/src/graphql/validation/rules/ScalarLeafs.cs deleted file mode 100644 index 1b27a32ac..000000000 --- a/src/graphql/validation/rules/ScalarLeafs.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Linq; -using tanka.graphql.type; -using tanka.graphql.type.converters; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Scalar leafs - /// A GraphQL document is valid only if all leaf fields (fields without - /// sub selections) are of scalar or enum types. - /// - public class ScalarLeafs : IValidationRule - { - public Func NoSubselectionAllowedMessage = (field, type) => - $"Field {field} of type {type} must not have a sub selection"; - - public Func RequiredSubselectionMessage = (field, type) => - $"Field {field} of type {type} must have a sub selection"; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(f => Field(context.TypeInfo.GetLastType()?.Unwrap(), f, context)); - }); - } - - private void Field(IType type, GraphQLFieldSelection field, ValidationContext context) - { - if (type == null) return; - - if (type is IValueConverter) - { - if (field.SelectionSet != null && field.SelectionSet.Selections.Any()) - { - var error = new ValidationError(NoSubselectionAllowedMessage(field.Name.Value, type?.ToString()), - field.SelectionSet); - context.ReportError(error); - } - } - else if (field.SelectionSet == null || !field.SelectionSet.Selections.Any()) - { - var error = new ValidationError(RequiredSubselectionMessage(field.Name.Value, type?.ToString()), field); - context.ReportError(error); - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/SubscriptionHasSingleRootField.cs b/src/graphql/validation/rules/SubscriptionHasSingleRootField.cs deleted file mode 100644 index a18cc4608..000000000 --- a/src/graphql/validation/rules/SubscriptionHasSingleRootField.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using tanka.graphql.execution; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - public class SubscriptionHasSingleRootField : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener( - _ => { _.Match(node => Validate(context, node)); }); - } - - private void Validate(ValidationContext context, GraphQLOperationDefinition node) - { - if (node.Operation != OperationType.Subscription) - return; - - var subscriptionType = context.Schema.Subscription; - var selectionSet = node.SelectionSet; - var variableValues = new Dictionary(); - - var groupedFieldSet = SelectionSets.CollectFields( - context.Schema, - context.Document, - subscriptionType, - selectionSet, - variableValues); - - if (groupedFieldSet.Count != 1) - context.ReportError(new ValidationError( - "Subscription operations must have exactly one root field.", - node)); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/UniqueArgumentNames.cs b/src/graphql/validation/rules/UniqueArgumentNames.cs deleted file mode 100644 index cbc7daca8..000000000 --- a/src/graphql/validation/rules/UniqueArgumentNames.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - public class UniqueArgumentNames : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - var knownArgs = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match(field => knownArgs = new Dictionary()); - _.Match(field => knownArgs = new Dictionary()); - - _.Match(argument => - { - var argName = argument.Name.Value; - if (knownArgs.ContainsKey(argName)) - { - var error = new ValidationError( - DuplicateArgMessage(argName), - knownArgs[argName], - argument); - context.ReportError(error); - } - else - { - knownArgs[argName] = argument; - } - }); - }); - } - - public string DuplicateArgMessage(string argName) - { - return $"There can be only one argument named \"{argName}\"."; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/UniqueDirectivesPerLocation.cs b/src/graphql/validation/rules/UniqueDirectivesPerLocation.cs deleted file mode 100644 index c36364762..000000000 --- a/src/graphql/validation/rules/UniqueDirectivesPerLocation.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Unique directive names per location - /// A GraphQL document is only valid if all directives at a given location - /// are uniquely named. - /// - public class UniqueDirectivesPerLocation : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(f => { CheckDirectives(context, f.Directives); }); - - _.Match(f => { CheckDirectives(context, f.Directives); }); - - _.Match(f => { CheckDirectives(context, f.Directives); }); - - _.Match(f => { CheckDirectives(context, f.Directives); }); - - _.Match(f => { CheckDirectives(context, f.Directives); }); - }); - } - - public string DuplicateDirectiveMessage(string directiveName) - { - return $"The directive \"{directiveName}\" can only be used once at this location."; - } - - private void CheckDirectives(ValidationContext context, IEnumerable directives) - { - var knownDirectives = new Dictionary(); - directives?.ToList().ForEach(directive => - { - var directiveName = directive.Name.Value; - if (knownDirectives.ContainsKey(directiveName)) - { - var error = new ValidationError( - DuplicateDirectiveMessage(directiveName), - knownDirectives[directiveName], - directive); - context.ReportError(error); - } - else - { - knownDirectives[directiveName] = directive; - } - }); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/UniqueFragmentNames.cs b/src/graphql/validation/rules/UniqueFragmentNames.cs deleted file mode 100644 index 3cc3a2bf8..000000000 --- a/src/graphql/validation/rules/UniqueFragmentNames.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Unique fragment names - /// A GraphQL document is only valid if all defined fragments have unique names. - /// - public class UniqueFragmentNames : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - var knownFragments = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match(fragmentDefinition => - { - var fragmentName = fragmentDefinition.Name.Value; - if (knownFragments.ContainsKey(fragmentName)) - { - var error = new ValidationError( - DuplicateFragmentNameMessage(fragmentName), - knownFragments[fragmentName], - fragmentDefinition); - context.ReportError(error); - } - else - { - knownFragments[fragmentName] = fragmentDefinition; - } - }); - }); - } - - public string DuplicateFragmentNameMessage(string fragName) - { - return $"There can only be one fragment named \"{fragName}\""; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/UniqueInputFieldNames.cs b/src/graphql/validation/rules/UniqueInputFieldNames.cs deleted file mode 100644 index 5165f4834..000000000 --- a/src/graphql/validation/rules/UniqueInputFieldNames.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Unique input field names - /// - /// A GraphQL input object value is only valid if all supplied fields are - /// uniquely named. - /// - public class UniqueInputFieldNames : IValidationRule - { - public Func DuplicateInputField = - fieldName => $"There can be only one input field named {fieldName}."; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - var knownNameStack = new Stack>(); - var knownNames = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match( - enter: objVal => - { - knownNameStack.Push(knownNames); - knownNames = new Dictionary(); - }, - leave: objVal => - { - knownNames = knownNameStack.Pop(); - }); - - _.Match( - leave: objField => - { - if (knownNames.ContainsKey(objField.Name.Value)) - { - context.ReportError(new ValidationError( - DuplicateInputField(objField.Name.Value), - knownNames[objField.Name.Value], - objField.Value)); - } - else - { - knownNames[objField.Name.Value] = objField.Value; - } - }); - }); - } - } -} diff --git a/src/graphql/validation/rules/UniqueOperationNames.cs b/src/graphql/validation/rules/UniqueOperationNames.cs deleted file mode 100644 index 7eda2bbb6..000000000 --- a/src/graphql/validation/rules/UniqueOperationNames.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Unique operation names - /// A GraphQL document is only valid if all defined operations have unique names. - /// - public class UniqueOperationNames : IValidationRule - { - public Func DuplicateOperationNameMessage => opName => - $"There can only be one operation named {opName}."; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - var frequency = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match( - op => - { - if (context.Document.Definitions.OfType().Count() < 2) - return; - - if (string.IsNullOrWhiteSpace(op.Name?.Value)) return; - - if (frequency.ContainsKey(op.Name.Value)) - { - var error = new ValidationError( - DuplicateOperationNameMessage(op.Name.Value), - op); - context.ReportError(error); - } - else - { - frequency[op.Name.Value] = op.Name.Value; - } - }); - }); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/UniqueVariableNames.cs b/src/graphql/validation/rules/UniqueVariableNames.cs deleted file mode 100644 index 5f3f44a04..000000000 --- a/src/graphql/validation/rules/UniqueVariableNames.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Unique variable names - /// A GraphQL operation is onlys valid if all its variables are uniquely named. - /// - public class UniqueVariableNames : IValidationRule - { - public INodeVisitor CreateVisitor(ValidationContext context) - { - var knownVariables = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match(op => - knownVariables = new Dictionary()); - - _.Match(variableDefinition => - { - var variableName = variableDefinition.Variable.Name.Value; - if (knownVariables.ContainsKey(variableName)) - { - var error = new ValidationError( - DuplicateVariableMessage(variableName), - knownVariables[variableName], - variableDefinition); - context.ReportError(error); - } - else - { - knownVariables[variableName] = variableDefinition; - } - }); - }); - } - - public string DuplicateVariableMessage(string variableName) - { - return $"There can be only one variable named \"{variableName}\""; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules/VariablesAreInputTypes.cs b/src/graphql/validation/rules/VariablesAreInputTypes.cs deleted file mode 100644 index e612942a9..000000000 --- a/src/graphql/validation/rules/VariablesAreInputTypes.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using tanka.graphql.type; -using tanka.graphql.type.converters; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Variables are input types - /// - /// A GraphQL operation is only valid if all the variables it defines are of - /// input types (scalar, enum, or input object). - /// - public class VariablesAreInputTypes : IValidationRule - { - public Func UndefinedVarMessage = (variableName, typeName) => - $"Variable \"{variableName}\" cannot be non-input type \"{typeName}\"."; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - return new EnterLeaveListener(_ => - { - _.Match(varDef => - { - var type = Ast.TypeFromAst(context.Schema, varDef.Type)?.Unwrap(); - - if (type is InputObjectType) - return; - - if (type is IValueConverter) - return; - - context.ReportError(new ValidationError(UndefinedVarMessage(varDef.Variable.Name.Value, type), varDef)); - }); - }); - } - } -} diff --git a/src/graphql/validation/rules/VariablesInAllowedPosition.cs b/src/graphql/validation/rules/VariablesInAllowedPosition.cs deleted file mode 100644 index 76ea38272..000000000 --- a/src/graphql/validation/rules/VariablesInAllowedPosition.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using tanka.graphql.type; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules -{ - /// - /// Variables passed to field arguments conform to type - /// - public class VariablesInAllowedPosition : IValidationRule - { - public Func BadVarPosMessage => - (varName, varType, expectedType) => - $"Variable \"${varName}\" of type \"{varType}\" used in position " + - $"expecting type \"{expectedType}\"."; - - public INodeVisitor CreateVisitor(ValidationContext context) - { - var varDefMap = new Dictionary(); - - return new EnterLeaveListener(_ => - { - _.Match( - varDefAst => varDefMap[varDefAst.Variable.Name.Value] = varDefAst - ); - - _.Match( - op => varDefMap = new Dictionary(), - op => - { - var usages = context.GetRecursiveVariables(op).ToList(); - usages.ForEach(usage => - { - var varName = usage.Node.Name.Value; - if (!varDefMap.TryGetValue(varName, out var varDef)) return; - - if (varDef != null && usage.Type != null) - { - var varType = Ast.TypeFromAst(context.Schema, varDef.Type); - /* - if (varType != null && - !EffectiveType(varType, varDef).IsSubtypeOf(usage.Type, context.Schema)) - { - var error = new ValidationError( - BadVarPosMessage(varName, context.Print(varType), context.Print(usage.Type))); - - var source = new Source(context.OriginalQuery); - var varDefPos = new Location(source, varDef.SourceLocation.Start); - var usagePos = new Location(source, usage.Node.SourceLocation.Start); - - error.AddLocation(varDefPos.Line, varDefPos.Column); - error.AddLocation(usagePos.Line, usagePos.Column); - - context.ReportError(error); - }*/ - } - }); - } - ); - }); - } - - /// - /// if a variable definition has a default value, it is effectively non-null. - /// - private IType EffectiveType(IType varType, GraphQLVariableDefinition varDef) - { - if (varDef.DefaultValue == null || varType is NonNull) return varType; - - return new NonNull(varType); - } - } -} \ No newline at end of file diff --git a/tanka-graphql.sln.DotSettings b/tanka-graphql.sln.DotSettings index 5e6eee247..f44f64fa0 100644 --- a/tanka-graphql.sln.DotSettings +++ b/tanka-graphql.sln.DotSettings @@ -2,4 +2,5 @@ ReturnDefaultValue True True + True True \ No newline at end of file diff --git a/tests/graphql.tests/type/BooleanTypeFacts.cs b/tests/graphql.tests/type/BooleanTypeFacts.cs index 48a52321d..19c6126b2 100644 --- a/tests/graphql.tests/type/BooleanTypeFacts.cs +++ b/tests/graphql.tests/type/BooleanTypeFacts.cs @@ -52,7 +52,7 @@ public void ParseLiteral(string input, bool expected) Assert.Equal(expected, actual); } - [Theory] + [Theory(Skip = "Coercion not allowed")] [InlineData("1", true)] [InlineData("0", false)] public void ParseIntLiteral(string input, bool expected) diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 2a3a95f28..2c5b0b3ba 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; +using System.Linq; using GraphQLParser.AST; using tanka.graphql.sdl; using tanka.graphql.type; using tanka.graphql.validation; -using tanka.graphql.validation.rules; using Xunit; namespace tanka.graphql.tests.validation @@ -15,8 +14,26 @@ public class ValidatorFacts public ValidatorFacts() { var sdl = - @"type Query { + @" + schema { + query: Query + subscription: Subscription + } + + type Query { dog: Dog + human: Human + pet: Pet + catOrDog: CatOrDog + } + + type Subscription { + newMessage: Message + } + + type Message { + body: String + sender: String } enum DogCommand { SIT, DOWN, HEEL } @@ -58,30 +75,52 @@ type Cat implements Pet { union CatOrDog = Cat | Dog union DogOrHuman = Dog | Human - union HumanOrAlien = Human | Alien"; + union HumanOrAlien = Human | Alien + + type Arguments { + multipleReqs(x: Int!, y: Int!): Int! + booleanArgField(booleanArg: Boolean): Boolean + floatArgField(floatArg: Float): Float + intArgField(intArg: Int): Int + nonNullBooleanArgField(nonNullBooleanArg: Boolean!): Boolean! + booleanListArgField(booleanListArg: [Boolean]!): [Boolean] + optionalNonNullBooleanArgField(optionalBooleanArg: Boolean! = false): Boolean! + } + + extend type Query { + arguments: Arguments + } + + input ComplexInput { name: String!, owner: String } + + extend type Query { + findDog(complex: ComplexInput): Dog + booleanList(booleanListArg: [Boolean!]): Boolean + } + "; Schema = Sdl.Schema(Parser.ParseDocument(sdl)); } public ISchema Schema { get; } - private Task ValidateAsync( + private ValidationResult Validate( GraphQLDocument document, - IValidationRule rule, + CombineRule rule, Dictionary variables = null) { if (document == null) throw new ArgumentNullException(nameof(document)); if (rule == null) throw new ArgumentNullException(nameof(rule)); - return Validator.ValidateAsync( + return Validator.Validate( + new[] {rule}, Schema, document, - variables, - new[] {rule}); + variables); } - [Fact(Skip = "Some validation rules are behaving strangely. #16")] - public async Task Rule_511_Executable_Definitions() + [Fact] + public void Rule_511_Executable_Definitions() { /* Given */ var document = Parser.ParseDocument( @@ -97,15 +136,1601 @@ extend type Dog { }"); /* When */ - var result = await ValidateAsync( + var result = Validate( + document, + ExecutionRules.R511ExecutableDefinitions()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R511ExecutableDefinitions); + } + + [Fact] + public void Rule_5211_Operation_Name_Uniqueness_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"query getDogName { + dog { + name + } + } + + query getOwnerName { + dog { + owner { + name + } + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5211OperationNameUniqueness()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5211_Operation_Name_Uniqueness_invalid() + { + /* Given */ + var document = Parser.ParseDocument( + @"query getName { + dog { + name + } + } + + query getName { + dog { + owner { + name + } + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5211OperationNameUniqueness()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5211OperationNameUniqueness); + } + + [Fact] + public void Rule_5221_Lone_Anonymous_Operation_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + name + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5221LoneAnonymousOperation()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5221_Lone_Anonymous_Operation_invalid() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + name + } + } + + query getName { + dog { + owner { + name + } + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5221LoneAnonymousOperation()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5221LoneAnonymousOperation); + } + + [Fact] + public void Rule_5231_Single_root_field_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"subscription sub { + newMessage { + body + sender + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5221LoneAnonymousOperation()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5231_Single_root_field_valid_with_fragment() + { + /* Given */ + var document = Parser.ParseDocument( + @"subscription sub { + ...newMessageFields + } + + fragment newMessageFields on Subscription { + newMessage { + body + sender + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5231SingleRootField()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5231_Single_root_field_invalid() + { + /* Given */ + var document = Parser.ParseDocument( + @"subscription sub { + newMessage { + body + sender + } + disallowedSecondRootField + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5231SingleRootField()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5231SingleRootField); + } + + [Fact] + public void Rule_5231_Single_root_field_invalid_with_fragment() + { + /* Given */ + var document = Parser.ParseDocument( + @"subscription sub { + ...multipleSubscriptions + } + + fragment multipleSubscriptions on Subscription { + newMessage { + body + sender + } + disallowedSecondRootField + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5231SingleRootField()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5231SingleRootField); + } + + [Fact] + public void Rule_5231_Single_root_field_invalid_with_typename() + { + /* Given */ + var document = Parser.ParseDocument( + @"subscription sub { + newMessage { + body + sender + } + __typename + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5231SingleRootField()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5231SingleRootField); + } + + [Fact] + public void Rule_531_Field_Selections_invalid_with_fragment() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment fieldNotDefined on Dog { + meowVolume + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R531FieldSelections); + } + + [Fact] + public void Rule_531_Field_Selections_invalid_with_alias() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment aliasedLyingFieldTargetNotDefined on Dog { + barkVolume: kawVolume + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R531FieldSelections); + } + + [Fact] + public void Rule_531_Field_Selections_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + name + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R531FieldSelections()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_531_Field_Selections_valid_with_interface() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment interfaceFieldSelection on Pet { + name + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R531FieldSelections()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_531_Field_Selections_invalid_with_interface() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment definedOnImplementorsButNotInterface on Pet { + nickname + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R531FieldSelections); + } + + [Fact] + public void Rule_531_Field_Selections_valid_with_union() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment inDirectFieldSelectionOnUnion on CatOrDog { + __typename + ... on Pet { + name + } + ... on Dog { + barkVolume + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R531FieldSelections()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_531_Field_Selections_invalid_with_union() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment directFieldSelectionOnUnion on CatOrDog { + name + barkVolume + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R531FieldSelections + && error.Nodes.OfType() + .Any(n => n.Name.Value == "name")); + + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R531FieldSelections + && error.Nodes.OfType() + .Any(n => n.Name.Value == "barkVolume")); + } + + [Fact(Skip = "Not implemented")] + public void Rule_532_Field_Selection_Merging() + { + //todo + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment scalarSelection on Dog { + barkVolume + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R533LeafFieldSelections()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment scalarSelectionsNotAllowedOnInt on Dog { + barkVolume { + sinceWhen + } + }"); + + /* When */ + var result = Validate( document, - new R511ExecutableDefinitions()); + ExecutionRules.R533LeafFieldSelections()); /* Then */ Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R511ExecutableDefinitions); + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"query directQueryOnObjectWithoutSubFields { + human + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R533LeafFieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_invalid3() + { + /* Given */ + var document = Parser.ParseDocument( + @"query directQueryOnInterfaceWithoutSubFields { + pet + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R533LeafFieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_invalid4() + { + /* Given */ + var document = Parser.ParseDocument( + @"query directQueryOnUnionWithoutSubFields { + catOrDog + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R533LeafFieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); + } + + [Fact] + public void Rule_541_Argument_Names_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment argOnRequiredArg on Dog { + doesKnowCommand(dogCommand: SIT) + } + + fragment argOnOptional on Dog { + isHousetrained(atOtherHomes: true) @include(if: true) + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R541ArgumentNames()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_541_Argument_Names_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment invalidArgName on Dog { + doesKnowCommand(command: CLEAN_UP_HOUSE) + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R541ArgumentNames()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R541ArgumentNames); + } + + [Fact] + public void Rule_541_Argument_Names_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment invalidArgName on Dog { + isHousetrained(atOtherHomes: true) @include(unless: false) + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R541ArgumentNames()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R541ArgumentNames); + } + + [Fact] + public void Rule_542_Argument_Uniqueness_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment argOnRequiredArg on Dog { + doesKnowCommand(dogCommand: SIT) + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R542ArgumentUniqueness()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_542_Argument_Uniqueness_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment invalidArgName on Dog { + doesKnowCommand(command: SIT, command: SIT) + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R542ArgumentUniqueness()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R542ArgumentUniqueness); + } + + [Fact] + public void Rule_542_Argument_Uniqueness_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment invalidArgName on Dog { + doesKnowCommand(command: SIT) @skip(if: true, if: true) + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R542ArgumentUniqueness()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R542ArgumentUniqueness); + } + + [Fact] + public void Rule_5421_Required_Arguments_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment goodBooleanArg on Arguments { + booleanArgField(booleanArg: true) + } + + fragment goodNonNullArg on Arguments { + nonNullBooleanArgField(nonNullBooleanArg: true) + } + + fragment goodBooleanArgDefault on Arguments { + booleanArgField + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5421RequiredArguments()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5421_Required_Arguments_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment missingRequiredArg on Arguments { + nonNullBooleanArgField + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5421RequiredArguments()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5421RequiredArguments); + } + + [Fact] + public void Rule_5421_Required_Arguments_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment missingRequiredArg on Arguments { + nonNullBooleanArgField(nonNullBooleanArg: null) + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5421RequiredArguments()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5421RequiredArguments); + } + + [Fact] + public void Rule_5511_Fragment_Name_Uniqueness_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + ...fragmentOne + ...fragmentTwo + } + } + + fragment fragmentOne on Dog { + name + } + + fragment fragmentTwo on Dog { + owner { + name + } + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5511FragmentNameUniqueness()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5511_Fragment_Name_Uniqueness_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + ...fragmentOne + } + } + + fragment fragmentOne on Dog { + name + } + + fragment fragmentOne on Dog { + owner { + name + } + }"); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5511FragmentNameUniqueness()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5511FragmentNameUniqueness); + } + + [Fact] + public void Rule_5512_Fragment_Spread_Type_Existence_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment correctType on Dog { + name + } + + fragment inlineFragment on Dog { + ... on Dog { + name + } + } + + fragment inlineFragment2 on Dog { + ... @include(if: true) { + name + } + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5512FragmentSpreadTypeExistence()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5512_Fragment_Spread_Type_Existence_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment notOnExistingType on NotInSchema { + name + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5512FragmentSpreadTypeExistence()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5512FragmentSpreadTypeExistence); + } + + [Fact] + public void Rule_5512_Fragment_Spread_Type_Existence_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment inlineNotExistingType on Dog { + ... on NotInSchema { + name + } + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5512FragmentSpreadTypeExistence()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5512FragmentSpreadTypeExistence); + } + + [Fact] + public void Rule_5513_FragmentsOnCompositeTypes_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment fragOnObject on Dog { + name + } + + fragment fragOnInterface on Pet { + name + } + + fragment fragOnUnion on CatOrDog { + ... on Dog { + name + } + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5513FragmentsOnCompositeTypes()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5513_FragmentsOnCompositeTypes_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment fragOnScalar on Int { + something + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5513FragmentsOnCompositeTypes()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5513FragmentsOnCompositeTypes); + } + + [Fact] + public void Rule_5513_FragmentsOnCompositeTypes_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment inlineFragOnScalar on Dog { + ... on Boolean { + somethingElse + } + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5513FragmentsOnCompositeTypes()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5513FragmentsOnCompositeTypes); + } + + [Fact] + public void Rule_5514_FragmentsMustBeUsed_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment nameFragment on Dog { + name + } + + { + dog { + ...nameFragment + } + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5514FragmentsMustBeUsed()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5514_FragmentsMustBeUsed_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment nameFragment on Dog { + name + } + + { + dog { + name + } + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5514FragmentsMustBeUsed()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5514FragmentsMustBeUsed); + } + + [Fact] + public void Rule_5522_FragmentSpreadsMustNotFormCycles_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + ...nameFragment + } + } + + fragment nameFragment on Dog { + name + ...barkVolumeFragment + } + + fragment barkVolumeFragment on Dog { + barkVolume + ...nameFragment + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5522FragmentSpreadsMustNotFormCycles()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Contains( + result.Errors, + error => error.Code == ValidationErrorCodes.R5522FragmentSpreadsMustNotFormCycles); + } + + [Fact] + public void Rule_5522_FragmentSpreadsMustNotFormCycles_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + ...dogFragment + } + } + + fragment dogFragment on Dog { + name + owner { + ...ownerFragment + } + } + + fragment ownerFragment on Dog { + name + pets { + ...dogFragment + } + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5522FragmentSpreadsMustNotFormCycles()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Contains( + result.Errors, + error => error.Code == ValidationErrorCodes.R5522FragmentSpreadsMustNotFormCycles); + } + + [Fact] + public void Rule_5523_FragmentSpreadIsPossible_in_scope_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment dogFragment on Dog { + ... on Dog { + barkVolume + } + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5523FragmentSpreadIsPossible()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5523_FragmentSpreadIsPossible_in_scope_invalid() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment catInDogFragmentInvalid on Dog { + ... on Cat { + meowVolume + } + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5523FragmentSpreadIsPossible()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5523FragmentSpreadIsPossible); + } + + [Fact] + public void Rule_5523_FragmentSpreadIsPossible_in_abstract_scope_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment petNameFragment on Pet { + name + } + + fragment interfaceWithinObjectFragment on Dog { + ...petNameFragment + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5523FragmentSpreadIsPossible()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5523_FragmentSpreadIsPossible_in_abstract_scope_valid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment catOrDogNameFragment on CatOrDog { + ... on Cat { + meowVolume + } + } + + fragment unionWithObjectFragment on Dog { + ...catOrDogNameFragment + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5523FragmentSpreadIsPossible()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5523_FragmentSpreadIsPossible_abstract_in_abstract_scope_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment unionWithInterface on Pet { + ...dogOrHumanFragment + } + + fragment dogOrHumanFragment on DogOrHuman { + ... on Dog { + barkVolume + } + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5523FragmentSpreadIsPossible()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_5523_FragmentSpreadIsPossible_abstract_in_abstract_scope_invalid() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment nonIntersectingInterfaces on Pet { + ...sentientFragment + } + + fragment sentientFragment on Sentient { + name + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R5523FragmentSpreadIsPossible()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5523FragmentSpreadIsPossible); + } + + [Fact] + public void Rule_561_ValuesOfCorrectType_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment goodBooleanArg on Arguments { + booleanArgField(booleanArg: true) + } + + fragment coercedIntIntoFloatArg on Arguments { + # Note: The input coercion rules for Float allow Int literals. + floatArgField(floatArg: 123) + } + + query goodComplexDefaultValue($search: ComplexInput = { name: ""Fido"" }) { + findDog(complex: $search) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R561ValuesOfCorrectType()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_561_ValuesOfCorrectType_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"fragment stringIntoInt on Arguments { + intArgField(intArg: ""123"") + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R561ValuesOfCorrectType()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R561ValuesOfCorrectType); + } + + [Fact] + public void Rule_561_ValuesOfCorrectType_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"query badComplexValue { + findDog(complex: { name: 123 }) + }" + ); + + /* When */ + var result = Validate( + document, + ExecutionRules.R561ValuesOfCorrectType()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R561ValuesOfCorrectType); + } + + [Fact] + public void Rule_562_InputObjectFieldNames_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + findDog(complex: { name: ""Fido"" }) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R562InputObjectFieldNames()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_562_InputObjectFieldNames_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + findDog(complex: { favoriteCookieFlavor: ""Bacon"" }) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R562InputObjectFieldNames()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R562InputObjectFieldNames); + } + + [Fact] + public void Rule_563_InputObjectFieldUniqueness_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + field(arg: { field: true, field: false }) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R563InputObjectFieldUniqueness()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Contains( + result.Errors, + error => error.Code == ValidationErrorCodes.R563InputObjectFieldUniqueness); + } + + [Fact] + public void Rule_564_InputObjectRequiredFields_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + findDog(complex: { owner: ""Fido"" }) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R564InputObjectRequiredFields()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R564InputObjectRequiredFields); + } + + [Fact] + public void Rule_564_InputObjectRequiredFields_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + findDog(complex: { name: null }) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R564InputObjectRequiredFields()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R564InputObjectRequiredFields); + } + + [Fact] + public void Rule_57_DirectivesAreDefined_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + findDog(complex: { name: ""Fido"" }) @skip(if: false) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R57Directives()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_57_DirectivesAreDefined_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + findDog(complex: { name: ""Fido"" }) @doesNotExists + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R57Directives()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R57Directives); + } + + [Fact(Skip = "TODO: 5.7.2")] + public void Rule_572_DirectivesAreInValidLocations_valid1() + { + } + + [Fact] + public void Rule_573_DirectivesAreUniquePerLocation_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"query ($foo: Boolean = true, $bar: Boolean = false) { + field @skip(if: $foo) { + subfieldA + } + field @skip(if: $bar) { + subfieldB + } + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R57Directives()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_573_DirectivesAreUniquePerLocation_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"query ($foo: Boolean = true, $bar: Boolean = false) { + field @skip(if: $foo) @skip(if: $bar) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R57Directives()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R57Directives); + } + + [Fact] + public void Rule_58_Variables_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"query A($atOtherHomes: Boolean) { + ...HouseTrainedFragment + } + + query B($atOtherHomes: Boolean) { + ...HouseTrainedFragment + } + + fragment HouseTrainedFragment on Query { + dog { + isHousetrained(atOtherHomes: $atOtherHomes) + } + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R58Variables()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_58_Variables_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) { + dog { + isHousetrained(atOtherHomes: $atOtherHomes) + } + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R58Variables()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R58Variables); + } + + [Fact] + public void Rule_582_VariablesAreInputTypes_valid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"query takesBoolean($atOtherHomes: Boolean) { + dog { + isHousetrained(atOtherHomes: $atOtherHomes) + } + } + + query takesComplexInput($complexInput: ComplexInput) { + findDog(complex: $complexInput) { + name + } + } + + query TakesListOfBooleanBang($booleans: [Boolean!]) { + booleanList(booleanListArg: $booleans) + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R58Variables()); + + /* Then */ + Assert.True(result.IsValid); + } + + [Fact] + public void Rule_582_VariablesAreInputTypes_invalid1() + { + /* Given */ + var document = Parser.ParseDocument( + @"query takesCat($cat: Cat) { + __typename + } + + query takesDogBang($dog: Dog!) { + __typename + } + + query takesListOfPet($pets: [Pet]) { + __typename + } + + query takesCatOrDog($catOrDog: CatOrDog) { + __typename + } + "); + + /* When */ + var result = Validate( + document, + ExecutionRules.R58Variables()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Equal(4, result.Errors.Count()); + Assert.Contains( + result.Errors, + error => error.Code == ValidationErrorCodes.R58Variables + && error.Message.StartsWith("Variables can only be input types. Objects, unions,")); + } + + [Fact(Skip = "TODO")] + public void Rule_583_AllVariableUsesDefined() + { + + } + + [Fact(Skip = "TODO")] + public void Rule_584_AllVariablesUsed_valid1() + { + } + + [Fact(Skip = "TODO")] + public void Rule_585_AllVariableUsagesAreAllowed_valid1() + { } } } \ No newline at end of file