From e43d770d1ae6cdddb09a6f8b06f1704155980524 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Mon, 19 Aug 2024 17:08:48 -0400 Subject: [PATCH 1/2] Add GetDirectiveLocation extension method --- .../GraphQLParser.approved.txt | 10 ++ .../GetDirectiveLocationTests.cs | 86 +++++++++++++ .../GraphQLInputValueDefinition.cs | 115 ++++++++++++++++++ .../Extensions/ASTNodeExtensions.cs | 32 +++++ src/GraphQLParser/NodeHelper.cs | 33 +++-- src/GraphQLParser/Parser.cs | 6 +- src/GraphQLParser/ParserContext.Parse.cs | 8 +- 7 files changed, 277 insertions(+), 13 deletions(-) create mode 100644 src/GraphQLParser.Tests/GetDirectiveLocationTests.cs diff --git a/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt b/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt index bbd773b7..8b0590e3 100644 --- a/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt +++ b/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt @@ -130,6 +130,10 @@ namespace GraphQLParser.AST public GraphQLParser.AST.GraphQLName Name { get; set; } public GraphQLParser.AST.GraphQLValue Value { get; set; } } + public class GraphQLArgumentDefinition : GraphQLParser.AST.GraphQLInputValueDefinition + { + public GraphQLArgumentDefinition(GraphQLParser.AST.GraphQLName name, GraphQLParser.AST.GraphQLType type) { } + } public class GraphQLArguments : GraphQLParser.AST.ASTListNode { public GraphQLArguments(System.Collections.Generic.List items) { } @@ -304,6 +308,10 @@ namespace GraphQLParser.AST public GraphQLParser.AST.GraphQLSelectionSet SelectionSet { get; set; } public GraphQLParser.AST.GraphQLTypeCondition? TypeCondition { get; set; } } + public class GraphQLInputFieldDefinition : GraphQLParser.AST.GraphQLInputValueDefinition + { + public GraphQLInputFieldDefinition(GraphQLParser.AST.GraphQLName name, GraphQLParser.AST.GraphQLType type) { } + } public class GraphQLInputFieldsDefinition : GraphQLParser.AST.ASTListNode { public GraphQLInputFieldsDefinition(System.Collections.Generic.List items) { } @@ -325,6 +333,8 @@ namespace GraphQLParser.AST } public class GraphQLInputValueDefinition : GraphQLParser.AST.GraphQLTypeDefinition, GraphQLParser.AST.IHasDefaultValueNode, GraphQLParser.AST.IHasDirectivesNode { + [System.Obsolete("Please use the GraphQLArgumentDefinition or GraphQLInputFieldDefinition construct" + + "or.")] public GraphQLInputValueDefinition(GraphQLParser.AST.GraphQLName name, GraphQLParser.AST.GraphQLType type) { } public GraphQLParser.AST.GraphQLValue? DefaultValue { get; set; } public GraphQLParser.AST.GraphQLDirectives? Directives { get; set; } diff --git a/src/GraphQLParser.Tests/GetDirectiveLocationTests.cs b/src/GraphQLParser.Tests/GetDirectiveLocationTests.cs new file mode 100644 index 00000000..6b5eb78e --- /dev/null +++ b/src/GraphQLParser.Tests/GetDirectiveLocationTests.cs @@ -0,0 +1,86 @@ +using GraphQLParser.Visitors; + +namespace GraphQLParser.Tests; + +public class GetDirectiveLocationTests +{ + [Fact] + public async Task ItWorks() + { + var sdl = $$""" + schema @test(value: "{{DirectiveLocation.Schema}}") { + query: Query + } + + type Query @test(value: "{{DirectiveLocation.Object}}") { + field(arg:String @test(value:"{{DirectiveLocation.ArgumentDefinition}}")): String @test(value: "{{DirectiveLocation.FieldDefinition}}") + } + + scalar CustomScalar @test(value: "{{DirectiveLocation.Scalar}}") + + interface CustomInterface @test(value: "{{DirectiveLocation.Interface}}") { + field: String @test(value: "{{DirectiveLocation.FieldDefinition}}") + } + + union CustomUnion @test(value: "{{DirectiveLocation.Union}}") = A | B + + enum CustomEnum @test(value: "{{DirectiveLocation.Enum}}") { + A @test(value: "{{DirectiveLocation.EnumValue}}") + } + + input CustomInput @test(value: "{{DirectiveLocation.InputObject}}") { + field: String @test(value: "{{DirectiveLocation.InputFieldDefinition}}") + } + + query Query @test(value: "{{DirectiveLocation.Query}}") { + field @test(value: "{{DirectiveLocation.Field}}") + ...fragment1 @test(value: "{{DirectiveLocation.FragmentSpread}}") + ... on CustomType @test(value: "{{DirectiveLocation.InlineFragment}}") { + field @test(value: "{{DirectiveLocation.Field}}") + } + } + + fragment fragment1 on CustomType @test(value: "{{DirectiveLocation.FragmentDefinition}}") { + field @test(value: "{{DirectiveLocation.Field}}") + } + + mutation($arg: String @test(value: "{{DirectiveLocation.VariableDefinition}}")) @test(value: "{{DirectiveLocation.Mutation}}") { + field @test(value: "{{DirectiveLocation.Field}}") + } + + subscription @test(value: "{{DirectiveLocation.Subscription}}") { + field @test(value: "{{DirectiveLocation.Field}}") + } + """; + var ast = Parser.Parse(sdl); + var context = new MyVisitor.MyContext(); + await new MyVisitor().VisitAsync(ast, context); + context.Count.ShouldBe(24); + } + + private sealed class MyVisitor : ASTVisitor + { + public override ValueTask VisitAsync(ASTNode node, MyContext context) + { + if (node is IHasDirectivesNode directivesNode) + { + var d = directivesNode.Directives?.FirstOrDefault(x => x.Name == "test"); + if (d != null) + { + var arg = d?.Arguments?.FirstOrDefault(x => x.Name == "value")?.Value; + var argValue = (arg as GraphQLStringValue)?.Value; + var location = node.GetDirectiveLocation(); + location.ToString().ShouldBe(argValue?.ToString()); + context.Count++; + } + } + return base.VisitAsync(node, context); + } + + internal sealed class MyContext : IASTVisitorContext + { + public int Count { get; set; } + public CancellationToken CancellationToken => default; + } + } +} diff --git a/src/GraphQLParser/AST/Definitions/GraphQLInputValueDefinition.cs b/src/GraphQLParser/AST/Definitions/GraphQLInputValueDefinition.cs index 0cd52d2e..2db5e360 100644 --- a/src/GraphQLParser/AST/Definitions/GraphQLInputValueDefinition.cs +++ b/src/GraphQLParser/AST/Definitions/GraphQLInputValueDefinition.cs @@ -16,6 +16,7 @@ internal GraphQLInputValueDefinition() /// /// Creates a new instance of . /// + [Obsolete($"Please use the {nameof(GraphQLArgumentDefinition)} or {nameof(GraphQLInputFieldDefinition)} constructor.")] public GraphQLInputValueDefinition(GraphQLName name, GraphQLType type) : base(name) { @@ -79,3 +80,117 @@ public override List? Comments set => _comments = value; } } + +/// +/// AST node for , where it is used as an argument definition. +/// +[DebuggerDisplay("GraphQLArgumentDefinition: {Name}: {Type}")] +public class GraphQLArgumentDefinition : GraphQLInputValueDefinition +{ + internal GraphQLArgumentDefinition() : base() { } + + /// + /// Creates a new instance of . + /// + public GraphQLArgumentDefinition(GraphQLName name, GraphQLType type) +#pragma warning disable CS0618 // Type or member is obsolete + : base(name, type) { } +#pragma warning restore CS0618 // Type or member is obsolete +} + +internal sealed class GraphQLArgumentDefinitionWithLocation : GraphQLArgumentDefinition +{ + private GraphQLLocation _location; + + public override GraphQLLocation Location + { + get => _location; + set => _location = value; + } +} + +internal sealed class GraphQLArgumentDefinitionWithComment : GraphQLArgumentDefinition +{ + private List? _comments; + + public override List? Comments + { + get => _comments; + set => _comments = value; + } +} + +internal sealed class GraphQLArgumentDefinitionFull : GraphQLArgumentDefinition +{ + private GraphQLLocation _location; + private List? _comments; + + public override GraphQLLocation Location + { + get => _location; + set => _location = value; + } + + public override List? Comments + { + get => _comments; + set => _comments = value; + } +} + +/// +/// AST node for , where it is used as an input field definition. +/// +[DebuggerDisplay("GraphQLInputFieldDefinition: {Name}: {Type}")] +public class GraphQLInputFieldDefinition : GraphQLInputValueDefinition +{ + internal GraphQLInputFieldDefinition() : base() { } + + /// + /// Creates a new instance of . + /// + public GraphQLInputFieldDefinition(GraphQLName name, GraphQLType type) +#pragma warning disable CS0618 // Type or member is obsolete + : base(name, type) { } +#pragma warning restore CS0618 // Type or member is obsolete +} + +internal sealed class GraphQLInputFieldDefinitionWithLocation : GraphQLInputFieldDefinition +{ + private GraphQLLocation _location; + + public override GraphQLLocation Location + { + get => _location; + set => _location = value; + } +} + +internal sealed class GraphQLInputFieldDefinitionWithComment : GraphQLInputFieldDefinition +{ + private List? _comments; + + public override List? Comments + { + get => _comments; + set => _comments = value; + } +} + +internal sealed class GraphQLInputFieldDefinitionFull : GraphQLInputFieldDefinition +{ + private GraphQLLocation _location; + private List? _comments; + + public override GraphQLLocation Location + { + get => _location; + set => _location = value; + } + + public override List? Comments + { + get => _comments; + set => _comments = value; + } +} diff --git a/src/GraphQLParser/Extensions/ASTNodeExtensions.cs b/src/GraphQLParser/Extensions/ASTNodeExtensions.cs index f2211bb9..8b528d47 100644 --- a/src/GraphQLParser/Extensions/ASTNodeExtensions.cs +++ b/src/GraphQLParser/Extensions/ASTNodeExtensions.cs @@ -132,4 +132,36 @@ public static int FragmentsCount(this GraphQLDocument document) return null; } + + /// + /// Returns the directive location for the specified AST node. + /// + /// + public static DirectiveLocation GetDirectiveLocation(this ASTNode node) => node switch + { + // type definitions + GraphQLSchemaDefinition => DirectiveLocation.Schema, + GraphQLScalarTypeDefinition => DirectiveLocation.Scalar, + GraphQLObjectTypeDefinition => DirectiveLocation.Object, + GraphQLFieldDefinition => DirectiveLocation.FieldDefinition, + GraphQLArgumentDefinition => DirectiveLocation.ArgumentDefinition, + GraphQLInterfaceTypeDefinition => DirectiveLocation.Interface, + GraphQLUnionTypeDefinition => DirectiveLocation.Union, + GraphQLEnumTypeDefinition => DirectiveLocation.Enum, + GraphQLEnumValueDefinition => DirectiveLocation.EnumValue, + GraphQLInputObjectTypeDefinition => DirectiveLocation.InputObject, + GraphQLInputFieldDefinition => DirectiveLocation.InputFieldDefinition, + + // executable definitions + GraphQLOperationDefinition opDef when opDef.Operation == OperationType.Query => DirectiveLocation.Query, + GraphQLOperationDefinition opDef when opDef.Operation == OperationType.Mutation => DirectiveLocation.Mutation, + GraphQLOperationDefinition opDef when opDef.Operation == OperationType.Subscription => DirectiveLocation.Subscription, + GraphQLField => DirectiveLocation.Field, + GraphQLFragmentDefinition => DirectiveLocation.FragmentDefinition, + GraphQLFragmentSpread => DirectiveLocation.FragmentSpread, + GraphQLInlineFragment => DirectiveLocation.InlineFragment, + GraphQLVariableDefinition => DirectiveLocation.VariableDefinition, + + _ => throw new ArgumentOutOfRangeException(nameof(node), "The supplied node cannot") + }; } diff --git a/src/GraphQLParser/NodeHelper.cs b/src/GraphQLParser/NodeHelper.cs index 5cb27f5c..30c72ca9 100644 --- a/src/GraphQLParser/NodeHelper.cs +++ b/src/GraphQLParser/NodeHelper.cs @@ -483,15 +483,32 @@ public static GraphQLObjectValue CreateGraphQLObjectValue(IgnoreOptions options) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static GraphQLInputValueDefinition CreateGraphQLInputValueDefinition(IgnoreOptions options) + public static GraphQLInputValueDefinition CreateGraphQLInputValueDefinition(IgnoreOptions options, bool? argument) { - return options switch - { - IgnoreOptions.All => new GraphQLInputValueDefinition(), - IgnoreOptions.Comments => new GraphQLInputValueDefinitionWithLocation(), - IgnoreOptions.Locations => new GraphQLInputValueDefinitionWithComment(), - _ => new GraphQLInputValueDefinitionFull(), - }; + if (argument == true) + return options switch + { + IgnoreOptions.All => new GraphQLArgumentDefinition(), + IgnoreOptions.Comments => new GraphQLArgumentDefinitionWithLocation(), + IgnoreOptions.Locations => new GraphQLArgumentDefinitionWithComment(), + _ => new GraphQLArgumentDefinitionFull(), + }; + else if (argument == false) + return options switch + { + IgnoreOptions.All => new GraphQLInputFieldDefinition(), + IgnoreOptions.Comments => new GraphQLInputFieldDefinitionWithLocation(), + IgnoreOptions.Locations => new GraphQLInputFieldDefinitionWithComment(), + _ => new GraphQLInputFieldDefinitionFull(), + }; + else + return options switch + { + IgnoreOptions.All => new GraphQLInputValueDefinition(), + IgnoreOptions.Comments => new GraphQLInputValueDefinitionWithLocation(), + IgnoreOptions.Locations => new GraphQLInputValueDefinitionWithComment(), + _ => new GraphQLInputValueDefinitionFull(), + }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/GraphQLParser/Parser.cs b/src/GraphQLParser/Parser.cs index e590ee25..f1e7b18e 100644 --- a/src/GraphQLParser/Parser.cs +++ b/src/GraphQLParser/Parser.cs @@ -80,7 +80,11 @@ public static T Parse(ROM source, ParserOptions options = default) else if (typeof(T) == typeof(GraphQLInputObjectTypeDefinition)) result = (T)(object)context.ParseInputObjectTypeDefinition(); else if (typeof(T) == typeof(GraphQLInputValueDefinition)) - result = (T)(object)context.ParseInputValueDefinition(); + result = (T)(object)context.ParseInputValueDefinition(null); + else if (typeof(T) == typeof(GraphQLInputFieldDefinition)) + result = (T)(object)context.ParseInputValueDefinition(false); + else if (typeof(T) == typeof(GraphQLArgumentDefinition)) + result = (T)(object)context.ParseInputValueDefinition(true); else if (typeof(T) == typeof(GraphQLInterfaceTypeDefinition)) result = (T)(object)context.ParseInterfaceTypeDefinition(); else if (typeof(T) == typeof(GraphQLObjectTypeDefinition)) diff --git a/src/GraphQLParser/ParserContext.Parse.cs b/src/GraphQLParser/ParserContext.Parse.cs index eba642e8..3282ce84 100644 --- a/src/GraphQLParser/ParserContext.Parse.cs +++ b/src/GraphQLParser/ParserContext.Parse.cs @@ -97,7 +97,7 @@ public GraphQLArgumentsDefinition ParseArgumentsDefinition() var argsDef = NodeHelper.CreateGraphQLArgumentsDefinition(_ignoreOptions); argsDef.Comments = GetComments(); - argsDef.Items = OneOrMore(TokenKind.PAREN_L, (ref ParserContext context) => context.ParseInputValueDefinition(), TokenKind.PAREN_R); + argsDef.Items = OneOrMore(TokenKind.PAREN_L, (ref ParserContext context) => context.ParseInputValueDefinition(true), TokenKind.PAREN_R); argsDef.Location = GetLocation(start); DecreaseDepth(); @@ -114,7 +114,7 @@ public GraphQLInputFieldsDefinition ParseInputFieldsDefinition() var inputFieldsDef = NodeHelper.CreateGraphQLInputFieldsDefinition(_ignoreOptions); inputFieldsDef.Comments = GetComments(); - inputFieldsDef.Items = OneOrMore(TokenKind.BRACE_L, (ref ParserContext context) => context.ParseInputValueDefinition(), TokenKind.BRACE_R); + inputFieldsDef.Items = OneOrMore(TokenKind.BRACE_L, (ref ParserContext context) => context.ParseInputValueDefinition(false), TokenKind.BRACE_R); inputFieldsDef.Location = GetLocation(start); DecreaseDepth(); @@ -735,13 +735,13 @@ private GraphQLInputObjectTypeExtension ParseInputObjectTypeExtension(int start, } // http://spec.graphql.org/October2021/#InputValueDefinition - public GraphQLInputValueDefinition ParseInputValueDefinition() + public GraphQLInputValueDefinition ParseInputValueDefinition(bool? argument) { IncreaseDepth(); int start = _currentToken.Start; - var def = NodeHelper.CreateGraphQLInputValueDefinition(_ignoreOptions); + var def = NodeHelper.CreateGraphQLInputValueDefinition(_ignoreOptions, argument); def.Description = Peek(TokenKind.STRING) ? ParseDescription() : null; def.Comments = GetComments(); From 7ff8b987fca6022bfc9d066579359dcf380bf755 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Mon, 19 Aug 2024 17:17:32 -0400 Subject: [PATCH 2/2] Update --- src/GraphQLParser.ApiTests/GraphQLParser.approved.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt b/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt index 8b0590e3..371d2126 100644 --- a/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt +++ b/src/GraphQLParser.ApiTests/GraphQLParser.approved.txt @@ -643,6 +643,7 @@ namespace GraphQLParser where TNode : class, GraphQLParser.AST.INamedNode { } public static GraphQLParser.AST.GraphQLFragmentDefinition? FindFragmentDefinition(this GraphQLParser.AST.GraphQLDocument document, GraphQLParser.ROM name) { } public static int FragmentsCount(this GraphQLParser.AST.GraphQLDocument document) { } + public static GraphQLParser.AST.DirectiveLocation GetDirectiveLocation(this GraphQLParser.AST.ASTNode node) { } public static int MaxNestedDepth(this GraphQLParser.AST.ASTNode node) { } public static GraphQLParser.AST.GraphQLOperationDefinition? OperationWithName(this GraphQLParser.AST.GraphQLDocument document, GraphQLParser.ROM operationName) { } public static int OperationsCount(this GraphQLParser.AST.GraphQLDocument document) { }