From 5686f690f72fef3560fe8a71a71126c07739021d Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Wed, 20 Feb 2019 13:41:09 +0200 Subject: [PATCH 01/20] wip: new validator with rules --- src/graphql/graphql.csproj | 3 +- .../validation/DocumentRulesVisitor.cs | 364 ++++++++++++++++++ .../validation/IDocumentRuleVisitor.cs | 89 +++++ src/graphql/validation/IRule.cs | 172 +++++++++ src/graphql/validation/Validator.cs | 12 +- .../rules2/R511ExecutableDefinitions2.cs | 28 ++ .../validation/ValidatorFacts.cs | 19 +- 7 files changed, 674 insertions(+), 13 deletions(-) create mode 100644 src/graphql/validation/DocumentRulesVisitor.cs create mode 100644 src/graphql/validation/IDocumentRuleVisitor.cs create mode 100644 src/graphql/validation/IRule.cs create mode 100644 src/graphql/validation/rules2/R511ExecutableDefinitions2.cs 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/validation/DocumentRulesVisitor.cs b/src/graphql/validation/DocumentRulesVisitor.cs new file mode 100644 index 000000000..4a2137561 --- /dev/null +++ b/src/graphql/validation/DocumentRulesVisitor.cs @@ -0,0 +1,364 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser; +using GraphQLParser.AST; +using tanka.graphql.type; + +namespace tanka.graphql.validation +{ + public class DocumentRulesVisitor : GraphQLAstVisitor + { + private readonly Dictionary> _visitorMap; + private List<(IRule, IEnumerable)> _errors = new List<(IRule, IEnumerable)>(); + + public DocumentRulesVisitor( + IEnumerable rules, + ISchema schema, + GraphQLDocument document, + Dictionary variableValues = null) + : this(InitializeRuleActionMap(rules), schema, document, variableValues) + { + } + + + public DocumentRulesVisitor( + Dictionary> ruleMap, + ISchema schema, + GraphQLDocument document, + Dictionary variableValues = null) + { + Schema = schema; + Document = document; + VariableValues = variableValues; + _visitorMap = ruleMap; + } + + public GraphQLDocument Document { get; } + + public Dictionary VariableValues { get; } + + public ISchema Schema { get; } + + public static Dictionary> InitializeRuleActionMap(IEnumerable rules) + { + var visitors = new Dictionary>(); + + foreach (var rule in rules) + foreach (var nodeKind in rule.AppliesToNodeKinds) + { + if (!visitors.ContainsKey(nodeKind)) + visitors[nodeKind] = new List(); + + visitors[nodeKind].Add(rule); + } + + return visitors; + } + + public ValidationResult Validate() + { + Visit(Document); + return BuildResult(); + } + + public override void Visit(GraphQLDocument ast) + { + var rules = GetRules(ast); + foreach (var rule in rules) CollectErrors(rule, rule.Visit(ast)); + + base.Visit(ast); + } + + private void CollectErrors(IRule rule, IEnumerable validationErrors) + { + _errors.Add((rule, validationErrors)); + } + + public override GraphQLName BeginVisitAlias(GraphQLName alias) + { + var rules = GetRules(alias); + foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitAlias(alias)); + + return base.BeginVisitAlias(alias); + } + + public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) + { + var rules = GetRules(argument); + foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitArgument(argument)); + + return base.BeginVisitArgument(argument); + } + + public override IEnumerable BeginVisitArguments(IEnumerable arguments) + { + var rules = GetRules(ASTNodeKind.Argument); + foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitArguments(arguments)); + + return base.BeginVisitArguments(arguments); + } + + public override GraphQLScalarValue BeginVisitBooleanValue( + GraphQLScalarValue value) + { + var rules = GetRules(value); + foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitBooleanValue(value)); + + return base.BeginVisitBooleanValue(value); + } + + public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) + { + var rules = GetRules(directive); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitDirective(directive)); + + return base.BeginVisitDirective(directive); + } + + public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) + { + var rules = GetRules(value); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitEnumValue(value)); + + return base.BeginVisitEnumValue(value); + } + + public override GraphQLFieldSelection BeginVisitFieldSelection( + GraphQLFieldSelection selection) + { + var rules = GetRules(selection); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFieldSelection(selection)); + + return base.BeginVisitFieldSelection(selection); + } + + public override GraphQLScalarValue BeginVisitFloatValue( + GraphQLScalarValue value) + { + var rules = GetRules(value); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFloatValue(value)); + + return base.BeginVisitFloatValue(value); + } + + public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( + GraphQLFragmentDefinition node) + { + var rules = GetRules(node); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFragmentDefinition(node)); + + return base.BeginVisitFragmentDefinition(node); + } + + public override GraphQLFragmentSpread BeginVisitFragmentSpread( + GraphQLFragmentSpread fragmentSpread) + { + var rules = GetRules(fragmentSpread); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFragmentSpread(fragmentSpread)); + + return base.BeginVisitFragmentSpread(fragmentSpread); + } + + public override GraphQLInlineFragment BeginVisitInlineFragment( + GraphQLInlineFragment inlineFragment) + { + var rules = GetRules(inlineFragment); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitInlineFragment(inlineFragment)); + + return base.BeginVisitInlineFragment(inlineFragment); + } + + public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) + { + var rules = GetRules(value); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitIntValue(value)); + + return base.BeginVisitIntValue(value); + } + + public override GraphQLName BeginVisitName(GraphQLName name) + { + var rules = GetRules(name); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitName(name)); + + return base.BeginVisitName(name); + } + + public override GraphQLNamedType BeginVisitNamedType( + GraphQLNamedType typeCondition) + { + var rules = GetRules(typeCondition); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitNamedType(typeCondition)); + + return base.BeginVisitNamedType(typeCondition); + } + + public override GraphQLOperationDefinition BeginVisitOperationDefinition( + GraphQLOperationDefinition definition) + { + var rules = GetRules(definition); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitOperationDefinition(definition)); + + return base.BeginVisitOperationDefinition(definition); + } + + public override GraphQLOperationDefinition EndVisitOperationDefinition( + GraphQLOperationDefinition definition) + { + var rules = GetRules(definition); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitOperationDefinition(definition)); + + return base.EndVisitOperationDefinition(definition); + } + + public override GraphQLSelectionSet BeginVisitSelectionSet( + GraphQLSelectionSet selectionSet) + { + var rules = GetRules(selectionSet); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitSelectionSet(selectionSet)); + + return base.BeginVisitSelectionSet(selectionSet); + } + + public override GraphQLScalarValue BeginVisitStringValue( + GraphQLScalarValue value) + { + var rules = GetRules(value); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitStringValue(value)); + + return base.BeginVisitStringValue(value); + } + + public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) + { + var rules = GetRules(variable); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitVariable(variable)); + + return base.BeginVisitVariable(variable); + } + + public override GraphQLVariableDefinition BeginVisitVariableDefinition( + GraphQLVariableDefinition node) + { + var rules = GetRules(node); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitVariableDefinition(node)); + + return base.BeginVisitVariableDefinition(node); + } + + public override IEnumerable BeginVisitVariableDefinitions( + IEnumerable variableDefinitions) + { + var rules = GetRules(ASTNodeKind.VariableDefinition); + + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitVariableDefinitions(variableDefinitions)); + + return base.BeginVisitVariableDefinitions(variableDefinitions); + } + + public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) + { + var rules = GetRules(argument); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitArgument(argument)); + + return base.EndVisitArgument(argument); + } + + public override GraphQLFieldSelection EndVisitFieldSelection( + GraphQLFieldSelection selection) + { + var rules = GetRules(selection); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitFieldSelection(selection)); + + return base.EndVisitFieldSelection(selection); + } + + public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) + { + var rules = GetRules(variable); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitVariable(variable)); + + return base.EndVisitVariable(variable); + } + + public override GraphQLObjectField BeginVisitObjectField( + GraphQLObjectField node) + { + var rules = GetRules(node); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitObjectField(node)); + + return base.BeginVisitObjectField(node); + } + + public override GraphQLObjectValue BeginVisitObjectValue( + GraphQLObjectValue node) + { + var rules = GetRules(node); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitObjectValue(node)); + + return base.BeginVisitObjectValue(node); + } + + public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) + { + var rules = GetRules(node); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitObjectValue(node)); + + return base.EndVisitObjectValue(node); + } + + public override GraphQLListValue EndVisitListValue(GraphQLListValue node) + { + var rules = GetRules(node); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitListValue(node)); + + return base.EndVisitListValue(node); + } + + private ValidationResult BuildResult() + { + return new ValidationResult() + { + Errors = _errors.SelectMany(e => e.Item2).ToList() + }; + } + + private IEnumerable GetRules(ASTNode node) + { + var nodeKind = node.Kind; + return GetRules(nodeKind); + } + + private IEnumerable GetRules(ASTNodeKind nodeKind) + { + if (!_visitorMap.ContainsKey(nodeKind)) + return Enumerable.Empty(); + + return _visitorMap[nodeKind]; + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/IDocumentRuleVisitor.cs b/src/graphql/validation/IDocumentRuleVisitor.cs new file mode 100644 index 000000000..1f10263a5 --- /dev/null +++ b/src/graphql/validation/IDocumentRuleVisitor.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using GraphQLParser.AST; + +namespace tanka.graphql.validation +{ + public interface IDocumentRuleVisitor + { + IEnumerable BeginVisitAlias(GraphQLName alias); + + IEnumerable BeginVisitArgument(GraphQLArgument argument); + + IEnumerable BeginVisitArguments( + IEnumerable arguments); + + IEnumerable BeginVisitBooleanValue( + GraphQLScalarValue value); + + IEnumerable BeginVisitDirective(GraphQLDirective directive); + + IEnumerable BeginVisitDirectives( + IEnumerable directives); + + IEnumerable BeginVisitEnumValue(GraphQLScalarValue value); + + IEnumerable BeginVisitFieldSelection( + GraphQLFieldSelection selection); + + IEnumerable BeginVisitFloatValue( + GraphQLScalarValue value); + + IEnumerable BeginVisitFragmentDefinition( + GraphQLFragmentDefinition node); + + IEnumerable BeginVisitFragmentSpread( + GraphQLFragmentSpread fragmentSpread); + + IEnumerable BeginVisitInlineFragment( + GraphQLInlineFragment inlineFragment); + + IEnumerable BeginVisitIntValue(GraphQLScalarValue value); + + IEnumerable BeginVisitName(GraphQLName name); + + IEnumerable BeginVisitNamedType( + GraphQLNamedType typeCondition); + + IEnumerable BeginVisitNode(ASTNode node); + + IEnumerable BeginVisitOperationDefinition( + GraphQLOperationDefinition definition); + + IEnumerable EndVisitOperationDefinition( + GraphQLOperationDefinition definition); + + IEnumerable BeginVisitSelectionSet( + GraphQLSelectionSet selectionSet); + + IEnumerable BeginVisitStringValue( + GraphQLScalarValue value); + + IEnumerable BeginVisitVariable(GraphQLVariable variable); + + IEnumerable BeginVisitVariableDefinition( + GraphQLVariableDefinition node); + + IEnumerable BeginVisitVariableDefinitions( + IEnumerable variableDefinitions); + + IEnumerable EndVisitArgument(GraphQLArgument argument); + + IEnumerable EndVisitFieldSelection( + GraphQLFieldSelection selection); + + IEnumerable EndVisitVariable(GraphQLVariable variable); + + IEnumerable Visit(GraphQLDocument document); + + IEnumerable BeginVisitObjectField( + GraphQLObjectField node); + + IEnumerable BeginVisitObjectValue( + GraphQLObjectValue node); + + IEnumerable EndVisitObjectValue(GraphQLObjectValue node); + + IEnumerable EndVisitListValue(GraphQLListValue node); + + } +} \ No newline at end of file diff --git a/src/graphql/validation/IRule.cs b/src/graphql/validation/IRule.cs new file mode 100644 index 000000000..403d1fee1 --- /dev/null +++ b/src/graphql/validation/IRule.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; + +namespace tanka.graphql.validation +{ + public interface IRule : IDocumentRuleVisitor + { + IEnumerable AppliesToNodeKinds { get; } + } + + public abstract class Rule : IRule + { + public virtual IEnumerable BeginVisitAlias(GraphQLName alias) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitArgument(GraphQLArgument argument) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitArguments(IEnumerable arguments) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitBooleanValue(GraphQLScalarValue value) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitDirective(GraphQLDirective directive) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitDirectives(IEnumerable directives) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitEnumValue(GraphQLScalarValue value) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitFloatValue(GraphQLScalarValue value) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitIntValue(GraphQLScalarValue value) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitName(GraphQLName name) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitNamedType(GraphQLNamedType typeCondition) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitNode(ASTNode node) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitOperationDefinition(GraphQLOperationDefinition definition) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitStringValue(GraphQLScalarValue value) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitVariable(GraphQLVariable variable) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitVariableDefinitions( + IEnumerable variableDefinitions) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable EndVisitArgument(GraphQLArgument argument) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable EndVisitVariable(GraphQLVariable variable) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable Visit(GraphQLDocument document) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitObjectField(GraphQLObjectField node) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitObjectValue(GraphQLObjectValue node) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable EndVisitObjectValue(GraphQLObjectValue node) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable EndVisitListValue(GraphQLListValue node) + { + return Enumerable.Empty(); + } + + public abstract IEnumerable AppliesToNodeKinds { get; } + } +} \ No newline at end of file diff --git a/src/graphql/validation/Validator.cs b/src/graphql/validation/Validator.cs index 5ce334dc0..67f5e770d 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -1,14 +1,24 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using GraphQLParser.AST; using tanka.graphql.type; using tanka.graphql.validation.rules; -using GraphQLParser.AST; namespace tanka.graphql.validation { public static class Validator { + public static ValidationResult Validate( + IEnumerable rules, + ISchema schema, + GraphQLDocument document, + Dictionary variableValues = null) + { + var visitor = new DocumentRulesVisitor(rules, schema, document, variableValues); + return visitor.Validate(); + } + public static async Task ValidateAsync( ISchema schema, GraphQLDocument document, diff --git a/src/graphql/validation/rules2/R511ExecutableDefinitions2.cs b/src/graphql/validation/rules2/R511ExecutableDefinitions2.cs new file mode 100644 index 000000000..d1a7ee379 --- /dev/null +++ b/src/graphql/validation/rules2/R511ExecutableDefinitions2.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using GraphQLParser.AST; + +namespace tanka.graphql.validation.rules2 +{ + public class R511ExecutableDefinitions : Rule + { + public override IEnumerable AppliesToNodeKinds => new[] {ASTNodeKind.Document}; + + public override IEnumerable Visit(GraphQLDocument document) + { + foreach (var definition in document.Definitions) + { + var valid = definition.Kind == ASTNodeKind.OperationDefinition + || definition.Kind == ASTNodeKind.FragmentDefinition; + + if (!valid) + yield return 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/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 2a3a95f28..d0aa5b7e1 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 GraphQLParser.AST; using tanka.graphql.sdl; using tanka.graphql.type; using tanka.graphql.validation; -using tanka.graphql.validation.rules; +using tanka.graphql.validation.rules2; using Xunit; namespace tanka.graphql.tests.validation @@ -65,23 +64,23 @@ type Cat implements Pet { public ISchema Schema { get; } - private Task ValidateAsync( + private ValidationResult Validate( GraphQLDocument document, - IValidationRule rule, + IRule 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,7 +96,7 @@ extend type Dog { }"); /* When */ - var result = await ValidateAsync( + var result = Validate( document, new R511ExecutableDefinitions()); From 9b66651e101cab6d9e3086b14891e02b9f7d04cf Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Wed, 20 Feb 2019 17:29:22 +0200 Subject: [PATCH 02/20] 5.2.1.1 --- src/graphql/validation/Errors.cs | 4 +- src/graphql/validation/ValidationError.cs | 12 +++- ...tions2.cs => R511ExecutableDefinitions.cs} | 6 ++ .../rules2/R5211OperationNameUniqueness.cs | 53 +++++++++++++++++ .../validation/ValidatorFacts.cs | 59 +++++++++++++++++++ 5 files changed, 129 insertions(+), 5 deletions(-) rename src/graphql/validation/rules2/{R511ExecutableDefinitions2.cs => R511ExecutableDefinitions.cs} (82%) create mode 100644 src/graphql/validation/rules2/R5211OperationNameUniqueness.cs diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/Errors.cs index 37fc200b3..cabac7e89 100644 --- a/src/graphql/validation/Errors.cs +++ b/src/graphql/validation/Errors.cs @@ -2,8 +2,8 @@ { public static class Errors { - public const int R511ExecutableDefinitions = 1; + public const string R5211OperationNameUniqueness = "5.2.1.1 Operation Name Uniqueness"; - public const int UserErrorCode = 1000; + public const string R511ExecutableDefinitions = "5.1.1 Executable Definitions"; } } \ No newline at end of file diff --git a/src/graphql/validation/ValidationError.cs b/src/graphql/validation/ValidationError.cs index 3032cfb2e..fc68aa5ff 100644 --- a/src/graphql/validation/ValidationError.cs +++ b/src/graphql/validation/ValidationError.cs @@ -15,21 +15,27 @@ 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() { var builder = new StringBuilder(); + builder.AppendLine(Code); builder.Append(Message); if (Nodes.Any()) diff --git a/src/graphql/validation/rules2/R511ExecutableDefinitions2.cs b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs similarity index 82% rename from src/graphql/validation/rules2/R511ExecutableDefinitions2.cs rename to src/graphql/validation/rules2/R511ExecutableDefinitions.cs index d1a7ee379..88d87295f 100644 --- a/src/graphql/validation/rules2/R511ExecutableDefinitions2.cs +++ b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs @@ -3,6 +3,12 @@ namespace tanka.graphql.validation.rules2 { + /// + /// Formal Specification + /// + /// For each definition definition in the document. + /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). + /// public class R511ExecutableDefinitions : Rule { public override IEnumerable AppliesToNodeKinds => new[] {ASTNodeKind.Document}; diff --git a/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs b/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs new file mode 100644 index 000000000..e7fe85956 --- /dev/null +++ b/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R5211OperationNameUniqueness : Rule + { + public override IEnumerable AppliesToNodeKinds => new[] + { + ASTNodeKind.Document + }; + + public override IEnumerable Visit(GraphQLDocument document) + { + if (document.Definitions.OfType().Count() < 2) + yield break; + + var operations = document.Definitions.OfType() + .ToList(); + + foreach (var op in operations) + { + var operationName = op.Name?.Value; + + if (string.IsNullOrWhiteSpace(operationName)) + continue; + + var matchingOperations = operations.Where(def => def.Name?.Value == operationName) + .ToList(); + + if (matchingOperations.Count() > 1) + { + yield return new ValidationError( + Errors.R5211OperationNameUniqueness, + "Each named operation definition must be unique within a document when referred to by its name.", + matchingOperations); + + yield break; + } + } + + } + } +} \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index d0aa5b7e1..9c0e54763 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -106,5 +106,64 @@ extend type Dog { result.Errors, error => error.Code == Errors.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, + new 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, + new R5211OperationNameUniqueness()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R5211OperationNameUniqueness); + } } } \ No newline at end of file From 6bb306b192f007a5d6f8e58d65d58900278b745b Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 21 Feb 2019 09:14:01 +0200 Subject: [PATCH 03/20] 5.2.2.1 --- src/graphql/validation/Errors.cs | 2 + .../rules2/R5221LoneAnonymousOperation.cs | 43 ++++++++++++++++ .../validation/ValidatorFacts.cs | 51 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/Errors.cs index cabac7e89..1fd277f2c 100644 --- a/src/graphql/validation/Errors.cs +++ b/src/graphql/validation/Errors.cs @@ -5,5 +5,7 @@ public static class Errors public const string R5211OperationNameUniqueness = "5.2.1.1 Operation Name Uniqueness"; public const string R511ExecutableDefinitions = "5.1.1 Executable Definitions"; + + public const string R5221LoneAnonymousOperation = "5.2.2.1 Lone Anonymous Operation"; } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs b/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs new file mode 100644 index 000000000..f5d7cd23b --- /dev/null +++ b/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R5221LoneAnonymousOperation : Rule + { + public override IEnumerable AppliesToNodeKinds => new[] + { + ASTNodeKind.Document + }; + + public override IEnumerable Visit(GraphQLDocument document) + { + var operations = document.Definitions + .OfType() + .ToList(); + + var anonymous = operations + .Count(op => string.IsNullOrEmpty(op.Name?.Value)); + + if (operations.Count() > 1) + { + if (anonymous > 0) + { + yield return new ValidationError( + Errors.R5221LoneAnonymousOperation, + "GraphQL allows a short‐hand form for defining " + + "query operations when only that one operation exists in " + + "the document.", + operations); + } + } + } + } +} \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 9c0e54763..a53dcf7e8 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -165,5 +165,56 @@ query getName { result.Errors, error => error.Code == Errors.R5211OperationNameUniqueness); } + + [Fact] + public void Rule_5221_Lone_Anonymous_Operation_invalid_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + name + } + }"); + + /* When */ + var result = Validate( + document, + new 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, + new R5221LoneAnonymousOperation()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R5221LoneAnonymousOperation); + } } } \ No newline at end of file From 1e37ae4d789dbf16935e7b460326aaf6263df585 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 21 Feb 2019 10:04:49 +0200 Subject: [PATCH 04/20] 5.2.3.1 --- .../validation/DocumentRulesVisitor.cs | 129 ++++++++-------- src/graphql/validation/Errors.cs | 2 + .../validation/IDocumentRuleVisitor.cs | 78 ++++++---- src/graphql/validation/IRule.cs | 63 ++++---- src/graphql/validation/IValidationContext.cs | 15 ++ .../rules2/R511ExecutableDefinitions.cs | 3 +- .../rules2/R5211OperationNameUniqueness.cs | 3 +- .../rules2/R5221LoneAnonymousOperation.cs | 3 +- .../validation/rules2/R5231SingleRootField.cs | 61 ++++++++ .../validation/ValidatorFacts.cs | 142 +++++++++++++++++- 10 files changed, 371 insertions(+), 128 deletions(-) create mode 100644 src/graphql/validation/IValidationContext.cs create mode 100644 src/graphql/validation/rules2/R5231SingleRootField.cs diff --git a/src/graphql/validation/DocumentRulesVisitor.cs b/src/graphql/validation/DocumentRulesVisitor.cs index 4a2137561..5b62b1d18 100644 --- a/src/graphql/validation/DocumentRulesVisitor.cs +++ b/src/graphql/validation/DocumentRulesVisitor.cs @@ -6,10 +6,12 @@ namespace tanka.graphql.validation { - public class DocumentRulesVisitor : GraphQLAstVisitor + public class DocumentRulesVisitor : GraphQLAstVisitor, IValidationContext { private readonly Dictionary> _visitorMap; - private List<(IRule, IEnumerable)> _errors = new List<(IRule, IEnumerable)>(); + + private readonly List<(IRule, IEnumerable)> _errors = + new List<(IRule, IEnumerable)>(); public DocumentRulesVisitor( IEnumerable rules, @@ -64,20 +66,17 @@ public ValidationResult Validate() public override void Visit(GraphQLDocument ast) { var rules = GetRules(ast); - foreach (var rule in rules) CollectErrors(rule, rule.Visit(ast)); + foreach (var rule in rules) + CollectErrors(rule, rule.Visit(ast, this)); base.Visit(ast); } - private void CollectErrors(IRule rule, IEnumerable validationErrors) - { - _errors.Add((rule, validationErrors)); - } - public override GraphQLName BeginVisitAlias(GraphQLName alias) { var rules = GetRules(alias); - foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitAlias(alias)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitAlias(alias, this)); return base.BeginVisitAlias(alias); } @@ -85,7 +84,8 @@ public override GraphQLName BeginVisitAlias(GraphQLName alias) public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) { var rules = GetRules(argument); - foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitArgument(argument)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitArgument(argument, this)); return base.BeginVisitArgument(argument); } @@ -93,7 +93,8 @@ public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) public override IEnumerable BeginVisitArguments(IEnumerable arguments) { var rules = GetRules(ASTNodeKind.Argument); - foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitArguments(arguments)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitArguments(arguments, this)); return base.BeginVisitArguments(arguments); } @@ -102,7 +103,8 @@ public override GraphQLScalarValue BeginVisitBooleanValue( GraphQLScalarValue value) { var rules = GetRules(value); - foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitBooleanValue(value)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitBooleanValue(value, this)); return base.BeginVisitBooleanValue(value); } @@ -110,8 +112,8 @@ public override GraphQLScalarValue BeginVisitBooleanValue( public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) { var rules = GetRules(directive); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitDirective(directive)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitDirective(directive, this)); return base.BeginVisitDirective(directive); } @@ -119,8 +121,8 @@ public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) { var rules = GetRules(value); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitEnumValue(value)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitEnumValue(value, this)); return base.BeginVisitEnumValue(value); } @@ -129,8 +131,8 @@ public override GraphQLFieldSelection BeginVisitFieldSelection( GraphQLFieldSelection selection) { var rules = GetRules(selection); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitFieldSelection(selection)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFieldSelection(selection, this)); return base.BeginVisitFieldSelection(selection); } @@ -139,8 +141,8 @@ public override GraphQLScalarValue BeginVisitFloatValue( GraphQLScalarValue value) { var rules = GetRules(value); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitFloatValue(value)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFloatValue(value, this)); return base.BeginVisitFloatValue(value); } @@ -149,8 +151,8 @@ public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( GraphQLFragmentDefinition node) { var rules = GetRules(node); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitFragmentDefinition(node)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFragmentDefinition(node, this)); return base.BeginVisitFragmentDefinition(node); } @@ -159,8 +161,8 @@ public override GraphQLFragmentSpread BeginVisitFragmentSpread( GraphQLFragmentSpread fragmentSpread) { var rules = GetRules(fragmentSpread); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitFragmentSpread(fragmentSpread)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitFragmentSpread(fragmentSpread, this)); return base.BeginVisitFragmentSpread(fragmentSpread); } @@ -169,8 +171,8 @@ public override GraphQLInlineFragment BeginVisitInlineFragment( GraphQLInlineFragment inlineFragment) { var rules = GetRules(inlineFragment); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitInlineFragment(inlineFragment)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitInlineFragment(inlineFragment, this)); return base.BeginVisitInlineFragment(inlineFragment); } @@ -178,8 +180,8 @@ public override GraphQLInlineFragment BeginVisitInlineFragment( public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) { var rules = GetRules(value); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitIntValue(value)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitIntValue(value, this)); return base.BeginVisitIntValue(value); } @@ -187,8 +189,8 @@ public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) public override GraphQLName BeginVisitName(GraphQLName name) { var rules = GetRules(name); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitName(name)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitName(name, this)); return base.BeginVisitName(name); } @@ -197,8 +199,8 @@ public override GraphQLNamedType BeginVisitNamedType( GraphQLNamedType typeCondition) { var rules = GetRules(typeCondition); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitNamedType(typeCondition)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitNamedType(typeCondition, this)); return base.BeginVisitNamedType(typeCondition); } @@ -207,8 +209,8 @@ public override GraphQLOperationDefinition BeginVisitOperationDefinition( GraphQLOperationDefinition definition) { var rules = GetRules(definition); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitOperationDefinition(definition)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitOperationDefinition(definition, this)); return base.BeginVisitOperationDefinition(definition); } @@ -217,8 +219,8 @@ public override GraphQLOperationDefinition EndVisitOperationDefinition( GraphQLOperationDefinition definition) { var rules = GetRules(definition); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitOperationDefinition(definition)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitOperationDefinition(definition, this)); return base.EndVisitOperationDefinition(definition); } @@ -227,8 +229,8 @@ public override GraphQLSelectionSet BeginVisitSelectionSet( GraphQLSelectionSet selectionSet) { var rules = GetRules(selectionSet); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitSelectionSet(selectionSet)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitSelectionSet(selectionSet, this)); return base.BeginVisitSelectionSet(selectionSet); } @@ -237,8 +239,8 @@ public override GraphQLScalarValue BeginVisitStringValue( GraphQLScalarValue value) { var rules = GetRules(value); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitStringValue(value)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitStringValue(value, this)); return base.BeginVisitStringValue(value); } @@ -246,8 +248,8 @@ public override GraphQLScalarValue BeginVisitStringValue( public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) { var rules = GetRules(variable); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitVariable(variable)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitVariable(variable, this)); return base.BeginVisitVariable(variable); } @@ -256,8 +258,8 @@ public override GraphQLVariableDefinition BeginVisitVariableDefinition( GraphQLVariableDefinition node) { var rules = GetRules(node); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitVariableDefinition(node)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitVariableDefinition(node, this)); return base.BeginVisitVariableDefinition(node); } @@ -267,8 +269,8 @@ public override IEnumerable BeginVisitVariableDefinit { var rules = GetRules(ASTNodeKind.VariableDefinition); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitVariableDefinitions(variableDefinitions)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitVariableDefinitions(variableDefinitions, this)); return base.BeginVisitVariableDefinitions(variableDefinitions); } @@ -276,8 +278,8 @@ public override IEnumerable BeginVisitVariableDefinit public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) { var rules = GetRules(argument); - foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitArgument(argument)); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitArgument(argument, this)); return base.EndVisitArgument(argument); } @@ -286,8 +288,8 @@ public override GraphQLFieldSelection EndVisitFieldSelection( GraphQLFieldSelection selection) { var rules = GetRules(selection); - foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitFieldSelection(selection)); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitFieldSelection(selection, this)); return base.EndVisitFieldSelection(selection); } @@ -295,8 +297,8 @@ public override GraphQLFieldSelection EndVisitFieldSelection( public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) { var rules = GetRules(variable); - foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitVariable(variable)); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitVariable(variable, this)); return base.EndVisitVariable(variable); } @@ -305,8 +307,8 @@ public override GraphQLObjectField BeginVisitObjectField( GraphQLObjectField node) { var rules = GetRules(node); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitObjectField(node)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitObjectField(node, this)); return base.BeginVisitObjectField(node); } @@ -315,8 +317,8 @@ public override GraphQLObjectValue BeginVisitObjectValue( GraphQLObjectValue node) { var rules = GetRules(node); - foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitObjectValue(node)); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitObjectValue(node, this)); return base.BeginVisitObjectValue(node); } @@ -324,8 +326,8 @@ public override GraphQLObjectValue BeginVisitObjectValue( public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) { var rules = GetRules(node); - foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitObjectValue(node)); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitObjectValue(node, this)); return base.EndVisitObjectValue(node); } @@ -333,15 +335,20 @@ public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) public override GraphQLListValue EndVisitListValue(GraphQLListValue node) { var rules = GetRules(node); - foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitListValue(node)); + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitListValue(node, this)); return base.EndVisitListValue(node); } + private void CollectErrors(IRule rule, IEnumerable validationErrors) + { + _errors.Add((rule, validationErrors)); + } + private ValidationResult BuildResult() { - return new ValidationResult() + return new ValidationResult { Errors = _errors.SelectMany(e => e.Item2).ToList() }; diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/Errors.cs index 1fd277f2c..8d3c901d0 100644 --- a/src/graphql/validation/Errors.cs +++ b/src/graphql/validation/Errors.cs @@ -7,5 +7,7 @@ public static class Errors public const string R511ExecutableDefinitions = "5.1.1 Executable Definitions"; public const string R5221LoneAnonymousOperation = "5.2.2.1 Lone Anonymous Operation"; + + public const string R5231SingleRootField = "5.2.3.1 Single root field"; } } \ No newline at end of file diff --git a/src/graphql/validation/IDocumentRuleVisitor.cs b/src/graphql/validation/IDocumentRuleVisitor.cs index 1f10263a5..dced4b399 100644 --- a/src/graphql/validation/IDocumentRuleVisitor.cs +++ b/src/graphql/validation/IDocumentRuleVisitor.cs @@ -5,85 +5,99 @@ namespace tanka.graphql.validation { public interface IDocumentRuleVisitor { - IEnumerable BeginVisitAlias(GraphQLName alias); + IEnumerable BeginVisitAlias(GraphQLName alias, + IValidationContext context); - IEnumerable BeginVisitArgument(GraphQLArgument argument); + IEnumerable BeginVisitArgument(GraphQLArgument argument, + IValidationContext context); IEnumerable BeginVisitArguments( - IEnumerable arguments); + IEnumerable arguments, + IValidationContext context); IEnumerable BeginVisitBooleanValue( - GraphQLScalarValue value); + GraphQLScalarValue value, IValidationContext context); - IEnumerable BeginVisitDirective(GraphQLDirective directive); + IEnumerable BeginVisitDirective(GraphQLDirective directive, + IValidationContext context); IEnumerable BeginVisitDirectives( - IEnumerable directives); + IEnumerable directives, IValidationContext context); - IEnumerable BeginVisitEnumValue(GraphQLScalarValue value); + IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, + IValidationContext context); IEnumerable BeginVisitFieldSelection( - GraphQLFieldSelection selection); + GraphQLFieldSelection selection, IValidationContext context); IEnumerable BeginVisitFloatValue( - GraphQLScalarValue value); + GraphQLScalarValue value, IValidationContext context); IEnumerable BeginVisitFragmentDefinition( - GraphQLFragmentDefinition node); + GraphQLFragmentDefinition node, IValidationContext context); IEnumerable BeginVisitFragmentSpread( - GraphQLFragmentSpread fragmentSpread); + GraphQLFragmentSpread fragmentSpread, IValidationContext context); IEnumerable BeginVisitInlineFragment( - GraphQLInlineFragment inlineFragment); + GraphQLInlineFragment inlineFragment, IValidationContext context); - IEnumerable BeginVisitIntValue(GraphQLScalarValue value); + IEnumerable BeginVisitIntValue(GraphQLScalarValue value, + IValidationContext context); - IEnumerable BeginVisitName(GraphQLName name); + IEnumerable BeginVisitName(GraphQLName name, + IValidationContext context); IEnumerable BeginVisitNamedType( - GraphQLNamedType typeCondition); + GraphQLNamedType typeCondition, IValidationContext context); - IEnumerable BeginVisitNode(ASTNode node); + IEnumerable BeginVisitNode(ASTNode node, + IValidationContext context); IEnumerable BeginVisitOperationDefinition( - GraphQLOperationDefinition definition); + GraphQLOperationDefinition definition, IValidationContext context); IEnumerable EndVisitOperationDefinition( - GraphQLOperationDefinition definition); + GraphQLOperationDefinition definition, IValidationContext context); IEnumerable BeginVisitSelectionSet( - GraphQLSelectionSet selectionSet); + GraphQLSelectionSet selectionSet, IValidationContext context); IEnumerable BeginVisitStringValue( - GraphQLScalarValue value); + GraphQLScalarValue value, IValidationContext context); - IEnumerable BeginVisitVariable(GraphQLVariable variable); + IEnumerable BeginVisitVariable(GraphQLVariable variable, + IValidationContext context); IEnumerable BeginVisitVariableDefinition( - GraphQLVariableDefinition node); + GraphQLVariableDefinition node, IValidationContext context); IEnumerable BeginVisitVariableDefinitions( - IEnumerable variableDefinitions); + IEnumerable variableDefinitions, + IValidationContext context); - IEnumerable EndVisitArgument(GraphQLArgument argument); + IEnumerable EndVisitArgument(GraphQLArgument argument, + IValidationContext context); IEnumerable EndVisitFieldSelection( - GraphQLFieldSelection selection); + GraphQLFieldSelection selection, IValidationContext context); - IEnumerable EndVisitVariable(GraphQLVariable variable); + IEnumerable EndVisitVariable(GraphQLVariable variable, + IValidationContext context); - IEnumerable Visit(GraphQLDocument document); + IEnumerable Visit(GraphQLDocument document, + IValidationContext context); IEnumerable BeginVisitObjectField( - GraphQLObjectField node); + GraphQLObjectField node, IValidationContext context); IEnumerable BeginVisitObjectValue( - GraphQLObjectValue node); + GraphQLObjectValue node, IValidationContext context); - IEnumerable EndVisitObjectValue(GraphQLObjectValue node); - - IEnumerable EndVisitListValue(GraphQLListValue node); + IEnumerable EndVisitObjectValue(GraphQLObjectValue node, + IValidationContext context); + IEnumerable EndVisitListValue(GraphQLListValue node, + IValidationContext context); } } \ No newline at end of file diff --git a/src/graphql/validation/IRule.cs b/src/graphql/validation/IRule.cs index 403d1fee1..efd38d9a5 100644 --- a/src/graphql/validation/IRule.cs +++ b/src/graphql/validation/IRule.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using GraphQLParser.AST; +using tanka.graphql.type; namespace tanka.graphql.validation { @@ -11,158 +12,158 @@ public interface IRule : IDocumentRuleVisitor public abstract class Rule : IRule { - public virtual IEnumerable BeginVisitAlias(GraphQLName alias) + public virtual IEnumerable BeginVisitAlias(GraphQLName alias, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitArgument(GraphQLArgument argument) + public virtual IEnumerable BeginVisitArgument(GraphQLArgument argument, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitArguments(IEnumerable arguments) + public virtual IEnumerable BeginVisitArguments(IEnumerable arguments, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitBooleanValue(GraphQLScalarValue value) + public virtual IEnumerable BeginVisitBooleanValue(GraphQLScalarValue value, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitDirective(GraphQLDirective directive) + public virtual IEnumerable BeginVisitDirective(GraphQLDirective directive, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitDirectives(IEnumerable directives) + public virtual IEnumerable BeginVisitDirectives(IEnumerable directives, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitEnumValue(GraphQLScalarValue value) + public virtual IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection) + public virtual IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFloatValue(GraphQLScalarValue value) + public virtual IEnumerable BeginVisitFloatValue(GraphQLScalarValue value, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node) + public virtual IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread) + public virtual IEnumerable BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment) + public virtual IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitIntValue(GraphQLScalarValue value) + public virtual IEnumerable BeginVisitIntValue(GraphQLScalarValue value, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitName(GraphQLName name) + public virtual IEnumerable BeginVisitName(GraphQLName name, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitNamedType(GraphQLNamedType typeCondition) + public virtual IEnumerable BeginVisitNamedType(GraphQLNamedType typeCondition, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitNode(ASTNode node) + public virtual IEnumerable BeginVisitNode(ASTNode node, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitOperationDefinition(GraphQLOperationDefinition definition) + public virtual IEnumerable BeginVisitOperationDefinition(GraphQLOperationDefinition definition, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition) + public virtual IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet) + public virtual IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitStringValue(GraphQLScalarValue value) + public virtual IEnumerable BeginVisitStringValue(GraphQLScalarValue value, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitVariable(GraphQLVariable variable) + public virtual IEnumerable BeginVisitVariable(GraphQLVariable variable, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node) + public virtual IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context) { return Enumerable.Empty(); } public virtual IEnumerable BeginVisitVariableDefinitions( - IEnumerable variableDefinitions) + IEnumerable variableDefinitions, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitArgument(GraphQLArgument argument) + public virtual IEnumerable EndVisitArgument(GraphQLArgument argument, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection) + public virtual IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitVariable(GraphQLVariable variable) + public virtual IEnumerable EndVisitVariable(GraphQLVariable variable, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable Visit(GraphQLDocument document) + public virtual IEnumerable Visit(GraphQLDocument document, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitObjectField(GraphQLObjectField node) + public virtual IEnumerable BeginVisitObjectField(GraphQLObjectField node, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitObjectValue(GraphQLObjectValue node) + public virtual IEnumerable BeginVisitObjectValue(GraphQLObjectValue node, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitObjectValue(GraphQLObjectValue node) + public virtual IEnumerable EndVisitObjectValue(GraphQLObjectValue node, IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitListValue(GraphQLListValue node) + public virtual IEnumerable EndVisitListValue(GraphQLListValue node, IValidationContext context) { return Enumerable.Empty(); } diff --git a/src/graphql/validation/IValidationContext.cs b/src/graphql/validation/IValidationContext.cs new file mode 100644 index 000000000..b4de168a8 --- /dev/null +++ b/src/graphql/validation/IValidationContext.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using GraphQLParser.AST; +using tanka.graphql.type; + +namespace tanka.graphql.validation +{ + public interface IValidationContext + { + ISchema Schema { get; } + + GraphQLDocument Document { get; } + + Dictionary VariableValues { get; } + } +} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R511ExecutableDefinitions.cs b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs index 88d87295f..30332b60d 100644 --- a/src/graphql/validation/rules2/R511ExecutableDefinitions.cs +++ b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using GraphQLParser.AST; +using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { @@ -13,7 +14,7 @@ public class R511ExecutableDefinitions : Rule { public override IEnumerable AppliesToNodeKinds => new[] {ASTNodeKind.Document}; - public override IEnumerable Visit(GraphQLDocument document) + public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) { foreach (var definition in document.Definitions) { diff --git a/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs b/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs index e7fe85956..2d8bf57d2 100644 --- a/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs +++ b/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using GraphQLParser.AST; +using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { @@ -19,7 +20,7 @@ public class R5211OperationNameUniqueness : Rule ASTNodeKind.Document }; - public override IEnumerable Visit(GraphQLDocument document) + public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) { if (document.Definitions.OfType().Count() < 2) yield break; diff --git a/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs b/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs index f5d7cd23b..04b92f827 100644 --- a/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs +++ b/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using GraphQLParser.AST; +using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { @@ -17,7 +18,7 @@ public class R5221LoneAnonymousOperation : Rule ASTNodeKind.Document }; - public override IEnumerable Visit(GraphQLDocument document) + public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) { var operations = document.Definitions .OfType() diff --git a/src/graphql/validation/rules2/R5231SingleRootField.cs b/src/graphql/validation/rules2/R5231SingleRootField.cs new file mode 100644 index 000000000..ded149c09 --- /dev/null +++ b/src/graphql/validation/rules2/R5231SingleRootField.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.execution; +using tanka.graphql.type; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R5231SingleRootField : Rule + { + public override IEnumerable AppliesToNodeKinds + => new[] + { + ASTNodeKind.Document + }; + + public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) + { + var subscriptions = document.Definitions + .OfType() + .Where(op => op.Operation == OperationType.Subscription) + .ToList(); + + if (!subscriptions.Any()) + yield break; + + var schema = context.Schema; + //todo(pekka): should this report error? + if (schema.Subscription == null) + yield break; + + 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) + yield return new ValidationError( + Errors.R5231SingleRootField, + "Subscription operations must have exactly one root field.", + subscription); + } + } + } +} \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index a53dcf7e8..c9a285bb6 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -14,10 +14,25 @@ public class ValidatorFacts public ValidatorFacts() { var sdl = - @"type Query { + @" + schema { + query: Query + subscription: Subscription + } + + type Query { dog: Dog } + type Subscription { + newMessage: Message + } + + type Message { + body: String + sender: String + } + enum DogCommand { SIT, DOWN, HEEL } type Dog implements Pet { @@ -216,5 +231,130 @@ query getName { result.Errors, error => error.Code == Errors.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, + new 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, + new 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, + new R5231SingleRootField()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.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, + new R5231SingleRootField()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.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, + new R5231SingleRootField()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R5231SingleRootField); + } } } \ No newline at end of file From d8efc74790dafd3303ffba29f72882e3e9a613b0 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 21 Feb 2019 13:43:45 +0200 Subject: [PATCH 05/20] 5.3.1 --- src/graphql/sdl/SdlReader.cs | 19 ++- .../validation/DocumentRulesVisitor.cs | 2 +- src/graphql/validation/Errors.cs | 2 + .../validation/rules2/R531FieldSelections.cs | 98 +++++++++++ .../validation/ValidatorFacts.cs | 158 +++++++++++++++++- 5 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 src/graphql/validation/rules2/R531FieldSelections.cs 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/validation/DocumentRulesVisitor.cs b/src/graphql/validation/DocumentRulesVisitor.cs index 5b62b1d18..ea7173d31 100644 --- a/src/graphql/validation/DocumentRulesVisitor.cs +++ b/src/graphql/validation/DocumentRulesVisitor.cs @@ -220,7 +220,7 @@ public override GraphQLOperationDefinition EndVisitOperationDefinition( { var rules = GetRules(definition); foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitOperationDefinition(definition, this)); + CollectErrors(rule, rule.EndVisitOperationDefinition(definition, this)); return base.EndVisitOperationDefinition(definition); } diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/Errors.cs index 8d3c901d0..20b2f6b0e 100644 --- a/src/graphql/validation/Errors.cs +++ b/src/graphql/validation/Errors.cs @@ -9,5 +9,7 @@ public static class Errors 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"; } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R531FieldSelections.cs b/src/graphql/validation/rules2/R531FieldSelections.cs new file mode 100644 index 000000000..e1d5c99e1 --- /dev/null +++ b/src/graphql/validation/rules2/R531FieldSelections.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using GraphQLParser.AST; +using tanka.graphql.type; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// For each selection in the document. + /// Let fieldName be the target field of selection + /// fieldName must be defined on type in scope + /// + public class R531FieldSelections : Rule + { + public override IEnumerable AppliesToNodeKinds => new[] + { + ASTNodeKind.OperationDefinition, + ASTNodeKind.InlineFragment, + ASTNodeKind.FragmentDefinition, + ASTNodeKind.Field + }; + + public INamedType ParentType { get; set; } + + public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) + { + var typeName = inlineFragment.TypeCondition.Name.Value; + ParentType = context.Schema.GetNamedType(typeName); + yield break; + } + + public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) + { + var typeName = node.TypeCondition.Name.Value; + ParentType = context.Schema.GetNamedType(typeName); + yield break; + } + + public override IEnumerable BeginVisitOperationDefinition( + GraphQLOperationDefinition definition, IValidationContext context) + { + var schema = context.Schema; + + switch (definition.Operation) + { + case OperationType.Query: + ParentType = schema.Query; + break; + case OperationType.Mutation: + ParentType = schema.Mutation; + break; + case OperationType.Subscription: + ParentType = schema.Subscription; + break; + } + + yield break; + } + + public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + var fieldName = selection.Name.Value; + + if (fieldName == "__typename") + yield break; + + var field = GetField(fieldName, context.Schema); + + if (field == null) + { + yield return new ValidationError( + Errors.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); + } + else + { + if (field.Type is ComplexType complexType) + ParentType = complexType; + } + } + + private IField GetField(string fieldName, ISchema schema) + { + if (ParentType is null) + return null; + + if (!(ParentType is ComplexType)) + return null; + + return schema.GetField(ParentType.Name, fieldName); + } + } +} \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index c9a285bb6..2f1c14120 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using GraphQLParser.AST; using tanka.graphql.sdl; using tanka.graphql.type; @@ -182,7 +183,7 @@ query getName { } [Fact] - public void Rule_5221_Lone_Anonymous_Operation_invalid_valid() + public void Rule_5221_Lone_Anonymous_Operation_valid() { /* Given */ var document = Parser.ParseDocument( @@ -356,5 +357,160 @@ public void Rule_5231_Single_root_field_invalid_with_typename() result.Errors, error => error.Code == Errors.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, + new R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.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, + new R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R531FieldSelections); + } + + [Fact] + public void Rule_531_Field_Selections_valid() + { + /* Given */ + var document = Parser.ParseDocument( + @"{ + dog { + name + } + }"); + + /* When */ + var result = Validate( + document, + new 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, + new 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, + new R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.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, + new 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, + new R531FieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R531FieldSelections + && error.Nodes.OfType() + .Any(n => n.Name.Value == "name")); + + Assert.Single( + result.Errors, + error => error.Code == Errors.R531FieldSelections + && error.Nodes.OfType() + .Any(n => n.Name.Value == "barkVolume")); + } } } \ No newline at end of file From 3da087c46882417fb4e1c7affa3d38661f4e5b9c Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 21 Feb 2019 14:11:46 +0200 Subject: [PATCH 06/20] 5.3.3 --- src/graphql/validation/Errors.cs | 2 + .../rules2/R511ExecutableDefinitions.cs | 8 +- .../rules2/R533LeafFieldSelections.cs | 129 ++++++++++++++++++ tanka-graphql.sln.DotSettings | 1 + .../validation/ValidatorFacts.cs | 113 +++++++++++++++ 5 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 src/graphql/validation/rules2/R533LeafFieldSelections.cs diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/Errors.cs index 20b2f6b0e..ee039beae 100644 --- a/src/graphql/validation/Errors.cs +++ b/src/graphql/validation/Errors.cs @@ -11,5 +11,7 @@ public static class Errors 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"; } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R511ExecutableDefinitions.cs b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs index 30332b60d..b97a20a29 100644 --- a/src/graphql/validation/rules2/R511ExecutableDefinitions.cs +++ b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs @@ -1,14 +1,12 @@ using System.Collections.Generic; using GraphQLParser.AST; -using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { /// - /// Formal Specification - /// - /// For each definition definition in the document. - /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). + /// Formal Specification + /// For each definition definition in the document. + /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). /// public class R511ExecutableDefinitions : Rule { diff --git a/src/graphql/validation/rules2/R533LeafFieldSelections.cs b/src/graphql/validation/rules2/R533LeafFieldSelections.cs new file mode 100644 index 000000000..e435e97ac --- /dev/null +++ b/src/graphql/validation/rules2/R533LeafFieldSelections.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.type; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R533LeafFieldSelections : Rule + { + public override IEnumerable AppliesToNodeKinds => new[] + { + ASTNodeKind.OperationDefinition, + ASTNodeKind.InlineFragment, + ASTNodeKind.FragmentDefinition, + ASTNodeKind.Field + }; + + public INamedType ParentType { get; set; } + + public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) + { + var typeName = inlineFragment.TypeCondition.Name.Value; + ParentType = context.Schema.GetNamedType(typeName); + yield break; + } + + public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) + { + var typeName = node.TypeCondition.Name.Value; + ParentType = context.Schema.GetNamedType(typeName); + yield break; + } + + public override IEnumerable BeginVisitOperationDefinition( + GraphQLOperationDefinition definition, IValidationContext context) + { + var schema = context.Schema; + + switch (definition.Operation) + { + case OperationType.Query: + ParentType = schema.Query; + break; + case OperationType.Mutation: + ParentType = schema.Mutation; + break; + case OperationType.Subscription: + ParentType = schema.Subscription; + break; + } + + yield break; + } + + public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + var fieldName = selection.Name.Value; + + if (fieldName == "__typename") + yield break; + + var field = GetField(fieldName, context.Schema); + + if (field != null) + { + var selectionType = field.Type; + var hasSubSelection = selection.SelectionSet?.Selections?.Any(); + + if (selectionType is ScalarType && hasSubSelection == true) + { + yield return new ValidationError( + Errors.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) + { + yield return new ValidationError( + Errors.R533LeafFieldSelections, + "Field selections on scalars or enums are never " + + "allowed, because they are the leaf nodes of any GraphQL query.", + selection); + } + + if (selectionType is ComplexType && hasSubSelection == null) + { + yield return new ValidationError( + Errors.R533LeafFieldSelections, + "Leaf selections on objects, interfaces, and unions " + + "without subfields are disallowed.", + selection); + } + + if (selectionType is UnionType && hasSubSelection == null) + { + yield return new ValidationError( + Errors.R533LeafFieldSelections, + "Leaf selections on objects, interfaces, and unions " + + "without subfields are disallowed.", + selection); + } + } + } + + private IField GetField(string fieldName, ISchema schema) + { + if (ParentType is null) + return null; + + if (!(ParentType is ComplexType)) + return null; + + return schema.GetField(ParentType.Name, fieldName); + } + } +} \ 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/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 2f1c14120..0d8cab4c8 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -23,6 +23,9 @@ public ValidatorFacts() type Query { dog: Dog + human: Human + pet: Pet + catOrDog: CatOrDog } type Subscription { @@ -512,5 +515,115 @@ public void Rule_531_Field_Selections_invalid_with_union() && 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, + new 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 R533LeafFieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R533LeafFieldSelections); + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_invalid2() + { + /* Given */ + var document = Parser.ParseDocument( + @"query directQueryOnObjectWithoutSubFields { + human + }"); + + /* When */ + var result = Validate( + document, + new R533LeafFieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R533LeafFieldSelections); + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_invalid3() + { + /* Given */ + var document = Parser.ParseDocument( + @"query directQueryOnInterfaceWithoutSubFields { + pet + }"); + + /* When */ + var result = Validate( + document, + new R533LeafFieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R533LeafFieldSelections); + } + + [Fact] + public void Rule_533_Leaf_Field_Selections_invalid4() + { + /* Given */ + var document = Parser.ParseDocument( + @"query directQueryOnUnionWithoutSubFields { + catOrDog + }"); + + /* When */ + var result = Validate( + document, + new R533LeafFieldSelections()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R533LeafFieldSelections); + } } } \ No newline at end of file From e8fbd35ebec3cbe710034c5892815d62cbe29cd4 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 21 Feb 2019 16:34:38 +0200 Subject: [PATCH 07/20] 5.4.1 --- src/graphql/type/IField.cs | 3 + .../validation/DocumentRulesVisitor.cs | 8 +- src/graphql/validation/Errors.cs | 2 + .../validation/rules2/R541ArgumentNames.cs | 127 ++++++++++++++++++ .../validation/ValidatorFacts.cs | 64 +++++++++ 5 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 src/graphql/validation/rules2/R541ArgumentNames.cs 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/validation/DocumentRulesVisitor.cs b/src/graphql/validation/DocumentRulesVisitor.cs index ea7173d31..60a5870bc 100644 --- a/src/graphql/validation/DocumentRulesVisitor.cs +++ b/src/graphql/validation/DocumentRulesVisitor.cs @@ -63,13 +63,13 @@ public ValidationResult Validate() return BuildResult(); } - public override void Visit(GraphQLDocument ast) + public override void Visit(GraphQLDocument document) { - var rules = GetRules(ast); + var rules = GetRules(document); foreach (var rule in rules) - CollectErrors(rule, rule.Visit(ast, this)); + CollectErrors(rule, rule.Visit(document, this)); - base.Visit(ast); + base.Visit(document); } public override GraphQLName BeginVisitAlias(GraphQLName alias) diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/Errors.cs index ee039beae..68b5213a2 100644 --- a/src/graphql/validation/Errors.cs +++ b/src/graphql/validation/Errors.cs @@ -13,5 +13,7 @@ public static class Errors 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"; } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R541ArgumentNames.cs b/src/graphql/validation/rules2/R541ArgumentNames.cs new file mode 100644 index 000000000..a5049def6 --- /dev/null +++ b/src/graphql/validation/rules2/R541ArgumentNames.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.type; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R541ArgumentNames : Rule + { + public override IEnumerable AppliesToNodeKinds => new[] + { + ASTNodeKind.OperationDefinition, + ASTNodeKind.InlineFragment, + ASTNodeKind.FragmentDefinition, + ASTNodeKind.Field, + ASTNodeKind.Directive, + ASTNodeKind.Argument + }; + + public override IEnumerable BeginVisitArgument( + GraphQLArgument argument, + IValidationContext context) + { + var argumentName = argument.Name.Value; + + Argument argumentDefinition = null; + + if (ParentDirectiveType != null) + { + argumentDefinition = ParentDirectiveType.GetArgument(argumentName); + } + else + { + argumentDefinition = ParentField?.GetArgument(argumentName); + } + + if (argumentDefinition == null) + { + yield return new ValidationError( + Errors.R541ArgumentNames, + "Every argument provided to a field or directive " + + "must be defined in the set of possible arguments of that " + + "field or directive.", + argument); + } + } + + public INamedType ParentType { get; set; } + + public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) + { + var typeName = inlineFragment.TypeCondition.Name.Value; + ParentType = context.Schema.GetNamedType(typeName); + yield break; + } + + public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) + { + var typeName = node.TypeCondition.Name.Value; + ParentType = context.Schema.GetNamedType(typeName); + yield break; + } + + public override IEnumerable BeginVisitOperationDefinition( + GraphQLOperationDefinition definition, IValidationContext context) + { + var schema = context.Schema; + + switch (definition.Operation) + { + case OperationType.Query: + ParentType = schema.Query; + break; + case OperationType.Mutation: + ParentType = schema.Mutation; + break; + case OperationType.Subscription: + ParentType = schema.Subscription; + break; + } + + yield break; + } + + public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + var fieldName = selection.Name.Value; + + if (fieldName == "__typename") + yield break; + + ParentField = GetField(fieldName, context.Schema); + } + + public override IEnumerable BeginVisitDirective( + GraphQLDirective directive, + IValidationContext context) + { + ParentDirectiveType = context.Schema.GetDirective(directive.Name.Value); + yield break; + } + + public DirectiveType ParentDirectiveType { get; set; } + + public IField ParentField { get; set; } + + private IField GetField(string fieldName, ISchema schema) + { + if (ParentType is null) + return null; + + if (!(ParentType is ComplexType)) + return null; + + return schema.GetField(ParentType.Name, fieldName); + } + } +} \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 0d8cab4c8..07677d120 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -625,5 +625,69 @@ public void Rule_533_Leaf_Field_Selections_invalid4() result.Errors, error => error.Code == Errors.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, + new 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, + new R541ArgumentNames()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.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, + new R541ArgumentNames()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == Errors.R541ArgumentNames); + } } } \ No newline at end of file From de4330943b41b9c8ae262a266885699f4dadb8e6 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Fri, 22 Feb 2019 17:10:27 +0200 Subject: [PATCH 08/20] TypeTrackingRuleBase based on JS TypeInfo --- src/graphql.benchmarks/Benchmarks.cs | 45 +- src/graphql/language/Visitor.cs | 302 +++++++++++++ .../validation/DocumentRulesVisitor.cs | 83 +++- .../validation/IDocumentRuleVisitor.cs | 14 + src/graphql/validation/IRule.cs | 122 ++++-- src/graphql/validation/Validator.cs | 36 +- .../validation/rules2/R531FieldSelections.cs | 72 +--- .../rules2/R533LeafFieldSelections.cs | 64 +-- .../validation/rules2/R541ArgumentNames.cs | 105 +---- .../validation/rules2/TypeTrackingRuleBase.cs | 404 ++++++++++++++++++ 10 files changed, 977 insertions(+), 270 deletions(-) create mode 100644 src/graphql/language/Visitor.cs create mode 100644 src/graphql/validation/rules2/TypeTrackingRuleBase.cs diff --git a/src/graphql.benchmarks/Benchmarks.cs b/src/graphql.benchmarks/Benchmarks.cs index 2a0f64058..764b74dae 100644 --- a/src/graphql.benchmarks/Benchmarks.cs +++ b/src/graphql.benchmarks/Benchmarks.cs @@ -20,6 +20,8 @@ public class Benchmarks private ISchema _schema; private GraphQLDocument _mutation; private GraphQLDocument _subscription; + private Dictionary> _defaultRulesMap; + private IEnumerable _defaultRulesList; [GlobalSetup] public async Task Setup() @@ -28,8 +30,12 @@ public async Task Setup() _query = Utils.InitializeQuery(); _mutation = Utils.InitializeMutation(); _subscription = Utils.InitializeSubscription(); + _defaultRulesMap = Validator.DefaultRules; + _defaultRulesList = Validator.DefaultRules + .SelectMany(r => r.Value) + .ToList(); } - + /* [Benchmark] public async Task Query_with_defaults() { @@ -135,6 +141,7 @@ 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() @@ -150,6 +157,42 @@ public async Task Validate_query_with_defaults() } } + [Benchmark] + public void Validate_query_with_defaults_v2_rules() + { + var result = Validator.Validate( + _defaultRulesList, + _schema, + _query); + + if (!result.IsValid) + { + throw new InvalidOperationException( + $"Validation failed. {result}"); + } + } + + [Benchmark] + public void Validate_query_with_defaults_v2_rulesMap() + { + var result = Validator.Validate( + _defaultRulesMap, + _schema, + _query); + + if (!result.IsValid) + { + throw new InvalidOperationException( + $"Validation failed. {result}"); + } + } + + [Benchmark] + public void Initialize_validation_rules_map() + { + DocumentRulesVisitor.InitializeRuleActionMap(_defaultRulesList); + } + private static void AssertResult(IEnumerable errors) { if (errors != null && errors.Any()) diff --git a/src/graphql/language/Visitor.cs b/src/graphql/language/Visitor.cs new file mode 100644 index 000000000..c497b8b9b --- /dev/null +++ b/src/graphql/language/Visitor.cs @@ -0,0 +1,302 @@ +using System.Collections.Generic; +using GraphQLParser.AST; + +namespace tanka.graphql.language +{ + public class Visitor + { + public Visitor() + { + Fragments = + new Dictionary(); + } + + protected IDictionary Fragments { get; } + + 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); + 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) + if (definition.Kind == ASTNodeKind.FragmentDefinition) + { + var fragmentDefinition = (GraphQLFragmentDefinition) definition; + Fragments.Add(fragmentDefinition.Name.Value, fragmentDefinition); + } + + 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/validation/DocumentRulesVisitor.cs b/src/graphql/validation/DocumentRulesVisitor.cs index 60a5870bc..4b64133a8 100644 --- a/src/graphql/validation/DocumentRulesVisitor.cs +++ b/src/graphql/validation/DocumentRulesVisitor.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Linq; -using GraphQLParser; using GraphQLParser.AST; +using tanka.graphql.language; using tanka.graphql.type; namespace tanka.graphql.validation { - public class DocumentRulesVisitor : GraphQLAstVisitor, IValidationContext + public class DocumentRulesVisitor : Visitor, IValidationContext { private readonly Dictionary> _visitorMap; @@ -111,20 +111,30 @@ public override GraphQLScalarValue BeginVisitBooleanValue( public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) { - var rules = GetRules(directive); + var rules = GetRules(directive).ToList(); foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitDirective(directive, this)); - return base.BeginVisitDirective(directive); + var _ = base.BeginVisitDirective(directive); + + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitDirective(directive, this)); + + return _; } public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) { - var rules = GetRules(value); + var rules = GetRules(value).ToList(); foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitEnumValue(value, this)); - return base.BeginVisitEnumValue(value); + var _ = base.BeginVisitEnumValue(value); + + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitEnumValue(value, this)); + + return _; } public override GraphQLFieldSelection BeginVisitFieldSelection( @@ -150,11 +160,16 @@ public override GraphQLScalarValue BeginVisitFloatValue( public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( GraphQLFragmentDefinition node) { - var rules = GetRules(node); + var rules = GetRules(node).ToList(); foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitFragmentDefinition(node, this)); - return base.BeginVisitFragmentDefinition(node); + var result = base.BeginVisitFragmentDefinition(node); + + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitFragmentDefinition(node, this)); + + return result; } public override GraphQLFragmentSpread BeginVisitFragmentSpread( @@ -170,11 +185,16 @@ public override GraphQLFragmentSpread BeginVisitFragmentSpread( public override GraphQLInlineFragment BeginVisitInlineFragment( GraphQLInlineFragment inlineFragment) { - var rules = GetRules(inlineFragment); + var rules = GetRules(inlineFragment).ToList(); foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitInlineFragment(inlineFragment, this)); - return base.BeginVisitInlineFragment(inlineFragment); + var _ = base.BeginVisitInlineFragment(inlineFragment); + + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitInlineFragment(inlineFragment, this)); + + return _; } public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) @@ -228,11 +248,16 @@ public override GraphQLOperationDefinition EndVisitOperationDefinition( public override GraphQLSelectionSet BeginVisitSelectionSet( GraphQLSelectionSet selectionSet) { - var rules = GetRules(selectionSet); + var rules = GetRules(selectionSet).ToList(); foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitSelectionSet(selectionSet, this)); - return base.BeginVisitSelectionSet(selectionSet); + var _ = base.BeginVisitSelectionSet(selectionSet); + + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitSelectionSet(selectionSet, this)); + + return _; } public override GraphQLScalarValue BeginVisitStringValue( @@ -257,11 +282,16 @@ public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) public override GraphQLVariableDefinition BeginVisitVariableDefinition( GraphQLVariableDefinition node) { - var rules = GetRules(node); + var rules = GetRules(node).ToList(); foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitVariableDefinition(node, this)); - return base.BeginVisitVariableDefinition(node); + var _ = base.BeginVisitVariableDefinition(node); + + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitVariableDefinition(node, this)); + + return _; } public override IEnumerable BeginVisitVariableDefinitions( @@ -306,11 +336,16 @@ public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) public override GraphQLObjectField BeginVisitObjectField( GraphQLObjectField node) { - var rules = GetRules(node); + var rules = GetRules(node).ToList(); foreach (var rule in rules) CollectErrors(rule, rule.BeginVisitObjectField(node, this)); - return base.BeginVisitObjectField(node); + var _= base.BeginVisitObjectField(node); + + foreach (var rule in rules) + CollectErrors(rule, rule.EndVisitObjectField(node, this)); + + return _; } public override GraphQLObjectValue BeginVisitObjectValue( @@ -332,6 +367,20 @@ public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) return base.EndVisitObjectValue(node); } + public override ASTNode BeginVisitNode(ASTNode node) + { + return base.BeginVisitNode(node); + } + + public override GraphQLListValue BeginVisitListValue(GraphQLListValue node) + { + var rules = GetRules(node); + foreach (var rule in rules) + CollectErrors(rule, rule.BeginVisitListValue(node, this)); + + return base.BeginVisitListValue(node); + } + public override GraphQLListValue EndVisitListValue(GraphQLListValue node) { var rules = GetRules(node); @@ -343,7 +392,7 @@ public override GraphQLListValue EndVisitListValue(GraphQLListValue node) private void CollectErrors(IRule rule, IEnumerable validationErrors) { - _errors.Add((rule, validationErrors)); + _errors.Add((rule, validationErrors.ToList())); } private ValidationResult BuildResult() diff --git a/src/graphql/validation/IDocumentRuleVisitor.cs b/src/graphql/validation/IDocumentRuleVisitor.cs index dced4b399..8ff3fc504 100644 --- a/src/graphql/validation/IDocumentRuleVisitor.cs +++ b/src/graphql/validation/IDocumentRuleVisitor.cs @@ -99,5 +99,19 @@ IEnumerable EndVisitObjectValue(GraphQLObjectValue node, IEnumerable EndVisitListValue(GraphQLListValue node, IValidationContext context); + + IEnumerable EndVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context); + + IEnumerable EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context); + + IEnumerable EndVisitDirective(GraphQLDirective directive, + IValidationContext context); + + IEnumerable BeginVisitListValue(GraphQLListValue node, IValidationContext context); + IEnumerable EndVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context); + IEnumerable EndVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context); + IEnumerable EndVisitObjectField(GraphQLObjectField node, IValidationContext context); + IEnumerable EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context); } } \ No newline at end of file diff --git a/src/graphql/validation/IRule.cs b/src/graphql/validation/IRule.cs index efd38d9a5..07d53368b 100644 --- a/src/graphql/validation/IRule.cs +++ b/src/graphql/validation/IRule.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using GraphQLParser.AST; -using tanka.graphql.type; namespace tanka.graphql.validation { @@ -17,62 +16,120 @@ public virtual IEnumerable BeginVisitAlias(GraphQLName alias, I return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitArgument(GraphQLArgument argument, IValidationContext context) + public virtual IEnumerable BeginVisitArgument(GraphQLArgument argument, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitArguments(IEnumerable arguments, IValidationContext context) + public virtual IEnumerable BeginVisitArguments(IEnumerable arguments, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitBooleanValue(GraphQLScalarValue value, IValidationContext context) + public virtual IEnumerable BeginVisitBooleanValue(GraphQLScalarValue value, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitDirective(GraphQLDirective directive, IValidationContext context) + public virtual IEnumerable BeginVisitDirective(GraphQLDirective directive, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitDirectives(IEnumerable directives, IValidationContext context) + public virtual IEnumerable EndVisitDirective(GraphQLDirective directive, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, IValidationContext context) + public virtual IEnumerable BeginVisitListValue(GraphQLListValue node, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) + public virtual IEnumerable EndVisitSelectionSet(GraphQLSelectionSet selectionSet, + IValidationContext context) + { + yield break; + } + + public virtual IEnumerable EndVisitVariableDefinition(GraphQLVariableDefinition node, + IValidationContext context) + { + yield break; + } + + public virtual IEnumerable EndVisitObjectField(GraphQLObjectField node, IValidationContext context) + { + yield break; + } + + public virtual IEnumerable EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context) + { + yield break; + } + + public virtual IEnumerable BeginVisitDirectives(IEnumerable directives, + IValidationContext context) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, + IValidationContext context) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitFloatValue(GraphQLScalarValue value, + IValidationContext context) + { + return Enumerable.Empty(); + } + + public virtual IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFloatValue(GraphQLScalarValue value, IValidationContext context) + public virtual IEnumerable EndVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) + public virtual IEnumerable BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread, IValidationContext context) + public virtual IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) + public virtual IEnumerable EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitIntValue(GraphQLScalarValue value, IValidationContext context) + public virtual IEnumerable BeginVisitIntValue(GraphQLScalarValue value, + IValidationContext context) { return Enumerable.Empty(); } @@ -82,7 +139,8 @@ public virtual IEnumerable BeginVisitName(GraphQLName name, IVa return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitNamedType(GraphQLNamedType typeCondition, IValidationContext context) + public virtual IEnumerable BeginVisitNamedType(GraphQLNamedType typeCondition, + IValidationContext context) { return Enumerable.Empty(); } @@ -92,32 +150,38 @@ public virtual IEnumerable BeginVisitNode(ASTNode node, IValida return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitOperationDefinition(GraphQLOperationDefinition definition, IValidationContext context) + public virtual IEnumerable BeginVisitOperationDefinition(GraphQLOperationDefinition definition, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition, IValidationContext context) + public virtual IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context) + public virtual IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitStringValue(GraphQLScalarValue value, IValidationContext context) + public virtual IEnumerable BeginVisitStringValue(GraphQLScalarValue value, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitVariable(GraphQLVariable variable, IValidationContext context) + public virtual IEnumerable BeginVisitVariable(GraphQLVariable variable, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context) + public virtual IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node, + IValidationContext context) { return Enumerable.Empty(); } @@ -128,17 +192,20 @@ public virtual IEnumerable BeginVisitVariableDefinitions( return Enumerable.Empty(); } - public virtual IEnumerable EndVisitArgument(GraphQLArgument argument, IValidationContext context) + public virtual IEnumerable EndVisitArgument(GraphQLArgument argument, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) + public virtual IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitVariable(GraphQLVariable variable, IValidationContext context) + public virtual IEnumerable EndVisitVariable(GraphQLVariable variable, + IValidationContext context) { return Enumerable.Empty(); } @@ -148,17 +215,20 @@ public virtual IEnumerable Visit(GraphQLDocument document, IVal return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitObjectField(GraphQLObjectField node, IValidationContext context) + public virtual IEnumerable BeginVisitObjectField(GraphQLObjectField node, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable BeginVisitObjectValue(GraphQLObjectValue node, IValidationContext context) + public virtual IEnumerable BeginVisitObjectValue(GraphQLObjectValue node, + IValidationContext context) { return Enumerable.Empty(); } - public virtual IEnumerable EndVisitObjectValue(GraphQLObjectValue node, IValidationContext context) + public virtual IEnumerable EndVisitObjectValue(GraphQLObjectValue node, + IValidationContext context) { return Enumerable.Empty(); } diff --git a/src/graphql/validation/Validator.cs b/src/graphql/validation/Validator.cs index 67f5e770d..bb70bc606 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -4,18 +4,52 @@ using GraphQLParser.AST; using tanka.graphql.type; using tanka.graphql.validation.rules; +using V2 = tanka.graphql.validation.rules2; namespace tanka.graphql.validation { public static class Validator { + public static Dictionary> DefaultRules = + DocumentRulesVisitor.InitializeRuleActionMap(new IRule[] + { + new V2.R5211OperationNameUniqueness(), + new V2.R5221LoneAnonymousOperation(), + new V2.R5231SingleRootField(), + new V2.R531FieldSelections(), + new V2.R533LeafFieldSelections(), + new V2.R541ArgumentNames(), + new V2.R511ExecutableDefinitions(), + + }); + public static ValidationResult Validate( IEnumerable rules, ISchema schema, GraphQLDocument document, Dictionary variableValues = null) { - var visitor = new DocumentRulesVisitor(rules, schema, document, variableValues); + var visitor = new DocumentRulesVisitor( + rules, + schema, + document, + variableValues); + + return visitor.Validate(); + } + + public static ValidationResult Validate( + Dictionary> ruleMap, + ISchema schema, + GraphQLDocument document, + Dictionary variableValues = null) + { + var visitor = new DocumentRulesVisitor( + ruleMap, + schema, + document, + variableValues); + return visitor.Validate(); } diff --git a/src/graphql/validation/rules2/R531FieldSelections.cs b/src/graphql/validation/rules2/R531FieldSelections.cs index e1d5c99e1..790d5d978 100644 --- a/src/graphql/validation/rules2/R531FieldSelections.cs +++ b/src/graphql/validation/rules2/R531FieldSelections.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using GraphQLParser.AST; -using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { @@ -9,90 +8,31 @@ namespace tanka.graphql.validation.rules2 /// Let fieldName be the target field of selection /// fieldName must be defined on type in scope /// - public class R531FieldSelections : Rule + public class R531FieldSelections : TypeTrackingRuleBase { - public override IEnumerable AppliesToNodeKinds => new[] - { - ASTNodeKind.OperationDefinition, - ASTNodeKind.InlineFragment, - ASTNodeKind.FragmentDefinition, - ASTNodeKind.Field - }; - - public INamedType ParentType { get; set; } - - public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - var typeName = inlineFragment.TypeCondition.Name.Value; - ParentType = context.Schema.GetNamedType(typeName); - yield break; - } - - public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + public override IEnumerable BeginVisitFieldSelection( + GraphQLFieldSelection selection, IValidationContext context) { - var typeName = node.TypeCondition.Name.Value; - ParentType = context.Schema.GetNamedType(typeName); - yield break; - } - - public override IEnumerable BeginVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context) - { - var schema = context.Schema; - - switch (definition.Operation) + foreach (var validationError in base.BeginVisitFieldSelection(selection, context)) { - case OperationType.Query: - ParentType = schema.Query; - break; - case OperationType.Mutation: - ParentType = schema.Mutation; - break; - case OperationType.Subscription: - ParentType = schema.Subscription; - break; + yield return validationError; } - yield break; - } - - public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { var fieldName = selection.Name.Value; if (fieldName == "__typename") yield break; - var field = GetField(fieldName, context.Schema); - if (field == null) - { + if (getFieldDef() == null) yield return new ValidationError( Errors.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); - } - else - { - if (field.Type is ComplexType complexType) - ParentType = complexType; - } - } - - private IField GetField(string fieldName, ISchema schema) - { - if (ParentType is null) - return null; - - if (!(ParentType is ComplexType)) - return null; - return schema.GetField(ParentType.Name, fieldName); } } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R533LeafFieldSelections.cs b/src/graphql/validation/rules2/R533LeafFieldSelections.cs index e435e97ac..3458c0d45 100644 --- a/src/graphql/validation/rules2/R533LeafFieldSelections.cs +++ b/src/graphql/validation/rules2/R533LeafFieldSelections.cs @@ -13,68 +13,27 @@ namespace tanka.graphql.validation.rules2 /// If selectionType is an interface, union, or object /// The subselection set of that selection must NOT BE empty /// - public class R533LeafFieldSelections : Rule + public class R533LeafFieldSelections : TypeTrackingRuleBase { - public override IEnumerable AppliesToNodeKinds => new[] - { - ASTNodeKind.OperationDefinition, - ASTNodeKind.InlineFragment, - ASTNodeKind.FragmentDefinition, - ASTNodeKind.Field - }; - - public INamedType ParentType { get; set; } - - public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - var typeName = inlineFragment.TypeCondition.Name.Value; - ParentType = context.Schema.GetNamedType(typeName); - yield break; - } - public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) { - var typeName = node.TypeCondition.Name.Value; - ParentType = context.Schema.GetNamedType(typeName); - yield break; - } - - public override IEnumerable BeginVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context) - { - var schema = context.Schema; - - switch (definition.Operation) + foreach (var validationError in base.BeginVisitFieldSelection(selection, context)) { - case OperationType.Query: - ParentType = schema.Query; - break; - case OperationType.Mutation: - ParentType = schema.Mutation; - break; - case OperationType.Subscription: - ParentType = schema.Subscription; - break; + yield return validationError; } - yield break; - } - - public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { var fieldName = selection.Name.Value; if (fieldName == "__typename") yield break; - var field = GetField(fieldName, context.Schema); + var field = getFieldDef(); if (field != null) { - var selectionType = field.Type; + var selectionType = field.Value.Field.Type; var hasSubSelection = selection.SelectionSet?.Selections?.Any(); if (selectionType is ScalarType && hasSubSelection == true) @@ -114,16 +73,5 @@ public override IEnumerable BeginVisitFieldSelection(GraphQLFie } } } - - private IField GetField(string fieldName, ISchema schema) - { - if (ParentType is null) - return null; - - if (!(ParentType is ComplexType)) - return null; - - return schema.GetField(ParentType.Name, fieldName); - } } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R541ArgumentNames.cs b/src/graphql/validation/rules2/R541ArgumentNames.cs index a5049def6..fbe9f783b 100644 --- a/src/graphql/validation/rules2/R541ArgumentNames.cs +++ b/src/graphql/validation/rules2/R541ArgumentNames.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.Linq; using GraphQLParser.AST; -using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { @@ -11,117 +9,22 @@ namespace tanka.graphql.validation.rules2 /// Let argumentDefinition be the argument definition provided by the parent field or definition named argumentName. /// argumentDefinition must exist. /// - public class R541ArgumentNames : Rule + public class R541ArgumentNames : TypeTrackingRuleBase { - public override IEnumerable AppliesToNodeKinds => new[] - { - ASTNodeKind.OperationDefinition, - ASTNodeKind.InlineFragment, - ASTNodeKind.FragmentDefinition, - ASTNodeKind.Field, - ASTNodeKind.Directive, - ASTNodeKind.Argument - }; - public override IEnumerable BeginVisitArgument( GraphQLArgument argument, IValidationContext context) { - var argumentName = argument.Name.Value; - - Argument argumentDefinition = null; + foreach (var validationError in base.BeginVisitArgument(argument, context)) + yield return validationError; - if (ParentDirectiveType != null) - { - argumentDefinition = ParentDirectiveType.GetArgument(argumentName); - } - else - { - argumentDefinition = ParentField?.GetArgument(argumentName); - } - - if (argumentDefinition == null) - { + if (getArgument() == null) yield return new ValidationError( Errors.R541ArgumentNames, "Every argument provided to a field or directive " + "must be defined in the set of possible arguments of that " + "field or directive.", argument); - } - } - - public INamedType ParentType { get; set; } - - public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - var typeName = inlineFragment.TypeCondition.Name.Value; - ParentType = context.Schema.GetNamedType(typeName); - yield break; - } - - public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context) - { - var typeName = node.TypeCondition.Name.Value; - ParentType = context.Schema.GetNamedType(typeName); - yield break; - } - - public override IEnumerable BeginVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context) - { - var schema = context.Schema; - - switch (definition.Operation) - { - case OperationType.Query: - ParentType = schema.Query; - break; - case OperationType.Mutation: - ParentType = schema.Mutation; - break; - case OperationType.Subscription: - ParentType = schema.Subscription; - break; - } - - yield break; - } - - public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - var fieldName = selection.Name.Value; - - if (fieldName == "__typename") - yield break; - - ParentField = GetField(fieldName, context.Schema); - } - - public override IEnumerable BeginVisitDirective( - GraphQLDirective directive, - IValidationContext context) - { - ParentDirectiveType = context.Schema.GetDirective(directive.Name.Value); - yield break; - } - - public DirectiveType ParentDirectiveType { get; set; } - - public IField ParentField { get; set; } - - private IField GetField(string fieldName, ISchema schema) - { - if (ParentType is null) - return null; - - if (!(ParentType is ComplexType)) - return null; - - return schema.GetField(ParentType.Name, fieldName); } } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/TypeTrackingRuleBase.cs b/src/graphql/validation/rules2/TypeTrackingRuleBase.cs new file mode 100644 index 000000000..5a63a210b --- /dev/null +++ b/src/graphql/validation/rules2/TypeTrackingRuleBase.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.execution; +using tanka.graphql.type; + +namespace tanka.graphql.validation.rules2 +{ + public abstract class TypeTrackingRuleBase : Rule + { + private Argument _argument; + + private readonly Stack _defaultValueStack = new Stack(); + + private DirectiveType _directive; + + private object _enumValue; + + 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(); + + public override IEnumerable AppliesToNodeKinds => new[] + { + ASTNodeKind.SelectionSet, + ASTNodeKind.Field, + ASTNodeKind.OperationDefinition, + ASTNodeKind.InlineFragment, + ASTNodeKind.FragmentDefinition, + ASTNodeKind.Directive, + ASTNodeKind.VariableDefinition, + ASTNodeKind.Argument, + ASTNodeKind.ListValue, + ASTNodeKind.ObjectField, + ASTNodeKind.EnumValue + }; + + public override IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, + IValidationContext context) + { + var namedType = getNamedType(getType()); + var complexType = namedType as ComplexType; + _parentTypeStack.Push(complexType); + + yield break; + } + + public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + var parentType = getParentType(); + (string Name, IField Field)? fieldDef = null; + IType fieldType = null; + + if (parentType != null) + { + fieldDef = getFieldDef(context.Schema, parentType, selection); + + if (fieldDef != null) fieldType = fieldDef.Value.Field.Type; + } + + _fieldDefStack.Push(fieldDef); + _typeStack.Push(TypeIs.IsOutputType(fieldType) ? fieldType : null); + + yield break; + } + + public override IEnumerable BeginVisitDirective(GraphQLDirective directive, + IValidationContext context) + { + _directive = context.Schema.GetDirective(directive.Name.Value); + + yield break; + } + + public override IEnumerable BeginVisitOperationDefinition( + GraphQLOperationDefinition definition, IValidationContext context) + { + ObjectType type = null; + switch (definition.Operation) + { + case OperationType.Query: + type = context.Schema.Query; + break; + case OperationType.Mutation: + type = context.Schema.Mutation; + break; + case OperationType.Subscription: + type = context.Schema.Subscription; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + _typeStack.Push(type); + + yield break; + } + + public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) + { + var typeConditionAst = inlineFragment.TypeCondition; + var outputType = Ast.TypeFromAst(context.Schema, typeConditionAst) + ?? getNamedType(getType()); + _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); + + yield break; + } + + public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) + { + var typeConditionAst = node.TypeCondition; + var outputType = Ast.TypeFromAst(context.Schema, typeConditionAst) + ?? getNamedType(getType()); + _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); + + yield break; + } + + public override IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node, + IValidationContext context) + { + var inputType = Ast.TypeFromAst(context.Schema, node.Type); + _inputTypeStack.Push(TypeIs.IsInputType(inputType) ? inputType : null); + + yield break; + } + + public override IEnumerable BeginVisitArgument(GraphQLArgument argument, + IValidationContext context) + { + 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); + + yield break; + } + + public override IEnumerable BeginVisitListValue(GraphQLListValue node, + IValidationContext context) + { + 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); + + yield break; + } + + public override IEnumerable BeginVisitObjectField(GraphQLObjectField node, + IValidationContext context) + { + var objectType = getNamedType(getInputType()); + IType inputFieldType = null; + InputObjectField inputField = null; + + if (objectType is InputObjectType inputObjectType) + { + inputField = context.Schema.GetInputField( + inputObjectType.Name, + node.Name.Value); + + if (inputField != null) + inputFieldType = inputField.Type; + } + + _defaultValueStack.Push(inputField?.DefaultValue); + _inputTypeStack.Push(TypeIs.IsInputType(inputFieldType) ? inputFieldType : null); + + yield break; + } + + public override IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, + IValidationContext context) + { + var maybeEnumType = getNamedType(getInputType()); + object enumValue = null; + if (maybeEnumType is EnumType enumType) enumValue = enumType.ParseLiteral(value); + + _enumValue = enumValue; + + yield break; + } + + public override IEnumerable EndVisitSelectionSet(GraphQLSelectionSet selectionSet, + IValidationContext context) + { + _parentTypeStack.Pop(); + + return base.EndVisitSelectionSet(selectionSet, context); + } + + public override IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + _fieldDefStack.Pop(); + _typeStack.Pop(); + + return base.EndVisitFieldSelection(selection, context); + } + + public override IEnumerable EndVisitDirective(GraphQLDirective directive, + IValidationContext context) + { + _directive = null; + return base.EndVisitDirective(directive, context); + } + + public override IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition, + IValidationContext context) + { + _typeStack.Pop(); + return base.EndVisitOperationDefinition(definition, context); + } + + public override IEnumerable EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) + { + _typeStack.Pop(); + return base.EndVisitInlineFragment(inlineFragment, context); + } + + public override IEnumerable EndVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) + { + _typeStack.Pop(); + return base.EndVisitFragmentDefinition(node, context); + } + + public override IEnumerable EndVisitVariableDefinition(GraphQLVariableDefinition node, + IValidationContext context) + { + _inputTypeStack.Pop(); + return base.EndVisitVariableDefinition(node, context); + } + + public override IEnumerable EndVisitArgument(GraphQLArgument argument, + IValidationContext context) + { + _argument = null; + _defaultValueStack.Pop(); + _inputTypeStack.Pop(); + return base.EndVisitArgument(argument, context); + } + + public override IEnumerable EndVisitListValue(GraphQLListValue node, + IValidationContext context) + { + _defaultValueStack.Pop(); + _inputTypeStack.Pop(); + return base.EndVisitListValue(node, context); + } + + public override IEnumerable EndVisitObjectField(GraphQLObjectField node, + IValidationContext context) + { + _defaultValueStack.Pop(); + _inputTypeStack.Pop(); + return base.EndVisitObjectField(node, context); + } + + public override IEnumerable EndVisitEnumValue(GraphQLScalarValue value, + IValidationContext context) + { + _enumValue = null; + return base.EndVisitEnumValue(value, context); + } + + protected IType getType() + { + if (_typeStack.Count == 0) + return null; + + return _typeStack.Peek(); + } + + protected ComplexType getParentType() + { + if (_typeStack.Count == 0) + return null; + + return _parentTypeStack.Peek(); + } + + //todo: originally returns an input type + protected IType getInputType() + { + if (_typeStack.Count == 0) + return null; + + return _inputTypeStack.Peek(); + } + + protected IType getParentInputType() + { + //todo: probably a bad idea + return _inputTypeStack.ElementAtOrDefault(_inputTypeStack.Count - 2); + } + + protected (string Name, IField Field)? getFieldDef() + { + if (_fieldDefStack.Count == 0) + return null; + + return _fieldDefStack.Peek(); + } + + protected object getDefaultValue() + { + if (_defaultValueStack.Count == 0) + return null; + + return _defaultValueStack.Peek(); + } + + protected DirectiveType getDirective() + { + return _directive; + } + + protected Argument getArgument() + { + return _argument; + } + + protected object getEnumValue() + { + return _enumValue; + } + + protected IType getNamedType(IType type) + { + return type?.Unwrap(); + } + + protected IType getNullableType(IType type) + { + if (type is NonNull nonNull) + return nonNull.WrappedType; + + return null; + } + + private (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 From c125dab3dc6afd0dfc9414dd95761de7335ddb84 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sat, 23 Feb 2019 10:15:47 +0200 Subject: [PATCH 09/20] Report errors on context, remove yielding on enumerable --- src/graphql.benchmarks/Benchmarks.cs | 27 +- .../validation/DocumentRulesVisitor.cs | 104 +++--- .../validation/IDocumentRuleVisitor.cs | 117 ------ src/graphql/validation/IRule.cs | 342 ++++++------------ src/graphql/validation/IValidationContext.cs | 6 + src/graphql/validation/RuleBase.cs | 198 ++++++++++ .../{Errors.cs => ValidationErrorCodes.cs} | 2 +- .../rules/R511ExecutableDefinitions.cs | 2 +- .../rules2/R511ExecutableDefinitions.cs | 8 +- .../rules2/R5211OperationNameUniqueness.cs | 20 +- .../rules2/R5221LoneAnonymousOperation.cs | 13 +- .../validation/rules2/R5231SingleRootField.cs | 25 +- .../validation/rules2/R531FieldSelections.cs | 20 +- .../rules2/R533LeafFieldSelections.cs | 39 +- .../validation/rules2/R541ArgumentNames.cs | 14 +- .../validation/rules2/TypeTrackingRuleBase.cs | 136 +++---- .../validation/ValidatorFacts.cs | 34 +- 17 files changed, 495 insertions(+), 612 deletions(-) delete mode 100644 src/graphql/validation/IDocumentRuleVisitor.cs create mode 100644 src/graphql/validation/RuleBase.cs rename src/graphql/validation/{Errors.cs => ValidationErrorCodes.cs} (93%) diff --git a/src/graphql.benchmarks/Benchmarks.cs b/src/graphql.benchmarks/Benchmarks.cs index 764b74dae..c95c7849c 100644 --- a/src/graphql.benchmarks/Benchmarks.cs +++ b/src/graphql.benchmarks/Benchmarks.cs @@ -21,7 +21,6 @@ public class Benchmarks private GraphQLDocument _mutation; private GraphQLDocument _subscription; private Dictionary> _defaultRulesMap; - private IEnumerable _defaultRulesList; [GlobalSetup] public async Task Setup() @@ -31,9 +30,6 @@ public async Task Setup() _mutation = Utils.InitializeMutation(); _subscription = Utils.InitializeSubscription(); _defaultRulesMap = Validator.DefaultRules; - _defaultRulesList = Validator.DefaultRules - .SelectMany(r => r.Value) - .ToList(); } /* [Benchmark] @@ -158,22 +154,7 @@ public async Task Validate_query_with_defaults() } [Benchmark] - public void Validate_query_with_defaults_v2_rules() - { - var result = Validator.Validate( - _defaultRulesList, - _schema, - _query); - - if (!result.IsValid) - { - throw new InvalidOperationException( - $"Validation failed. {result}"); - } - } - - [Benchmark] - public void Validate_query_with_defaults_v2_rulesMap() + public void Validate_query_with_defaults_v2() { var result = Validator.Validate( _defaultRulesMap, @@ -187,12 +168,6 @@ public void Validate_query_with_defaults_v2_rulesMap() } } - [Benchmark] - public void Initialize_validation_rules_map() - { - DocumentRulesVisitor.InitializeRuleActionMap(_defaultRulesList); - } - private static void AssertResult(IEnumerable errors) { if (errors != null && errors.Any()) diff --git a/src/graphql/validation/DocumentRulesVisitor.cs b/src/graphql/validation/DocumentRulesVisitor.cs index 4b64133a8..67be8476d 100644 --- a/src/graphql/validation/DocumentRulesVisitor.cs +++ b/src/graphql/validation/DocumentRulesVisitor.cs @@ -8,10 +8,10 @@ namespace tanka.graphql.validation { public class DocumentRulesVisitor : Visitor, IValidationContext { - private readonly Dictionary> _visitorMap; + private readonly List _errors = + new List(); - private readonly List<(IRule, IEnumerable)> _errors = - new List<(IRule, IEnumerable)>(); + private readonly Dictionary> _visitorMap; public DocumentRulesVisitor( IEnumerable rules, @@ -41,6 +41,21 @@ public DocumentRulesVisitor( 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 static Dictionary> InitializeRuleActionMap(IEnumerable rules) { var visitors = new Dictionary>(); @@ -67,7 +82,7 @@ public override void Visit(GraphQLDocument document) { var rules = GetRules(document); foreach (var rule in rules) - CollectErrors(rule, rule.Visit(document, this)); + rule.Visit(document, this); base.Visit(document); } @@ -76,7 +91,7 @@ public override GraphQLName BeginVisitAlias(GraphQLName alias) { var rules = GetRules(alias); foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitAlias(alias, this)); + rule.BeginVisitAlias(alias, this); return base.BeginVisitAlias(alias); } @@ -85,7 +100,7 @@ public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) { var rules = GetRules(argument); foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitArgument(argument, this)); + rule.BeginVisitArgument(argument, this); return base.BeginVisitArgument(argument); } @@ -94,7 +109,7 @@ public override IEnumerable BeginVisitArguments(IEnumerable BeginVisitVariableDefinit var rules = GetRules(ASTNodeKind.VariableDefinition); foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitVariableDefinitions(variableDefinitions, this)); + rule.BeginVisitVariableDefinitions(variableDefinitions, this); return base.BeginVisitVariableDefinitions(variableDefinitions); } @@ -309,7 +324,7 @@ public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) { var rules = GetRules(argument); foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitArgument(argument, this)); + rule.EndVisitArgument(argument, this); return base.EndVisitArgument(argument); } @@ -319,7 +334,7 @@ public override GraphQLFieldSelection EndVisitFieldSelection( { var rules = GetRules(selection); foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitFieldSelection(selection, this)); + rule.EndVisitFieldSelection(selection, this); return base.EndVisitFieldSelection(selection); } @@ -328,7 +343,7 @@ public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) { var rules = GetRules(variable); foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitVariable(variable, this)); + rule.EndVisitVariable(variable, this); return base.EndVisitVariable(variable); } @@ -338,12 +353,12 @@ public override GraphQLObjectField BeginVisitObjectField( { var rules = GetRules(node).ToList(); foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitObjectField(node, this)); + rule.BeginVisitObjectField(node, this); - var _= base.BeginVisitObjectField(node); + var _ = base.BeginVisitObjectField(node); foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitObjectField(node, this)); + rule.EndVisitObjectField(node, this); return _; } @@ -353,7 +368,7 @@ public override GraphQLObjectValue BeginVisitObjectValue( { var rules = GetRules(node); foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitObjectValue(node, this)); + rule.BeginVisitObjectValue(node, this); return base.BeginVisitObjectValue(node); } @@ -362,7 +377,7 @@ public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) { var rules = GetRules(node); foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitObjectValue(node, this)); + rule.EndVisitObjectValue(node, this); return base.EndVisitObjectValue(node); } @@ -376,7 +391,7 @@ public override GraphQLListValue BeginVisitListValue(GraphQLListValue node) { var rules = GetRules(node); foreach (var rule in rules) - CollectErrors(rule, rule.BeginVisitListValue(node, this)); + rule.BeginVisitListValue(node, this); return base.BeginVisitListValue(node); } @@ -385,21 +400,16 @@ public override GraphQLListValue EndVisitListValue(GraphQLListValue node) { var rules = GetRules(node); foreach (var rule in rules) - CollectErrors(rule, rule.EndVisitListValue(node, this)); + rule.EndVisitListValue(node, this); return base.EndVisitListValue(node); } - private void CollectErrors(IRule rule, IEnumerable validationErrors) - { - _errors.Add((rule, validationErrors.ToList())); - } - private ValidationResult BuildResult() { return new ValidationResult { - Errors = _errors.SelectMany(e => e.Item2).ToList() + Errors = _errors }; } diff --git a/src/graphql/validation/IDocumentRuleVisitor.cs b/src/graphql/validation/IDocumentRuleVisitor.cs deleted file mode 100644 index 8ff3fc504..000000000 --- a/src/graphql/validation/IDocumentRuleVisitor.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public interface IDocumentRuleVisitor - { - IEnumerable BeginVisitAlias(GraphQLName alias, - IValidationContext context); - - IEnumerable BeginVisitArgument(GraphQLArgument argument, - IValidationContext context); - - IEnumerable BeginVisitArguments( - IEnumerable arguments, - IValidationContext context); - - IEnumerable BeginVisitBooleanValue( - GraphQLScalarValue value, IValidationContext context); - - IEnumerable BeginVisitDirective(GraphQLDirective directive, - IValidationContext context); - - IEnumerable BeginVisitDirectives( - IEnumerable directives, IValidationContext context); - - IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, - IValidationContext context); - - IEnumerable BeginVisitFieldSelection( - GraphQLFieldSelection selection, IValidationContext context); - - IEnumerable BeginVisitFloatValue( - GraphQLScalarValue value, IValidationContext context); - - IEnumerable BeginVisitFragmentDefinition( - GraphQLFragmentDefinition node, IValidationContext context); - - IEnumerable BeginVisitFragmentSpread( - GraphQLFragmentSpread fragmentSpread, IValidationContext context); - - IEnumerable BeginVisitInlineFragment( - GraphQLInlineFragment inlineFragment, IValidationContext context); - - IEnumerable BeginVisitIntValue(GraphQLScalarValue value, - IValidationContext context); - - IEnumerable BeginVisitName(GraphQLName name, - IValidationContext context); - - IEnumerable BeginVisitNamedType( - GraphQLNamedType typeCondition, IValidationContext context); - - IEnumerable BeginVisitNode(ASTNode node, - IValidationContext context); - - IEnumerable BeginVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context); - - IEnumerable EndVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context); - - IEnumerable BeginVisitSelectionSet( - GraphQLSelectionSet selectionSet, IValidationContext context); - - IEnumerable BeginVisitStringValue( - GraphQLScalarValue value, IValidationContext context); - - IEnumerable BeginVisitVariable(GraphQLVariable variable, - IValidationContext context); - - IEnumerable BeginVisitVariableDefinition( - GraphQLVariableDefinition node, IValidationContext context); - - IEnumerable BeginVisitVariableDefinitions( - IEnumerable variableDefinitions, - IValidationContext context); - - IEnumerable EndVisitArgument(GraphQLArgument argument, - IValidationContext context); - - IEnumerable EndVisitFieldSelection( - GraphQLFieldSelection selection, IValidationContext context); - - IEnumerable EndVisitVariable(GraphQLVariable variable, - IValidationContext context); - - IEnumerable Visit(GraphQLDocument document, - IValidationContext context); - - IEnumerable BeginVisitObjectField( - GraphQLObjectField node, IValidationContext context); - - IEnumerable BeginVisitObjectValue( - GraphQLObjectValue node, IValidationContext context); - - IEnumerable EndVisitObjectValue(GraphQLObjectValue node, - IValidationContext context); - - IEnumerable EndVisitListValue(GraphQLListValue node, - IValidationContext context); - - IEnumerable EndVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context); - - IEnumerable EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context); - - IEnumerable EndVisitDirective(GraphQLDirective directive, - IValidationContext context); - - IEnumerable BeginVisitListValue(GraphQLListValue node, IValidationContext context); - IEnumerable EndVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context); - IEnumerable EndVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context); - IEnumerable EndVisitObjectField(GraphQLObjectField node, IValidationContext context); - IEnumerable EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context); - } -} \ No newline at end of file diff --git a/src/graphql/validation/IRule.cs b/src/graphql/validation/IRule.cs index 07d53368b..473c83f7f 100644 --- a/src/graphql/validation/IRule.cs +++ b/src/graphql/validation/IRule.cs @@ -1,243 +1,119 @@ using System.Collections.Generic; -using System.Linq; using GraphQLParser.AST; namespace tanka.graphql.validation { - public interface IRule : IDocumentRuleVisitor + public interface IRule { IEnumerable AppliesToNodeKinds { get; } - } - public abstract class Rule : IRule - { - public virtual IEnumerable BeginVisitAlias(GraphQLName alias, IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitArgument(GraphQLArgument argument, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitArguments(IEnumerable arguments, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitBooleanValue(GraphQLScalarValue value, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitDirective(GraphQLDirective directive, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitDirective(GraphQLDirective directive, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitListValue(GraphQLListValue node, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitSelectionSet(GraphQLSelectionSet selectionSet, - IValidationContext context) - { - yield break; - } - - public virtual IEnumerable EndVisitVariableDefinition(GraphQLVariableDefinition node, - IValidationContext context) - { - yield break; - } - - public virtual IEnumerable EndVisitObjectField(GraphQLObjectField node, IValidationContext context) - { - yield break; - } - - public virtual IEnumerable EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context) - { - yield break; - } - - public virtual IEnumerable BeginVisitDirectives(IEnumerable directives, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitFloatValue(GraphQLScalarValue value, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitIntValue(GraphQLScalarValue value, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitName(GraphQLName name, IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitNamedType(GraphQLNamedType typeCondition, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitNode(ASTNode node, IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitOperationDefinition(GraphQLOperationDefinition definition, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitStringValue(GraphQLScalarValue value, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitVariable(GraphQLVariable variable, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitVariableDefinitions( - IEnumerable variableDefinitions, IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitArgument(GraphQLArgument argument, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitVariable(GraphQLVariable variable, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable Visit(GraphQLDocument document, IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitObjectField(GraphQLObjectField node, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable BeginVisitObjectValue(GraphQLObjectValue node, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitObjectValue(GraphQLObjectValue node, - IValidationContext context) - { - return Enumerable.Empty(); - } - - public virtual IEnumerable EndVisitListValue(GraphQLListValue node, IValidationContext context) - { - return Enumerable.Empty(); - } - - public abstract IEnumerable AppliesToNodeKinds { get; } + void BeginVisitAlias(GraphQLName alias, + IValidationContext context); + + void BeginVisitArgument(GraphQLArgument argument, + IValidationContext context); + + void BeginVisitArguments( + IEnumerable arguments, + IValidationContext context); + + void BeginVisitBooleanValue( + GraphQLScalarValue value, IValidationContext context); + + void BeginVisitDirective(GraphQLDirective directive, + IValidationContext context); + + void BeginVisitDirectives( + IEnumerable directives, IValidationContext context); + + void BeginVisitEnumValue(GraphQLScalarValue value, + IValidationContext context); + + void BeginVisitFieldSelection( + GraphQLFieldSelection selection, IValidationContext context); + + void BeginVisitFloatValue( + GraphQLScalarValue value, IValidationContext context); + + void BeginVisitFragmentDefinition( + GraphQLFragmentDefinition node, IValidationContext context); + + void BeginVisitFragmentSpread( + GraphQLFragmentSpread fragmentSpread, IValidationContext context); + + void BeginVisitInlineFragment( + GraphQLInlineFragment inlineFragment, IValidationContext context); + + void BeginVisitIntValue(GraphQLScalarValue value, + IValidationContext context); + + void BeginVisitName(GraphQLName name, + IValidationContext context); + + void BeginVisitNamedType( + GraphQLNamedType typeCondition, IValidationContext context); + + void BeginVisitNode(ASTNode node, + IValidationContext context); + + void BeginVisitOperationDefinition( + GraphQLOperationDefinition definition, IValidationContext context); + + void EndVisitOperationDefinition( + GraphQLOperationDefinition definition, IValidationContext context); + + void BeginVisitSelectionSet( + GraphQLSelectionSet selectionSet, IValidationContext context); + + void BeginVisitStringValue( + GraphQLScalarValue value, IValidationContext context); + + void BeginVisitVariable(GraphQLVariable variable, + IValidationContext context); + + void BeginVisitVariableDefinition( + GraphQLVariableDefinition node, IValidationContext context); + + void BeginVisitVariableDefinitions( + IEnumerable variableDefinitions, + IValidationContext context); + + void EndVisitArgument(GraphQLArgument argument, + IValidationContext context); + + void EndVisitFieldSelection( + GraphQLFieldSelection selection, IValidationContext context); + + void EndVisitVariable(GraphQLVariable variable, + IValidationContext context); + + void Visit(GraphQLDocument document, + IValidationContext context); + + void BeginVisitObjectField( + GraphQLObjectField node, IValidationContext context); + + void BeginVisitObjectValue( + GraphQLObjectValue node, IValidationContext context); + + void EndVisitObjectValue(GraphQLObjectValue node, + IValidationContext context); + + void EndVisitListValue(GraphQLListValue node, + IValidationContext context); + + void EndVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context); + + void EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context); + + void EndVisitDirective(GraphQLDirective directive, + IValidationContext context); + + void BeginVisitListValue(GraphQLListValue node, IValidationContext context); + void EndVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context); + void EndVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context); + void EndVisitObjectField(GraphQLObjectField node, IValidationContext context); + void EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context); } } \ No newline at end of file diff --git a/src/graphql/validation/IValidationContext.cs b/src/graphql/validation/IValidationContext.cs index b4de168a8..7c3a88e75 100644 --- a/src/graphql/validation/IValidationContext.cs +++ b/src/graphql/validation/IValidationContext.cs @@ -11,5 +11,11 @@ public interface IValidationContext GraphQLDocument Document { get; } Dictionary VariableValues { 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/RuleBase.cs b/src/graphql/validation/RuleBase.cs new file mode 100644 index 000000000..464617314 --- /dev/null +++ b/src/graphql/validation/RuleBase.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using GraphQLParser.AST; + +namespace tanka.graphql.validation +{ + public abstract class RuleBase : IRule + { + public virtual void BeginVisitAlias(GraphQLName alias, IValidationContext context) + { + } + + public virtual void BeginVisitArgument(GraphQLArgument argument, + IValidationContext context) + { + } + + public virtual void BeginVisitArguments(IEnumerable arguments, + IValidationContext context) + { + } + + public virtual void BeginVisitBooleanValue(GraphQLScalarValue value, + IValidationContext context) + { + } + + public virtual void BeginVisitDirective(GraphQLDirective directive, + IValidationContext context) + { + } + + public virtual void EndVisitDirective(GraphQLDirective directive, + IValidationContext context) + { + } + + public virtual void BeginVisitListValue(GraphQLListValue node, + IValidationContext context) + { + } + + public virtual void EndVisitSelectionSet(GraphQLSelectionSet selectionSet, + IValidationContext context) + { + } + + public virtual void EndVisitVariableDefinition(GraphQLVariableDefinition node, + IValidationContext context) + { + } + + public virtual void EndVisitObjectField(GraphQLObjectField node, IValidationContext context) + { + } + + public virtual void EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context) + { + } + + public virtual void BeginVisitDirectives(IEnumerable directives, + IValidationContext context) + { + } + + public virtual void BeginVisitEnumValue(GraphQLScalarValue value, + IValidationContext context) + { + } + + public virtual void BeginVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + } + + public virtual void BeginVisitFloatValue(GraphQLScalarValue value, + IValidationContext context) + { + } + + public virtual void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) + { + } + + public virtual void EndVisitFragmentDefinition(GraphQLFragmentDefinition node, + IValidationContext context) + { + } + + public virtual void BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread, + IValidationContext context) + { + } + + public virtual void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) + { + } + + public virtual void EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, + IValidationContext context) + { + } + + public virtual void BeginVisitIntValue(GraphQLScalarValue value, + IValidationContext context) + { + } + + public virtual void BeginVisitName(GraphQLName name, IValidationContext context) + { + } + + public virtual void BeginVisitNamedType(GraphQLNamedType typeCondition, + IValidationContext context) + { + } + + public virtual void BeginVisitNode(ASTNode node, IValidationContext context) + { + } + + public virtual void BeginVisitOperationDefinition(GraphQLOperationDefinition definition, + IValidationContext context) + { + } + + public virtual void EndVisitOperationDefinition(GraphQLOperationDefinition definition, + IValidationContext context) + { + } + + public virtual void BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, + IValidationContext context) + { + } + + public virtual void BeginVisitStringValue(GraphQLScalarValue value, + IValidationContext context) + { + } + + public virtual void BeginVisitVariable(GraphQLVariable variable, + IValidationContext context) + { + } + + public virtual void BeginVisitVariableDefinition(GraphQLVariableDefinition node, + IValidationContext context) + { + } + + public virtual void BeginVisitVariableDefinitions( + IEnumerable variableDefinitions, IValidationContext context) + { + } + + public virtual void EndVisitArgument(GraphQLArgument argument, + IValidationContext context) + { + } + + public virtual void EndVisitFieldSelection(GraphQLFieldSelection selection, + IValidationContext context) + { + } + + public virtual void EndVisitVariable(GraphQLVariable variable, + IValidationContext context) + { + } + + public virtual void Visit(GraphQLDocument document, IValidationContext context) + { + } + + public virtual void BeginVisitObjectField(GraphQLObjectField node, + IValidationContext context) + { + } + + public virtual void BeginVisitObjectValue(GraphQLObjectValue node, + IValidationContext context) + { + } + + public virtual void EndVisitObjectValue(GraphQLObjectValue node, + IValidationContext context) + { + } + + public virtual void EndVisitListValue(GraphQLListValue node, IValidationContext context) + { + } + + public abstract IEnumerable AppliesToNodeKinds { get; } + } +} \ No newline at end of file diff --git a/src/graphql/validation/Errors.cs b/src/graphql/validation/ValidationErrorCodes.cs similarity index 93% rename from src/graphql/validation/Errors.cs rename to src/graphql/validation/ValidationErrorCodes.cs index 68b5213a2..752e8eb95 100644 --- a/src/graphql/validation/Errors.cs +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -1,6 +1,6 @@ namespace tanka.graphql.validation { - public static class Errors + public static class ValidationErrorCodes { public const string R5211OperationNameUniqueness = "5.2.1.1 Operation Name Uniqueness"; diff --git a/src/graphql/validation/rules/R511ExecutableDefinitions.cs b/src/graphql/validation/rules/R511ExecutableDefinitions.cs index 645245171..7cf34520f 100644 --- a/src/graphql/validation/rules/R511ExecutableDefinitions.cs +++ b/src/graphql/validation/rules/R511ExecutableDefinitions.cs @@ -18,7 +18,7 @@ public INodeVisitor CreateVisitor(ValidationContext context) if (!valid) context.ReportError(new ValidationError( - Errors.R511ExecutableDefinitions, + ValidationErrorCodes.R511ExecutableDefinitions, "GraphQL execution will only consider the " + "executable definitions Operation and Fragment. " + "Type system definitions and extensions are not " + diff --git a/src/graphql/validation/rules2/R511ExecutableDefinitions.cs b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs index b97a20a29..bb226b222 100644 --- a/src/graphql/validation/rules2/R511ExecutableDefinitions.cs +++ b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs @@ -8,11 +8,11 @@ namespace tanka.graphql.validation.rules2 /// For each definition definition in the document. /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). /// - public class R511ExecutableDefinitions : Rule + public class R511ExecutableDefinitions : RuleBase { public override IEnumerable AppliesToNodeKinds => new[] {ASTNodeKind.Document}; - public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) + public override void Visit(GraphQLDocument document, IValidationContext context) { foreach (var definition in document.Definitions) { @@ -20,8 +20,8 @@ public override IEnumerable Visit(GraphQLDocument document, IVa || definition.Kind == ASTNodeKind.FragmentDefinition; if (!valid) - yield return new ValidationError( - Errors.R511ExecutableDefinitions, + context.Error( + ValidationErrorCodes.R511ExecutableDefinitions, "GraphQL execution will only consider the " + "executable definitions Operation and Fragment. " + "Type system definitions and extensions are not " + diff --git a/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs b/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs index 2d8bf57d2..b6542d90c 100644 --- a/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs +++ b/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using GraphQLParser.AST; -using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { @@ -13,17 +12,17 @@ namespace tanka.graphql.validation.rules2 /// Let operations be all operation definitions in the document named operationName. /// operations must be a set of one. /// - public class R5211OperationNameUniqueness : Rule + public class R5211OperationNameUniqueness : RuleBase { public override IEnumerable AppliesToNodeKinds => new[] { ASTNodeKind.Document }; - public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) + public override void Visit(GraphQLDocument document, IValidationContext context) { - if (document.Definitions.OfType().Count() < 2) - yield break; + if (document.Definitions.OfType().Count() < 2) + return; var operations = document.Definitions.OfType() .ToList(); @@ -32,7 +31,7 @@ public override IEnumerable Visit(GraphQLDocument document, IVa { var operationName = op.Name?.Value; - if (string.IsNullOrWhiteSpace(operationName)) + if (string.IsNullOrWhiteSpace(operationName)) continue; var matchingOperations = operations.Where(def => def.Name?.Value == operationName) @@ -40,15 +39,14 @@ public override IEnumerable Visit(GraphQLDocument document, IVa if (matchingOperations.Count() > 1) { - yield return new ValidationError( - Errors.R5211OperationNameUniqueness, - "Each named operation definition must be unique within a document when referred to by its name.", + context.Error(ValidationErrorCodes.R5211OperationNameUniqueness, + "Each named operation definition must be unique within a " + + "document when referred to by its name.", matchingOperations); - yield break; + break; } } - } } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs b/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs index 04b92f827..95db17734 100644 --- a/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs +++ b/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using GraphQLParser.AST; -using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { @@ -11,14 +10,14 @@ namespace tanka.graphql.validation.rules2 /// If operations is a set of more than 1: /// anonymous must be empty. /// - public class R5221LoneAnonymousOperation : Rule + public class R5221LoneAnonymousOperation : RuleBase { public override IEnumerable AppliesToNodeKinds => new[] { ASTNodeKind.Document }; - public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) + public override void Visit(GraphQLDocument document, IValidationContext context) { var operations = document.Definitions .OfType() @@ -28,17 +27,13 @@ public override IEnumerable Visit(GraphQLDocument document, IVa .Count(op => string.IsNullOrEmpty(op.Name?.Value)); if (operations.Count() > 1) - { if (anonymous > 0) - { - yield return new ValidationError( - Errors.R5221LoneAnonymousOperation, + context.Error( + ValidationErrorCodes.R5221LoneAnonymousOperation, "GraphQL allows a short‐hand form for defining " + "query operations when only that one operation exists in " + "the document.", operations); - } - } } } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5231SingleRootField.cs b/src/graphql/validation/rules2/R5231SingleRootField.cs index ded149c09..dfafbb660 100644 --- a/src/graphql/validation/rules2/R5231SingleRootField.cs +++ b/src/graphql/validation/rules2/R5231SingleRootField.cs @@ -2,19 +2,18 @@ using System.Linq; using GraphQLParser.AST; using tanka.graphql.execution; -using tanka.graphql.type; namespace tanka.graphql.validation.rules2 { /// - /// 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. + /// 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 class R5231SingleRootField : Rule + public class R5231SingleRootField : RuleBase { public override IEnumerable AppliesToNodeKinds => new[] @@ -22,7 +21,7 @@ public override IEnumerable AppliesToNodeKinds ASTNodeKind.Document }; - public override IEnumerable Visit(GraphQLDocument document, IValidationContext context) + public override void Visit(GraphQLDocument document, IValidationContext context) { var subscriptions = document.Definitions .OfType() @@ -30,12 +29,12 @@ public override IEnumerable Visit(GraphQLDocument document, IVa .ToList(); if (!subscriptions.Any()) - yield break; + return; var schema = context.Schema; //todo(pekka): should this report error? if (schema.Subscription == null) - yield break; + return; var subscriptionType = schema.Subscription; foreach (var subscription in subscriptions) @@ -51,8 +50,8 @@ public override IEnumerable Visit(GraphQLDocument document, IVa variableValues); if (groupedFieldSet.Count != 1) - yield return new ValidationError( - Errors.R5231SingleRootField, + context.Error( + ValidationErrorCodes.R5231SingleRootField, "Subscription operations must have exactly one root field.", subscription); } diff --git a/src/graphql/validation/rules2/R531FieldSelections.cs b/src/graphql/validation/rules2/R531FieldSelections.cs index 790d5d978..c88bea281 100644 --- a/src/graphql/validation/rules2/R531FieldSelections.cs +++ b/src/graphql/validation/rules2/R531FieldSelections.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using GraphQLParser.AST; +using GraphQLParser.AST; namespace tanka.graphql.validation.rules2 { @@ -10,29 +9,24 @@ namespace tanka.graphql.validation.rules2 /// public class R531FieldSelections : TypeTrackingRuleBase { - public override IEnumerable BeginVisitFieldSelection( + public override void BeginVisitFieldSelection( GraphQLFieldSelection selection, IValidationContext context) { - foreach (var validationError in base.BeginVisitFieldSelection(selection, context)) - { - yield return validationError; - } + base.BeginVisitFieldSelection(selection, context); var fieldName = selection.Name.Value; if (fieldName == "__typename") - yield break; + return; - - if (getFieldDef() == null) - yield return new ValidationError( - Errors.R531FieldSelections, + if (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); - } } } \ No newline at end of file diff --git a/src/graphql/validation/rules2/R533LeafFieldSelections.cs b/src/graphql/validation/rules2/R533LeafFieldSelections.cs index 3458c0d45..a2e86aa3e 100644 --- a/src/graphql/validation/rules2/R533LeafFieldSelections.cs +++ b/src/graphql/validation/rules2/R533LeafFieldSelections.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using GraphQLParser.AST; using tanka.graphql.type; @@ -15,21 +14,17 @@ namespace tanka.graphql.validation.rules2 /// public class R533LeafFieldSelections : TypeTrackingRuleBase { - - public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, + public override void BeginVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) { - foreach (var validationError in base.BeginVisitFieldSelection(selection, context)) - { - yield return validationError; - } + base.BeginVisitFieldSelection(selection, context); var fieldName = selection.Name.Value; if (fieldName == "__typename") - yield break; + return; - var field = getFieldDef(); + var field = GetFieldDef(); if (field != null) { @@ -37,40 +32,32 @@ public override IEnumerable BeginVisitFieldSelection(GraphQLFie var hasSubSelection = selection.SelectionSet?.Selections?.Any(); if (selectionType is ScalarType && hasSubSelection == true) - { - yield return new ValidationError( - Errors.R533LeafFieldSelections, + 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) - { - yield return new ValidationError( - Errors.R533LeafFieldSelections, + 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 ComplexType && hasSubSelection == null) - { - yield return new ValidationError( - Errors.R533LeafFieldSelections, + context.Error( + ValidationErrorCodes.R533LeafFieldSelections, "Leaf selections on objects, interfaces, and unions " + "without subfields are disallowed.", selection); - } if (selectionType is UnionType && hasSubSelection == null) - { - yield return new ValidationError( - Errors.R533LeafFieldSelections, + context.Error( + ValidationErrorCodes.R533LeafFieldSelections, "Leaf selections on objects, interfaces, and unions " + "without subfields are disallowed.", selection); - } } } } diff --git a/src/graphql/validation/rules2/R541ArgumentNames.cs b/src/graphql/validation/rules2/R541ArgumentNames.cs index fbe9f783b..368e0c2db 100644 --- a/src/graphql/validation/rules2/R541ArgumentNames.cs +++ b/src/graphql/validation/rules2/R541ArgumentNames.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using GraphQLParser.AST; +using GraphQLParser.AST; namespace tanka.graphql.validation.rules2 { @@ -11,16 +10,15 @@ namespace tanka.graphql.validation.rules2 /// public class R541ArgumentNames : TypeTrackingRuleBase { - public override IEnumerable BeginVisitArgument( + public override void BeginVisitArgument( GraphQLArgument argument, IValidationContext context) { - foreach (var validationError in base.BeginVisitArgument(argument, context)) - yield return validationError; + base.BeginVisitArgument(argument, context); - if (getArgument() == null) - yield return new ValidationError( - Errors.R541ArgumentNames, + if (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.", diff --git a/src/graphql/validation/rules2/TypeTrackingRuleBase.cs b/src/graphql/validation/rules2/TypeTrackingRuleBase.cs index 5a63a210b..81045e907 100644 --- a/src/graphql/validation/rules2/TypeTrackingRuleBase.cs +++ b/src/graphql/validation/rules2/TypeTrackingRuleBase.cs @@ -7,16 +7,10 @@ namespace tanka.graphql.validation.rules2 { - public abstract class TypeTrackingRuleBase : Rule + public abstract class TypeTrackingRuleBase : RuleBase { - private Argument _argument; - private readonly Stack _defaultValueStack = new Stack(); - private DirectiveType _directive; - - private object _enumValue; - private readonly Stack<(string Name, IField Field)?> _fieldDefStack = new Stack<(string Name, IField Field)?>(); private readonly Stack _inputTypeStack = new Stack(); @@ -24,6 +18,11 @@ public abstract class TypeTrackingRuleBase : Rule private readonly Stack _parentTypeStack = new Stack(); private readonly Stack _typeStack = new Stack(); + private Argument _argument; + + private DirectiveType _directive; + + private object _enumValue; public override IEnumerable AppliesToNodeKinds => new[] { @@ -40,20 +39,18 @@ public abstract class TypeTrackingRuleBase : Rule ASTNodeKind.EnumValue }; - public override IEnumerable BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, + public override void BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context) { - var namedType = getNamedType(getType()); + var namedType = GetNamedType(GetCurrentType()); var complexType = namedType as ComplexType; _parentTypeStack.Push(complexType); - - yield break; } - public override IEnumerable BeginVisitFieldSelection(GraphQLFieldSelection selection, + public override void BeginVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) { - var parentType = getParentType(); + var parentType = GetParentType(); (string Name, IField Field)? fieldDef = null; IType fieldType = null; @@ -66,19 +63,15 @@ public override IEnumerable BeginVisitFieldSelection(GraphQLFie _fieldDefStack.Push(fieldDef); _typeStack.Push(TypeIs.IsOutputType(fieldType) ? fieldType : null); - - yield break; } - public override IEnumerable BeginVisitDirective(GraphQLDirective directive, + public override void BeginVisitDirective(GraphQLDirective directive, IValidationContext context) { _directive = context.Schema.GetDirective(directive.Name.Value); - - yield break; } - public override IEnumerable BeginVisitOperationDefinition( + public override void BeginVisitOperationDefinition( GraphQLOperationDefinition definition, IValidationContext context) { ObjectType type = null; @@ -98,82 +91,70 @@ public override IEnumerable BeginVisitOperationDefinition( } _typeStack.Push(type); - - yield break; } - public override IEnumerable BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, + public override void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) { var typeConditionAst = inlineFragment.TypeCondition; var outputType = Ast.TypeFromAst(context.Schema, typeConditionAst) - ?? getNamedType(getType()); + ?? GetNamedType(GetCurrentType()); _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); - - yield break; } - public override IEnumerable BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, + public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) { var typeConditionAst = node.TypeCondition; var outputType = Ast.TypeFromAst(context.Schema, typeConditionAst) - ?? getNamedType(getType()); + ?? GetNamedType(GetCurrentType()); _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); - - yield break; } - public override IEnumerable BeginVisitVariableDefinition(GraphQLVariableDefinition node, + public override void BeginVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context) { var inputType = Ast.TypeFromAst(context.Schema, node.Type); _inputTypeStack.Push(TypeIs.IsInputType(inputType) ? inputType : null); - - yield break; } - public override IEnumerable BeginVisitArgument(GraphQLArgument argument, + public override void BeginVisitArgument(GraphQLArgument argument, IValidationContext context) { Argument argDef = null; IType argType = null; - if (getDirective() != null) + if (GetDirective() != null) { - argDef = getDirective()?.GetArgument(argument.Name.Value); + argDef = GetDirective()?.GetArgument(argument.Name.Value); argType = argDef?.Type; } - else if (getFieldDef() != null) + else if (GetFieldDef() != null) { - argDef = getFieldDef()?.Field.GetArgument(argument.Name.Value); + argDef = GetFieldDef()?.Field.GetArgument(argument.Name.Value); argType = argDef?.Type; } _argument = argDef; _defaultValueStack.Push(argDef?.DefaultValue); _inputTypeStack.Push(TypeIs.IsInputType(argType) ? argType : null); - - yield break; } - public override IEnumerable BeginVisitListValue(GraphQLListValue node, + public override void BeginVisitListValue(GraphQLListValue node, IValidationContext context) { - var listType = getNullableType(getInputType()); + 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); - - yield break; } - public override IEnumerable BeginVisitObjectField(GraphQLObjectField node, + public override void BeginVisitObjectField(GraphQLObjectField node, IValidationContext context) { - var objectType = getNamedType(getInputType()); + var objectType = GetNamedType(GetInputType()); IType inputFieldType = null; InputObjectField inputField = null; @@ -189,107 +170,90 @@ public override IEnumerable BeginVisitObjectField(GraphQLObject _defaultValueStack.Push(inputField?.DefaultValue); _inputTypeStack.Push(TypeIs.IsInputType(inputFieldType) ? inputFieldType : null); - - yield break; } - public override IEnumerable BeginVisitEnumValue(GraphQLScalarValue value, + public override void BeginVisitEnumValue(GraphQLScalarValue value, IValidationContext context) { - var maybeEnumType = getNamedType(getInputType()); + var maybeEnumType = GetNamedType(GetInputType()); object enumValue = null; if (maybeEnumType is EnumType enumType) enumValue = enumType.ParseLiteral(value); _enumValue = enumValue; - - yield break; } - public override IEnumerable EndVisitSelectionSet(GraphQLSelectionSet selectionSet, + public override void EndVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context) { _parentTypeStack.Pop(); - - return base.EndVisitSelectionSet(selectionSet, context); } - public override IEnumerable EndVisitFieldSelection(GraphQLFieldSelection selection, + public override void EndVisitFieldSelection(GraphQLFieldSelection selection, IValidationContext context) { _fieldDefStack.Pop(); _typeStack.Pop(); - - return base.EndVisitFieldSelection(selection, context); } - public override IEnumerable EndVisitDirective(GraphQLDirective directive, + public override void EndVisitDirective(GraphQLDirective directive, IValidationContext context) { _directive = null; - return base.EndVisitDirective(directive, context); } - public override IEnumerable EndVisitOperationDefinition(GraphQLOperationDefinition definition, + public override void EndVisitOperationDefinition(GraphQLOperationDefinition definition, IValidationContext context) { _typeStack.Pop(); - return base.EndVisitOperationDefinition(definition, context); } - public override IEnumerable EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, + public override void EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) { _typeStack.Pop(); - return base.EndVisitInlineFragment(inlineFragment, context); } - public override IEnumerable EndVisitFragmentDefinition(GraphQLFragmentDefinition node, + public override void EndVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) { _typeStack.Pop(); - return base.EndVisitFragmentDefinition(node, context); } - public override IEnumerable EndVisitVariableDefinition(GraphQLVariableDefinition node, + public override void EndVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context) { _inputTypeStack.Pop(); - return base.EndVisitVariableDefinition(node, context); } - public override IEnumerable EndVisitArgument(GraphQLArgument argument, + public override void EndVisitArgument(GraphQLArgument argument, IValidationContext context) { _argument = null; _defaultValueStack.Pop(); _inputTypeStack.Pop(); - return base.EndVisitArgument(argument, context); } - public override IEnumerable EndVisitListValue(GraphQLListValue node, + public override void EndVisitListValue(GraphQLListValue node, IValidationContext context) { _defaultValueStack.Pop(); _inputTypeStack.Pop(); - return base.EndVisitListValue(node, context); } - public override IEnumerable EndVisitObjectField(GraphQLObjectField node, + public override void EndVisitObjectField(GraphQLObjectField node, IValidationContext context) { _defaultValueStack.Pop(); _inputTypeStack.Pop(); - return base.EndVisitObjectField(node, context); } - public override IEnumerable EndVisitEnumValue(GraphQLScalarValue value, + public override void EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context) { _enumValue = null; - return base.EndVisitEnumValue(value, context); } - protected IType getType() + protected IType GetCurrentType() { if (_typeStack.Count == 0) return null; @@ -297,7 +261,7 @@ protected IType getType() return _typeStack.Peek(); } - protected ComplexType getParentType() + protected ComplexType GetParentType() { if (_typeStack.Count == 0) return null; @@ -306,7 +270,7 @@ protected ComplexType getParentType() } //todo: originally returns an input type - protected IType getInputType() + protected IType GetInputType() { if (_typeStack.Count == 0) return null; @@ -314,13 +278,13 @@ protected IType getInputType() return _inputTypeStack.Peek(); } - protected IType getParentInputType() + protected IType GetParentInputType() { //todo: probably a bad idea return _inputTypeStack.ElementAtOrDefault(_inputTypeStack.Count - 2); } - protected (string Name, IField Field)? getFieldDef() + protected (string Name, IField Field)? GetFieldDef() { if (_fieldDefStack.Count == 0) return null; @@ -328,7 +292,7 @@ protected IType getParentInputType() return _fieldDefStack.Peek(); } - protected object getDefaultValue() + protected object GetDefaultValue() { if (_defaultValueStack.Count == 0) return null; @@ -336,27 +300,27 @@ protected object getDefaultValue() return _defaultValueStack.Peek(); } - protected DirectiveType getDirective() + protected DirectiveType GetDirective() { return _directive; } - protected Argument getArgument() + protected Argument GetArgument() { return _argument; } - protected object getEnumValue() + protected object GetEnumValue() { return _enumValue; } - protected IType getNamedType(IType type) + protected IType GetNamedType(IType type) { return type?.Unwrap(); } - protected IType getNullableType(IType type) + protected IType GetNullableType(IType type) { if (type is NonNull nonNull) return nonNull.WrappedType; diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 07677d120..bbf961066 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -123,7 +123,7 @@ extend type Dog { Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R511ExecutableDefinitions); + error => error.Code == ValidationErrorCodes.R511ExecutableDefinitions); } [Fact] @@ -182,7 +182,7 @@ query getName { Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R5211OperationNameUniqueness); + error => error.Code == ValidationErrorCodes.R5211OperationNameUniqueness); } [Fact] @@ -233,7 +233,7 @@ query getName { Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R5221LoneAnonymousOperation); + error => error.Code == ValidationErrorCodes.R5221LoneAnonymousOperation); } [Fact] @@ -304,7 +304,7 @@ public void Rule_5231_Single_root_field_invalid() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R5231SingleRootField); + error => error.Code == ValidationErrorCodes.R5231SingleRootField); } [Fact] @@ -333,7 +333,7 @@ fragment multipleSubscriptions on Subscription { Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R5231SingleRootField); + error => error.Code == ValidationErrorCodes.R5231SingleRootField); } [Fact] @@ -358,7 +358,7 @@ public void Rule_5231_Single_root_field_invalid_with_typename() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R5231SingleRootField); + error => error.Code == ValidationErrorCodes.R5231SingleRootField); } [Fact] @@ -379,7 +379,7 @@ public void Rule_531_Field_Selections_invalid_with_fragment() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R531FieldSelections); + error => error.Code == ValidationErrorCodes.R531FieldSelections); } [Fact] @@ -400,7 +400,7 @@ public void Rule_531_Field_Selections_invalid_with_alias() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R531FieldSelections); + error => error.Code == ValidationErrorCodes.R531FieldSelections); } [Fact] @@ -459,7 +459,7 @@ public void Rule_531_Field_Selections_invalid_with_interface() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R531FieldSelections); + error => error.Code == ValidationErrorCodes.R531FieldSelections); } [Fact] @@ -505,13 +505,13 @@ public void Rule_531_Field_Selections_invalid_with_union() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R531FieldSelections + error => error.Code == ValidationErrorCodes.R531FieldSelections && error.Nodes.OfType() .Any(n => n.Name.Value == "name")); Assert.Single( result.Errors, - error => error.Code == Errors.R531FieldSelections + error => error.Code == ValidationErrorCodes.R531FieldSelections && error.Nodes.OfType() .Any(n => n.Name.Value == "barkVolume")); } @@ -560,7 +560,7 @@ public void Rule_533_Leaf_Field_Selections_invalid1() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R533LeafFieldSelections); + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); } [Fact] @@ -581,7 +581,7 @@ public void Rule_533_Leaf_Field_Selections_invalid2() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R533LeafFieldSelections); + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); } [Fact] @@ -602,7 +602,7 @@ public void Rule_533_Leaf_Field_Selections_invalid3() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R533LeafFieldSelections); + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); } [Fact] @@ -623,7 +623,7 @@ public void Rule_533_Leaf_Field_Selections_invalid4() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R533LeafFieldSelections); + error => error.Code == ValidationErrorCodes.R533LeafFieldSelections); } [Fact] @@ -666,7 +666,7 @@ public void Rule_541_Argument_Names_invalid1() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R541ArgumentNames); + error => error.Code == ValidationErrorCodes.R541ArgumentNames); } [Fact] @@ -687,7 +687,7 @@ public void Rule_541_Argument_Names_invalid2() Assert.False(result.IsValid); Assert.Single( result.Errors, - error => error.Code == Errors.R541ArgumentNames); + error => error.Code == ValidationErrorCodes.R541ArgumentNames); } } } \ No newline at end of file From 5723c123677e69eb3437bad1de7de9b1b86cfa2c Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sat, 23 Feb 2019 12:49:37 +0200 Subject: [PATCH 10/20] 5.4.2, 5.4.2.1 --- .../validation/ValidationErrorCodes.cs | 4 + src/graphql/validation/Validator.cs | 5 +- .../rules2/R5421RequiredArguments.cs | 84 ++++++++++ .../rules2/R542ArgumentUniqueness.cs | 37 +++++ .../validation/ValidatorFacts.cs | 146 +++++++++++++++++- 5 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 src/graphql/validation/rules2/R5421RequiredArguments.cs create mode 100644 src/graphql/validation/rules2/R542ArgumentUniqueness.cs diff --git a/src/graphql/validation/ValidationErrorCodes.cs b/src/graphql/validation/ValidationErrorCodes.cs index 752e8eb95..8575d3710 100644 --- a/src/graphql/validation/ValidationErrorCodes.cs +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -15,5 +15,9 @@ public static class ValidationErrorCodes public const string R533LeafFieldSelections = "5.3.3 Leaf Field Selections"; public const string R541ArgumentNames = "5.4.1 Argument Names"; + + public const string R542ArgumentUniqueness = "5.4.2 Argument Uniqueness"; + + public static string R5421RequiredArguments = "5.4.2.1 Required Arguments"; } } \ No newline at end of file diff --git a/src/graphql/validation/Validator.cs b/src/graphql/validation/Validator.cs index bb70bc606..bb1702b91 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -13,14 +13,15 @@ public static class Validator public static Dictionary> DefaultRules = DocumentRulesVisitor.InitializeRuleActionMap(new IRule[] { + new V2.R511ExecutableDefinitions(), new V2.R5211OperationNameUniqueness(), new V2.R5221LoneAnonymousOperation(), new V2.R5231SingleRootField(), new V2.R531FieldSelections(), new V2.R533LeafFieldSelections(), new V2.R541ArgumentNames(), - new V2.R511ExecutableDefinitions(), - + new V2.R542ArgumentUniqueness(), + new V2.R5421RequiredArguments(), }); public static ValidationResult Validate( diff --git a/src/graphql/validation/rules2/R5421RequiredArguments.cs b/src/graphql/validation/rules2/R5421RequiredArguments.cs new file mode 100644 index 000000000..ec45f6fee --- /dev/null +++ b/src/graphql/validation/rules2/R5421RequiredArguments.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.execution; +using tanka.graphql.type; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R5421RequiredArguments : TypeTrackingRuleBase + { + public override void BeginVisitArguments(IEnumerable arguments, IValidationContext context) + { + var args = arguments.ToList(); + base.BeginVisitArguments(args, context); + + + var argumentDefinitions = GetArgumentDefinitions(); + + //todo: should this produce error? + if (argumentDefinitions == null) + return; + + foreach (var argumentDefinition in argumentDefinitions) + { + var type = argumentDefinition.Value.Type; + var defaultValue = argumentDefinition.Value.DefaultValue; + + if (type is NonNull nonNull && defaultValue == null) + { + var argumentName = argumentDefinition.Key; + var argument = args.SingleOrDefault(a => a.Name.Value == argumentName); + + if (argument == null) + { + context.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", + args); + + return; + } + + // We don't want to throw error here due to non-null so we use the WrappedType directly + var argumentValue = Values.CoerceValue(context.Schema, argument.Value, nonNull.WrappedType); + if (argumentValue == null) + { + context.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", + args); + } + } + } + } + + private IEnumerable> GetArgumentDefinitions() + { + var definitions = GetDirective()?.Arguments + ?? GetFieldDef()?.Field.Arguments; + + return definitions; + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R542ArgumentUniqueness.cs b/src/graphql/validation/rules2/R542ArgumentUniqueness.cs new file mode 100644 index 000000000..028f48ce4 --- /dev/null +++ b/src/graphql/validation/rules2/R542ArgumentUniqueness.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R542ArgumentUniqueness : TypeTrackingRuleBase + { + public override void BeginVisitArguments(IEnumerable arguments, IValidationContext context) + { + var args = arguments.ToList(); + base.BeginVisitArguments(args, context); + + var knownArgs = new List(); + foreach (var argument in args) + { + 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); + } + } + } +} \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index bbf961066..05d4d4f2c 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -76,7 +76,22 @@ 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 + } + "; Schema = Sdl.Schema(Parser.ParseDocument(sdl)); } @@ -689,5 +704,134 @@ public void Rule_541_Argument_Names_invalid2() 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, + new 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, + new 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, + new 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, + new 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, + new 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, + new R5421RequiredArguments()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5421RequiredArguments); + } } } \ No newline at end of file From 4180dc0bdfefef6d2760739b965be2d4353300be Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sat, 23 Feb 2019 14:52:59 +0200 Subject: [PATCH 11/20] 5.5.1.1, 5.5.1.2 --- src/graphql/language/Visitor.cs | 18 +-- src/graphql/type/Ast.cs | 3 + src/graphql/type/IAbstractType.cs | 7 + src/graphql/type/InterfaceType.cs | 7 +- src/graphql/type/UnionType.cs | 2 +- .../validation/DocumentRulesVisitor.cs | 9 -- src/graphql/validation/IValidationContext.cs | 2 + .../validation/ValidationErrorCodes.cs | 6 +- src/graphql/validation/Validator.cs | 8 +- .../rules2/R5511FragmentNameUniqueness.cs | 34 +++++ .../R5512FragmentSpreadTypeExistence.cs | 40 +++++ .../validation/rules2/TypeTrackingRuleBase.cs | 26 +++- .../validation/ValidatorFacts.cs | 142 ++++++++++++++++++ 13 files changed, 272 insertions(+), 32 deletions(-) create mode 100644 src/graphql/type/IAbstractType.cs create mode 100644 src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs create mode 100644 src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs diff --git a/src/graphql/language/Visitor.cs b/src/graphql/language/Visitor.cs index c497b8b9b..cda119152 100644 --- a/src/graphql/language/Visitor.cs +++ b/src/graphql/language/Visitor.cs @@ -5,13 +5,10 @@ namespace tanka.graphql.language { public class Visitor { - public Visitor() - { - Fragments = - new Dictionary(); - } + private readonly List _fragments = + new List(); - protected IDictionary Fragments { get; } + public IEnumerable Fragments => _fragments; public virtual GraphQLName BeginVisitAlias(GraphQLName alias) { @@ -91,6 +88,8 @@ public virtual GraphQLFragmentDefinition BeginVisitFragmentDefinition( BeginVisitNode(node.Name); if (node.SelectionSet != null) BeginVisitNode(node.SelectionSet); + + _fragments.Add(node); return node; } @@ -249,13 +248,6 @@ public virtual GraphQLVariable EndVisitVariable(GraphQLVariable variable) public virtual void Visit(GraphQLDocument ast) { - foreach (var definition in ast.Definitions) - if (definition.Kind == ASTNodeKind.FragmentDefinition) - { - var fragmentDefinition = (GraphQLFragmentDefinition) definition; - Fragments.Add(fragmentDefinition.Name.Value, fragmentDefinition); - } - foreach (var definition in ast.Definitions) BeginVisitNode(definition); } 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/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/UnionType.cs b/src/graphql/type/UnionType.cs index c75fa11d7..822acd3e2 100644 --- a/src/graphql/type/UnionType.cs +++ b/src/graphql/type/UnionType.cs @@ -3,7 +3,7 @@ namespace tanka.graphql.type { - public class UnionType : INamedType, IDescribable + public class UnionType : INamedType, IDescribable, IAbstractType { public UnionType(string name, IEnumerable possibleTypes, Meta meta = null) { diff --git a/src/graphql/validation/DocumentRulesVisitor.cs b/src/graphql/validation/DocumentRulesVisitor.cs index 67be8476d..35d452e28 100644 --- a/src/graphql/validation/DocumentRulesVisitor.cs +++ b/src/graphql/validation/DocumentRulesVisitor.cs @@ -13,15 +13,6 @@ public class DocumentRulesVisitor : Visitor, IValidationContext private readonly Dictionary> _visitorMap; - public DocumentRulesVisitor( - IEnumerable rules, - ISchema schema, - GraphQLDocument document, - Dictionary variableValues = null) - : this(InitializeRuleActionMap(rules), schema, document, variableValues) - { - } - public DocumentRulesVisitor( Dictionary> ruleMap, diff --git a/src/graphql/validation/IValidationContext.cs b/src/graphql/validation/IValidationContext.cs index 7c3a88e75..6bcfa44ab 100644 --- a/src/graphql/validation/IValidationContext.cs +++ b/src/graphql/validation/IValidationContext.cs @@ -17,5 +17,7 @@ public interface IValidationContext void Error(string code, string message, ASTNode node); void Error(string code, string message, IEnumerable nodes); + + IEnumerable Fragments { get; } } } \ No newline at end of file diff --git a/src/graphql/validation/ValidationErrorCodes.cs b/src/graphql/validation/ValidationErrorCodes.cs index 8575d3710..76426ea49 100644 --- a/src/graphql/validation/ValidationErrorCodes.cs +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -18,6 +18,10 @@ public static class ValidationErrorCodes public const string R542ArgumentUniqueness = "5.4.2 Argument Uniqueness"; - public static string R5421RequiredArguments = "5.4.2.1 Required Arguments"; + public const string R5421RequiredArguments = "5.4.2.1 Required Arguments"; + + public const string R5511FragmentNameUniqueness = "5.5.1.1 Fragment Name Uniqueness"; + + public const string R5512FragmentSpreadTypeExistence = "5.5.1.2 Fragment Spread Type Existence"; } } \ No newline at end of file diff --git a/src/graphql/validation/Validator.cs b/src/graphql/validation/Validator.cs index bb1702b91..1d1ff60d7 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -15,8 +15,10 @@ public static class Validator { new V2.R511ExecutableDefinitions(), new V2.R5211OperationNameUniqueness(), - new V2.R5221LoneAnonymousOperation(), - new V2.R5231SingleRootField(), + new V2.R5221LoneAnonymousOperation(), + new V2.R5511FragmentNameUniqueness(), + new V2.R5512FragmentSpreadTypeExistence(), + new V2.R5231SingleRootField(), new V2.R531FieldSelections(), new V2.R533LeafFieldSelections(), new V2.R541ArgumentNames(), @@ -31,7 +33,7 @@ public static ValidationResult Validate( Dictionary variableValues = null) { var visitor = new DocumentRulesVisitor( - rules, + DocumentRulesVisitor.InitializeRuleActionMap(rules), schema, document, variableValues); diff --git a/src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs b/src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs new file mode 100644 index 000000000..82f1c5cc8 --- /dev/null +++ b/src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R5511FragmentNameUniqueness : RuleBase + { + public override IEnumerable AppliesToNodeKinds => + new[] + { + ASTNodeKind.FragmentDefinition + }; + + public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) + { + if (context.Fragments.Any(f => f.Name.Value == node.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.", + node); + + } + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs b/src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs new file mode 100644 index 000000000..eefb07b23 --- /dev/null +++ b/src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs @@ -0,0 +1,40 @@ +using GraphQLParser.AST; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// 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 class R5512FragmentSpreadTypeExistence : TypeTrackingRuleBase + { + public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) + { + base.BeginVisitFragmentDefinition(node, context); + + var type = 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); + } + + public override void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) + { + base.BeginVisitInlineFragment(inlineFragment, context); + + var type = 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. ", + inlineFragment); + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/rules2/TypeTrackingRuleBase.cs b/src/graphql/validation/rules2/TypeTrackingRuleBase.cs index 81045e907..079b85652 100644 --- a/src/graphql/validation/rules2/TypeTrackingRuleBase.cs +++ b/src/graphql/validation/rules2/TypeTrackingRuleBase.cs @@ -97,8 +97,17 @@ public override void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragme IValidationContext context) { var typeConditionAst = inlineFragment.TypeCondition; - var outputType = Ast.TypeFromAst(context.Schema, typeConditionAst) - ?? GetNamedType(GetCurrentType()); + + IType outputType; + if (typeConditionAst != null) + { + outputType = Ast.TypeFromAst(context.Schema, typeConditionAst); + } + else + { + outputType = GetNamedType(GetCurrentType()); + } + _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); } @@ -106,8 +115,17 @@ public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node IValidationContext context) { var typeConditionAst = node.TypeCondition; - var outputType = Ast.TypeFromAst(context.Schema, typeConditionAst) - ?? GetNamedType(GetCurrentType()); + + IType outputType; + if (typeConditionAst != null) + { + outputType = Ast.TypeFromAst(context.Schema, typeConditionAst); + } + else + { + outputType = GetNamedType(GetCurrentType()); + } + _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); } diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 05d4d4f2c..dd68f3e02 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -833,5 +833,147 @@ public void Rule_5421_Required_Arguments_invalid2() 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, + new 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, + new 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, + new 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, + new 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, + new R5512FragmentSpreadTypeExistence()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5512FragmentSpreadTypeExistence); + } } } \ No newline at end of file From d5f492ec2fdd07307491b0424f6a389d674fa6ef Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sat, 23 Feb 2019 16:44:13 +0200 Subject: [PATCH 12/20] 5.5.1.3 --- .../validation/ValidationErrorCodes.cs | 4 +- src/graphql/validation/Validator.cs | 1 + .../rules2/R5513FragmentsOnCompositeTypes.cs | 48 ++++++++++++ .../validation/ValidatorFacts.cs | 75 +++++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs diff --git a/src/graphql/validation/ValidationErrorCodes.cs b/src/graphql/validation/ValidationErrorCodes.cs index 76426ea49..cf762c826 100644 --- a/src/graphql/validation/ValidationErrorCodes.cs +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -1,7 +1,7 @@ namespace tanka.graphql.validation { public static class ValidationErrorCodes - { + { public const string R5211OperationNameUniqueness = "5.2.1.1 Operation Name Uniqueness"; public const string R511ExecutableDefinitions = "5.1.1 Executable Definitions"; @@ -23,5 +23,7 @@ public static class ValidationErrorCodes 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"; } } \ No newline at end of file diff --git a/src/graphql/validation/Validator.cs b/src/graphql/validation/Validator.cs index 1d1ff60d7..559e98393 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -18,6 +18,7 @@ public static class Validator new V2.R5221LoneAnonymousOperation(), new V2.R5511FragmentNameUniqueness(), new V2.R5512FragmentSpreadTypeExistence(), + new V2.R5513FragmentsOnCompositeTypes(), new V2.R5231SingleRootField(), new V2.R531FieldSelections(), new V2.R533LeafFieldSelections(), diff --git a/src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs b/src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs new file mode 100644 index 000000000..64560dc67 --- /dev/null +++ b/src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs @@ -0,0 +1,48 @@ +using GraphQLParser.AST; +using tanka.graphql.type; + +namespace tanka.graphql.validation.rules2 +{ + /// + /// For each fragment defined in the document. + /// The target type of fragment must have kind UNION, INTERFACE, or OBJECT. + /// + public class R5513FragmentsOnCompositeTypes : TypeTrackingRuleBase + { + public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) + { + base.BeginVisitFragmentDefinition(node, context); + + var type = 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); + } + + public override void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) + { + base.BeginVisitInlineFragment(inlineFragment, context); + + var type = 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", + inlineFragment); + } + } +} \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index dd68f3e02..acfe803c3 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -975,5 +975,80 @@ ... on NotInSchema { 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, + new 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, + new 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, + new R5513FragmentsOnCompositeTypes()); + + /* Then */ + Assert.False(result.IsValid); + Assert.Single( + result.Errors, + error => error.Code == ValidationErrorCodes.R5513FragmentsOnCompositeTypes); + } } } \ No newline at end of file From 7ad5b735fd78f4d6eb20de0766273a29ce17f63f Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sun, 24 Feb 2019 18:22:13 +0200 Subject: [PATCH 13/20] Third validator impl --- src/graphql.benchmarks/Benchmarks.cs | 4 +- src/graphql/validation/CreateRule.cs | 4 + src/graphql/validation/ExecutionRules.cs | 539 ++++++++++++++++++ src/graphql/validation/IRule.cs | 6 + src/graphql/validation/IRuleVisitor.cs | 46 ++ src/graphql/validation/IRuleVisitorContext.cs | 23 + src/graphql/validation/NodeVisitor.cs | 6 + src/graphql/validation/RuleBase.cs | 4 + src/graphql/validation/RuleVisitor.cs | 83 +++ ...DocumentRulesVisitor.cs => RulesWalker.cs} | 327 ++++++----- src/graphql/validation/TypeTracker.cs | 322 +++++++++++ .../validation/ValidationErrorCodes.cs | 2 + src/graphql/validation/Validator.cs | 38 +- .../validation/ValidatorFacts.cs | 133 +++-- 14 files changed, 1304 insertions(+), 233 deletions(-) create mode 100644 src/graphql/validation/CreateRule.cs create mode 100644 src/graphql/validation/ExecutionRules.cs create mode 100644 src/graphql/validation/IRuleVisitor.cs create mode 100644 src/graphql/validation/IRuleVisitorContext.cs create mode 100644 src/graphql/validation/NodeVisitor.cs create mode 100644 src/graphql/validation/RuleVisitor.cs rename src/graphql/validation/{DocumentRulesVisitor.cs => RulesWalker.cs} (50%) create mode 100644 src/graphql/validation/TypeTracker.cs diff --git a/src/graphql.benchmarks/Benchmarks.cs b/src/graphql.benchmarks/Benchmarks.cs index c95c7849c..e7f45a1a1 100644 --- a/src/graphql.benchmarks/Benchmarks.cs +++ b/src/graphql.benchmarks/Benchmarks.cs @@ -20,7 +20,7 @@ public class Benchmarks private ISchema _schema; private GraphQLDocument _mutation; private GraphQLDocument _subscription; - private Dictionary> _defaultRulesMap; + private IEnumerable _defaultRulesMap; [GlobalSetup] public async Task Setup() @@ -29,7 +29,7 @@ public async Task Setup() _query = Utils.InitializeQuery(); _mutation = Utils.InitializeMutation(); _subscription = Utils.InitializeSubscription(); - _defaultRulesMap = Validator.DefaultRules; + _defaultRulesMap = ExecutionRules.All; } /* [Benchmark] diff --git a/src/graphql/validation/CreateRule.cs b/src/graphql/validation/CreateRule.cs new file mode 100644 index 000000000..dc196ac7a --- /dev/null +++ b/src/graphql/validation/CreateRule.cs @@ -0,0 +1,4 @@ +namespace tanka.graphql.validation +{ + public delegate RuleVisitor CreateRule(IRuleVisitorContext context); +} \ 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..0470fdc93 --- /dev/null +++ b/src/graphql/validation/ExecutionRules.cs @@ -0,0 +1,539 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQLParser.AST; +using tanka.graphql.execution; +using tanka.graphql.type; + +namespace tanka.graphql.validation +{ + public static class ExecutionRules + { + public static IEnumerable All = new[] + { + R511ExecutableDefinitions(), + R5211OperationNameUniqueness(), + R5221LoneAnonymousOperation(), + R5511FragmentNameUniqueness(), + R5512FragmentSpreadTypeExistence(), + R5513FragmentsOnCompositeTypes(), + R5514FragmentsMustBeUsed(), + R5231SingleRootField(), + R531FieldSelections(), + R533LeafFieldSelections(), + R541ArgumentNames(), + R542ArgumentUniqueness(), + R5421RequiredArguments(), + }; + + + /// + /// Formal Specification + /// For each definition definition in the document. + /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). + /// + public static CreateRule R511ExecutableDefinitions() + { + return context => new RuleVisitor + { + 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 CreateRule R5211OperationNameUniqueness() + { + return context => + { + var known = new List(); + return new RuleVisitor + { + 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 CreateRule R5221LoneAnonymousOperation() + { + return context => + { + return new RuleVisitor + { + 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 CreateRule R5231SingleRootField() + { + return context => new RuleVisitor + { + 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 CreateRule R531FieldSelections() + { + return context => new RuleVisitor + { + 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 CreateRule R533LeafFieldSelections() + { + return context => new RuleVisitor + { + 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; + 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 ComplexType && 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 CreateRule R541ArgumentNames() + { + return context => new RuleVisitor + { + 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 CreateRule 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 => new RuleVisitor + { + EnterFieldSelection = field => + { + var args = field.Arguments.ToList(); + var argumentDefinitions = GetArgumentDefinitions(context); + + //todo: should this produce error? + if (argumentDefinitions == null) + return; + + ValidateArguments(argumentDefinitions, args, context); + }, + 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 CreateRule R542ArgumentUniqueness() + { + return context => + { + var knownArgs = new List(); + return new RuleVisitor + { + 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 CreateRule R5511FragmentNameUniqueness() + { + return context => + { + var knownFragments = new List(); + return new RuleVisitor + { + 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 CreateRule R5512FragmentSpreadTypeExistence() + { + return context => new RuleVisitor + { + 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); + }, + 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 CreateRule R5513FragmentsOnCompositeTypes() + { + return context => new RuleVisitor + { + 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); + }, + 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 CreateRule R5514FragmentsMustBeUsed() + { + return context => + { + var fragments = new Dictionary(); + var fragmentSpreads = new List(); + + return new RuleVisitor + { + EnterFragmentDefinition = fragment => { fragments.Add(fragment.Name.Value, fragment); }, + EnterFragmentSpread = spread => { fragmentSpreads.Add(spread.Name.Value); }, + 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); + } + } + }; + }; + } + } +} \ No newline at end of file diff --git a/src/graphql/validation/IRule.cs b/src/graphql/validation/IRule.cs index 473c83f7f..3bd58f855 100644 --- a/src/graphql/validation/IRule.cs +++ b/src/graphql/validation/IRule.cs @@ -111,9 +111,15 @@ void EndVisitDirective(GraphQLDirective directive, IValidationContext context); void BeginVisitListValue(GraphQLListValue node, IValidationContext context); + void EndVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context); + void EndVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context); + void EndVisitObjectField(GraphQLObjectField node, IValidationContext context); + void EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context); + + void EndVisit(GraphQLDocument document, IValidationContext context); } } \ No newline at end of file diff --git a/src/graphql/validation/IRuleVisitor.cs b/src/graphql/validation/IRuleVisitor.cs new file mode 100644 index 000000000..1bb767a13 --- /dev/null +++ b/src/graphql/validation/IRuleVisitor.cs @@ -0,0 +1,46 @@ +using GraphQLParser.AST; + +namespace tanka.graphql.validation +{ + public interface IRuleVisitor + { + NodeVisitor EnterAlias { get; set; } + NodeVisitor EnterArgument { get; set; } + NodeVisitor EnterBooleanValue { get; set; } + NodeVisitor EnterDirective { get; set; } + NodeVisitor EnterDirectives { get; set; } + NodeVisitor EnterDocument { get; set; } + NodeVisitor EnterEnumValue { get; set; } + NodeVisitor EnterFieldSelection { get; set; } + NodeVisitor EnterFloatValue { get; set; } + NodeVisitor EnterFragmentDefinition { get; set; } + NodeVisitor EnterFragmentSpread { get; set; } + NodeVisitor EnterInlineFragment { get; set; } + NodeVisitor EnterIntValue { get; set; } + NodeVisitor EnterListValue { get; set; } + NodeVisitor EnterName { get; set; } + NodeVisitor EnterNamedType { get; set; } + NodeVisitor EnterNode { get; set; } + NodeVisitor EnterObjectField { get; set; } + NodeVisitor EnterObjectValue { get; set; } + NodeVisitor EnterOperationDefinition { get; set; } + NodeVisitor EnterSelectionSet { get; set; } + NodeVisitor EnterStringValue { get; set; } + NodeVisitor EnterVariable { get; set; } + NodeVisitor EnterVariableDefinition { get; set; } + NodeVisitor LeaveArgument { get; set; } + NodeVisitor LeaveDirective { get; set; } + NodeVisitor LeaveDocument { get; set; } + NodeVisitor LeaveEnumValue { get; set; } + NodeVisitor LeaveFieldSelection { get; set; } + NodeVisitor LeaveFragmentDefinition { get; set; } + NodeVisitor LeaveInlineFragment { get; set; } + NodeVisitor LeaveListValue { get; set; } + NodeVisitor LeaveObjectField { get; set; } + NodeVisitor LeaveObjectValue { get; set; } + NodeVisitor LeaveOperationDefinition { get; set; } + NodeVisitor LeaveSelectionSet { get; set; } + NodeVisitor LeaveVariable { get; set; } + NodeVisitor LeaveVariableDefinition { get; set; } + } +} \ No newline at end of file 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/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/RuleBase.cs b/src/graphql/validation/RuleBase.cs index 464617314..32364ac5b 100644 --- a/src/graphql/validation/RuleBase.cs +++ b/src/graphql/validation/RuleBase.cs @@ -57,6 +57,10 @@ public virtual void EndVisitEnumValue(GraphQLScalarValue value, IValidationConte { } + public virtual void EndVisit(GraphQLDocument document, IValidationContext context) + { + } + public virtual void BeginVisitDirectives(IEnumerable directives, IValidationContext context) { diff --git a/src/graphql/validation/RuleVisitor.cs b/src/graphql/validation/RuleVisitor.cs new file mode 100644 index 000000000..4168046ec --- /dev/null +++ b/src/graphql/validation/RuleVisitor.cs @@ -0,0 +1,83 @@ +using GraphQLParser.AST; + +namespace tanka.graphql.validation +{ + public class RuleVisitor : IRuleVisitor + { + 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/DocumentRulesVisitor.cs b/src/graphql/validation/RulesWalker.cs similarity index 50% rename from src/graphql/validation/DocumentRulesVisitor.cs rename to src/graphql/validation/RulesWalker.cs index 35d452e28..fefc9cce6 100644 --- a/src/graphql/validation/DocumentRulesVisitor.cs +++ b/src/graphql/validation/RulesWalker.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using GraphQLParser.AST; @@ -6,16 +7,13 @@ namespace tanka.graphql.validation { - public class DocumentRulesVisitor : Visitor, IValidationContext + public class RulesWalker : Visitor, IRuleVisitorContext { private readonly List _errors = new List(); - private readonly Dictionary> _visitorMap; - - - public DocumentRulesVisitor( - Dictionary> ruleMap, + public RulesWalker( + IEnumerable rules, ISchema schema, GraphQLDocument document, Dictionary variableValues = null) @@ -23,12 +21,27 @@ public DocumentRulesVisitor( Schema = schema; Document = document; VariableValues = variableValues; - _visitorMap = ruleMap; + NodeVisitors = CreateVisitors(rules).ToList(); + + //todo: this will break + Tracker = NodeVisitors.First() as TypeTracker; + } + + protected IEnumerable NodeVisitors { get; set; } + + protected IEnumerable CreateVisitors(IEnumerable rules) + { + var createRules = new List(rules); + createRules.Insert(0, context => new TypeTracker(context.Schema)); + + return createRules.Select(r => r(this)); } public GraphQLDocument Document { get; } - public Dictionary VariableValues { get; } + public IDictionary VariableValues { get; } + + public TypeTracker Tracker { get; } public ISchema Schema { get; } @@ -47,22 +60,6 @@ public void Error(string code, string message, IEnumerable nodes) _errors.Add(new ValidationError(code, message, nodes)); } - public static Dictionary> InitializeRuleActionMap(IEnumerable rules) - { - var visitors = new Dictionary>(); - - foreach (var rule in rules) - foreach (var nodeKind in rule.AppliesToNodeKinds) - { - if (!visitors.ContainsKey(nodeKind)) - visitors[nodeKind] = new List(); - - visitors[nodeKind].Add(rule); - } - - return visitors; - } - public ValidationResult Validate() { Visit(Document); @@ -71,74 +68,80 @@ public ValidationResult Validate() public override void Visit(GraphQLDocument document) { - var rules = GetRules(document); - foreach (var rule in rules) - rule.Visit(document, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterDocument?.Invoke(document); + } base.Visit(document); + + foreach (var visitor in NodeVisitors) + { + visitor.LeaveDocument?.Invoke(document); + } } public override GraphQLName BeginVisitAlias(GraphQLName alias) { - var rules = GetRules(alias); - foreach (var rule in rules) - rule.BeginVisitAlias(alias, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterAlias?.Invoke(alias); + } return base.BeginVisitAlias(alias); } public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) { - var rules = GetRules(argument); - foreach (var rule in rules) - rule.BeginVisitArgument(argument, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterArgument?.Invoke(argument); + } return base.BeginVisitArgument(argument); } - public override IEnumerable BeginVisitArguments(IEnumerable arguments) - { - var rules = GetRules(ASTNodeKind.Argument); - foreach (var rule in rules) - rule.BeginVisitArguments(arguments, this); - - return base.BeginVisitArguments(arguments); - } - public override GraphQLScalarValue BeginVisitBooleanValue( GraphQLScalarValue value) { - var rules = GetRules(value); - foreach (var rule in rules) - rule.BeginVisitBooleanValue(value, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterBooleanValue?.Invoke(value); + } return base.BeginVisitBooleanValue(value); } public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) { - var rules = GetRules(directive).ToList(); - foreach (var rule in rules) - rule.BeginVisitDirective(directive, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterDirective?.Invoke(directive); + } var _ = base.BeginVisitDirective(directive); - foreach (var rule in rules) - rule.EndVisitDirective(directive, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveDirective?.Invoke(directive); + } return _; } public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) { - var rules = GetRules(value).ToList(); - foreach (var rule in rules) - rule.BeginVisitEnumValue(value, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterEnumValue?.Invoke(value); + } var _ = base.BeginVisitEnumValue(value); - foreach (var rule in rules) - rule.EndVisitEnumValue(value, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveEnumValue?.Invoke(value); + } return _; } @@ -146,9 +149,10 @@ public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) public override GraphQLFieldSelection BeginVisitFieldSelection( GraphQLFieldSelection selection) { - var rules = GetRules(selection); - foreach (var rule in rules) - rule.BeginVisitFieldSelection(selection, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterFieldSelection?.Invoke(selection); + } return base.BeginVisitFieldSelection(selection); } @@ -156,9 +160,10 @@ public override GraphQLFieldSelection BeginVisitFieldSelection( public override GraphQLScalarValue BeginVisitFloatValue( GraphQLScalarValue value) { - var rules = GetRules(value); - foreach (var rule in rules) - rule.BeginVisitFloatValue(value, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterFloatValue?.Invoke(value); + } return base.BeginVisitFloatValue(value); } @@ -166,14 +171,17 @@ public override GraphQLScalarValue BeginVisitFloatValue( public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( GraphQLFragmentDefinition node) { - var rules = GetRules(node).ToList(); - foreach (var rule in rules) - rule.BeginVisitFragmentDefinition(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterFragmentDefinition?.Invoke(node); + } var result = base.BeginVisitFragmentDefinition(node); - foreach (var rule in rules) - rule.EndVisitFragmentDefinition(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveFragmentDefinition?.Invoke(node); + } return result; } @@ -181,9 +189,10 @@ public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( public override GraphQLFragmentSpread BeginVisitFragmentSpread( GraphQLFragmentSpread fragmentSpread) { - var rules = GetRules(fragmentSpread); - foreach (var rule in rules) - rule.BeginVisitFragmentSpread(fragmentSpread, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterFragmentSpread?.Invoke(fragmentSpread); + } return base.BeginVisitFragmentSpread(fragmentSpread); } @@ -191,32 +200,37 @@ public override GraphQLFragmentSpread BeginVisitFragmentSpread( public override GraphQLInlineFragment BeginVisitInlineFragment( GraphQLInlineFragment inlineFragment) { - var rules = GetRules(inlineFragment).ToList(); - foreach (var rule in rules) - rule.BeginVisitInlineFragment(inlineFragment, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterInlineFragment?.Invoke(inlineFragment); + } var _ = base.BeginVisitInlineFragment(inlineFragment); - foreach (var rule in rules) - rule.EndVisitInlineFragment(inlineFragment, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveInlineFragment?.Invoke(inlineFragment); + } return _; } public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) { - var rules = GetRules(value); - foreach (var rule in rules) - rule.BeginVisitIntValue(value, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterIntValue?.Invoke(value); + } return base.BeginVisitIntValue(value); } public override GraphQLName BeginVisitName(GraphQLName name) { - var rules = GetRules(name); - foreach (var rule in rules) - rule.BeginVisitName(name, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterName?.Invoke(name); + } return base.BeginVisitName(name); } @@ -224,9 +238,10 @@ public override GraphQLName BeginVisitName(GraphQLName name) public override GraphQLNamedType BeginVisitNamedType( GraphQLNamedType typeCondition) { - var rules = GetRules(typeCondition); - foreach (var rule in rules) - rule.BeginVisitNamedType(typeCondition, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterNamedType?.Invoke(typeCondition); + } return base.BeginVisitNamedType(typeCondition); } @@ -234,9 +249,10 @@ public override GraphQLNamedType BeginVisitNamedType( public override GraphQLOperationDefinition BeginVisitOperationDefinition( GraphQLOperationDefinition definition) { - var rules = GetRules(definition); - foreach (var rule in rules) - rule.BeginVisitOperationDefinition(definition, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterOperationDefinition?.Invoke(definition); + } return base.BeginVisitOperationDefinition(definition); } @@ -244,9 +260,10 @@ public override GraphQLOperationDefinition BeginVisitOperationDefinition( public override GraphQLOperationDefinition EndVisitOperationDefinition( GraphQLOperationDefinition definition) { - var rules = GetRules(definition); - foreach (var rule in rules) - rule.EndVisitOperationDefinition(definition, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveOperationDefinition?.Invoke(definition); + } return base.EndVisitOperationDefinition(definition); } @@ -254,14 +271,17 @@ public override GraphQLOperationDefinition EndVisitOperationDefinition( public override GraphQLSelectionSet BeginVisitSelectionSet( GraphQLSelectionSet selectionSet) { - var rules = GetRules(selectionSet).ToList(); - foreach (var rule in rules) - rule.BeginVisitSelectionSet(selectionSet, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterSelectionSet?.Invoke(selectionSet); + } var _ = base.BeginVisitSelectionSet(selectionSet); - foreach (var rule in rules) - rule.EndVisitSelectionSet(selectionSet, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveSelectionSet?.Invoke(selectionSet); + } return _; } @@ -269,18 +289,20 @@ public override GraphQLSelectionSet BeginVisitSelectionSet( public override GraphQLScalarValue BeginVisitStringValue( GraphQLScalarValue value) { - var rules = GetRules(value); - foreach (var rule in rules) - rule.BeginVisitStringValue(value, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterStringValue?.Invoke(value); + } return base.BeginVisitStringValue(value); } public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) { - var rules = GetRules(variable); - foreach (var rule in rules) - rule.BeginVisitVariable(variable, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterVariable?.Invoke(variable); + } return base.BeginVisitVariable(variable); } @@ -288,34 +310,27 @@ public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) public override GraphQLVariableDefinition BeginVisitVariableDefinition( GraphQLVariableDefinition node) { - var rules = GetRules(node).ToList(); - foreach (var rule in rules) - rule.BeginVisitVariableDefinition(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterVariableDefinition?.Invoke(node); + } var _ = base.BeginVisitVariableDefinition(node); - foreach (var rule in rules) - rule.EndVisitVariableDefinition(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveVariableDefinition?.Invoke(node); + } return _; } - public override IEnumerable BeginVisitVariableDefinitions( - IEnumerable variableDefinitions) - { - var rules = GetRules(ASTNodeKind.VariableDefinition); - - foreach (var rule in rules) - rule.BeginVisitVariableDefinitions(variableDefinitions, this); - - return base.BeginVisitVariableDefinitions(variableDefinitions); - } - public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) { - var rules = GetRules(argument); - foreach (var rule in rules) - rule.EndVisitArgument(argument, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveArgument?.Invoke(argument); + } return base.EndVisitArgument(argument); } @@ -323,18 +338,20 @@ public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) public override GraphQLFieldSelection EndVisitFieldSelection( GraphQLFieldSelection selection) { - var rules = GetRules(selection); - foreach (var rule in rules) - rule.EndVisitFieldSelection(selection, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveFieldSelection?.Invoke(selection); + } return base.EndVisitFieldSelection(selection); } public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) { - var rules = GetRules(variable); - foreach (var rule in rules) - rule.EndVisitVariable(variable, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterVariable?.Invoke(variable); + } return base.EndVisitVariable(variable); } @@ -342,14 +359,17 @@ public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) public override GraphQLObjectField BeginVisitObjectField( GraphQLObjectField node) { - var rules = GetRules(node).ToList(); - foreach (var rule in rules) - rule.BeginVisitObjectField(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterObjectField?.Invoke(node); + } var _ = base.BeginVisitObjectField(node); - foreach (var rule in rules) - rule.EndVisitObjectField(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveObjectField?.Invoke(node); + } return _; } @@ -357,41 +377,50 @@ public override GraphQLObjectField BeginVisitObjectField( public override GraphQLObjectValue BeginVisitObjectValue( GraphQLObjectValue node) { - var rules = GetRules(node); - foreach (var rule in rules) - rule.BeginVisitObjectValue(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterObjectValue?.Invoke(node); + } return base.BeginVisitObjectValue(node); } public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) { - var rules = GetRules(node); - foreach (var rule in rules) - rule.EndVisitObjectValue(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveObjectValue?.Invoke(node); + } return base.EndVisitObjectValue(node); } public override ASTNode BeginVisitNode(ASTNode node) { + foreach (var visitor in NodeVisitors) + { + visitor.EnterNode?.Invoke(node); + } + return base.BeginVisitNode(node); } public override GraphQLListValue BeginVisitListValue(GraphQLListValue node) { - var rules = GetRules(node); - foreach (var rule in rules) - rule.BeginVisitListValue(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.EnterListValue?.Invoke(node); + } return base.BeginVisitListValue(node); } public override GraphQLListValue EndVisitListValue(GraphQLListValue node) { - var rules = GetRules(node); - foreach (var rule in rules) - rule.EndVisitListValue(node, this); + foreach (var visitor in NodeVisitors) + { + visitor.LeaveListValue?.Invoke(node); + } return base.EndVisitListValue(node); } @@ -403,19 +432,5 @@ private ValidationResult BuildResult() Errors = _errors }; } - - private IEnumerable GetRules(ASTNode node) - { - var nodeKind = node.Kind; - return GetRules(nodeKind); - } - - private IEnumerable GetRules(ASTNodeKind nodeKind) - { - if (!_visitorMap.ContainsKey(nodeKind)) - return Enumerable.Empty(); - - return _visitorMap[nodeKind]; - } } } \ 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..42e4d54d3 --- /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 - 2); + } + + 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/ValidationErrorCodes.cs b/src/graphql/validation/ValidationErrorCodes.cs index cf762c826..aaf967862 100644 --- a/src/graphql/validation/ValidationErrorCodes.cs +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -25,5 +25,7 @@ public static class ValidationErrorCodes 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"; } } \ No newline at end of file diff --git a/src/graphql/validation/Validator.cs b/src/graphql/validation/Validator.cs index 559e98393..4cab30740 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -10,46 +10,14 @@ namespace tanka.graphql.validation { public static class Validator { - public static Dictionary> DefaultRules = - DocumentRulesVisitor.InitializeRuleActionMap(new IRule[] - { - new V2.R511ExecutableDefinitions(), - new V2.R5211OperationNameUniqueness(), - new V2.R5221LoneAnonymousOperation(), - new V2.R5511FragmentNameUniqueness(), - new V2.R5512FragmentSpreadTypeExistence(), - new V2.R5513FragmentsOnCompositeTypes(), - new V2.R5231SingleRootField(), - new V2.R531FieldSelections(), - new V2.R533LeafFieldSelections(), - new V2.R541ArgumentNames(), - new V2.R542ArgumentUniqueness(), - new V2.R5421RequiredArguments(), - }); - - public static ValidationResult Validate( - IEnumerable rules, - ISchema schema, - GraphQLDocument document, - Dictionary variableValues = null) - { - var visitor = new DocumentRulesVisitor( - DocumentRulesVisitor.InitializeRuleActionMap(rules), - schema, - document, - variableValues); - - return visitor.Validate(); - } - public static ValidationResult Validate( - Dictionary> ruleMap, + IEnumerable rules, ISchema schema, GraphQLDocument document, Dictionary variableValues = null) { - var visitor = new DocumentRulesVisitor( - ruleMap, + var visitor = new RulesWalker( + rules, schema, document, variableValues); diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index acfe803c3..533125241 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -100,7 +100,7 @@ extend type Query { private ValidationResult Validate( GraphQLDocument document, - IRule rule, + CreateRule rule, Dictionary variables = null) { if (document == null) throw new ArgumentNullException(nameof(document)); @@ -132,7 +132,7 @@ extend type Dog { /* When */ var result = Validate( document, - new R511ExecutableDefinitions()); + ExecutionRules.R511ExecutableDefinitions()); /* Then */ Assert.False(result.IsValid); @@ -163,7 +163,7 @@ query getOwnerName { /* When */ var result = Validate( document, - new R5211OperationNameUniqueness()); + ExecutionRules.R5211OperationNameUniqueness()); /* Then */ Assert.True(result.IsValid); @@ -191,7 +191,7 @@ query getName { /* When */ var result = Validate( document, - new R5211OperationNameUniqueness()); + ExecutionRules.R5211OperationNameUniqueness()); /* Then */ Assert.False(result.IsValid); @@ -214,7 +214,7 @@ public void Rule_5221_Lone_Anonymous_Operation_valid() /* When */ var result = Validate( document, - new R5221LoneAnonymousOperation()); + ExecutionRules.R5221LoneAnonymousOperation()); /* Then */ Assert.True(result.IsValid); @@ -242,7 +242,7 @@ query getName { /* When */ var result = Validate( document, - new R5221LoneAnonymousOperation()); + ExecutionRules.R5221LoneAnonymousOperation()); /* Then */ Assert.False(result.IsValid); @@ -266,7 +266,7 @@ public void Rule_5231_Single_root_field_valid() /* When */ var result = Validate( document, - new R5221LoneAnonymousOperation()); + ExecutionRules.R5221LoneAnonymousOperation()); /* Then */ Assert.True(result.IsValid); @@ -291,7 +291,7 @@ fragment newMessageFields on Subscription { /* When */ var result = Validate( document, - new R5231SingleRootField()); + ExecutionRules.R5231SingleRootField()); /* Then */ Assert.True(result.IsValid); @@ -313,7 +313,7 @@ public void Rule_5231_Single_root_field_invalid() /* When */ var result = Validate( document, - new R5231SingleRootField()); + ExecutionRules.R5231SingleRootField()); /* Then */ Assert.False(result.IsValid); @@ -342,7 +342,7 @@ fragment multipleSubscriptions on Subscription { /* When */ var result = Validate( document, - new R5231SingleRootField()); + ExecutionRules.R5231SingleRootField()); /* Then */ Assert.False(result.IsValid); @@ -367,7 +367,7 @@ public void Rule_5231_Single_root_field_invalid_with_typename() /* When */ var result = Validate( document, - new R5231SingleRootField()); + ExecutionRules.R5231SingleRootField()); /* Then */ Assert.False(result.IsValid); @@ -388,7 +388,7 @@ public void Rule_531_Field_Selections_invalid_with_fragment() /* When */ var result = Validate( document, - new R531FieldSelections()); + ExecutionRules.R531FieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -409,7 +409,7 @@ public void Rule_531_Field_Selections_invalid_with_alias() /* When */ var result = Validate( document, - new R531FieldSelections()); + ExecutionRules.R531FieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -432,7 +432,7 @@ public void Rule_531_Field_Selections_valid() /* When */ var result = Validate( document, - new R531FieldSelections()); + ExecutionRules.R531FieldSelections()); /* Then */ Assert.True(result.IsValid); @@ -450,7 +450,7 @@ public void Rule_531_Field_Selections_valid_with_interface() /* When */ var result = Validate( document, - new R531FieldSelections()); + ExecutionRules.R531FieldSelections()); /* Then */ Assert.True(result.IsValid); @@ -468,7 +468,7 @@ public void Rule_531_Field_Selections_invalid_with_interface() /* When */ var result = Validate( document, - new R531FieldSelections()); + ExecutionRules.R531FieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -495,7 +495,7 @@ ... on Dog { /* When */ var result = Validate( document, - new R531FieldSelections()); + ExecutionRules.R531FieldSelections()); /* Then */ Assert.True(result.IsValid); @@ -514,7 +514,7 @@ public void Rule_531_Field_Selections_invalid_with_union() /* When */ var result = Validate( document, - new R531FieldSelections()); + ExecutionRules.R531FieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -549,7 +549,7 @@ public void Rule_533_Leaf_Field_Selections_valid() /* When */ var result = Validate( document, - new R533LeafFieldSelections()); + ExecutionRules.R533LeafFieldSelections()); /* Then */ Assert.True(result.IsValid); @@ -569,7 +569,7 @@ public void Rule_533_Leaf_Field_Selections_invalid1() /* When */ var result = Validate( document, - new R533LeafFieldSelections()); + ExecutionRules.R533LeafFieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -590,7 +590,7 @@ public void Rule_533_Leaf_Field_Selections_invalid2() /* When */ var result = Validate( document, - new R533LeafFieldSelections()); + ExecutionRules.R533LeafFieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -611,7 +611,7 @@ public void Rule_533_Leaf_Field_Selections_invalid3() /* When */ var result = Validate( document, - new R533LeafFieldSelections()); + ExecutionRules.R533LeafFieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -632,7 +632,7 @@ public void Rule_533_Leaf_Field_Selections_invalid4() /* When */ var result = Validate( document, - new R533LeafFieldSelections()); + ExecutionRules.R533LeafFieldSelections()); /* Then */ Assert.False(result.IsValid); @@ -657,7 +657,7 @@ fragment argOnOptional on Dog { /* When */ var result = Validate( document, - new R541ArgumentNames()); + ExecutionRules.R541ArgumentNames()); /* Then */ Assert.True(result.IsValid); @@ -675,7 +675,7 @@ public void Rule_541_Argument_Names_invalid1() /* When */ var result = Validate( document, - new R541ArgumentNames()); + ExecutionRules.R541ArgumentNames()); /* Then */ Assert.False(result.IsValid); @@ -696,7 +696,7 @@ public void Rule_541_Argument_Names_invalid2() /* When */ var result = Validate( document, - new R541ArgumentNames()); + ExecutionRules.R541ArgumentNames()); /* Then */ Assert.False(result.IsValid); @@ -717,7 +717,7 @@ public void Rule_542_Argument_Uniqueness_valid1() /* When */ var result = Validate( document, - new R542ArgumentUniqueness()); + ExecutionRules.R542ArgumentUniqueness()); /* Then */ Assert.True(result.IsValid); @@ -735,7 +735,7 @@ public void Rule_542_Argument_Uniqueness_invalid1() /* When */ var result = Validate( document, - new R542ArgumentUniqueness()); + ExecutionRules.R542ArgumentUniqueness()); /* Then */ Assert.False(result.IsValid); @@ -756,7 +756,7 @@ public void Rule_542_Argument_Uniqueness_invalid2() /* When */ var result = Validate( document, - new R542ArgumentUniqueness()); + ExecutionRules.R542ArgumentUniqueness()); /* Then */ Assert.False(result.IsValid); @@ -786,7 +786,7 @@ fragment goodBooleanArgDefault on Arguments { /* When */ var result = Validate( document, - new R5421RequiredArguments()); + ExecutionRules.R5421RequiredArguments()); /* Then */ Assert.True(result.IsValid); @@ -804,7 +804,7 @@ public void Rule_5421_Required_Arguments_invalid1() /* When */ var result = Validate( document, - new R5421RequiredArguments()); + ExecutionRules.R5421RequiredArguments()); /* Then */ Assert.False(result.IsValid); @@ -825,7 +825,7 @@ public void Rule_5421_Required_Arguments_invalid2() /* When */ var result = Validate( document, - new R5421RequiredArguments()); + ExecutionRules.R5421RequiredArguments()); /* Then */ Assert.False(result.IsValid); @@ -860,7 +860,7 @@ fragment fragmentTwo on Dog { /* When */ var result = Validate( document, - new R5511FragmentNameUniqueness()); + ExecutionRules.R5511FragmentNameUniqueness()); /* Then */ Assert.True(result.IsValid); @@ -890,7 +890,7 @@ fragment fragmentOne on Dog { /* When */ var result = Validate( document, - new R5511FragmentNameUniqueness()); + ExecutionRules.R5511FragmentNameUniqueness()); /* Then */ Assert.False(result.IsValid); @@ -924,7 +924,7 @@ ... @include(if: true) { /* When */ var result = Validate( document, - new R5512FragmentSpreadTypeExistence()); + ExecutionRules.R5512FragmentSpreadTypeExistence()); /* Then */ Assert.True(result.IsValid); @@ -943,7 +943,7 @@ public void Rule_5512_Fragment_Spread_Type_Existence_invalid1() /* When */ var result = Validate( document, - new R5512FragmentSpreadTypeExistence()); + ExecutionRules.R5512FragmentSpreadTypeExistence()); /* Then */ Assert.False(result.IsValid); @@ -967,7 +967,7 @@ ... on NotInSchema { /* When */ var result = Validate( document, - new R5512FragmentSpreadTypeExistence()); + ExecutionRules.R5512FragmentSpreadTypeExistence()); /* Then */ Assert.False(result.IsValid); @@ -999,7 +999,7 @@ ... on Dog { /* When */ var result = Validate( document, - new R5513FragmentsOnCompositeTypes()); + ExecutionRules.R5513FragmentsOnCompositeTypes()); /* Then */ Assert.True(result.IsValid); @@ -1018,7 +1018,7 @@ public void Rule_5513_FragmentsOnCompositeTypes_invalid1() /* When */ var result = Validate( document, - new R5513FragmentsOnCompositeTypes()); + ExecutionRules.R5513FragmentsOnCompositeTypes()); /* Then */ Assert.False(result.IsValid); @@ -1042,7 +1042,7 @@ ... on Boolean { /* When */ var result = Validate( document, - new R5513FragmentsOnCompositeTypes()); + ExecutionRules.R5513FragmentsOnCompositeTypes()); /* Then */ Assert.False(result.IsValid); @@ -1050,5 +1050,58 @@ ... on Boolean { 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); + } } } \ No newline at end of file From 1ba75fc072d0cc98b638607fe9e76f4aa8429757 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sun, 24 Feb 2019 21:33:54 +0200 Subject: [PATCH 14/20] Remove interface --- src/graphql.benchmarks/Benchmarks.cs | 3 +- src/graphql/validation/IRuleVisitor.cs | 46 -------------------------- src/graphql/validation/RuleVisitor.cs | 2 +- 3 files changed, 3 insertions(+), 48 deletions(-) delete mode 100644 src/graphql/validation/IRuleVisitor.cs diff --git a/src/graphql.benchmarks/Benchmarks.cs b/src/graphql.benchmarks/Benchmarks.cs index e7f45a1a1..9c62955c7 100644 --- a/src/graphql.benchmarks/Benchmarks.cs +++ b/src/graphql.benchmarks/Benchmarks.cs @@ -137,7 +137,7 @@ 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() @@ -152,6 +152,7 @@ public async Task Validate_query_with_defaults() $"Validation failed. {result}"); } } + */ [Benchmark] public void Validate_query_with_defaults_v2() diff --git a/src/graphql/validation/IRuleVisitor.cs b/src/graphql/validation/IRuleVisitor.cs deleted file mode 100644 index 1bb767a13..000000000 --- a/src/graphql/validation/IRuleVisitor.cs +++ /dev/null @@ -1,46 +0,0 @@ -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public interface IRuleVisitor - { - NodeVisitor EnterAlias { get; set; } - NodeVisitor EnterArgument { get; set; } - NodeVisitor EnterBooleanValue { get; set; } - NodeVisitor EnterDirective { get; set; } - NodeVisitor EnterDirectives { get; set; } - NodeVisitor EnterDocument { get; set; } - NodeVisitor EnterEnumValue { get; set; } - NodeVisitor EnterFieldSelection { get; set; } - NodeVisitor EnterFloatValue { get; set; } - NodeVisitor EnterFragmentDefinition { get; set; } - NodeVisitor EnterFragmentSpread { get; set; } - NodeVisitor EnterInlineFragment { get; set; } - NodeVisitor EnterIntValue { get; set; } - NodeVisitor EnterListValue { get; set; } - NodeVisitor EnterName { get; set; } - NodeVisitor EnterNamedType { get; set; } - NodeVisitor EnterNode { get; set; } - NodeVisitor EnterObjectField { get; set; } - NodeVisitor EnterObjectValue { get; set; } - NodeVisitor EnterOperationDefinition { get; set; } - NodeVisitor EnterSelectionSet { get; set; } - NodeVisitor EnterStringValue { get; set; } - NodeVisitor EnterVariable { get; set; } - NodeVisitor EnterVariableDefinition { get; set; } - NodeVisitor LeaveArgument { get; set; } - NodeVisitor LeaveDirective { get; set; } - NodeVisitor LeaveDocument { get; set; } - NodeVisitor LeaveEnumValue { get; set; } - NodeVisitor LeaveFieldSelection { get; set; } - NodeVisitor LeaveFragmentDefinition { get; set; } - NodeVisitor LeaveInlineFragment { get; set; } - NodeVisitor LeaveListValue { get; set; } - NodeVisitor LeaveObjectField { get; set; } - NodeVisitor LeaveObjectValue { get; set; } - NodeVisitor LeaveOperationDefinition { get; set; } - NodeVisitor LeaveSelectionSet { get; set; } - NodeVisitor LeaveVariable { get; set; } - NodeVisitor LeaveVariableDefinition { get; set; } - } -} \ No newline at end of file diff --git a/src/graphql/validation/RuleVisitor.cs b/src/graphql/validation/RuleVisitor.cs index 4168046ec..f2f6942ea 100644 --- a/src/graphql/validation/RuleVisitor.cs +++ b/src/graphql/validation/RuleVisitor.cs @@ -2,7 +2,7 @@ namespace tanka.graphql.validation { - public class RuleVisitor : IRuleVisitor + public class RuleVisitor { public NodeVisitor EnterAlias { get; set; } From 5588438e22708b8aa6f28bdebd5e9b24eeefb052 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Mon, 25 Feb 2019 20:44:14 +0200 Subject: [PATCH 15/20] Rule chaining --- src/graphql/validation/CreateRule.cs | 2 +- src/graphql/validation/ExecutionRules.cs | 145 ++++++++++------------- src/graphql/validation/RulesWalker.cs | 145 +++++++++-------------- 3 files changed, 120 insertions(+), 172 deletions(-) diff --git a/src/graphql/validation/CreateRule.cs b/src/graphql/validation/CreateRule.cs index dc196ac7a..127d7f0a4 100644 --- a/src/graphql/validation/CreateRule.cs +++ b/src/graphql/validation/CreateRule.cs @@ -1,4 +1,4 @@ namespace tanka.graphql.validation { - public delegate RuleVisitor CreateRule(IRuleVisitorContext context); + public delegate void CreateRule(IRuleVisitorContext context, RuleVisitor rule); } \ No newline at end of file diff --git a/src/graphql/validation/ExecutionRules.cs b/src/graphql/validation/ExecutionRules.cs index 0470fdc93..9c6aaed98 100644 --- a/src/graphql/validation/ExecutionRules.cs +++ b/src/graphql/validation/ExecutionRules.cs @@ -33,9 +33,9 @@ public static class ExecutionRules /// public static CreateRule R511ExecutableDefinitions() { - return context => new RuleVisitor + return (context, rule) => { - EnterDocument = document => + rule.EnterDocument += document => { foreach (var definition in document.Definitions) { @@ -51,7 +51,7 @@ public static CreateRule R511ExecutableDefinitions() "executable, and are not considered during execution.", definition); } - } + }; }; } @@ -65,26 +65,23 @@ public static CreateRule R511ExecutableDefinitions() /// public static CreateRule R5211OperationNameUniqueness() { - return context => + return (context, rule) => { var known = new List(); - return new RuleVisitor + rule.EnterOperationDefinition += definition => { - EnterOperationDefinition = definition => - { - var operationName = definition.Name?.Value; + var operationName = definition.Name?.Value; - if (string.IsNullOrWhiteSpace(operationName)) - return; + 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); + 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); - } + known.Add(operationName); }; }; } @@ -97,11 +94,9 @@ public static CreateRule R5211OperationNameUniqueness() /// public static CreateRule R5221LoneAnonymousOperation() { - return context => + return (context, rule) => { - return new RuleVisitor - { - EnterDocument = document => + rule.EnterDocument += document => { var operations = document.Definitions .OfType() @@ -118,7 +113,6 @@ public static CreateRule R5221LoneAnonymousOperation() "query operations when only that one operation exists in " + "the document.", operations); - } }; }; } @@ -133,9 +127,9 @@ public static CreateRule R5221LoneAnonymousOperation() /// public static CreateRule R5231SingleRootField() { - return context => new RuleVisitor + return (context, rule) => { - EnterDocument = document => + rule.EnterDocument += document => { var subscriptions = document.Definitions .OfType() @@ -169,7 +163,7 @@ public static CreateRule R5231SingleRootField() "Subscription operations must have exactly one root field.", subscription); } - } + }; }; } @@ -180,9 +174,9 @@ public static CreateRule R5231SingleRootField() /// public static CreateRule R531FieldSelections() { - return context => new RuleVisitor + return (context, rule) => { - EnterFieldSelection = selection => + rule.EnterFieldSelection += selection => { var fieldName = selection.Name.Value; @@ -196,7 +190,7 @@ public static CreateRule R531FieldSelections() "on the scoped type of the selection set. There are no " + "limitations on alias names.", selection); - } + }; }; } @@ -210,9 +204,9 @@ public static CreateRule R531FieldSelections() /// public static CreateRule R533LeafFieldSelections() { - return context => new RuleVisitor + return (context, rule) => { - EnterFieldSelection = selection => + rule.EnterFieldSelection += selection => { var fieldName = selection.Name.Value; @@ -254,7 +248,7 @@ public static CreateRule R533LeafFieldSelections() "without subfields are disallowed.", selection); } - } + }; }; } @@ -266,9 +260,9 @@ public static CreateRule R533LeafFieldSelections() /// public static CreateRule R541ArgumentNames() { - return context => new RuleVisitor + return (context, rule) => { - EnterArgument = argument => + rule.EnterArgument += argument => { if (context.Tracker.GetArgument() == null) context.Error( @@ -277,7 +271,7 @@ public static CreateRule R541ArgumentNames() "must be defined in the set of possible arguments of that " + "field or directive.", argument); - } + }; }; } @@ -346,9 +340,9 @@ void ValidateArguments(IEnumerable> keyValuePairs } } - return context => new RuleVisitor + return (context, rule) => { - EnterFieldSelection = field => + rule.EnterFieldSelection += field => { var args = field.Arguments.ToList(); var argumentDefinitions = GetArgumentDefinitions(context); @@ -358,8 +352,8 @@ void ValidateArguments(IEnumerable> keyValuePairs return; ValidateArguments(argumentDefinitions, args, context); - }, - EnterDirective = directive => + }; + rule.EnterDirective += directive => { var args = directive.Arguments.ToList(); var argumentDefinitions = GetArgumentDefinitions(context); @@ -369,7 +363,7 @@ void ValidateArguments(IEnumerable> keyValuePairs return; ValidateArguments(argumentDefinitions, args, context); - } + }; }; } @@ -381,12 +375,10 @@ void ValidateArguments(IEnumerable> keyValuePairs /// public static CreateRule R542ArgumentUniqueness() { - return context => + return (context, rule) => { var knownArgs = new List(); - return new RuleVisitor - { - EnterArgument = argument => + rule.EnterArgument += argument => { if (knownArgs.Contains(argument.Name.Value)) context.Error( @@ -397,7 +389,6 @@ public static CreateRule R542ArgumentUniqueness() argument); knownArgs.Add(argument.Name.Value); - } }; }; } @@ -410,22 +401,19 @@ public static CreateRule R542ArgumentUniqueness() /// public static CreateRule R5511FragmentNameUniqueness() { - return context => + return (context, rule) => { var knownFragments = new List(); - return new RuleVisitor + rule.EnterFragmentDefinition += fragment => { - 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); + 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); - } + knownFragments.Add(fragment.Name.Value); }; }; } @@ -437,9 +425,9 @@ public static CreateRule R5511FragmentNameUniqueness() /// public static CreateRule R5512FragmentSpreadTypeExistence() { - return context => new RuleVisitor + return (context, rule) => { - EnterFragmentDefinition = node => + rule.EnterFragmentDefinition += node => { var type = context.Tracker.GetCurrentType(); @@ -449,8 +437,8 @@ public static CreateRule R5512FragmentSpreadTypeExistence() "Fragments must be specified on types that exist in the schema. This " + "applies for both named and inline fragments. ", node); - }, - EnterInlineFragment = node => + }; + rule.EnterInlineFragment += node => { var type = context.Tracker.GetCurrentType(); @@ -460,7 +448,7 @@ public static CreateRule R5512FragmentSpreadTypeExistence() "Fragments must be specified on types that exist in the schema. This " + "applies for both named and inline fragments. ", node); - } + }; }; } @@ -470,9 +458,9 @@ public static CreateRule R5512FragmentSpreadTypeExistence() /// public static CreateRule R5513FragmentsOnCompositeTypes() { - return context => new RuleVisitor + return (context, rule) => { - EnterFragmentDefinition = node => + rule.EnterFragmentDefinition += node => { var type = context.Tracker.GetCurrentType(); @@ -486,8 +474,8 @@ public static CreateRule R5513FragmentsOnCompositeTypes() ValidationErrorCodes.R5513FragmentsOnCompositeTypes, "Fragments can only be declared on unions, interfaces, and objects", node); - }, - EnterInlineFragment = node => + }; + rule.EnterInlineFragment += node => { var type = context.Tracker.GetCurrentType(); @@ -501,7 +489,7 @@ public static CreateRule R5513FragmentsOnCompositeTypes() ValidationErrorCodes.R5513FragmentsOnCompositeTypes, "Fragments can only be declared on unions, interfaces, and objects", node); - } + }; }; } @@ -511,26 +499,23 @@ public static CreateRule R5513FragmentsOnCompositeTypes() /// public static CreateRule R5514FragmentsMustBeUsed() { - return context => + return (context, rule) => { var fragments = new Dictionary(); var fragmentSpreads = new List(); - return new RuleVisitor + rule.EnterFragmentDefinition += fragment => { fragments.Add(fragment.Name.Value, fragment); }; + rule.EnterFragmentSpread += spread => { fragmentSpreads.Add(spread.Name.Value); }; + rule.LeaveDocument += document => { - EnterFragmentDefinition = fragment => { fragments.Add(fragment.Name.Value, fragment); }, - EnterFragmentSpread = spread => { fragmentSpreads.Add(spread.Name.Value); }, - LeaveDocument = document => + foreach (var fragment in fragments) { - 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); - } + var name = fragment.Key; + if (!fragmentSpreads.Contains(name)) + context.Error( + ValidationErrorCodes.R5514FragmentsMustBeUsed, + "Defined fragments must be used within a document.", + fragment.Value); } }; }; diff --git a/src/graphql/validation/RulesWalker.cs b/src/graphql/validation/RulesWalker.cs index fefc9cce6..62fe397f2 100644 --- a/src/graphql/validation/RulesWalker.cs +++ b/src/graphql/validation/RulesWalker.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; using GraphQLParser.AST; using tanka.graphql.language; using tanka.graphql.type; @@ -21,27 +19,14 @@ public RulesWalker( Schema = schema; Document = document; VariableValues = variableValues; - NodeVisitors = CreateVisitors(rules).ToList(); - - //todo: this will break - Tracker = NodeVisitors.First() as TypeTracker; - } - - protected IEnumerable NodeVisitors { get; set; } - - protected IEnumerable CreateVisitors(IEnumerable rules) - { - var createRules = new List(rules); - createRules.Insert(0, context => new TypeTracker(context.Schema)); - - return createRules.Select(r => r(this)); + CreateVisitors(rules); } public GraphQLDocument Document { get; } public IDictionary VariableValues { get; } - public TypeTracker Tracker { get; } + public TypeTracker Tracker { get; protected set; } public ISchema Schema { get; } @@ -68,24 +53,22 @@ public ValidationResult Validate() public override void Visit(GraphQLDocument document) { - foreach (var visitor in NodeVisitors) { - visitor.EnterDocument?.Invoke(document); + Tracker.EnterDocument?.Invoke(document); } base.Visit(document); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveDocument?.Invoke(document); + Tracker.LeaveDocument?.Invoke(document); } } public override GraphQLName BeginVisitAlias(GraphQLName alias) { - foreach (var visitor in NodeVisitors) { - visitor.EnterAlias?.Invoke(alias); + Tracker.EnterAlias?.Invoke(alias); } return base.BeginVisitAlias(alias); @@ -93,9 +76,8 @@ public override GraphQLName BeginVisitAlias(GraphQLName alias) public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) { - foreach (var visitor in NodeVisitors) { - visitor.EnterArgument?.Invoke(argument); + Tracker.EnterArgument?.Invoke(argument); } return base.BeginVisitArgument(argument); @@ -104,9 +86,8 @@ public override GraphQLArgument BeginVisitArgument(GraphQLArgument argument) public override GraphQLScalarValue BeginVisitBooleanValue( GraphQLScalarValue value) { - foreach (var visitor in NodeVisitors) { - visitor.EnterBooleanValue?.Invoke(value); + Tracker.EnterBooleanValue?.Invoke(value); } return base.BeginVisitBooleanValue(value); @@ -114,16 +95,15 @@ public override GraphQLScalarValue BeginVisitBooleanValue( public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) { - foreach (var visitor in NodeVisitors) { - visitor.EnterDirective?.Invoke(directive); + Tracker.EnterDirective?.Invoke(directive); } var _ = base.BeginVisitDirective(directive); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveDirective?.Invoke(directive); + Tracker.LeaveDirective?.Invoke(directive); } return _; @@ -131,16 +111,15 @@ public override GraphQLDirective BeginVisitDirective(GraphQLDirective directive) public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) { - foreach (var visitor in NodeVisitors) { - visitor.EnterEnumValue?.Invoke(value); + Tracker.EnterEnumValue?.Invoke(value); } var _ = base.BeginVisitEnumValue(value); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveEnumValue?.Invoke(value); + Tracker.LeaveEnumValue?.Invoke(value); } return _; @@ -149,9 +128,8 @@ public override GraphQLScalarValue BeginVisitEnumValue(GraphQLScalarValue value) public override GraphQLFieldSelection BeginVisitFieldSelection( GraphQLFieldSelection selection) { - foreach (var visitor in NodeVisitors) { - visitor.EnterFieldSelection?.Invoke(selection); + Tracker.EnterFieldSelection?.Invoke(selection); } return base.BeginVisitFieldSelection(selection); @@ -160,9 +138,8 @@ public override GraphQLFieldSelection BeginVisitFieldSelection( public override GraphQLScalarValue BeginVisitFloatValue( GraphQLScalarValue value) { - foreach (var visitor in NodeVisitors) { - visitor.EnterFloatValue?.Invoke(value); + Tracker.EnterFloatValue?.Invoke(value); } return base.BeginVisitFloatValue(value); @@ -171,16 +148,15 @@ public override GraphQLScalarValue BeginVisitFloatValue( public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( GraphQLFragmentDefinition node) { - foreach (var visitor in NodeVisitors) { - visitor.EnterFragmentDefinition?.Invoke(node); + Tracker.EnterFragmentDefinition?.Invoke(node); } var result = base.BeginVisitFragmentDefinition(node); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveFragmentDefinition?.Invoke(node); + Tracker.LeaveFragmentDefinition?.Invoke(node); } return result; @@ -189,9 +165,8 @@ public override GraphQLFragmentDefinition BeginVisitFragmentDefinition( public override GraphQLFragmentSpread BeginVisitFragmentSpread( GraphQLFragmentSpread fragmentSpread) { - foreach (var visitor in NodeVisitors) { - visitor.EnterFragmentSpread?.Invoke(fragmentSpread); + Tracker.EnterFragmentSpread?.Invoke(fragmentSpread); } return base.BeginVisitFragmentSpread(fragmentSpread); @@ -200,16 +175,15 @@ public override GraphQLFragmentSpread BeginVisitFragmentSpread( public override GraphQLInlineFragment BeginVisitInlineFragment( GraphQLInlineFragment inlineFragment) { - foreach (var visitor in NodeVisitors) { - visitor.EnterInlineFragment?.Invoke(inlineFragment); + Tracker.EnterInlineFragment?.Invoke(inlineFragment); } var _ = base.BeginVisitInlineFragment(inlineFragment); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveInlineFragment?.Invoke(inlineFragment); + Tracker.LeaveInlineFragment?.Invoke(inlineFragment); } return _; @@ -217,9 +191,8 @@ public override GraphQLInlineFragment BeginVisitInlineFragment( public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) { - foreach (var visitor in NodeVisitors) { - visitor.EnterIntValue?.Invoke(value); + Tracker.EnterIntValue?.Invoke(value); } return base.BeginVisitIntValue(value); @@ -227,9 +200,8 @@ public override GraphQLScalarValue BeginVisitIntValue(GraphQLScalarValue value) public override GraphQLName BeginVisitName(GraphQLName name) { - foreach (var visitor in NodeVisitors) { - visitor.EnterName?.Invoke(name); + Tracker.EnterName?.Invoke(name); } return base.BeginVisitName(name); @@ -238,9 +210,8 @@ public override GraphQLName BeginVisitName(GraphQLName name) public override GraphQLNamedType BeginVisitNamedType( GraphQLNamedType typeCondition) { - foreach (var visitor in NodeVisitors) { - visitor.EnterNamedType?.Invoke(typeCondition); + Tracker.EnterNamedType?.Invoke(typeCondition); } return base.BeginVisitNamedType(typeCondition); @@ -249,9 +220,8 @@ public override GraphQLNamedType BeginVisitNamedType( public override GraphQLOperationDefinition BeginVisitOperationDefinition( GraphQLOperationDefinition definition) { - foreach (var visitor in NodeVisitors) { - visitor.EnterOperationDefinition?.Invoke(definition); + Tracker.EnterOperationDefinition?.Invoke(definition); } return base.BeginVisitOperationDefinition(definition); @@ -260,9 +230,8 @@ public override GraphQLOperationDefinition BeginVisitOperationDefinition( public override GraphQLOperationDefinition EndVisitOperationDefinition( GraphQLOperationDefinition definition) { - foreach (var visitor in NodeVisitors) { - visitor.LeaveOperationDefinition?.Invoke(definition); + Tracker.LeaveOperationDefinition?.Invoke(definition); } return base.EndVisitOperationDefinition(definition); @@ -271,16 +240,15 @@ public override GraphQLOperationDefinition EndVisitOperationDefinition( public override GraphQLSelectionSet BeginVisitSelectionSet( GraphQLSelectionSet selectionSet) { - foreach (var visitor in NodeVisitors) { - visitor.EnterSelectionSet?.Invoke(selectionSet); + Tracker.EnterSelectionSet?.Invoke(selectionSet); } var _ = base.BeginVisitSelectionSet(selectionSet); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveSelectionSet?.Invoke(selectionSet); + Tracker.LeaveSelectionSet?.Invoke(selectionSet); } return _; @@ -289,9 +257,8 @@ public override GraphQLSelectionSet BeginVisitSelectionSet( public override GraphQLScalarValue BeginVisitStringValue( GraphQLScalarValue value) { - foreach (var visitor in NodeVisitors) { - visitor.EnterStringValue?.Invoke(value); + Tracker.EnterStringValue?.Invoke(value); } return base.BeginVisitStringValue(value); @@ -299,9 +266,8 @@ public override GraphQLScalarValue BeginVisitStringValue( public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) { - foreach (var visitor in NodeVisitors) { - visitor.EnterVariable?.Invoke(variable); + Tracker.EnterVariable?.Invoke(variable); } return base.BeginVisitVariable(variable); @@ -310,16 +276,15 @@ public override GraphQLVariable BeginVisitVariable(GraphQLVariable variable) public override GraphQLVariableDefinition BeginVisitVariableDefinition( GraphQLVariableDefinition node) { - foreach (var visitor in NodeVisitors) { - visitor.EnterVariableDefinition?.Invoke(node); + Tracker.EnterVariableDefinition?.Invoke(node); } var _ = base.BeginVisitVariableDefinition(node); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveVariableDefinition?.Invoke(node); + Tracker.LeaveVariableDefinition?.Invoke(node); } return _; @@ -327,9 +292,8 @@ public override GraphQLVariableDefinition BeginVisitVariableDefinition( public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) { - foreach (var visitor in NodeVisitors) { - visitor.LeaveArgument?.Invoke(argument); + Tracker.LeaveArgument?.Invoke(argument); } return base.EndVisitArgument(argument); @@ -338,9 +302,8 @@ public override GraphQLArgument EndVisitArgument(GraphQLArgument argument) public override GraphQLFieldSelection EndVisitFieldSelection( GraphQLFieldSelection selection) { - foreach (var visitor in NodeVisitors) { - visitor.LeaveFieldSelection?.Invoke(selection); + Tracker.LeaveFieldSelection?.Invoke(selection); } return base.EndVisitFieldSelection(selection); @@ -348,9 +311,8 @@ public override GraphQLFieldSelection EndVisitFieldSelection( public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) { - foreach (var visitor in NodeVisitors) { - visitor.EnterVariable?.Invoke(variable); + Tracker.EnterVariable?.Invoke(variable); } return base.EndVisitVariable(variable); @@ -359,16 +321,15 @@ public override GraphQLVariable EndVisitVariable(GraphQLVariable variable) public override GraphQLObjectField BeginVisitObjectField( GraphQLObjectField node) { - foreach (var visitor in NodeVisitors) { - visitor.EnterObjectField?.Invoke(node); + Tracker.EnterObjectField?.Invoke(node); } var _ = base.BeginVisitObjectField(node); - foreach (var visitor in NodeVisitors) + { - visitor.LeaveObjectField?.Invoke(node); + Tracker.LeaveObjectField?.Invoke(node); } return _; @@ -377,9 +338,8 @@ public override GraphQLObjectField BeginVisitObjectField( public override GraphQLObjectValue BeginVisitObjectValue( GraphQLObjectValue node) { - foreach (var visitor in NodeVisitors) { - visitor.EnterObjectValue?.Invoke(node); + Tracker.EnterObjectValue?.Invoke(node); } return base.BeginVisitObjectValue(node); @@ -387,9 +347,8 @@ public override GraphQLObjectValue BeginVisitObjectValue( public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) { - foreach (var visitor in NodeVisitors) { - visitor.LeaveObjectValue?.Invoke(node); + Tracker.LeaveObjectValue?.Invoke(node); } return base.EndVisitObjectValue(node); @@ -397,9 +356,8 @@ public override GraphQLObjectValue EndVisitObjectValue(GraphQLObjectValue node) public override ASTNode BeginVisitNode(ASTNode node) { - foreach (var visitor in NodeVisitors) { - visitor.EnterNode?.Invoke(node); + Tracker.EnterNode?.Invoke(node); } return base.BeginVisitNode(node); @@ -407,9 +365,8 @@ public override ASTNode BeginVisitNode(ASTNode node) public override GraphQLListValue BeginVisitListValue(GraphQLListValue node) { - foreach (var visitor in NodeVisitors) { - visitor.EnterListValue?.Invoke(node); + Tracker.EnterListValue?.Invoke(node); } return base.BeginVisitListValue(node); @@ -417,14 +374,20 @@ public override GraphQLListValue BeginVisitListValue(GraphQLListValue node) public override GraphQLListValue EndVisitListValue(GraphQLListValue node) { - foreach (var visitor in NodeVisitors) { - visitor.LeaveListValue?.Invoke(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 From 079bfa5f56066ad00b98e7b80c96c778fbb680e6 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Tue, 26 Feb 2019 19:01:13 +0200 Subject: [PATCH 16/20] 5.5.2.2 --- src/graphql/validation/ExecutionRules.cs | 172 +++++++++++++++--- .../validation/ValidationErrorCodes.cs | 4 +- .../validation/ValidatorFacts.cs | 73 +++++++- 3 files changed, 221 insertions(+), 28 deletions(-) diff --git a/src/graphql/validation/ExecutionRules.cs b/src/graphql/validation/ExecutionRules.cs index 9c6aaed98..dd1f09ea8 100644 --- a/src/graphql/validation/ExecutionRules.cs +++ b/src/graphql/validation/ExecutionRules.cs @@ -17,12 +17,13 @@ public static class ExecutionRules R5512FragmentSpreadTypeExistence(), R5513FragmentsOnCompositeTypes(), R5514FragmentsMustBeUsed(), + R5522FragmentSpreadsMustNotFormCycles(), R5231SingleRootField(), R531FieldSelections(), R533LeafFieldSelections(), R541ArgumentNames(), R542ArgumentUniqueness(), - R5421RequiredArguments(), + R5421RequiredArguments() }; @@ -97,22 +98,22 @@ public static CreateRule 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); + { + 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); }; }; } @@ -379,16 +380,16 @@ public static CreateRule R542ArgumentUniqueness() { var 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); + { + 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); + knownArgs.Add(argument.Name.Value); }; }; } @@ -520,5 +521,124 @@ public static CreateRule R5514FragmentsMustBeUsed() }; }; } + + public static CreateRule 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 $"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 (int i = 0; i < spreadNodes.Length; i++) + { + var spreadNode = spreadNodes[i]; + var spreadName = spreadNode.Name.Value; + int? 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; + } + } } } \ No newline at end of file diff --git a/src/graphql/validation/ValidationErrorCodes.cs b/src/graphql/validation/ValidationErrorCodes.cs index aaf967862..464341cb2 100644 --- a/src/graphql/validation/ValidationErrorCodes.cs +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -1,7 +1,7 @@ namespace tanka.graphql.validation { public static class ValidationErrorCodes - { + { public const string R5211OperationNameUniqueness = "5.2.1.1 Operation Name Uniqueness"; public const string R511ExecutableDefinitions = "5.1.1 Executable Definitions"; @@ -27,5 +27,7 @@ public static class ValidationErrorCodes 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"; } } \ No newline at end of file diff --git a/tests/graphql.tests/validation/ValidatorFacts.cs b/tests/graphql.tests/validation/ValidatorFacts.cs index 533125241..0a51a7a16 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -5,7 +5,6 @@ using tanka.graphql.sdl; using tanka.graphql.type; using tanka.graphql.validation; -using tanka.graphql.validation.rules2; using Xunit; namespace tanka.graphql.tests.validation @@ -1103,5 +1102,77 @@ public void Rule_5514_FragmentsMustBeUsed_invalid1() 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); + } } } \ No newline at end of file From f192e57b6f7cca8bdcb2c4383d885b560dfcd609 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Tue, 26 Feb 2019 19:03:38 +0200 Subject: [PATCH 17/20] 5.5.2.2 - append explanation --- src/graphql/validation/ExecutionRules.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/graphql/validation/ExecutionRules.cs b/src/graphql/validation/ExecutionRules.cs index dd1f09ea8..16ddcd7e1 100644 --- a/src/graphql/validation/ExecutionRules.cs +++ b/src/graphql/validation/ExecutionRules.cs @@ -550,7 +550,10 @@ public static CreateRule R5522FragmentSpreadsMustNotFormCycles() string CycleErrorMessage(string fragName, string[] spreadNames) { var via = spreadNames.Any() ? " via " + string.Join(", ", spreadNames) : ""; - return $"Cannot spread fragment \"{fragName}\" within itself {via}."; + 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) From 0202c85bce0acf6e9563fd70070ee1d0b189602e Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 28 Feb 2019 18:59:39 +0200 Subject: [PATCH 18/20] Use new validation --- src/graphql.benchmarks/Benchmarks.cs | 11 +- src/graphql/Error.cs | 2 +- src/graphql/Executor.cs | 7 +- src/graphql/type/ISchema.cs | 2 + src/graphql/type/SchemaGraph.cs | 18 +- src/graphql/type/UnionType.cs | 8 +- .../type/converters/BooleanConverter.cs | 11 +- .../type/converters/DoubleConverter.cs | 8 +- src/graphql/type/converters/IdConverter.cs | 10 +- src/graphql/type/converters/LongConverter.cs | 8 +- .../type/converters/StringConverter.cs | 8 +- src/graphql/validation/CombineRule.cs | 4 + src/graphql/validation/CreateRule.cs | 4 - src/graphql/validation/ExecutionRules.cs | 494 ++++++++++++++- src/graphql/validation/RulesWalker.cs | 4 +- src/graphql/validation/TypeTracker.cs | 2 +- src/graphql/validation/ValidationError.cs | 18 +- .../validation/ValidationErrorCodes.cs | 24 +- src/graphql/validation/Validator.cs | 2 +- tests/graphql.tests/type/BooleanTypeFacts.cs | 2 +- .../validation/ValidatorFacts.cs | 560 +++++++++++++++++- 21 files changed, 1135 insertions(+), 72 deletions(-) create mode 100644 src/graphql/validation/CombineRule.cs delete mode 100644 src/graphql/validation/CreateRule.cs diff --git a/src/graphql.benchmarks/Benchmarks.cs b/src/graphql.benchmarks/Benchmarks.cs index 9c62955c7..9e5a4ec1c 100644 --- a/src/graphql.benchmarks/Benchmarks.cs +++ b/src/graphql.benchmarks/Benchmarks.cs @@ -20,7 +20,7 @@ public class Benchmarks private ISchema _schema; private GraphQLDocument _mutation; private GraphQLDocument _subscription; - private IEnumerable _defaultRulesMap; + private IEnumerable _defaultRulesMap; [GlobalSetup] public async Task Setup() @@ -31,7 +31,7 @@ public async Task Setup() _subscription = Utils.InitializeSubscription(); _defaultRulesMap = ExecutionRules.All; } - /* + [Benchmark] public async Task Query_with_defaults() { @@ -140,7 +140,7 @@ public async Task Subscribe_without_validation_and_get_value() [Benchmark] - public async Task Validate_query_with_defaults() + public async Task Validate_query_with_defaults_old() { var result = await Validator.ValidateAsync( _schema, @@ -152,10 +152,9 @@ public async Task Validate_query_with_defaults() $"Validation failed. {result}"); } } - */ - + [Benchmark] - public void Validate_query_with_defaults_v2() + public void Validate_query_with_defaults() { var result = Validator.Validate( _defaultRulesMap, 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/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/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 822acd3e2..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, IAbstractType + 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/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/CreateRule.cs b/src/graphql/validation/CreateRule.cs deleted file mode 100644 index 127d7f0a4..000000000 --- a/src/graphql/validation/CreateRule.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace tanka.graphql.validation -{ - public delegate void CreateRule(IRuleVisitorContext context, RuleVisitor rule); -} \ No newline at end of file diff --git a/src/graphql/validation/ExecutionRules.cs b/src/graphql/validation/ExecutionRules.cs index 16ddcd7e1..17fc851b2 100644 --- a/src/graphql/validation/ExecutionRules.cs +++ b/src/graphql/validation/ExecutionRules.cs @@ -1,14 +1,16 @@ -using System.Collections.Generic; +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[] + public static IEnumerable All = new[] { R511ExecutableDefinitions(), R5211OperationNameUniqueness(), @@ -18,12 +20,19 @@ public static class ExecutionRules R5513FragmentsOnCompositeTypes(), R5514FragmentsMustBeUsed(), R5522FragmentSpreadsMustNotFormCycles(), + R5523FragmentSpreadIsPossible(), R5231SingleRootField(), R531FieldSelections(), R533LeafFieldSelections(), R541ArgumentNames(), R542ArgumentUniqueness(), - R5421RequiredArguments() + R5421RequiredArguments(), + R561ValuesOfCorrectType(), + R562InputObjectFieldNames(), + R563InputObjectFieldUniqueness(), + R564InputObjectRequiredFields(), + R57Directives(), + R58Variables() }; @@ -32,7 +41,7 @@ public static class ExecutionRules /// For each definition definition in the document. /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). /// - public static CreateRule R511ExecutableDefinitions() + public static CombineRule R511ExecutableDefinitions() { return (context, rule) => { @@ -64,7 +73,7 @@ public static CreateRule R511ExecutableDefinitions() /// Let operations be all operation definitions in the document named operationName. /// operations must be a set of one. /// - public static CreateRule R5211OperationNameUniqueness() + public static CombineRule R5211OperationNameUniqueness() { return (context, rule) => { @@ -93,7 +102,7 @@ public static CreateRule R5211OperationNameUniqueness() /// If operations is a set of more than 1: /// anonymous must be empty. /// - public static CreateRule R5221LoneAnonymousOperation() + public static CombineRule R5221LoneAnonymousOperation() { return (context, rule) => { @@ -126,7 +135,7 @@ public static CreateRule R5221LoneAnonymousOperation() /// Let groupedFieldSet be the result of CollectFields(subscriptionType, selectionSet, variableValues). /// groupedFieldSet must have exactly one entry. /// - public static CreateRule R5231SingleRootField() + public static CombineRule R5231SingleRootField() { return (context, rule) => { @@ -173,7 +182,7 @@ public static CreateRule R5231SingleRootField() /// Let fieldName be the target field of selection /// fieldName must be defined on type in scope /// - public static CreateRule R531FieldSelections() + public static CombineRule R531FieldSelections() { return (context, rule) => { @@ -203,7 +212,7 @@ public static CreateRule R531FieldSelections() /// If selectionType is an interface, union, or object /// The subselection set of that selection must NOT BE empty /// - public static CreateRule R533LeafFieldSelections() + public static CombineRule R533LeafFieldSelections() { return (context, rule) => { @@ -218,7 +227,7 @@ public static CreateRule R533LeafFieldSelections() if (field != null) { - var selectionType = field.Value.Field.Type; + var selectionType = field.Value.Field.Type.Unwrap(); var hasSubSelection = selection.SelectionSet?.Selections?.Any(); if (selectionType is ScalarType && hasSubSelection == true) @@ -235,7 +244,14 @@ public static CreateRule R533LeafFieldSelections() "allowed, because they are the leaf nodes of any GraphQL query.", selection); - if (selectionType is ComplexType && hasSubSelection == null) + 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 " + @@ -259,7 +275,7 @@ public static CreateRule R533LeafFieldSelections() /// Let argumentDefinition be the argument definition provided by the parent field or definition named argumentName. /// argumentDefinition must exist. /// - public static CreateRule R541ArgumentNames() + public static CombineRule R541ArgumentNames() { return (context, rule) => { @@ -290,7 +306,7 @@ public static CreateRule R541ArgumentNames() /// - Let value be the value of argument. /// value must not be the null literal. /// - public static CreateRule R5421RequiredArguments() + public static CombineRule R5421RequiredArguments() { IEnumerable> GetArgumentDefinitions(IRuleVisitorContext context) { @@ -374,11 +390,13 @@ void ValidateArguments(IEnumerable> keyValuePairs /// Let arguments be all Arguments named argumentName in the Argument Set which contains argument. /// arguments must be the set containing only argument. /// - public static CreateRule R542ArgumentUniqueness() + 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)) @@ -400,7 +418,7 @@ public static CreateRule R542ArgumentUniqueness() /// Let fragments be all fragment definitions in the document named fragmentName. /// fragments must be a set of one. /// - public static CreateRule R5511FragmentNameUniqueness() + public static CombineRule R5511FragmentNameUniqueness() { return (context, rule) => { @@ -424,7 +442,7 @@ public static CreateRule R5511FragmentNameUniqueness() /// Let fragment be the target of namedSpread /// The target type of fragment must be defined in the schema /// - public static CreateRule R5512FragmentSpreadTypeExistence() + public static CombineRule R5512FragmentSpreadTypeExistence() { return (context, rule) => { @@ -457,7 +475,7 @@ public static CreateRule R5512FragmentSpreadTypeExistence() /// For each fragment defined in the document. /// The target type of fragment must have kind UNION, INTERFACE, or OBJECT. /// - public static CreateRule R5513FragmentsOnCompositeTypes() + public static CombineRule R5513FragmentsOnCompositeTypes() { return (context, rule) => { @@ -498,7 +516,7 @@ public static CreateRule R5513FragmentsOnCompositeTypes() /// For each fragment defined in the document. /// fragment must be the target of at least one spread in the document /// - public static CreateRule R5514FragmentsMustBeUsed() + public static CombineRule R5514FragmentsMustBeUsed() { return (context, rule) => { @@ -522,7 +540,7 @@ public static CreateRule R5514FragmentsMustBeUsed() }; } - public static CreateRule R5522FragmentSpreadsMustNotFormCycles() + public static CombineRule R5522FragmentSpreadsMustNotFormCycles() { return (context, rule) => { @@ -538,11 +556,11 @@ public static CreateRule R5522FragmentSpreadsMustNotFormCycles() rule.EnterFragmentDefinition += node => { DetectCycleRecursive( - node, - spreadPath, - visitedFrags, - spreadPathIndexByName, - context, + node, + spreadPath, + visitedFrags, + spreadPathIndexByName, + context, fragments); }; }; @@ -568,14 +586,10 @@ IEnumerable GetFragmentSpreads(GraphQLSelectionSet node) 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; @@ -595,17 +609,17 @@ void DetectCycleRecursive( var spreadNodes = GetFragmentSpreads(fragment.SelectionSet) .ToArray(); - - if (!spreadNodes.Any()) + + if (!spreadNodes.Any()) return; spreadPathIndexByName[fragmentName] = spreadPath.Count; - for (int i = 0; i < spreadNodes.Length; i++) + for (var i = 0; i < spreadNodes.Length; i++) { var spreadNode = spreadNodes[i]; var spreadName = spreadNode.Name.Value; - int? cycleIndex = spreadPathIndexByName.ContainsKey(spreadName) + var cycleIndex = spreadPathIndexByName.ContainsKey(spreadName) ? spreadPathIndexByName[spreadName] : default; @@ -643,5 +657,421 @@ void DetectCycleRecursive( 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/RulesWalker.cs b/src/graphql/validation/RulesWalker.cs index 62fe397f2..56f83a345 100644 --- a/src/graphql/validation/RulesWalker.cs +++ b/src/graphql/validation/RulesWalker.cs @@ -11,7 +11,7 @@ public class RulesWalker : Visitor, IRuleVisitorContext new List(); public RulesWalker( - IEnumerable rules, + IEnumerable rules, ISchema schema, GraphQLDocument document, Dictionary variableValues = null) @@ -381,7 +381,7 @@ public override GraphQLListValue EndVisitListValue(GraphQLListValue node) return base.EndVisitListValue(node); } - protected void CreateVisitors(IEnumerable rules) + protected void CreateVisitors(IEnumerable rules) { Tracker = new TypeTracker(Schema); diff --git a/src/graphql/validation/TypeTracker.cs b/src/graphql/validation/TypeTracker.cs index 42e4d54d3..f4cfb4738 100644 --- a/src/graphql/validation/TypeTracker.cs +++ b/src/graphql/validation/TypeTracker.cs @@ -235,7 +235,7 @@ public IType GetInputType() public IType GetParentInputType() { //todo: probably a bad idea - return _inputTypeStack.ElementAtOrDefault(_inputTypeStack.Count - 2); + return _inputTypeStack.ElementAtOrDefault(_inputTypeStack.Count - 1); } public (string Name, IField Field)? GetFieldDef() diff --git a/src/graphql/validation/ValidationError.cs b/src/graphql/validation/ValidationError.cs index fc68aa5ff..a870d7b05 100644 --- a/src/graphql/validation/ValidationError.cs +++ b/src/graphql/validation/ValidationError.cs @@ -35,7 +35,6 @@ public ValidationError(string code, string message, ASTNode node) public override string ToString() { var builder = new StringBuilder(); - builder.AppendLine(Code); builder.Append(Message); if (Nodes.Any()) @@ -51,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 index 464341cb2..f701ce268 100644 --- a/src/graphql/validation/ValidationErrorCodes.cs +++ b/src/graphql/validation/ValidationErrorCodes.cs @@ -2,10 +2,10 @@ { public static class ValidationErrorCodes { - public const string R5211OperationNameUniqueness = "5.2.1.1 Operation Name Uniqueness"; - 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"; @@ -15,13 +15,13 @@ public static class ValidationErrorCodes public const string R533LeafFieldSelections = "5.3.3 Leaf Field Selections"; public const string R541ArgumentNames = "5.4.1 Argument Names"; - - public const string R542ArgumentUniqueness = "5.4.2 Argument Uniqueness"; 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"; @@ -29,5 +29,19 @@ public static class ValidationErrorCodes 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 4cab30740..fcccfc2fc 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -11,7 +11,7 @@ namespace tanka.graphql.validation public static class Validator { public static ValidationResult Validate( - IEnumerable rules, + IEnumerable rules, ISchema schema, GraphQLDocument document, Dictionary variableValues = null) 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 0a51a7a16..2c5b0b3ba 100644 --- a/tests/graphql.tests/validation/ValidatorFacts.cs +++ b/tests/graphql.tests/validation/ValidatorFacts.cs @@ -90,6 +90,13 @@ type Arguments { 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)); @@ -99,7 +106,7 @@ extend type Query { private ValidationResult Validate( GraphQLDocument document, - CreateRule rule, + CombineRule rule, Dictionary variables = null) { if (document == null) throw new ArgumentNullException(nameof(document)); @@ -1174,5 +1181,556 @@ fragment ownerFragment on Dog { 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 From 430a92df6660cb759d5c8f13271da16544da3b4a Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 28 Feb 2019 19:02:27 +0200 Subject: [PATCH 19/20] Remove old validations --- src/graphql.benchmarks/Benchmarks.cs | 15 - src/graphql/validation/BasicVisitor.cs | 35 -- src/graphql/validation/DebugNodeVisitor.cs | 18 - src/graphql/validation/EnterLeaveListener.cs | 71 ---- src/graphql/validation/INodeVisitor.cs | 11 - src/graphql/validation/IRule.cs | 125 ------ src/graphql/validation/IValidationContext.cs | 23 -- src/graphql/validation/IValidationRule.cs | 7 - src/graphql/validation/RuleBase.cs | 202 --------- src/graphql/validation/TypeInfo.cs | 253 ------------ src/graphql/validation/ValidationContext.cs | 151 ------- src/graphql/validation/Validator.cs | 74 +--- .../rules/ArgumentsOfCorrectType.cs | 90 ---- .../rules/DefaultValuesOfCorrectType.cs | 95 ----- .../validation/rules/FieldsOnCorrectType.cs | 138 ------- .../rules/FragmentsOnCompositeTypes.cs | 48 --- .../validation/rules/KnownArgumentNames.cs | 84 ---- .../validation/rules/KnownDirectives.cs | 83 ---- .../validation/rules/KnownFragmentNames.cs | 34 -- .../validation/rules/KnownTypeNames.cs | 40 -- .../rules/LoneAnonymousOperation.cs | 37 -- .../validation/rules/NoFragmentCycles.cs | 92 ----- .../validation/rules/NoUndefinedVariables.cs | 49 --- .../validation/rules/NoUnusedFragments.cs | 53 --- .../validation/rules/NoUnusedVariables.cs | 49 --- .../rules/PossibleFragmentSpreads.cs | 64 --- .../rules/ProvidedNonNullArguments.cs | 79 ---- .../rules/R511ExecutableDefinitions.cs | 32 -- src/graphql/validation/rules/ScalarLeafs.cs | 50 --- .../rules/SubscriptionHasSingleRootField.cs | 37 -- .../validation/rules/UniqueArgumentNames.cs | 41 -- .../rules/UniqueDirectivesPerLocation.cs | 56 --- .../validation/rules/UniqueFragmentNames.cs | 42 -- .../validation/rules/UniqueInputFieldNames.cs | 54 --- .../validation/rules/UniqueOperationNames.cs | 46 --- .../validation/rules/UniqueVariableNames.cs | 45 -- .../rules/VariablesAreInputTypes.cs | 38 -- .../rules/VariablesInAllowedPosition.cs | 75 ---- .../rules2/R511ExecutableDefinitions.cs | 33 -- .../rules2/R5211OperationNameUniqueness.cs | 52 --- .../rules2/R5221LoneAnonymousOperation.cs | 39 -- .../validation/rules2/R5231SingleRootField.cs | 60 --- .../validation/rules2/R531FieldSelections.cs | 32 -- .../rules2/R533LeafFieldSelections.cs | 64 --- .../validation/rules2/R541ArgumentNames.cs | 28 -- .../rules2/R5421RequiredArguments.cs | 84 ---- .../rules2/R542ArgumentUniqueness.cs | 37 -- .../rules2/R5511FragmentNameUniqueness.cs | 34 -- .../R5512FragmentSpreadTypeExistence.cs | 40 -- .../rules2/R5513FragmentsOnCompositeTypes.cs | 48 --- .../validation/rules2/TypeTrackingRuleBase.cs | 386 ------------------ 51 files changed, 3 insertions(+), 3470 deletions(-) delete mode 100644 src/graphql/validation/BasicVisitor.cs delete mode 100644 src/graphql/validation/DebugNodeVisitor.cs delete mode 100644 src/graphql/validation/EnterLeaveListener.cs delete mode 100644 src/graphql/validation/INodeVisitor.cs delete mode 100644 src/graphql/validation/IRule.cs delete mode 100644 src/graphql/validation/IValidationContext.cs delete mode 100644 src/graphql/validation/IValidationRule.cs delete mode 100644 src/graphql/validation/RuleBase.cs delete mode 100644 src/graphql/validation/TypeInfo.cs delete mode 100644 src/graphql/validation/ValidationContext.cs delete mode 100644 src/graphql/validation/rules/ArgumentsOfCorrectType.cs delete mode 100644 src/graphql/validation/rules/DefaultValuesOfCorrectType.cs delete mode 100644 src/graphql/validation/rules/FieldsOnCorrectType.cs delete mode 100644 src/graphql/validation/rules/FragmentsOnCompositeTypes.cs delete mode 100644 src/graphql/validation/rules/KnownArgumentNames.cs delete mode 100644 src/graphql/validation/rules/KnownDirectives.cs delete mode 100644 src/graphql/validation/rules/KnownFragmentNames.cs delete mode 100644 src/graphql/validation/rules/KnownTypeNames.cs delete mode 100644 src/graphql/validation/rules/LoneAnonymousOperation.cs delete mode 100644 src/graphql/validation/rules/NoFragmentCycles.cs delete mode 100644 src/graphql/validation/rules/NoUndefinedVariables.cs delete mode 100644 src/graphql/validation/rules/NoUnusedFragments.cs delete mode 100644 src/graphql/validation/rules/NoUnusedVariables.cs delete mode 100644 src/graphql/validation/rules/PossibleFragmentSpreads.cs delete mode 100644 src/graphql/validation/rules/ProvidedNonNullArguments.cs delete mode 100644 src/graphql/validation/rules/R511ExecutableDefinitions.cs delete mode 100644 src/graphql/validation/rules/ScalarLeafs.cs delete mode 100644 src/graphql/validation/rules/SubscriptionHasSingleRootField.cs delete mode 100644 src/graphql/validation/rules/UniqueArgumentNames.cs delete mode 100644 src/graphql/validation/rules/UniqueDirectivesPerLocation.cs delete mode 100644 src/graphql/validation/rules/UniqueFragmentNames.cs delete mode 100644 src/graphql/validation/rules/UniqueInputFieldNames.cs delete mode 100644 src/graphql/validation/rules/UniqueOperationNames.cs delete mode 100644 src/graphql/validation/rules/UniqueVariableNames.cs delete mode 100644 src/graphql/validation/rules/VariablesAreInputTypes.cs delete mode 100644 src/graphql/validation/rules/VariablesInAllowedPosition.cs delete mode 100644 src/graphql/validation/rules2/R511ExecutableDefinitions.cs delete mode 100644 src/graphql/validation/rules2/R5211OperationNameUniqueness.cs delete mode 100644 src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs delete mode 100644 src/graphql/validation/rules2/R5231SingleRootField.cs delete mode 100644 src/graphql/validation/rules2/R531FieldSelections.cs delete mode 100644 src/graphql/validation/rules2/R533LeafFieldSelections.cs delete mode 100644 src/graphql/validation/rules2/R541ArgumentNames.cs delete mode 100644 src/graphql/validation/rules2/R5421RequiredArguments.cs delete mode 100644 src/graphql/validation/rules2/R542ArgumentUniqueness.cs delete mode 100644 src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs delete mode 100644 src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs delete mode 100644 src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs delete mode 100644 src/graphql/validation/rules2/TypeTrackingRuleBase.cs diff --git a/src/graphql.benchmarks/Benchmarks.cs b/src/graphql.benchmarks/Benchmarks.cs index 9e5a4ec1c..4c1025935 100644 --- a/src/graphql.benchmarks/Benchmarks.cs +++ b/src/graphql.benchmarks/Benchmarks.cs @@ -138,21 +138,6 @@ public async Task Subscribe_without_validation_and_get_value() AssertResult(value.Errors); } - - [Benchmark] - public async Task Validate_query_with_defaults_old() - { - var result = await Validator.ValidateAsync( - _schema, - _query); - - if (!result.IsValid) - { - throw new InvalidOperationException( - $"Validation failed. {result}"); - } - } - [Benchmark] public void Validate_query_with_defaults() { 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/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/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/IRule.cs b/src/graphql/validation/IRule.cs deleted file mode 100644 index 3bd58f855..000000000 --- a/src/graphql/validation/IRule.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public interface IRule - { - IEnumerable AppliesToNodeKinds { get; } - - void BeginVisitAlias(GraphQLName alias, - IValidationContext context); - - void BeginVisitArgument(GraphQLArgument argument, - IValidationContext context); - - void BeginVisitArguments( - IEnumerable arguments, - IValidationContext context); - - void BeginVisitBooleanValue( - GraphQLScalarValue value, IValidationContext context); - - void BeginVisitDirective(GraphQLDirective directive, - IValidationContext context); - - void BeginVisitDirectives( - IEnumerable directives, IValidationContext context); - - void BeginVisitEnumValue(GraphQLScalarValue value, - IValidationContext context); - - void BeginVisitFieldSelection( - GraphQLFieldSelection selection, IValidationContext context); - - void BeginVisitFloatValue( - GraphQLScalarValue value, IValidationContext context); - - void BeginVisitFragmentDefinition( - GraphQLFragmentDefinition node, IValidationContext context); - - void BeginVisitFragmentSpread( - GraphQLFragmentSpread fragmentSpread, IValidationContext context); - - void BeginVisitInlineFragment( - GraphQLInlineFragment inlineFragment, IValidationContext context); - - void BeginVisitIntValue(GraphQLScalarValue value, - IValidationContext context); - - void BeginVisitName(GraphQLName name, - IValidationContext context); - - void BeginVisitNamedType( - GraphQLNamedType typeCondition, IValidationContext context); - - void BeginVisitNode(ASTNode node, - IValidationContext context); - - void BeginVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context); - - void EndVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context); - - void BeginVisitSelectionSet( - GraphQLSelectionSet selectionSet, IValidationContext context); - - void BeginVisitStringValue( - GraphQLScalarValue value, IValidationContext context); - - void BeginVisitVariable(GraphQLVariable variable, - IValidationContext context); - - void BeginVisitVariableDefinition( - GraphQLVariableDefinition node, IValidationContext context); - - void BeginVisitVariableDefinitions( - IEnumerable variableDefinitions, - IValidationContext context); - - void EndVisitArgument(GraphQLArgument argument, - IValidationContext context); - - void EndVisitFieldSelection( - GraphQLFieldSelection selection, IValidationContext context); - - void EndVisitVariable(GraphQLVariable variable, - IValidationContext context); - - void Visit(GraphQLDocument document, - IValidationContext context); - - void BeginVisitObjectField( - GraphQLObjectField node, IValidationContext context); - - void BeginVisitObjectValue( - GraphQLObjectValue node, IValidationContext context); - - void EndVisitObjectValue(GraphQLObjectValue node, - IValidationContext context); - - void EndVisitListValue(GraphQLListValue node, - IValidationContext context); - - void EndVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context); - - void EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context); - - void EndVisitDirective(GraphQLDirective directive, - IValidationContext context); - - void BeginVisitListValue(GraphQLListValue node, IValidationContext context); - - void EndVisitSelectionSet(GraphQLSelectionSet selectionSet, IValidationContext context); - - void EndVisitVariableDefinition(GraphQLVariableDefinition node, IValidationContext context); - - void EndVisitObjectField(GraphQLObjectField node, IValidationContext context); - - void EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context); - - void EndVisit(GraphQLDocument document, IValidationContext context); - } -} \ No newline at end of file diff --git a/src/graphql/validation/IValidationContext.cs b/src/graphql/validation/IValidationContext.cs deleted file mode 100644 index 6bcfa44ab..000000000 --- a/src/graphql/validation/IValidationContext.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; -using tanka.graphql.type; - -namespace tanka.graphql.validation -{ - public interface IValidationContext - { - ISchema Schema { get; } - - GraphQLDocument Document { get; } - - Dictionary VariableValues { 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); - - IEnumerable Fragments { get; } - } -} \ 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/RuleBase.cs b/src/graphql/validation/RuleBase.cs deleted file mode 100644 index 32364ac5b..000000000 --- a/src/graphql/validation/RuleBase.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation -{ - public abstract class RuleBase : IRule - { - public virtual void BeginVisitAlias(GraphQLName alias, IValidationContext context) - { - } - - public virtual void BeginVisitArgument(GraphQLArgument argument, - IValidationContext context) - { - } - - public virtual void BeginVisitArguments(IEnumerable arguments, - IValidationContext context) - { - } - - public virtual void BeginVisitBooleanValue(GraphQLScalarValue value, - IValidationContext context) - { - } - - public virtual void BeginVisitDirective(GraphQLDirective directive, - IValidationContext context) - { - } - - public virtual void EndVisitDirective(GraphQLDirective directive, - IValidationContext context) - { - } - - public virtual void BeginVisitListValue(GraphQLListValue node, - IValidationContext context) - { - } - - public virtual void EndVisitSelectionSet(GraphQLSelectionSet selectionSet, - IValidationContext context) - { - } - - public virtual void EndVisitVariableDefinition(GraphQLVariableDefinition node, - IValidationContext context) - { - } - - public virtual void EndVisitObjectField(GraphQLObjectField node, IValidationContext context) - { - } - - public virtual void EndVisitEnumValue(GraphQLScalarValue value, IValidationContext context) - { - } - - public virtual void EndVisit(GraphQLDocument document, IValidationContext context) - { - } - - public virtual void BeginVisitDirectives(IEnumerable directives, - IValidationContext context) - { - } - - public virtual void BeginVisitEnumValue(GraphQLScalarValue value, - IValidationContext context) - { - } - - public virtual void BeginVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - } - - public virtual void BeginVisitFloatValue(GraphQLScalarValue value, - IValidationContext context) - { - } - - public virtual void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context) - { - } - - public virtual void EndVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context) - { - } - - public virtual void BeginVisitFragmentSpread(GraphQLFragmentSpread fragmentSpread, - IValidationContext context) - { - } - - public virtual void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - } - - public virtual void EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - } - - public virtual void BeginVisitIntValue(GraphQLScalarValue value, - IValidationContext context) - { - } - - public virtual void BeginVisitName(GraphQLName name, IValidationContext context) - { - } - - public virtual void BeginVisitNamedType(GraphQLNamedType typeCondition, - IValidationContext context) - { - } - - public virtual void BeginVisitNode(ASTNode node, IValidationContext context) - { - } - - public virtual void BeginVisitOperationDefinition(GraphQLOperationDefinition definition, - IValidationContext context) - { - } - - public virtual void EndVisitOperationDefinition(GraphQLOperationDefinition definition, - IValidationContext context) - { - } - - public virtual void BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, - IValidationContext context) - { - } - - public virtual void BeginVisitStringValue(GraphQLScalarValue value, - IValidationContext context) - { - } - - public virtual void BeginVisitVariable(GraphQLVariable variable, - IValidationContext context) - { - } - - public virtual void BeginVisitVariableDefinition(GraphQLVariableDefinition node, - IValidationContext context) - { - } - - public virtual void BeginVisitVariableDefinitions( - IEnumerable variableDefinitions, IValidationContext context) - { - } - - public virtual void EndVisitArgument(GraphQLArgument argument, - IValidationContext context) - { - } - - public virtual void EndVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - } - - public virtual void EndVisitVariable(GraphQLVariable variable, - IValidationContext context) - { - } - - public virtual void Visit(GraphQLDocument document, IValidationContext context) - { - } - - public virtual void BeginVisitObjectField(GraphQLObjectField node, - IValidationContext context) - { - } - - public virtual void BeginVisitObjectValue(GraphQLObjectValue node, - IValidationContext context) - { - } - - public virtual void EndVisitObjectValue(GraphQLObjectValue node, - IValidationContext context) - { - } - - public virtual void EndVisitListValue(GraphQLListValue node, IValidationContext context) - { - } - - public abstract IEnumerable AppliesToNodeKinds { get; } - } -} \ 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/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/Validator.cs b/src/graphql/validation/Validator.cs index fcccfc2fc..1d333f948 100644 --- a/src/graphql/validation/Validator.cs +++ b/src/graphql/validation/Validator.cs @@ -1,10 +1,6 @@ using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using GraphQLParser.AST; using tanka.graphql.type; -using tanka.graphql.validation.rules; -using V2 = tanka.graphql.validation.rules2; namespace tanka.graphql.validation { @@ -17,76 +13,12 @@ public static ValidationResult Validate( Dictionary variableValues = null) { var visitor = new RulesWalker( - rules, - schema, - document, + rules, + schema, + document, variableValues); return visitor.Validate(); } - - public static async Task ValidateAsync( - ISchema schema, - GraphQLDocument document, - IDictionary variables = null, - IEnumerable rules = 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 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; - } } } \ 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 7cf34520f..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( - 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)); - } - }); - }); - } - } -} \ 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/src/graphql/validation/rules2/R511ExecutableDefinitions.cs b/src/graphql/validation/rules2/R511ExecutableDefinitions.cs deleted file mode 100644 index bb226b222..000000000 --- a/src/graphql/validation/rules2/R511ExecutableDefinitions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// Formal Specification - /// For each definition definition in the document. - /// definition must be OperationDefinition or FragmentDefinition (it must not be TypeSystemDefinition). - /// - public class R511ExecutableDefinitions : RuleBase - { - public override IEnumerable AppliesToNodeKinds => new[] {ASTNodeKind.Document}; - - public override void Visit(GraphQLDocument document, IValidationContext context) - { - 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); - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs b/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs deleted file mode 100644 index b6542d90c..000000000 --- a/src/graphql/validation/rules2/R5211OperationNameUniqueness.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R5211OperationNameUniqueness : RuleBase - { - public override IEnumerable AppliesToNodeKinds => new[] - { - ASTNodeKind.Document - }; - - public override void Visit(GraphQLDocument document, IValidationContext context) - { - if (document.Definitions.OfType().Count() < 2) - return; - - var operations = document.Definitions.OfType() - .ToList(); - - foreach (var op in operations) - { - var operationName = op.Name?.Value; - - if (string.IsNullOrWhiteSpace(operationName)) - continue; - - var matchingOperations = operations.Where(def => def.Name?.Value == operationName) - .ToList(); - - if (matchingOperations.Count() > 1) - { - context.Error(ValidationErrorCodes.R5211OperationNameUniqueness, - "Each named operation definition must be unique within a " + - "document when referred to by its name.", - matchingOperations); - - break; - } - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs b/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs deleted file mode 100644 index 95db17734..000000000 --- a/src/graphql/validation/rules2/R5221LoneAnonymousOperation.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R5221LoneAnonymousOperation : RuleBase - { - public override IEnumerable AppliesToNodeKinds => new[] - { - ASTNodeKind.Document - }; - - public override void Visit(GraphQLDocument document, IValidationContext context) - { - 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); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5231SingleRootField.cs b/src/graphql/validation/rules2/R5231SingleRootField.cs deleted file mode 100644 index dfafbb660..000000000 --- a/src/graphql/validation/rules2/R5231SingleRootField.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; -using tanka.graphql.execution; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R5231SingleRootField : RuleBase - { - public override IEnumerable AppliesToNodeKinds - => new[] - { - ASTNodeKind.Document - }; - - public override void Visit(GraphQLDocument document, IValidationContext context) - { - 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); - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R531FieldSelections.cs b/src/graphql/validation/rules2/R531FieldSelections.cs deleted file mode 100644 index c88bea281..000000000 --- a/src/graphql/validation/rules2/R531FieldSelections.cs +++ /dev/null @@ -1,32 +0,0 @@ -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// For each selection in the document. - /// Let fieldName be the target field of selection - /// fieldName must be defined on type in scope - /// - public class R531FieldSelections : TypeTrackingRuleBase - { - public override void BeginVisitFieldSelection( - GraphQLFieldSelection selection, - IValidationContext context) - { - base.BeginVisitFieldSelection(selection, context); - - var fieldName = selection.Name.Value; - - if (fieldName == "__typename") - return; - - if (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); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R533LeafFieldSelections.cs b/src/graphql/validation/rules2/R533LeafFieldSelections.cs deleted file mode 100644 index a2e86aa3e..000000000 --- a/src/graphql/validation/rules2/R533LeafFieldSelections.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Linq; -using GraphQLParser.AST; -using tanka.graphql.type; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R533LeafFieldSelections : TypeTrackingRuleBase - { - public override void BeginVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - base.BeginVisitFieldSelection(selection, context); - - var fieldName = selection.Name.Value; - - if (fieldName == "__typename") - return; - - var field = GetFieldDef(); - - if (field != null) - { - var selectionType = field.Value.Field.Type; - 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 ComplexType && 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); - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R541ArgumentNames.cs b/src/graphql/validation/rules2/R541ArgumentNames.cs deleted file mode 100644 index 368e0c2db..000000000 --- a/src/graphql/validation/rules2/R541ArgumentNames.cs +++ /dev/null @@ -1,28 +0,0 @@ -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R541ArgumentNames : TypeTrackingRuleBase - { - public override void BeginVisitArgument( - GraphQLArgument argument, - IValidationContext context) - { - base.BeginVisitArgument(argument, context); - - if (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); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5421RequiredArguments.cs b/src/graphql/validation/rules2/R5421RequiredArguments.cs deleted file mode 100644 index ec45f6fee..000000000 --- a/src/graphql/validation/rules2/R5421RequiredArguments.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; -using tanka.graphql.execution; -using tanka.graphql.type; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R5421RequiredArguments : TypeTrackingRuleBase - { - public override void BeginVisitArguments(IEnumerable arguments, IValidationContext context) - { - var args = arguments.ToList(); - base.BeginVisitArguments(args, context); - - - var argumentDefinitions = GetArgumentDefinitions(); - - //todo: should this produce error? - if (argumentDefinitions == null) - return; - - foreach (var argumentDefinition in argumentDefinitions) - { - var type = argumentDefinition.Value.Type; - var defaultValue = argumentDefinition.Value.DefaultValue; - - if (type is NonNull nonNull && defaultValue == null) - { - var argumentName = argumentDefinition.Key; - var argument = args.SingleOrDefault(a => a.Name.Value == argumentName); - - if (argument == null) - { - context.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", - args); - - return; - } - - // We don't want to throw error here due to non-null so we use the WrappedType directly - var argumentValue = Values.CoerceValue(context.Schema, argument.Value, nonNull.WrappedType); - if (argumentValue == null) - { - context.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", - args); - } - } - } - } - - private IEnumerable> GetArgumentDefinitions() - { - var definitions = GetDirective()?.Arguments - ?? GetFieldDef()?.Field.Arguments; - - return definitions; - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R542ArgumentUniqueness.cs b/src/graphql/validation/rules2/R542ArgumentUniqueness.cs deleted file mode 100644 index 028f48ce4..000000000 --- a/src/graphql/validation/rules2/R542ArgumentUniqueness.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R542ArgumentUniqueness : TypeTrackingRuleBase - { - public override void BeginVisitArguments(IEnumerable arguments, IValidationContext context) - { - var args = arguments.ToList(); - base.BeginVisitArguments(args, context); - - var knownArgs = new List(); - foreach (var argument in args) - { - 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); - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs b/src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs deleted file mode 100644 index 82f1c5cc8..000000000 --- a/src/graphql/validation/rules2/R5511FragmentNameUniqueness.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R5511FragmentNameUniqueness : RuleBase - { - public override IEnumerable AppliesToNodeKinds => - new[] - { - ASTNodeKind.FragmentDefinition - }; - - public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) - { - if (context.Fragments.Any(f => f.Name.Value == node.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.", - node); - - } - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs b/src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs deleted file mode 100644 index eefb07b23..000000000 --- a/src/graphql/validation/rules2/R5512FragmentSpreadTypeExistence.cs +++ /dev/null @@ -1,40 +0,0 @@ -using GraphQLParser.AST; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// 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 class R5512FragmentSpreadTypeExistence : TypeTrackingRuleBase - { - public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) - { - base.BeginVisitFragmentDefinition(node, context); - - var type = 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); - } - - public override void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) - { - base.BeginVisitInlineFragment(inlineFragment, context); - - var type = 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. ", - inlineFragment); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs b/src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs deleted file mode 100644 index 64560dc67..000000000 --- a/src/graphql/validation/rules2/R5513FragmentsOnCompositeTypes.cs +++ /dev/null @@ -1,48 +0,0 @@ -using GraphQLParser.AST; -using tanka.graphql.type; - -namespace tanka.graphql.validation.rules2 -{ - /// - /// For each fragment defined in the document. - /// The target type of fragment must have kind UNION, INTERFACE, or OBJECT. - /// - public class R5513FragmentsOnCompositeTypes : TypeTrackingRuleBase - { - public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, IValidationContext context) - { - base.BeginVisitFragmentDefinition(node, context); - - var type = 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); - } - - public override void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, IValidationContext context) - { - base.BeginVisitInlineFragment(inlineFragment, context); - - var type = 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", - inlineFragment); - } - } -} \ No newline at end of file diff --git a/src/graphql/validation/rules2/TypeTrackingRuleBase.cs b/src/graphql/validation/rules2/TypeTrackingRuleBase.cs deleted file mode 100644 index 079b85652..000000000 --- a/src/graphql/validation/rules2/TypeTrackingRuleBase.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQLParser.AST; -using tanka.graphql.execution; -using tanka.graphql.type; - -namespace tanka.graphql.validation.rules2 -{ - public abstract class TypeTrackingRuleBase : RuleBase - { - 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 override IEnumerable AppliesToNodeKinds => new[] - { - ASTNodeKind.SelectionSet, - ASTNodeKind.Field, - ASTNodeKind.OperationDefinition, - ASTNodeKind.InlineFragment, - ASTNodeKind.FragmentDefinition, - ASTNodeKind.Directive, - ASTNodeKind.VariableDefinition, - ASTNodeKind.Argument, - ASTNodeKind.ListValue, - ASTNodeKind.ObjectField, - ASTNodeKind.EnumValue - }; - - public override void BeginVisitSelectionSet(GraphQLSelectionSet selectionSet, - IValidationContext context) - { - var namedType = GetNamedType(GetCurrentType()); - var complexType = namedType as ComplexType; - _parentTypeStack.Push(complexType); - } - - public override void BeginVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - var parentType = GetParentType(); - (string Name, IField Field)? fieldDef = null; - IType fieldType = null; - - if (parentType != null) - { - fieldDef = getFieldDef(context.Schema, parentType, selection); - - if (fieldDef != null) fieldType = fieldDef.Value.Field.Type; - } - - _fieldDefStack.Push(fieldDef); - _typeStack.Push(TypeIs.IsOutputType(fieldType) ? fieldType : null); - } - - public override void BeginVisitDirective(GraphQLDirective directive, - IValidationContext context) - { - _directive = context.Schema.GetDirective(directive.Name.Value); - } - - public override void BeginVisitOperationDefinition( - GraphQLOperationDefinition definition, IValidationContext context) - { - ObjectType type = null; - switch (definition.Operation) - { - case OperationType.Query: - type = context.Schema.Query; - break; - case OperationType.Mutation: - type = context.Schema.Mutation; - break; - case OperationType.Subscription: - type = context.Schema.Subscription; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - _typeStack.Push(type); - } - - public override void BeginVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - var typeConditionAst = inlineFragment.TypeCondition; - - IType outputType; - if (typeConditionAst != null) - { - outputType = Ast.TypeFromAst(context.Schema, typeConditionAst); - } - else - { - outputType = GetNamedType(GetCurrentType()); - } - - _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); - } - - public override void BeginVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context) - { - var typeConditionAst = node.TypeCondition; - - IType outputType; - if (typeConditionAst != null) - { - outputType = Ast.TypeFromAst(context.Schema, typeConditionAst); - } - else - { - outputType = GetNamedType(GetCurrentType()); - } - - _typeStack.Push(TypeIs.IsOutputType(outputType) ? outputType : null); - } - - public override void BeginVisitVariableDefinition(GraphQLVariableDefinition node, - IValidationContext context) - { - var inputType = Ast.TypeFromAst(context.Schema, node.Type); - _inputTypeStack.Push(TypeIs.IsInputType(inputType) ? inputType : null); - } - - public override void BeginVisitArgument(GraphQLArgument argument, - IValidationContext context) - { - 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); - } - - public override void BeginVisitListValue(GraphQLListValue node, - IValidationContext context) - { - 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); - } - - public override void BeginVisitObjectField(GraphQLObjectField node, - IValidationContext context) - { - var objectType = GetNamedType(GetInputType()); - IType inputFieldType = null; - InputObjectField inputField = null; - - if (objectType is InputObjectType inputObjectType) - { - inputField = context.Schema.GetInputField( - inputObjectType.Name, - node.Name.Value); - - if (inputField != null) - inputFieldType = inputField.Type; - } - - _defaultValueStack.Push(inputField?.DefaultValue); - _inputTypeStack.Push(TypeIs.IsInputType(inputFieldType) ? inputFieldType : null); - } - - public override void BeginVisitEnumValue(GraphQLScalarValue value, - IValidationContext context) - { - var maybeEnumType = GetNamedType(GetInputType()); - object enumValue = null; - if (maybeEnumType is EnumType enumType) enumValue = enumType.ParseLiteral(value); - - _enumValue = enumValue; - } - - public override void EndVisitSelectionSet(GraphQLSelectionSet selectionSet, - IValidationContext context) - { - _parentTypeStack.Pop(); - } - - public override void EndVisitFieldSelection(GraphQLFieldSelection selection, - IValidationContext context) - { - _fieldDefStack.Pop(); - _typeStack.Pop(); - } - - public override void EndVisitDirective(GraphQLDirective directive, - IValidationContext context) - { - _directive = null; - } - - public override void EndVisitOperationDefinition(GraphQLOperationDefinition definition, - IValidationContext context) - { - _typeStack.Pop(); - } - - public override void EndVisitInlineFragment(GraphQLInlineFragment inlineFragment, - IValidationContext context) - { - _typeStack.Pop(); - } - - public override void EndVisitFragmentDefinition(GraphQLFragmentDefinition node, - IValidationContext context) - { - _typeStack.Pop(); - } - - public override void EndVisitVariableDefinition(GraphQLVariableDefinition node, - IValidationContext context) - { - _inputTypeStack.Pop(); - } - - public override void EndVisitArgument(GraphQLArgument argument, - IValidationContext context) - { - _argument = null; - _defaultValueStack.Pop(); - _inputTypeStack.Pop(); - } - - public override void EndVisitListValue(GraphQLListValue node, - IValidationContext context) - { - _defaultValueStack.Pop(); - _inputTypeStack.Pop(); - } - - public override void EndVisitObjectField(GraphQLObjectField node, - IValidationContext context) - { - _defaultValueStack.Pop(); - _inputTypeStack.Pop(); - } - - public override void EndVisitEnumValue(GraphQLScalarValue value, - IValidationContext context) - { - _enumValue = null; - } - - protected IType GetCurrentType() - { - if (_typeStack.Count == 0) - return null; - - return _typeStack.Peek(); - } - - protected ComplexType GetParentType() - { - if (_typeStack.Count == 0) - return null; - - return _parentTypeStack.Peek(); - } - - //todo: originally returns an input type - protected IType GetInputType() - { - if (_typeStack.Count == 0) - return null; - - return _inputTypeStack.Peek(); - } - - protected IType GetParentInputType() - { - //todo: probably a bad idea - return _inputTypeStack.ElementAtOrDefault(_inputTypeStack.Count - 2); - } - - protected (string Name, IField Field)? GetFieldDef() - { - if (_fieldDefStack.Count == 0) - return null; - - return _fieldDefStack.Peek(); - } - - protected object GetDefaultValue() - { - if (_defaultValueStack.Count == 0) - return null; - - return _defaultValueStack.Peek(); - } - - protected DirectiveType GetDirective() - { - return _directive; - } - - protected Argument GetArgument() - { - return _argument; - } - - protected object GetEnumValue() - { - return _enumValue; - } - - protected IType GetNamedType(IType type) - { - return type?.Unwrap(); - } - - protected IType GetNullableType(IType type) - { - if (type is NonNull nonNull) - return nonNull.WrappedType; - - return null; - } - - private (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 From 607e3c20060088f5c89f9e149dbeb4c11a9630ca Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 28 Feb 2019 19:27:55 +0200 Subject: [PATCH 20/20] docs: update --- README.md | 2 +- docs/1-execution/01-validation.md | 16 ++++++++++++---- docs/_template.html | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) 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