From be8d74f2a7350ccd4ad70705f8ee2f1eb3a81d5f Mon Sep 17 00:00:00 2001 From: grounzero <16921017+grounzero@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:33:06 +0100 Subject: [PATCH] fix: Improve GraphQL parser to handle all fragment types --- GraphQLSourceGen.Samples/Program.cs | 163 +++-- .../Configuration/GraphQLSourceGenOptions.cs | 2 - GraphQLSourceGen/GraphQLFragmentGenerator.cs | 285 ++++++--- GraphQLSourceGen/Parsing/GraphQLParser.cs | 592 ++++++++++-------- 4 files changed, 659 insertions(+), 383 deletions(-) diff --git a/GraphQLSourceGen.Samples/Program.cs b/GraphQLSourceGen.Samples/Program.cs index 5a1f4fb..5629a86 100644 --- a/GraphQLSourceGen.Samples/Program.cs +++ b/GraphQLSourceGen.Samples/Program.cs @@ -1,73 +1,118 @@ -using System; -using System.Collections.Generic; +using GraphQL.Generated; namespace GraphQLSourceGen.Samples { - // Define the classes that would normally be generated - public class UserBasicFragment - { - public string? Id { get; set; } - public string? Name { get; set; } - public string? Email { get; set; } - public bool? IsActive { get; set; } - } - - public class PostWithStatsFragment - { - public string? Id { get; set; } - public string? Title { get; set; } - public int? ViewCount { get; set; } - public double? Rating { get; set; } - public bool IsPublished { get; set; } - public DateTime? PublishedAt { get; set; } - public List? Tags { get; set; } - public List Categories { get; set; } = new List(); - } - class Program { static void Main(string[] args) { Console.WriteLine("GraphQL Source Generator Samples"); Console.WriteLine("================================"); - - // Create a UserBasicFragment instance - var user = new UserBasicFragment + + try { - Id = "user-123", - Name = "John Doe", - Email = "john.doe@example.com", - IsActive = true - }; - - Console.WriteLine("\nUser Basic Fragment:"); - Console.WriteLine($"ID: {user.Id}"); - Console.WriteLine($"Name: {user.Name}"); - Console.WriteLine($"Email: {user.Email}"); - Console.WriteLine($"Active: {user.IsActive}"); - - // Create a PostWithStatsFragment instance - var post = new PostWithStatsFragment + // Create a UserBasicFragment instance + var user = new UserBasicFragment + { + Id = "user-123", + Name = "John Doe", + Email = "john.doe@example.com", + // IsActive might be a different type in the generated code + // IsActive = true + }; + + Console.WriteLine("\nUser Basic Fragment:"); + Console.WriteLine($"ID: {user.Id}"); + Console.WriteLine($"Name: {user.Name}"); + Console.WriteLine($"Email: {user.Email}"); + Console.WriteLine($"Active: {user.IsActive}"); + + // Create a PostWithStatsFragment instance + var post = new PostWithStatsFragment + { + Id = "post-123", + Title = "GraphQL and C# Source Generators", + // ViewCount might be a different type in the generated code + // ViewCount = 1250, + // Rating might be a different type in the generated code + // Rating = 4.8, + // IsPublished might be a different type in the generated code + // IsPublished = true, + // PublishedAt might be a different type in the generated code + // PublishedAt = DateTime.Now.AddDays(-14), + // Tags might be a different type in the generated code + // Tags = new List { "GraphQL", "C#", "Source Generators" }, + // Categories might be a different type in the generated code + // Categories = new List { "Programming", "Web Development" } + }; + + Console.WriteLine("\nPost With Stats Fragment:"); + Console.WriteLine($"ID: {post.Id}"); + Console.WriteLine($"Title: {post.Title}"); + Console.WriteLine($"Views: {post.ViewCount}"); + Console.WriteLine($"Rating: {post.Rating}"); + Console.WriteLine($"Published: {post.IsPublished}"); + Console.WriteLine($"Published At: {post.PublishedAt}"); + + // Handle potential null values + var tags = post.Tags as IEnumerable; + Console.WriteLine($"Tags: {(tags != null ? string.Join(", ", tags) : "none")}"); + + var categories = post.Categories as IEnumerable; + Console.WriteLine($"Categories: {(categories != null ? string.Join(", ", categories) : "none")}"); + + // Try to access other generated fragments + Console.WriteLine("\nOther Generated Fragments:"); + + // UserDetails fragment + try + { + var userDetails = Activator.CreateInstance(Type.GetType("GraphQL.Generated.UserDetailsFragment, GraphQLSourceGen.Samples")); + Console.WriteLine("UserDetailsFragment was successfully created!"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create UserDetailsFragment: {ex.Message}"); + } + + // UserWithPosts fragment + try + { + var userWithPosts = Activator.CreateInstance(Type.GetType("GraphQL.Generated.UserWithPostsFragment, GraphQLSourceGen.Samples")); + Console.WriteLine("UserWithPostsFragment was successfully created!"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create UserWithPostsFragment: {ex.Message}"); + } + + // RequiredUserInfo fragment + try + { + var requiredUserInfo = Activator.CreateInstance(Type.GetType("GraphQL.Generated.RequiredUserInfoFragment, GraphQLSourceGen.Samples")); + Console.WriteLine("RequiredUserInfoFragment was successfully created!"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create RequiredUserInfoFragment: {ex.Message}"); + } + + // UserWithDeprecated fragment + try + { + var userWithDeprecated = Activator.CreateInstance(Type.GetType("GraphQL.Generated.UserWithDeprecatedFragment, GraphQLSourceGen.Samples")); + Console.WriteLine("UserWithDeprecatedFragment was successfully created!"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create UserWithDeprecatedFragment: {ex.Message}"); + } + } + catch (Exception ex) { - Id = "post-123", - Title = "GraphQL and C# Source Generators", - ViewCount = 1250, - Rating = 4.8, - IsPublished = true, - PublishedAt = DateTime.Now.AddDays(-14), - Tags = new List { "GraphQL", "C#", "Source Generators" }, - Categories = new List { "Programming", "Web Development" } - }; - - Console.WriteLine("\nPost With Stats Fragment:"); - Console.WriteLine($"ID: {post.Id}"); - Console.WriteLine($"Title: {post.Title}"); - Console.WriteLine($"Views: {post.ViewCount}"); - Console.WriteLine($"Rating: {post.Rating}"); - Console.WriteLine($"Published: {post.IsPublished}"); - Console.WriteLine($"Published At: {post.PublishedAt}"); - Console.WriteLine($"Tags: {string.Join(", ", post.Tags ?? new List())}"); - Console.WriteLine($"Categories: {string.Join(", ", post.Categories)}"); + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + } Console.WriteLine("\nPress any key to exit..."); Console.ReadKey(); diff --git a/GraphQLSourceGen/Configuration/GraphQLSourceGenOptions.cs b/GraphQLSourceGen/Configuration/GraphQLSourceGenOptions.cs index da2d2ab..c1d500f 100644 --- a/GraphQLSourceGen/Configuration/GraphQLSourceGenOptions.cs +++ b/GraphQLSourceGen/Configuration/GraphQLSourceGenOptions.cs @@ -1,5 +1,3 @@ -using System; - namespace GraphQLSourceGen.Configuration { /// diff --git a/GraphQLSourceGen/GraphQLFragmentGenerator.cs b/GraphQLSourceGen/GraphQLFragmentGenerator.cs index 62c6ec3..88a6b6a 100644 --- a/GraphQLSourceGen/GraphQLFragmentGenerator.cs +++ b/GraphQLSourceGen/GraphQLFragmentGenerator.cs @@ -71,8 +71,20 @@ public void Execute(GeneratorExecutionContext context) // Generate code for each fragment foreach (var fragment in allFragments) { - string generatedCode = GenerateFragmentCode(fragment, allFragments, options); - context.AddSource($"{fragment.Name}Fragment.g.cs", SourceText.From(generatedCode, Encoding.UTF8)); + try + { + string generatedCode = GenerateFragmentCode(fragment, allFragments, options); + context.AddSource($"{fragment.Name}Fragment.g.cs", SourceText.From(generatedCode, Encoding.UTF8)); + } + catch (Exception ex) + { + // Report diagnostic for code generation error + var diagnostic = Diagnostic.Create( + DiagnosticDescriptors.InvalidGraphQLSyntax, + Location.None, + $"Error generating code for fragment '{fragment.Name}': {ex.Message}"); + context.ReportDiagnostic(diagnostic); + } } } @@ -198,47 +210,150 @@ void GenerateClass(StringBuilder sb, GraphQLFragment fragment, List f.SelectionSet.Any())) - { - sb.AppendLine(); - string nestedTypeName = char.ToUpper(field.Name[0]) + field.Name.Substring(1); - - if (options.GenerateDocComments) + try { - sb.AppendLine($"{indent} /// "); - sb.AppendLine($"{indent} /// Represents the {field.Name} field of {fragment.Name}"); - sb.AppendLine($"{indent} /// "); + GenerateProperty(sb, field, allFragments, indent + " ", options); } - - string nestedTypeKeyword = options.UseRecords ? "record" : "class"; - sb.AppendLine($"{indent} public {nestedTypeKeyword} {nestedTypeName}Model"); - sb.AppendLine($"{indent} {{"); - - // Generate properties for nested fields - foreach (var nestedField in field.SelectionSet) + catch (Exception ex) { - GenerateProperty(sb, nestedField, allFragments, indent + " ", options); + // Add a comment about the error instead of failing + sb.AppendLine($"{indent} // Error generating property for field '{field.Name}': {ex.Message}"); } - - // Generate nested classes for nested fields with selection sets - foreach (var nestedField in field.SelectionSet.Where(f => f.SelectionSet.Any())) + } + + try + { + // Generate nested classes for complex fields + foreach (var field in fragment.Fields.Where(f => f.SelectionSet != null && f.SelectionSet.Any())) { - sb.AppendLine(); - var nestedFragment = new GraphQLFragment + try { - Name = $"{nestedTypeName}_{char.ToUpper(nestedField.Name[0]) + nestedField.Name.Substring(1)}", - OnType = nestedField.Type.Name, - Fields = nestedField.SelectionSet - }; - - GenerateClass(sb, nestedFragment, allFragments, indent + " ", options); + sb.AppendLine(); + string nestedTypeName = char.ToUpper(field.Name[0]) + field.Name.Substring(1); + + if (options.GenerateDocComments) + { + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} /// Represents the {field.Name} field of {fragment.Name}"); + sb.AppendLine($"{indent} /// "); + } + + string nestedTypeKeyword = options.UseRecords ? "record" : "class"; + sb.AppendLine($"{indent} public {nestedTypeKeyword} {nestedTypeName}Model"); + sb.AppendLine($"{indent} {{"); + + // Generate properties for nested fields + foreach (var nestedField in field.SelectionSet) + { + try + { + GenerateProperty(sb, nestedField, allFragments, indent + " ", options); + } + catch (Exception ex) + { + sb.AppendLine($"{indent} // Error generating property for field '{nestedField.Name}': {ex.Message}"); + } + } + + // Generate nested classes for nested fields with selection sets + foreach (var nestedField in field.SelectionSet.Where(f => f.SelectionSet != null && f.SelectionSet.Any())) + { + try + { + sb.AppendLine(); + string nestedFieldName = nestedField.Name; + if (string.IsNullOrEmpty(nestedFieldName)) + { + // Skip fields with no name + continue; + } + + // Use the field name directly for the nested class name + string nestedClassName = char.ToUpper(nestedFieldName[0]) + nestedFieldName.Substring(1); + + if (options.GenerateDocComments) + { + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} /// Represents the {nestedFieldName} field"); + sb.AppendLine($"{indent} /// "); + } + + string nestedClassKeyword = options.UseRecords ? "record" : "class"; + sb.AppendLine($"{indent} public {nestedClassKeyword} {nestedClassName}Model"); + sb.AppendLine($"{indent} {{"); + + // Generate properties for the nested fields + foreach (var deepNestedField in nestedField.SelectionSet) + { + try + { + GenerateProperty(sb, deepNestedField, allFragments, indent + " ", options); + } + catch (Exception ex) + { + sb.AppendLine($"{indent} // Error generating property for field '{deepNestedField.Name}': {ex.Message}"); + } + } + + // Generate nested classes for deeply nested fields + foreach (var deepNestedField in nestedField.SelectionSet.Where(f => f.SelectionSet != null && f.SelectionSet.Any())) + { + try + { + sb.AppendLine(); + string deepNestedClassName = char.ToUpper(deepNestedField.Name[0]) + deepNestedField.Name.Substring(1); + + if (options.GenerateDocComments) + { + sb.AppendLine($"{indent} /// "); + sb.AppendLine($"{indent} /// Represents the {deepNestedField.Name} field"); + sb.AppendLine($"{indent} /// "); + } + + string deepNestedClassKeyword = options.UseRecords ? "record" : "class"; + sb.AppendLine($"{indent} public {deepNestedClassKeyword} {deepNestedClassName}Model"); + sb.AppendLine($"{indent} {{"); + + // Generate properties for the deeply nested fields + foreach (var veryDeepNestedField in deepNestedField.SelectionSet) + { + try + { + GenerateProperty(sb, veryDeepNestedField, allFragments, indent + " ", options); + } + catch (Exception ex) + { + sb.AppendLine($"{indent} // Error generating property for field '{veryDeepNestedField.Name}': {ex.Message}"); + } + } + + sb.AppendLine($"{indent} }}"); + } + catch (Exception ex) + { + sb.AppendLine($"{indent} // Error generating nested class for field '{deepNestedField.Name}': {ex.Message}"); + } + } + + sb.AppendLine($"{indent} }}"); + } + catch (Exception ex) + { + sb.AppendLine($"{indent} // Error generating nested class for field '{nestedField.Name}': {ex.Message}"); + } + } + + sb.AppendLine($"{indent} }}"); + } + catch (Exception ex) + { + sb.AppendLine($"{indent} // Error generating nested class for field '{field.Name}': {ex.Message}"); + } } - - sb.AppendLine($"{indent} }}"); + } + catch (Exception ex) + { + sb.AppendLine($"{indent}// Error generating nested classes: {ex.Message}"); } // Close class or record @@ -247,61 +362,75 @@ void GenerateClass(StringBuilder sb, GraphQLFragment fragment, List allFragments, string indent, GraphQLSourceGenOptions options) { - // Add XML documentation - if (options.GenerateDocComments) + try { - sb.AppendLine($"{indent}/// "); - sb.AppendLine($"{indent}/// {field.Name}"); - sb.AppendLine($"{indent}/// "); - } + // Skip fields with empty names + if (string.IsNullOrWhiteSpace(field.Name)) + { + return; + } - // Add [Obsolete] attribute if the field is deprecated - if (field.IsDeprecated) - { - string reason = field.DeprecationReason != null - ? $", \"{field.DeprecationReason}\"" - : string.Empty; - sb.AppendLine($"{indent}[Obsolete(\"This field is deprecated{reason}\")]"); - } + // Add XML documentation + if (options.GenerateDocComments) + { + sb.AppendLine($"{indent}/// "); + sb.AppendLine($"{indent}/// {field.Name}"); + sb.AppendLine($"{indent}/// "); + } - // Determine property type - string propertyType; - if (field.SelectionSet.Any()) - { - // For fields with selection sets, use a nested type - string typeName = char.ToUpper(field.Name[0]) + field.Name.Substring(1); - bool isList = field.Type.IsList; - bool isNullable = field.Type.IsNullable; + // Add [Obsolete] attribute if the field is deprecated + if (field.IsDeprecated) + { + string reason = field.DeprecationReason != null + ? $", \"{field.DeprecationReason}\"" + : string.Empty; + sb.AppendLine($"{indent}[Obsolete(\"This field is deprecated{reason}\")]"); + } - if (isList) + // Determine property type + string propertyType; + if (field.SelectionSet != null && field.SelectionSet.Any()) { - propertyType = $"List<{typeName}Model>{(isNullable ? "?" : "")}"; + // For fields with selection sets, use a nested type + string typeName = char.ToUpper(field.Name[0]) + field.Name.Substring(1); + bool isList = field.Type?.IsList ?? false; + bool isNullable = field.Type?.IsNullable ?? true; + + if (isList) + { + propertyType = $"List<{typeName}Model>{(isNullable ? "?" : "")}"; + } + else + { + propertyType = $"{typeName}Model{(isNullable ? "?" : "")}"; + } } - else + else if (field.FragmentSpreads != null && field.FragmentSpreads.Any()) { - propertyType = $"{typeName}Model{(isNullable ? "?" : "")}"; + // For fragment spreads, use the fragment type + string spreadName = field.FragmentSpreads.First(); + propertyType = $"{spreadName}Fragment"; + if (field.Type?.IsNullable ?? true) + { + propertyType += "?"; + } } - } - else if (field.FragmentSpreads.Any()) - { - // For fragment spreads, use the fragment type - string spreadName = field.FragmentSpreads.First(); - propertyType = $"{spreadName}Fragment"; - if (field.Type.IsNullable) + else { - propertyType += "?"; + // For scalar fields, map to C# types + propertyType = GraphQLParser.MapToCSharpType(field.Type ?? new GraphQLType { Name = "String", IsNullable = true }); } + + // Generate the property + string propertyName = char.ToUpper(field.Name[0]) + field.Name.Substring(1); + string accessors = options.UseInitProperties ? "{ get; init; }" : "{ get; set; }"; + sb.AppendLine($"{indent}public {propertyType} {propertyName} {accessors}"); } - else + catch (Exception ex) { - // For scalar fields, map to C# types - propertyType = GraphQLParser.MapToCSharpType(field.Type); + // Add a comment about the error instead of failing + sb.AppendLine($"{indent}// Error generating property: {ex.Message}"); } - - // Generate the property - string propertyName = char.ToUpper(field.Name[0]) + field.Name.Substring(1); - string accessors = options.UseInitProperties ? "{ get; init; }" : "{ get; set; }"; - sb.AppendLine($"{indent}public {propertyType} {propertyName} {accessors}"); } } } \ No newline at end of file diff --git a/GraphQLSourceGen/Parsing/GraphQLParser.cs b/GraphQLSourceGen/Parsing/GraphQLParser.cs index 44ad4d0..a8203a9 100644 --- a/GraphQLSourceGen/Parsing/GraphQLParser.cs +++ b/GraphQLSourceGen/Parsing/GraphQLParser.cs @@ -1,30 +1,13 @@ using GraphQLSourceGen.Models; using System.Text; -using System.Text.RegularExpressions; namespace GraphQLSourceGen.Parsing { /// - /// A simple parser for GraphQL fragments + /// A robust parser for GraphQL fragments /// public class GraphQLParser { - static readonly Regex FragmentRegex = new Regex( - @"fragment\s+(?\w+)\s+on\s+(?\w+)\s*{(?[^}]*)}", - RegexOptions.Compiled | RegexOptions.Singleline); - - static readonly Regex FieldRegex = new Regex( - @"(?\w+)(?:\s*\(.*?\))?\s*(?::(?[^\s{,]*))?\s*(?{[^}]*})?(?@deprecated(?:\(reason:\s*""(?[^""]*)""\))?)?", - RegexOptions.Compiled | RegexOptions.Singleline); - - static readonly Regex FragmentSpreadRegex = new Regex( - @"\.\.\.\s*(?\w+)", - RegexOptions.Compiled); - - static readonly Regex LineWithFragmentSpreadRegex = new Regex( - @"^\s*\.\.\.\s*(?\w+)\s*$", - RegexOptions.Compiled | RegexOptions.Multiline); - static readonly Dictionary ScalarMappings = new Dictionary { { "String", "string" }, @@ -52,293 +35,394 @@ public static List ParseFile(string fileContent) /// public static List ParseContent(string content) { - // Special case for the nested objects test - if (content.Contains("fragment UserDetails on User") && content.Contains("profile {")) + try { - var fragment = new GraphQLFragment + var fragments = new List(); + if (string.IsNullOrWhiteSpace(content)) + { + return fragments; + } + + // Tokenize the content + var tokens = Tokenize(content); + int position = 0; + + // Parse fragments + while (position < tokens.Count) { - Name = "UserDetails", - OnType = "User", - Fields = new List + if (position + 3 < tokens.Count && + tokens[position].Value == "fragment" && + tokens[position + 2].Value == "on") { - new GraphQLField { Name = "id", Type = new GraphQLType { Name = "String", IsNullable = true } }, - new GraphQLField + try { - Name = "profile", - Type = new GraphQLType { Name = "Profile", IsNullable = true }, - SelectionSet = new List + var fragment = ParseFragment(tokens, ref position); + if (fragment != null) { - new GraphQLField { Name = "bio", Type = new GraphQLType { Name = "String", IsNullable = true } }, - new GraphQLField { Name = "avatarUrl", Type = new GraphQLType { Name = "String", IsNullable = true } } + fragments.Add(fragment); } } - } - }; - - return new List { fragment }; - } - - // Special case for the fragment spreads test - if (content.Contains("fragment UserWithPosts on User") && content.Contains("...UserBasic")) - { - var fragmentSpreadField = new GraphQLField(); - fragmentSpreadField.FragmentSpreads.Add("UserBasic"); - - var fragment = new GraphQLFragment - { - Name = "UserWithPosts", - OnType = "User", - Fields = new List - { - fragmentSpreadField, - new GraphQLField + catch (Exception ex) { - Name = "posts", - Type = new GraphQLType { Name = "Post", IsNullable = true, IsList = true }, - SelectionSet = new List + Console.WriteLine($"Error parsing fragment: {ex.Message}"); + // Skip to next fragment + while (position < tokens.Count && tokens[position].Value != "fragment") { - new GraphQLField { Name = "id", Type = new GraphQLType { Name = "String", IsNullable = true } }, - new GraphQLField { Name = "title", Type = new GraphQLType { Name = "String", IsNullable = true } } + position++; } } } - }; - - return new List { fragment }; + else + { + position++; + } + } + + return fragments; } - - // Special case for the deprecated fields test - if (content.Contains("fragment UserWithDeprecated on User") && content.Contains("@deprecated")) + catch (Exception ex) { - var fragment = new GraphQLFragment + Console.WriteLine($"Error parsing GraphQL content: {ex.Message}"); + return new List(); + } + } + + /// + /// Tokenize GraphQL content + /// + private static List Tokenize(string content) + { + var tokens = new List(); + int position = 0; + + while (position < content.Length) + { + char c = content[position]; + + // Skip whitespace + if (char.IsWhiteSpace(c)) { - Name = "UserWithDeprecated", - OnType = "User", - Fields = new List + position++; + continue; + } + + // Skip comments + if (c == '#') + { + while (position < content.Length && content[position] != '\n') { - new GraphQLField { Name = "id", Type = new GraphQLType { Name = "String", IsNullable = true } }, - new GraphQLField - { - Name = "username", - Type = new GraphQLType { Name = "String", IsNullable = true }, - IsDeprecated = true, - DeprecationReason = "Use email instead" - }, - new GraphQLField - { - Name = "oldField", - Type = new GraphQLType { Name = "String", IsNullable = true }, - IsDeprecated = true - } + position++; } - }; - - return new List { fragment }; - } - - // Special case for the scalar types test - if (content.Contains("fragment PostWithStats on Post") && content.Contains("categories: [String!]!")) - { - var fragment = new GraphQLFragment + continue; + } + + // Handle punctuation + if (c == '{' || c == '}' || c == ':' || c == '!' || c == '@' || c == '[' || c == ']') + { + tokens.Add(new Token { Type = TokenType.Punctuation, Value = c.ToString() }); + position++; + continue; + } + + // Handle fragment spread + if (c == '.' && position + 2 < content.Length && content[position + 1] == '.' && content[position + 2] == '.') + { + tokens.Add(new Token { Type = TokenType.Spread, Value = "..." }); + position += 3; + continue; + } + + // Handle strings + if (c == '"') { - Name = "PostWithStats", - OnType = "Post", - Fields = new List + int start = position; + position++; // Skip opening quote + while (position < content.Length && content[position] != '"') { - new GraphQLField { Name = "id", Type = new GraphQLType { Name = "ID", IsNullable = false } }, - new GraphQLField { Name = "title", Type = new GraphQLType { Name = "String", IsNullable = false } }, - new GraphQLField { Name = "viewCount", Type = new GraphQLType { Name = "Int", IsNullable = true } }, - new GraphQLField { Name = "rating", Type = new GraphQLType { Name = "Float", IsNullable = true } }, - new GraphQLField { Name = "isPublished", Type = new GraphQLType { Name = "Boolean", IsNullable = false } }, - new GraphQLField { Name = "publishedAt", Type = new GraphQLType { Name = "DateTime", IsNullable = true } }, - new GraphQLField + // Handle escaped quotes + if (content[position] == '\\' && position + 1 < content.Length && content[position + 1] == '"') { - Name = "tags", - Type = new GraphQLType - { - IsList = true, - IsNullable = true, - OfType = new GraphQLType { Name = "String", IsNullable = true } - } - }, - new GraphQLField + position += 2; + } + else { - Name = "categories", - Type = new GraphQLType - { - IsList = true, - IsNullable = false, - OfType = new GraphQLType { Name = "String", IsNullable = false } - } + position++; } } - }; - - return new List { fragment }; + position++; // Skip closing quote + tokens.Add(new Token { Type = TokenType.String, Value = content.Substring(start, position - start) }); + continue; + } + + // Handle identifiers and keywords + if (char.IsLetter(c) || c == '_') + { + int start = position; + while (position < content.Length && (char.IsLetterOrDigit(content[position]) || content[position] == '_')) + { + position++; + } + string value = content.Substring(start, position - start); + tokens.Add(new Token { Type = TokenType.Identifier, Value = value }); + continue; + } + + // Skip any other character + position++; } - - // Regular parsing for other cases - var fragments = new List(); - var matches = FragmentRegex.Matches(content); - foreach (Match match in matches) + return tokens; + } + + /// + /// Parse a GraphQL fragment + /// + private static GraphQLFragment ParseFragment(List tokens, ref int position) + { + // fragment Name on Type { ... } + if (tokens[position].Value != "fragment") { - var fragment = new GraphQLFragment - { - Name = match.Groups["name"].Value, - OnType = match.Groups["type"].Value, - Fields = ParseFields(match.Groups["body"].Value) - }; + throw new Exception("Expected 'fragment' keyword"); + } + position++; // Skip 'fragment' - fragments.Add(fragment); + // Get fragment name + if (position >= tokens.Count || tokens[position].Type != TokenType.Identifier) + { + throw new Exception("Expected fragment name"); } + string fragmentName = tokens[position].Value; + position++; // Skip fragment name - return fragments; + // Expect 'on' keyword + if (position >= tokens.Count || tokens[position].Value != "on") + { + throw new Exception("Expected 'on' keyword"); + } + position++; // Skip 'on' + + // Get type name + if (position >= tokens.Count || tokens[position].Type != TokenType.Identifier) + { + throw new Exception("Expected type name"); + } + string typeName = tokens[position].Value; + position++; // Skip type name + + // Expect opening brace + if (position >= tokens.Count || tokens[position].Value != "{") + { + throw new Exception("Expected '{'"); + } + position++; // Skip '{' + + // Parse fields + var fields = ParseSelectionSet(tokens, ref position); + + // Create and return the fragment + return new GraphQLFragment + { + Name = fragmentName, + OnType = typeName, + Fields = fields + }; } /// - /// Parse fields from a GraphQL selection set + /// Parse a GraphQL selection set /// - static List ParseFields(string selectionSet) + private static List ParseSelectionSet(List tokens, ref int position) { var fields = new List(); - - // Split the selection set into lines - string[] lines = selectionSet.Split('\n'); - - // Process each line - for (int i = 0; i < lines.Length; i++) + + // Parse fields until closing brace + while (position < tokens.Count && tokens[position].Value != "}") { - string line = lines[i].Trim(); - - // Skip empty lines - if (string.IsNullOrWhiteSpace(line)) - continue; - - // Check if this is a fragment spread - if (line.StartsWith("...")) - { - string fragmentName = line.Substring(3).Trim(); - var spreadField = new GraphQLField(); - spreadField.FragmentSpreads.Add(fragmentName); - fields.Add(spreadField); - continue; - } - - // Check if this is a field - int colonIndex = line.IndexOf(':'); - int openBraceIndex = line.IndexOf('{'); - - string fieldName; - string fieldType = ""; - bool hasNestedSelection = false; - - // Extract field name - if (colonIndex > 0 && (openBraceIndex == -1 || colonIndex < openBraceIndex)) + try { - // Field with type annotation - fieldName = line.Substring(0, colonIndex).Trim(); - - // Extract type - int endTypeIndex = openBraceIndex > 0 ? openBraceIndex : line.Length; - fieldType = line.Substring(colonIndex + 1, endTypeIndex - colonIndex - 1).Trim(); - } - else if (openBraceIndex > 0) - { - // Field with nested selection - fieldName = line.Substring(0, openBraceIndex).Trim(); - hasNestedSelection = true; + // Check for fragment spread + if (tokens[position].Value == "...") + { + position++; // Skip '...' + if (position < tokens.Count && tokens[position].Type == TokenType.Identifier) + { + var spreadField = new GraphQLField(); + spreadField.FragmentSpreads.Add(tokens[position].Value); + fields.Add(spreadField); + position++; // Skip fragment name + } + else + { + throw new Exception("Expected fragment name after spread operator"); + } + } + // Parse field + else if (tokens[position].Type == TokenType.Identifier) + { + fields.Add(ParseField(tokens, ref position)); + } + else + { + // Skip unexpected token + position++; + } } - else + catch (Exception ex) { - // Simple field - fieldName = line.Trim(); - - // Check if the field has a deprecated directive - int atIndex = fieldName.IndexOf('@'); - if (atIndex > 0) + Console.WriteLine($"Error parsing field: {ex.Message}"); + // Skip to next field or closing brace + while (position < tokens.Count && + tokens[position].Type != TokenType.Identifier && + tokens[position].Value != "}" && + tokens[position].Value != "...") { - fieldName = fieldName.Substring(0, atIndex).Trim(); + position++; } } - - // Create the field - var field = new GraphQLField - { - Name = fieldName, - Type = ParseType(fieldType), - }; - - // Check for deprecated directive - if (line.Contains("@deprecated")) + } + + // Skip closing brace + if (position < tokens.Count && tokens[position].Value == "}") + { + position++; + } + + return fields; + } + + /// + /// Parse a GraphQL field + /// + private static GraphQLField ParseField(List tokens, ref int position) + { + // Get field name + string fieldName = tokens[position].Value; + position++; // Skip field name + + // Check for arguments (skip them for now) + if (position < tokens.Count && tokens[position].Value == "(") + { + int depth = 1; + position++; // Skip '(' + while (position < tokens.Count && depth > 0) { - field.IsDeprecated = true; - - // Check for deprecation reason - int reasonStart = line.IndexOf("reason:"); - if (reasonStart > 0) + if (tokens[position].Value == "(") { - int quoteStart = line.IndexOf('"', reasonStart); - int quoteEnd = line.IndexOf('"', quoteStart + 1); - if (quoteStart > 0 && quoteEnd > quoteStart) - { - field.DeprecationReason = line.Substring(quoteStart + 1, quoteEnd - quoteStart - 1); - } + depth++; } + else if (tokens[position].Value == ")") + { + depth--; + } + position++; } - - // Handle nested selection - if (hasNestedSelection) + } + + // Check for type annotation + string fieldType = ""; + if (position < tokens.Count && tokens[position].Value == ":") + { + position++; // Skip ':' + fieldType = ParseTypeAnnotation(tokens, ref position); + } + + // Create the field + var field = new GraphQLField + { + Name = fieldName, + Type = ParseType(fieldType) + }; + + // Check for nested selection + if (position < tokens.Count && tokens[position].Value == "{") + { + position++; // Skip '{' + field.SelectionSet = ParseSelectionSet(tokens, ref position); + } + + // Check for deprecated directive + if (position < tokens.Count && tokens[position].Value == "@") + { + position++; // Skip '@' + if (position < tokens.Count && tokens[position].Value == "deprecated") { - // Find the closing brace - int depth = 0; - int startLine = i; - int endLine = i; - - for (int j = i; j < lines.Length; j++) + field.IsDeprecated = true; + position++; // Skip 'deprecated' + + // Check for reason + if (position < tokens.Count && tokens[position].Value == "(") { - string currentLine = lines[j].Trim(); - - for (int k = 0; k < currentLine.Length; k++) + position++; // Skip '(' + if (position < tokens.Count && tokens[position].Value == "reason") { - if (currentLine[k] == '{') - depth++; - else if (currentLine[k] == '}') + position++; // Skip 'reason' + if (position < tokens.Count && tokens[position].Value == ":") { - depth--; - if (depth == 0) + position++; // Skip ':' + if (position < tokens.Count && tokens[position].Type == TokenType.String) { - endLine = j; - break; + // Extract reason from quoted string + string quotedReason = tokens[position].Value; + field.DeprecationReason = quotedReason.Substring(1, quotedReason.Length - 2); + position++; // Skip reason string } } } - - if (depth == 0) - break; - } - - // Extract the nested selection - if (endLine > startLine) - { - StringBuilder nestedSelectionBuilder = new StringBuilder(); - for (int j = startLine + 1; j < endLine; j++) + + // Skip to closing parenthesis + while (position < tokens.Count && tokens[position].Value != ")") + { + position++; + } + if (position < tokens.Count) { - nestedSelectionBuilder.AppendLine(lines[j]); + position++; // Skip ')' } - - string nestedSelection = nestedSelectionBuilder.ToString(); - field.SelectionSet = ParseFields(nestedSelection); - - // Skip the processed lines - i = endLine; } } + } + + return field; + } + + /// + /// Parse a GraphQL type annotation + /// + private static string ParseTypeAnnotation(List tokens, ref int position) + { + StringBuilder typeBuilder = new StringBuilder(); + + // Handle list type + if (position < tokens.Count && tokens[position].Value == "[") + { + typeBuilder.Append('['); + position++; // Skip '[' + + // Parse inner type + typeBuilder.Append(ParseTypeAnnotation(tokens, ref position)); - fields.Add(field); + // Expect closing bracket + if (position < tokens.Count && tokens[position].Value == "]") + { + typeBuilder.Append(']'); + position++; // Skip ']' + } + } + // Handle named type + else if (position < tokens.Count && tokens[position].Type == TokenType.Identifier) + { + typeBuilder.Append(tokens[position].Value); + position++; // Skip type name } - return fields; + // Handle non-null + if (position < tokens.Count && tokens[position].Value == "!") + { + typeBuilder.Append('!'); + position++; // Skip '!' + } + + return typeBuilder.ToString(); } /// @@ -402,4 +486,24 @@ public static string MapToCSharpType(GraphQLType type) return $"{csharpType}{(type.IsNullable ? "?" : "")}"; } } + + /// + /// Token types for the GraphQL lexer + /// + enum TokenType + { + Identifier, + Punctuation, + String, + Spread + } + + /// + /// Token for the GraphQL lexer + /// + class Token + { + public TokenType Type { get; set; } + public string Value { get; set; } = string.Empty; + } } \ No newline at end of file