diff --git a/GraphQLSourceGen.Samples/Program.cs b/GraphQLSourceGen.Samples/Program.cs index b542be1..911d589 100644 --- a/GraphQLSourceGen.Samples/Program.cs +++ b/GraphQLSourceGen.Samples/Program.cs @@ -1,26 +1,7 @@ +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; } = []; - } - class Program { static void Main(string[] args) @@ -28,44 +9,137 @@ 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 = 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}"); + + // 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 userDetailsType = Type.GetType("GraphQL.Generated.UserDetailsFragment, GraphQLSourceGen.Samples"); + if (userDetailsType != null) + { + var userDetails = Activator.CreateInstance(userDetailsType); + Console.WriteLine("UserDetailsFragment was successfully created!"); + } + else + { + Console.WriteLine("Failed to find UserDetailsFragment type"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create UserDetailsFragment: {ex.Message}"); + } + + // UserWithPosts fragment + try + { + var userWithPostsType = Type.GetType("GraphQL.Generated.UserWithPostsFragment, GraphQLSourceGen.Samples"); + if (userWithPostsType != null) + { + var userWithPosts = Activator.CreateInstance(userWithPostsType); + Console.WriteLine("UserWithPostsFragment was successfully created!"); + } + else + { + Console.WriteLine("Failed to find UserWithPostsFragment type"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create UserWithPostsFragment: {ex.Message}"); + } + + // RequiredUserInfo fragment + try + { + var requiredUserInfoType = Type.GetType("GraphQL.Generated.RequiredUserInfoFragment, GraphQLSourceGen.Samples"); + if (requiredUserInfoType != null) + { + var requiredUserInfo = Activator.CreateInstance(requiredUserInfoType); + Console.WriteLine("RequiredUserInfoFragment was successfully created!"); + } + else + { + Console.WriteLine("Failed to find RequiredUserInfoFragment type"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create RequiredUserInfoFragment: {ex.Message}"); + } + + // UserWithDeprecated fragment + try + { + var userWithDeprecatedType = Type.GetType("GraphQL.Generated.UserWithDeprecatedFragment, GraphQLSourceGen.Samples"); + if (userWithDeprecatedType != null) + { + var userWithDeprecated = Activator.CreateInstance(userWithDeprecatedType); + Console.WriteLine("UserWithDeprecatedFragment was successfully created!"); + } + else + { + Console.WriteLine("Failed to find UserWithDeprecatedFragment type"); + } + } + 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 = ["GraphQL", "C#", "Source Generators"], - Categories = ["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 ?? [])}"); - 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 1769705..648585b 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,104 @@ 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; + + // Always make properties nullable to avoid warnings + if (isList) + { + propertyType = $"List<{typeName}Model>?"; + } + else + { + propertyType = $"{typeName}Model?"; + } } - 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?"; } - } - 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 and make them nullable + var baseType = GraphQLParser.MapToCSharpType(field.Type ?? new GraphQLType { Name = "String", IsNullable = true }); + + // If it's not already nullable and not a value type, make it nullable + if (!baseType.EndsWith("?") && !IsValueType(baseType)) + { + propertyType = baseType + "?"; + } + else + { + propertyType = baseType; + } } + + // 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}"); + } + + // Helper method to determine if a type is a value type + private bool IsValueType(string typeName) + { + return typeName == "int" || + typeName == "long" || + typeName == "float" || + typeName == "double" || + typeName == "decimal" || + typeName == "bool" || + typeName == "DateTime" || + typeName == "Guid" || + typeName == "TimeSpan" || + typeName == "DateTimeOffset" || + typeName == "byte" || + typeName == "sbyte" || + typeName == "short" || + typeName == "ushort" || + typeName == "uint" || + typeName == "ulong" || + typeName == "char"; } } } \ No newline at end of file diff --git a/GraphQLSourceGen/Parsing/GraphQLParser.cs b/GraphQLSourceGen/Parsing/GraphQLParser.cs index 41bb68d..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,308 +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)) { - Name = "UserDetails", - OnType = "User", - Fields = - [ - new GraphQLField { Name = "id", Type = new GraphQLType { Name = "String", IsNullable = true } }, - new GraphQLField - { - Name = "profile", - Type = new GraphQLType { Name = "Profile", IsNullable = true }, - SelectionSet = - [ - new GraphQLField - { Name = "bio", Type = new GraphQLType { Name = "String", IsNullable = true } }, - - new GraphQLField - { - Name = "avatarUrl", Type = new GraphQLType { Name = "String", IsNullable = true } - } - ] - } - ] - }; - - return [fragment]; - } + return fragments; + } - // 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"); + // Tokenize the content + var tokens = Tokenize(content); + int position = 0; - var fragment = new GraphQLFragment + // Parse fragments + while (position < tokens.Count) { - Name = "UserWithPosts", - OnType = "User", - Fields = - [ - fragmentSpreadField, - new GraphQLField + if (position + 3 < tokens.Count && + tokens[position].Value == "fragment" && + tokens[position + 2].Value == "on") + { + try { - Name = "posts", - Type = new GraphQLType { Name = "Post", IsNullable = true, IsList = true }, - SelectionSet = - [ - new GraphQLField - { Name = "id", Type = new GraphQLType { Name = "String", IsNullable = true } }, - - new GraphQLField - { Name = "title", Type = new GraphQLType { Name = "String", IsNullable = true } } - ] + var fragment = ParseFragment(tokens, ref position); + if (fragment != null) + { + fragments.Add(fragment); + } } - ] - }; + catch (Exception ex) + { + Console.WriteLine($"Error parsing fragment: {ex.Message}"); + // Skip to next fragment + while (position < tokens.Count && tokens[position].Value != "fragment") + { + position++; + } + } + } + else + { + position++; + } + } - return [fragment]; + return fragments; } + catch (Exception ex) + { + Console.WriteLine($"Error parsing GraphQL content: {ex.Message}"); + return new List(); + } + } - // Special case for the deprecated fields test - if (content.Contains("fragment UserWithDeprecated on User") && content.Contains("@deprecated")) + /// + /// Tokenize GraphQL content + /// + private static List Tokenize(string content) + { + var tokens = new List(); + int position = 0; + + while (position < content.Length) { - var fragment = new GraphQLFragment + char c = content[position]; + + // Skip whitespace + if (char.IsWhiteSpace(c)) { - Name = "UserWithDeprecated", - OnType = "User", - Fields = - [ - 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" - }, + position++; + continue; + } - new GraphQLField - { - Name = "oldField", - Type = new GraphQLType { Name = "String", IsNullable = true }, - IsDeprecated = true - } - ] - }; + // Skip comments + if (c == '#') + { + while (position < content.Length && content[position] != '\n') + { + position++; + } + continue; + } - return [fragment]; - } + // Handle punctuation + if (c == '{' || c == '}' || c == ':' || c == '!' || c == '@' || c == '[' || c == ']') + { + tokens.Add(new Token { Type = TokenType.Punctuation, Value = c.ToString() }); + position++; + continue; + } - // Special case for the scalar types test - if (content.Contains("fragment PostWithStats on Post") && content.Contains("categories: [String!]!")) - { - var fragment = new GraphQLFragment + // Handle fragment spread + if (c == '.' && position + 2 < content.Length && content[position + 1] == '.' && content[position + 2] == '.') { - Name = "PostWithStats", - OnType = "Post", - Fields = - [ - 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 - { - Name = "tags", - Type = new GraphQLType - { - IsList = true, - IsNullable = true, - OfType = new GraphQLType { Name = "String", IsNullable = true } - } - }, + tokens.Add(new Token { Type = TokenType.Spread, Value = "..." }); + position += 3; + continue; + } - new GraphQLField + // Handle strings + if (c == '"') + { + int start = position; + position++; // Skip opening quote + while (position < content.Length && content[position] != '"') + { + // Handle escaped quotes + if (content[position] == '\\' && position + 1 < content.Length && content[position + 1] == '"') { - Name = "categories", - Type = new GraphQLType - { - IsList = true, - IsNullable = false, - OfType = new GraphQLType { Name = "String", IsNullable = false } - } + position += 2; } - ] - }; - - return [fragment]; - } - - // Regular parsing for other cases - var fragments = new List(); - var matches = FragmentRegex.Matches(content); + else + { + position++; + } + } + position++; // Skip closing quote + tokens.Add(new Token { Type = TokenType.String, Value = content.Substring(start, position - start) }); + continue; + } - foreach (Match match in matches) - { - var fragment = new GraphQLFragment + // Handle identifiers and keywords + if (char.IsLetter(c) || c == '_') { - Name = match.Groups["name"].Value, - OnType = match.Groups["type"].Value, - Fields = ParseFields(match.Groups["body"].Value) - }; + 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; + } - fragments.Add(fragment); + // Skip any other character + position++; } - return fragments; + return tokens; } /// - /// Parse fields from a GraphQL selection set + /// Parse a GraphQL fragment /// - static List ParseFields(string selectionSet) + private static GraphQLFragment ParseFragment(List tokens, ref int position) { - var fields = new List(); + // fragment Name on Type { ... } + if (tokens[position].Value != "fragment") + { + throw new Exception("Expected 'fragment' keyword"); + } + position++; // Skip 'fragment' - // Split the selection set into lines - string[] lines = selectionSet.Split('\n'); + // 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 - // Process each line - for (int i = 0; i < lines.Length; i++) + // Expect 'on' keyword + if (position >= tokens.Count || tokens[position].Value != "on") { - string line = lines[i].Trim(); + throw new Exception("Expected 'on' keyword"); + } + position++; // Skip 'on' - // Skip empty lines - if (string.IsNullOrWhiteSpace(line)) - continue; + // 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 - // 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; - } + // Expect opening brace + if (position >= tokens.Count || tokens[position].Value != "{") + { + throw new Exception("Expected '{'"); + } + position++; // Skip '{' - // Check if this is a field - int colonIndex = line.IndexOf(':'); - int openBraceIndex = line.IndexOf('{'); + // Parse fields + var fields = ParseSelectionSet(tokens, ref position); - string fieldName; - string fieldType = ""; - bool hasNestedSelection = false; + // Create and return the fragment + return new GraphQLFragment + { + Name = fragmentName, + OnType = typeName, + Fields = fields + }; + } - // Extract field name - if (colonIndex > 0 && (openBraceIndex == -1 || colonIndex < openBraceIndex)) - { - // Field with type annotation - fieldName = line.Substring(0, colonIndex).Trim(); + /// + /// Parse a GraphQL selection set + /// + private static List ParseSelectionSet(List tokens, ref int position) + { + var fields = new List(); - // Extract type - int endTypeIndex = openBraceIndex > 0 ? openBraceIndex : line.Length; - fieldType = line.Substring(colonIndex + 1, endTypeIndex - colonIndex - 1).Trim(); - } - else if (openBraceIndex > 0) + // Parse fields until closing brace + while (position < tokens.Count && tokens[position].Value != "}") + { + try { - // 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), - }; + // Skip closing brace + if (position < tokens.Count && tokens[position].Value == "}") + { + position++; + } - // Check for deprecated directive - if (line.Contains("@deprecated")) - { - field.IsDeprecated = true; + return fields; + } - // Check for deprecation reason - int reasonStart = line.IndexOf("reason:"); - if (reasonStart > 0) + /// + /// 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) + { + 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++; } + } + + // 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) + }; - // Handle nested selection - if (hasNestedSelection) + // 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; + field.IsDeprecated = true; + position++; // Skip 'deprecated' - for (int j = i; j < lines.Length; j++) + // 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 != ")") { - nestedSelectionBuilder.AppendLine(lines[j]); + position++; + } + if (position < tokens.Count) + { + position++; // Skip ')' } - - string nestedSelection = nestedSelectionBuilder.ToString(); - field.SelectionSet = ParseFields(nestedSelection); - - // Skip the processed lines - i = endLine; } } - - fields.Add(field); } - return fields; + 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)); + + // 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 + } + + // Handle non-null + if (position < tokens.Count && tokens[position].Value == "!") + { + typeBuilder.Append('!'); + position++; // Skip '!' + } + + return typeBuilder.ToString(); } /// @@ -417,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