diff --git a/src/GraphQLParser.ApiTests/ApiApprovalTests.Public_Api_Should_Not_Change_Inadvertently.approved.txt b/src/GraphQLParser.ApiTests/ApiApprovalTests.Public_Api_Should_Not_Change_Inadvertently.approved.txt index 5da446c8..80407b73 100644 --- a/src/GraphQLParser.ApiTests/ApiApprovalTests.Public_Api_Should_Not_Change_Inadvertently.approved.txt +++ b/src/GraphQLParser.ApiTests/ApiApprovalTests.Public_Api_Should_Not_Change_Inadvertently.approved.txt @@ -47,6 +47,7 @@ namespace GraphQLParser.AST TypeExtensionDefinition = 35, DirectiveDefinition = 36, Comment = 37, + Description = 38, } public class GraphQLArgument : GraphQLParser.AST.ASTNode, GraphQLParser.AST.INamedNode { @@ -61,6 +62,12 @@ namespace GraphQLParser.AST public override GraphQLParser.AST.ASTNodeKind Kind { get; } public string Text { get; set; } } + public class GraphQLDescription : GraphQLParser.AST.ASTNode + { + public GraphQLDescription(string value) { } + public override GraphQLParser.AST.ASTNodeKind Kind { get; } + public string Value { get; set; } + } public class GraphQLDirective : GraphQLParser.AST.ASTNode, GraphQLParser.AST.INamedNode { public GraphQLDirective() { } @@ -207,7 +214,7 @@ namespace GraphQLParser.AST public GraphQLParser.AST.GraphQLName? Name { get; set; } public GraphQLParser.AST.GraphQLValue? Value { get; set; } } - public class GraphQLObjectTypeDefinition : GraphQLParser.AST.GraphQLTypeDefinition, GraphQLParser.AST.IHasDirectivesNode + public class GraphQLObjectTypeDefinition : GraphQLParser.AST.GraphQLTypeDefinition, GraphQLParser.AST.IHasDescriptionNode, GraphQLParser.AST.IHasDirectivesNode { public GraphQLObjectTypeDefinition() { } public System.Collections.Generic.List? Directives { get; set; } @@ -251,9 +258,10 @@ namespace GraphQLParser.AST public string? Value { get; set; } public override string? ToString() { } } - public class GraphQLSchemaDefinition : GraphQLParser.AST.ASTNode, GraphQLParser.AST.IHasDirectivesNode + public class GraphQLSchemaDefinition : GraphQLParser.AST.ASTNode, GraphQLParser.AST.IHasDescriptionNode, GraphQLParser.AST.IHasDirectivesNode { public GraphQLSchemaDefinition() { } + public GraphQLParser.AST.GraphQLDescription? Description { get; set; } public System.Collections.Generic.List? Directives { get; set; } public override GraphQLParser.AST.ASTNodeKind Kind { get; } public System.Collections.Generic.List? OperationTypes { get; set; } @@ -268,9 +276,10 @@ namespace GraphQLParser.AST { protected GraphQLType() { } } - public abstract class GraphQLTypeDefinition : GraphQLParser.AST.ASTNode, GraphQLParser.AST.INamedNode + public abstract class GraphQLTypeDefinition : GraphQLParser.AST.ASTNode, GraphQLParser.AST.IHasDescriptionNode, GraphQLParser.AST.INamedNode { protected GraphQLTypeDefinition() { } + public GraphQLParser.AST.GraphQLDescription? Description { get; set; } public GraphQLParser.AST.GraphQLName? Name { get; set; } } public class GraphQLTypeExtensionDefinition : GraphQLParser.AST.GraphQLTypeDefinition @@ -304,6 +313,10 @@ namespace GraphQLParser.AST public GraphQLParser.AST.GraphQLType? Type { get; set; } public GraphQLParser.AST.GraphQLVariable? Variable { get; set; } } + public interface IHasDescriptionNode + { + GraphQLParser.AST.GraphQLDescription? Description { get; set; } + } public interface IHasDirectivesNode { System.Collections.Generic.List? Directives { get; set; } diff --git a/src/GraphQLParser.Tests/ParserTests.cs b/src/GraphQLParser.Tests/ParserTests.cs index db3b3781..dcacb59c 100644 --- a/src/GraphQLParser.Tests/ParserTests.cs +++ b/src/GraphQLParser.Tests/ParserTests.cs @@ -530,5 +530,50 @@ public void Should_Parse_Unions(string text) { new Parser(new Lexer()).Parse(new Source(text)).ShouldNotBeNull(); } + + [Theory] + [InlineData("\"schema description\" schema { query : Query }", "schema description")] + [InlineData("\"scalar description\" scalar DateTime", "scalar description")] + [InlineData("\"type description\" type Query { name : String }", "type description")] + [InlineData("\"interface description\" interface Person { name : String }", "interface description")] + [InlineData("\"union description\" union Person = He | She", "union description")] + [InlineData("\"enum description\" enum Currency { EUR USD }", "enum description")] + [InlineData("\"input description\" input PersonInput { name : String }", "input description")] + [InlineData("\"directive description\" directive @example on FIELD", "directive description")] + public void Should_Parse_TypeDefinition_Description(string text, string expectedDescription) + { + var document = new Parser(new Lexer()).Parse(new Source(text)); + ((IHasDescriptionNode)document.Definitions[0]).Description.Value.ShouldBe(expectedDescription); + } + + [Fact] + public void Should_Parse_FieldDefinition_Description() + { + var text = "type Query { \"field description\" name : String }"; + var document = new Parser(new Lexer()).Parse(new Source(text)); + var objectTypeDefinition = document.Definitions.OfType().First(); + objectTypeDefinition.Fields[0].Description.Value.ShouldBe("field description"); + } + + [Fact] + public void Should_Parse_InputValueDefinition_Description() + { + var text = "type Query { name(\"input value description\" inputValue: Int) : String }"; + var document = new Parser(new Lexer()).Parse(new Source(text)); + var objectTypeDefinition = document.Definitions.OfType().First(); + objectTypeDefinition.Fields[0] + .Arguments[0].Description.Value.ShouldBe("input value description"); + } + + [Fact] + public void Should_Parse_EnumValueDefinition_Description() + { + var text = "enum Currency { \"EUR value description\" EUR \"USD value description\" USD }"; + var document = new Parser(new Lexer()).Parse(new Source(text)); + var enumTypeDefinition = document.Definitions.OfType().First(); + enumTypeDefinition.Values + .Select(v=>v.Description.Value) + .ShouldBe(new []{"EUR value description", "USD value description"}); + } } } diff --git a/src/GraphQLParser/AST/Enums.cs b/src/GraphQLParser/AST/Enums.cs index 55e56f9d..376a4898 100644 --- a/src/GraphQLParser/AST/Enums.cs +++ b/src/GraphQLParser/AST/Enums.cs @@ -40,6 +40,7 @@ public enum ASTNodeKind TypeExtensionDefinition, DirectiveDefinition, Comment, + Description } public enum OperationType @@ -48,4 +49,4 @@ public enum OperationType Mutation, Subscription } -} \ No newline at end of file +} diff --git a/src/GraphQLParser/AST/GraphQLDescription.cs b/src/GraphQLParser/AST/GraphQLDescription.cs new file mode 100644 index 00000000..1edb9105 --- /dev/null +++ b/src/GraphQLParser/AST/GraphQLDescription.cs @@ -0,0 +1,14 @@ +namespace GraphQLParser.AST +{ + public class GraphQLDescription : ASTNode + { + public GraphQLDescription(string value) + { + Value = value; + } + + public override ASTNodeKind Kind => ASTNodeKind.Description; + + public string Value { get; set; } + } +} diff --git a/src/GraphQLParser/AST/GraphQLObjectTypeDefinition.cs b/src/GraphQLParser/AST/GraphQLObjectTypeDefinition.cs index 243c798a..8d9ca7dc 100644 --- a/src/GraphQLParser/AST/GraphQLObjectTypeDefinition.cs +++ b/src/GraphQLParser/AST/GraphQLObjectTypeDefinition.cs @@ -2,7 +2,7 @@ namespace GraphQLParser.AST { - public class GraphQLObjectTypeDefinition : GraphQLTypeDefinition, IHasDirectivesNode + public class GraphQLObjectTypeDefinition : GraphQLTypeDefinition, IHasDirectivesNode, IHasDescriptionNode { public List? Directives { get; set; } @@ -12,4 +12,4 @@ public class GraphQLObjectTypeDefinition : GraphQLTypeDefinition, IHasDirectives public override ASTNodeKind Kind => ASTNodeKind.ObjectTypeDefinition; } -} \ No newline at end of file +} diff --git a/src/GraphQLParser/AST/GraphQLSchemaDefinition.cs b/src/GraphQLParser/AST/GraphQLSchemaDefinition.cs index fbdd42e2..88e38b32 100644 --- a/src/GraphQLParser/AST/GraphQLSchemaDefinition.cs +++ b/src/GraphQLParser/AST/GraphQLSchemaDefinition.cs @@ -2,12 +2,14 @@ namespace GraphQLParser.AST { - public class GraphQLSchemaDefinition : ASTNode, IHasDirectivesNode + public class GraphQLSchemaDefinition : ASTNode, IHasDirectivesNode, IHasDescriptionNode { public List? Directives { get; set; } public override ASTNodeKind Kind => ASTNodeKind.SchemaDefinition; public List? OperationTypes { get; set; } + + public GraphQLDescription? Description { get; set; } } -} \ No newline at end of file +} diff --git a/src/GraphQLParser/AST/GraphQLTypeDefinition.cs b/src/GraphQLParser/AST/GraphQLTypeDefinition.cs index 8410a642..4997b196 100644 --- a/src/GraphQLParser/AST/GraphQLTypeDefinition.cs +++ b/src/GraphQLParser/AST/GraphQLTypeDefinition.cs @@ -1,7 +1,9 @@ namespace GraphQLParser.AST { - public abstract class GraphQLTypeDefinition : ASTNode, INamedNode + public abstract class GraphQLTypeDefinition : ASTNode, INamedNode, IHasDescriptionNode { public GraphQLName? Name { get; set; } + + public GraphQLDescription? Description { get; set; } } -} \ No newline at end of file +} diff --git a/src/GraphQLParser/AST/IHasDescriptionNode.cs b/src/GraphQLParser/AST/IHasDescriptionNode.cs new file mode 100644 index 00000000..0114ef67 --- /dev/null +++ b/src/GraphQLParser/AST/IHasDescriptionNode.cs @@ -0,0 +1,7 @@ +namespace GraphQLParser.AST +{ + public interface IHasDescriptionNode + { + GraphQLDescription? Description { get; set; } + } +} diff --git a/src/GraphQLParser/ParserContext.cs b/src/GraphQLParser/ParserContext.cs index 4bdadc1f..eebcd888 100644 --- a/src/GraphQLParser/ParserContext.cs +++ b/src/GraphQLParser/ParserContext.cs @@ -268,20 +268,14 @@ private ASTNode ParseDefinition() { ParseComment(); - if (Peek(TokenKind.BRACE_L)) + return _currentToken.Kind switch { - return ParseOperationDefinition(); - } - - if (Peek(TokenKind.NAME)) - { - ASTNode? definition; - if ((definition = ParseNamedDefinition()) != null) - return definition; - } - - throw new GraphQLSyntaxErrorException( - $"Unexpected {_currentToken}", _source, _currentToken.Start); + TokenKind.BRACE_L => ParseOperationDefinition(), + TokenKind.STRING => ParseNamedDocumentedDefinition(hasDescription: true)!, + TokenKind.NAME => ParseNamedDefinition(), + _ => throw new GraphQLSyntaxErrorException( + $"Unexpected {_currentToken}", _source, _currentToken.Start) + }; } private List ParseDefinitionsIfNotEOF() @@ -336,6 +330,21 @@ private List ParseDefinitionsIfNotEOF() return comment; } + private GraphQLDescription? ParseDescription() + { + if (!Peek(TokenKind.STRING)) + return null; + + var documentation = new GraphQLDescription(_currentToken.Value!) + { + Location = GetLocation(_currentToken.Start) + }; + + Advance(); + + return documentation; + } + private GraphQLDirective ParseDirective() { int start = _currentToken.Start; @@ -346,6 +355,7 @@ private GraphQLDirective ParseDirective() Arguments = ParseArguments(), Location = GetLocation(start) }; + /// } /// @@ -354,10 +364,10 @@ private GraphQLDirective ParseDirective() /// Description(opt) directive @ Name ArgumentsDefinition(opt) repeatable(opt) on DirectiveLocations /// /// - private GraphQLDirectiveDefinition ParseDirectiveDefinition() + private GraphQLDirectiveDefinition ParseDirectiveDefinition(GraphQLDescription? description) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("directive"); Expect(TokenKind.AT); @@ -370,6 +380,7 @@ private GraphQLDirectiveDefinition ParseDirectiveDefinition() return new GraphQLDirectiveDefinition { + Description = description, Comment = comment, Name = name, Repeatable = repeatable, @@ -432,14 +443,15 @@ private GraphQLDocument ParseDocument() return CreateDocument(start, definitions); } - private GraphQLEnumTypeDefinition ParseEnumTypeDefinition() + private GraphQLEnumTypeDefinition ParseEnumTypeDefinition(GraphQLDescription? description) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("enum"); return new GraphQLEnumTypeDefinition { + Description = description, Comment = comment, Name = ParseName(), Directives = ParseDirectives(), @@ -462,9 +474,11 @@ private GraphQLEnumValueDefinition ParseEnumValueDefinition() { var comment = GetComment(); int start = _currentToken.Start; + var description = ParseDescription(); return new GraphQLEnumValueDefinition { + Description = description, Comment = comment, Name = ParseName(), Directives = ParseDirectives(), @@ -476,12 +490,14 @@ private GraphQLFieldDefinition ParseFieldDefinition() { var comment = GetComment(); int start = _currentToken.Start; + var description = ParseDescription(); var name = ParseName(); var args = ParseArgumentDefs(); Expect(TokenKind.COLON); return new GraphQLFieldDefinition { + Description = description, Comment = comment, Name = name, Arguments = args, @@ -581,14 +597,15 @@ private GraphQLName ParseFragmentName() return types; } - private GraphQLInputObjectTypeDefinition ParseInputObjectTypeDefinition() + private GraphQLInputObjectTypeDefinition ParseInputObjectTypeDefinition(GraphQLDescription? description) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("input"); return new GraphQLInputObjectTypeDefinition { + Description = description, Comment = comment, Name = ParseName(), Directives = ParseDirectives(), @@ -601,11 +618,13 @@ private GraphQLInputValueDefinition ParseInputValueDef() { var comment = GetComment(); int start = _currentToken.Start; + var description = ParseDescription(); var name = ParseName(); Expect(TokenKind.COLON); return new GraphQLInputValueDefinition { + Description = description, Comment = comment, Name = name, Type = ParseType(), @@ -627,14 +646,15 @@ private GraphQLValue ParseInt(bool isConstant) }; } - private GraphQLInterfaceTypeDefinition ParseInterfaceTypeDefinition() + private GraphQLInterfaceTypeDefinition ParseInterfaceTypeDefinition(GraphQLDescription? description) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("interface"); return new GraphQLInterfaceTypeDefinition { + Description = description, Comment = comment, Name = ParseName(), Directives = ParseDirectives(), @@ -672,24 +692,41 @@ private GraphQLName ParseName() }; } - private ASTNode? ParseNamedDefinition() - { - return _currentToken.Value switch + private ASTNode ParseNamedDefinition() => + _currentToken.Value switch { - "query" => ParseOperationDefinition(), - "mutation" => ParseOperationDefinition(), - "subscription" => ParseOperationDefinition(), + "query" => ParseOperationDefinition(), + "mutation" => ParseOperationDefinition(), + "subscription" => ParseOperationDefinition(), "fragment" => ParseFragmentDefinition(), - "schema" => ParseSchemaDefinition(), - "scalar" => ParseScalarTypeDefinition(), - "type" => ParseObjectTypeDefinition(), - "interface" => ParseInterfaceTypeDefinition(), - "union" => ParseUnionTypeDefinition(), - "enum" => ParseEnumTypeDefinition(), - "input" => ParseInputObjectTypeDefinition(), "extend" => ParseTypeExtensionDefinition(), - "directive" => ParseDirectiveDefinition(), - _ => null + _ => ParseNamedDocumentedDefinition(hasDescription: false) + ?? throw new GraphQLSyntaxErrorException($"Unexpected {_currentToken}", _source, _currentToken.Start) + }; + + /// + /// Parses the named definitions which may contain a description. + /// + /// Specifies whether the current position is at the string literal which should be parsed as a description. + /// Parsed node. + private ASTNode? ParseNamedDocumentedDefinition(bool hasDescription) + { + var description = hasDescription ? ParseDescription() : null; + + return _currentToken.Value switch + { + "schema" => ParseSchemaDefinition(description), + "scalar" => ParseScalarTypeDefinition(description), + "type" => ParseObjectTypeDefinition(description), + "interface" => ParseInterfaceTypeDefinition(description), + "union" => ParseUnionTypeDefinition(description), + "enum" => ParseEnumTypeDefinition(description), + "input" => ParseInputObjectTypeDefinition(description), + "directive" => ParseDirectiveDefinition(description), + _ => hasDescription + ? throw new GraphQLSyntaxErrorException( + $"Unexpected {_currentToken}. Expecting schema, scalar, type, interface, union, enum, input or directive.", _source, _currentToken.Start) + : (ASTNode?)null }; } @@ -769,15 +806,16 @@ private List ParseObjectFields(bool isConstant) return fields; } - private GraphQLObjectTypeDefinition ParseObjectTypeDefinition() + private GraphQLObjectTypeDefinition ParseObjectTypeDefinition(GraphQLDescription? description = null) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("type"); return new GraphQLObjectTypeDefinition { + Description = description, Comment = comment, Name = ParseName(), Interfaces = ParseImplementsInterfaces(), @@ -824,16 +862,17 @@ private GraphQLOperationTypeDefinition ParseOperationTypeDefinition() }; } - private GraphQLScalarTypeDefinition ParseScalarTypeDefinition() + private GraphQLScalarTypeDefinition ParseScalarTypeDefinition(GraphQLDescription? description) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("scalar"); var name = ParseName(); var directives = ParseDirectives(); return new GraphQLScalarTypeDefinition { + Description = description, Comment = comment, Name = name, Directives = directives, @@ -841,16 +880,17 @@ private GraphQLScalarTypeDefinition ParseScalarTypeDefinition() }; } - private GraphQLSchemaDefinition ParseSchemaDefinition() + private GraphQLSchemaDefinition ParseSchemaDefinition(GraphQLDescription? description) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("schema"); var directives = ParseDirectives(); var operationTypes = Many(TokenKind.BRACE_L, (ref ParserContext context) => context.ParseOperationTypeDefinition(), TokenKind.BRACE_R); return new GraphQLSchemaDefinition { + Description = description, Comment = comment, Directives = directives, OperationTypes = operationTypes, @@ -947,10 +987,10 @@ private List ParseUnionMembers() return members; } - private GraphQLUnionTypeDefinition ParseUnionTypeDefinition() + private GraphQLUnionTypeDefinition ParseUnionTypeDefinition(GraphQLDescription? description) { var comment = GetComment(); - int start = _currentToken.Start; + int start = description?.Location.Start ?? _currentToken.Start; ExpectKeyword("union"); var name = ParseName(); var directives = ParseDirectives(); @@ -959,6 +999,7 @@ private GraphQLUnionTypeDefinition ParseUnionTypeDefinition() return new GraphQLUnionTypeDefinition { + Description = description, Comment = comment, Name = name, Directives = directives,