diff --git a/src/Refitter.Core/ParameterExtractor.cs b/src/Refitter.Core/ParameterExtractor.cs index 51f4ac8f..5680e2f1 100644 --- a/src/Refitter.Core/ParameterExtractor.cs +++ b/src/Refitter.Core/ParameterExtractor.cs @@ -51,9 +51,7 @@ public static IEnumerable GetParameters( continue; } - if (securityScheme.Type == OpenApiSecuritySchemeType.ApiKey - && securityScheme.In == OpenApiSecurityApiKeyLocation.Header - && !operationModel.Parameters.Any(p => p.Kind == OpenApiParameterKind.Header && p.IsHeader && p.Name == securityScheme.Name)) + if (ShouldGenerateHeaderParameter(securityScheme, operationModel)) { headerParameters.Add($"[Header(\"{securityScheme.Name}\")] string {ReplaceUnsafeCharacters(securityScheme.Name)}"); } @@ -305,4 +303,11 @@ private static List GetQueryParameters(CSharpOperationModel operationMod return parameters; } + + private static bool ShouldGenerateHeaderParameter(OpenApiSecurityScheme securityScheme, CSharpOperationModel operationModel) + { + return securityScheme.Type == OpenApiSecuritySchemeType.ApiKey + && securityScheme.In == OpenApiSecurityApiKeyLocation.Header + && !operationModel.Parameters.Any(p => p.Kind == OpenApiParameterKind.Header && p.IsHeader && p.Name == securityScheme.Name); + } } diff --git a/src/Refitter.Core/RefitInterfaceGenerator.cs b/src/Refitter.Core/RefitInterfaceGenerator.cs index 0a15a43c..4acffc3a 100644 --- a/src/Refitter.Core/RefitInterfaceGenerator.cs +++ b/src/Refitter.Core/RefitInterfaceGenerator.cs @@ -8,6 +8,15 @@ namespace Refitter.Core; internal class RefitInterfaceGenerator : IRefitInterfaceGenerator { protected const string Separator = " "; + + // HTTP Status Code Constants + private static readonly string[] SuccessStatusCodes = { "200", "201", "203", "206" }; + private const string StatusCode2XX = "2XX"; + private const string DefaultResponseKey = "default"; + + // Content Type Constants + private const string MultipartFormDataContentType = "multipart/form-data"; + private const string ApplicationJsonContentType = "application/json"; protected readonly RefitGeneratorSettings settings; protected readonly OpenApiDocument document; @@ -116,22 +125,21 @@ protected string GetTypeName(OpenApiOperation operation) } // First check for explicit success status codes - var successCodes = new[] { "200", "201", "203", "206" }; - var returnTypeParameter = successCodes + var returnTypeParameter = SuccessStatusCodes .Where(operation.Responses.ContainsKey) .Select(code => GetTypeName(code, operation)) .FirstOrDefault(); // If no explicit success codes found, check for 2XX range - if (returnTypeParameter == null && operation.Responses.ContainsKey("2XX")) + if (returnTypeParameter == null && operation.Responses.ContainsKey(StatusCode2XX)) { - returnTypeParameter = GetTypeName("2XX", operation); + returnTypeParameter = GetTypeName(StatusCode2XX, operation); } // If no success codes or ranges found, check for default response - if (returnTypeParameter == null && operation.Responses.ContainsKey("default")) + if (returnTypeParameter == null && operation.Responses.ContainsKey(DefaultResponseKey)) { - returnTypeParameter = GetTypeName("default", operation); + returnTypeParameter = GetTypeName(DefaultResponseKey, operation); } return GetReturnType(returnTypeParameter); @@ -180,7 +188,7 @@ protected string GenerateOperationName( protected static void GenerateForMultipartFormData(CSharpOperationModel operationModel, StringBuilder code) { - if (operationModel.Consumes.Contains("multipart/form-data")) + if (operationModel.Consumes.Contains(MultipartFormDataContentType)) { code.AppendLine($"{Separator}{Separator}[Multipart]"); } @@ -216,10 +224,10 @@ protected void GenerateHeaders( { var uniqueContentTypes = operations.Value.RequestBody?.Content.Keys ?? Array.Empty(); var contentType = - uniqueContentTypes.FirstOrDefault(c => c.Equals("application/json", StringComparison.OrdinalIgnoreCase)) ?? + uniqueContentTypes.FirstOrDefault(c => c.Equals(ApplicationJsonContentType, StringComparison.OrdinalIgnoreCase)) ?? uniqueContentTypes.FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(contentType) && !operationModel.Consumes.Contains("multipart/form-data")) + if (!string.IsNullOrWhiteSpace(contentType) && !operationModel.Consumes.Contains(MultipartFormDataContentType)) { headers.Add($"\"Content-Type: {contentType}\""); } diff --git a/src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs b/src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs index d41fe360..c29ca310 100644 --- a/src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs +++ b/src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs @@ -23,16 +23,18 @@ public override IEnumerable GenerateCode() : settings.Naming.InterfaceName; ungroupedTitle = ungroupedTitle.CapitalizeFirstCharacter(); - var byGroup = document.Paths - .SelectMany(x => x.Value, (k, v) => (PathItem: k, Operation: v)) - .GroupBy(x => GetGroupName(x.Operation.Value, ungroupedTitle), (k, v) => new { Key = k, Combined = v }); + var pathOperations = document.Paths + .SelectMany(x => x.Value, (pathItem, operation) => new { PathItem = pathItem, Operation = operation }); + + var byGroup = pathOperations + .GroupBy(x => GetGroupName(x.Operation.Value, ungroupedTitle)); Dictionary interfacesByGroup = new(); Dictionary interfacesNamesByGroup = new(); - foreach (var kv in byGroup) + foreach (var group in byGroup) { - foreach (var op in kv.Combined) + foreach (var op in group) { var operations = op.Operation; var operation = operations.Value; @@ -46,18 +48,18 @@ public override IEnumerable GenerateCode() var verb = operations.Key.CapitalizeFirstCharacter(); string interfaceName = null!; - if (!interfacesByGroup.TryGetValue(kv.Key, out var sb)) + if (!interfacesByGroup.TryGetValue(group.Key, out var sb)) { - interfacesByGroup[kv.Key] = sb = new StringBuilder(); + interfacesByGroup[group.Key] = sb = new StringBuilder(); this.docGenerator.AppendInterfaceDocumentation(operation, sb); - interfaceName = GetInterfaceName(kv.Key); + interfaceName = GetInterfaceName(group.Key); sb.AppendLine($$""" {{GenerateInterfaceDeclaration(interfaceName)}} {{Separator}}{ """); - interfacesNamesByGroup[kv.Key] = interfaceName; + interfacesNamesByGroup[group.Key] = interfaceName; } var operationName = GetOperationName(interfaceName, op.PathItem.Key, operations.Key, operation); diff --git a/src/Refitter/GenerateCommand.cs b/src/Refitter/GenerateCommand.cs index d10e7e45..2402dfe2 100644 --- a/src/Refitter/GenerateCommand.cs +++ b/src/Refitter/GenerateCommand.cs @@ -11,6 +11,21 @@ namespace Refitter; public sealed class GenerateCommand : AsyncCommand { private static readonly string Crlf = Environment.NewLine; + + // Constants for better maintainability + private const int FileSize1024 = 1024; + private const int SpinnerDelayMs = 100; + private const string LocalBuildVersion = " (local build)"; + private const string GitHubIssuesUrl = "https://github.com/christianhelle/refitter/issues"; + private const string GitHubSponsorsUrl = "https://github.com/sponsors/christianhelle"; + private const string BuyMeCoffeeUrl = "https://www.buymeacoffee.com/christianhelle"; + private const string SkipValidationSuggestion = "💡 Try using the --skip-validation argument."; + private const string GenerationFailedMessage = "Generation failed!"; + private const string GenerationSuccessMessage = "Generation completed successfully!"; + private const int TableWidth = 55; + private const int FileNameColumnWidth = 30; + private const int FileSizeColumnWidth = 10; + private const int FileLinesColumnWidth = 10; public override ValidationResult Validate(CommandContext context, Settings settings) { @@ -30,70 +45,16 @@ public override async Task ExecuteAsync(CommandContext context, Settings se try { var stopwatch = Stopwatch.StartNew(); - var version = GetType().Assembly.GetName().Version!.ToString(); - if (version == "1.0.0.0") - version += " (local build)"; - - const string asciiArt = -""" - ██████╗ ███████╗███████╗██╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝██╔════╝██║╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ █████╗ ██║ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██╔══╝ ██║ ██║ ██║ ██╔══╝ ██╔══██╗ - ██║ ██║███████╗██║ ██║ ██║ ██║ ███████╗██║ ██║ - ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ -"""; - - // Header with branding - if (settings.SimpleOutput) - { - Console.WriteLine(); - Console.WriteLine($"Refitter v{version}"); - Console.WriteLine("OpenAPI to Refit Interface Generator"); - Console.WriteLine(); - } - else - { - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine($"[bold cyan]{asciiArt}[/]"); - AnsiConsole.MarkupLine($"[bold cyan]╔═══════════════════════════════════════════════════════════════╗[/]"); - AnsiConsole.MarkupLine($"[bold cyan]║[/] [bold white]🚀 Refitter v{version,-48}[/] [bold cyan]║[/]"); - AnsiConsole.MarkupLine($"[bold cyan]║[/] [dim] OpenAPI to Refit Interface Generator[/]{new string(' ', 22)} [bold cyan]║[/]"); - AnsiConsole.MarkupLine($"[bold cyan]╚═══════════════════════════════════════════════════════════════╝[/]"); - AnsiConsole.WriteLine(); - } - - // Support information - var supportKey = settings.NoLogging - ? "Unavailable when logging is disabled" - : SupportInformation.GetSupportKey(); - if (settings.SimpleOutput) - { - Console.WriteLine($"Support key: {supportKey}"); - Console.WriteLine(); - } - else - { - AnsiConsole.MarkupLine($"[dim]🔑 Support key: {supportKey}[/]"); - AnsiConsole.WriteLine(); - } + DisplayHeader(settings); + DisplaySupportInfo(settings); - if (context.Arguments.Any(a => a.Equals("--version", StringComparison.OrdinalIgnoreCase)) || - context.Arguments.Any(a => a.Equals("-v", StringComparison.OrdinalIgnoreCase))) + if (IsVersionRequest(context)) { return 0; } - if (!string.IsNullOrWhiteSpace(settings.SettingsFilePath)) - { - var json = await File.ReadAllTextAsync(settings.SettingsFilePath); - refitGeneratorSettings = Serializer.Deserialize(json); - refitGeneratorSettings.OpenApiPath = settings.OpenApiPath!; - - if (!string.IsNullOrWhiteSpace(refitGeneratorSettings.ContractsOutputFolder)) - refitGeneratorSettings.GenerateMultipleFiles = true; - } + refitGeneratorSettings = await LoadSettingsFileIfProvided(settings, refitGeneratorSettings); var generator = await RefitGenerator.CreateAsync(refitGeneratorSettings); if (!settings.SkipValidation) @@ -105,166 +66,15 @@ public override async Task ExecuteAsync(CommandContext context, Settings se Analytics.LogFeatureUsage(settings, refitGeneratorSettings); - if (refitGeneratorSettings.IncludePathMatches.Length > 0 && - generator.OpenApiDocument.Paths.Count == 0) - { - Console.WriteLine("⚠️ WARNING: All API paths were filtered out by --match-path patterns. ⚠️"); - Console.WriteLine($" Match Patterns used: {string.Join(", ", refitGeneratorSettings.IncludePathMatches)}"); - Console.WriteLine(); - Console.WriteLine(" This could indicate that:"); - Console.WriteLine(" 1. The regex patterns don't match any available paths"); - Console.WriteLine(" 2. There's a syntax error in the regex patterns"); - Console.WriteLine(" 3. The patterns were corrupted by command line interpretation"); - Console.WriteLine(); - Console.WriteLine(" This commonly happens when using the Windows Command Prompt (CMD) instead of PowerShell."); - Console.WriteLine(" The ^ character in regex patterns is interpreted as an escape character in CMD."); - Console.WriteLine(); - Console.WriteLine(" Solutions:"); - Console.WriteLine(" 1. Use PowerShell instead of CMD"); - Console.WriteLine(" 2. In CMD, escape the ^ character or use different quoting"); - Console.WriteLine(" 3. Use a .refitter settings file instead of command line arguments"); - Console.WriteLine(); - } - - // Success summary with performance metrics - stopwatch.Stop(); - if (settings.SimpleOutput) - { - Console.WriteLine("Generation completed successfully!"); - Console.WriteLine($"Duration: {stopwatch.Elapsed:mm\\:ss\\.ffff}"); - Console.WriteLine($"Performance: {(refitGeneratorSettings.GenerateMultipleFiles ? "Multi-file" : "Single-file")} generation"); - Console.WriteLine(); - } - else - { - var successPanel = new Panel( - $"[bold green]✅ Generation completed successfully![/]\n\n" + - $"[dim]📊 Duration:[/] [green]{stopwatch.Elapsed:mm\\:ss\\.ffff}[/]\n" + - $"[dim]🚀 Performance:[/] [green]{(refitGeneratorSettings.GenerateMultipleFiles ? "Multi-file" : "Single-file")} generation[/]" - ) - .BorderColor(Color.Green) - .RoundedBorder() - .Header("[bold green]🎉 Success[/]") - .HeaderAlignment(Justify.Center); - - AnsiConsole.Write(successPanel); - AnsiConsole.WriteLine(); - } - - if (!settings.NoBanner) - { - if (settings.SimpleOutput) - SimpleDonationBanner(); - else - DonationBanner(); - } - + ShowPathFilteringWarnings(refitGeneratorSettings, generator); + ShowSuccessSummary(settings, refitGeneratorSettings, stopwatch); + ShowDonationBanner(settings); ShowWarnings(refitGeneratorSettings, settings); return 0; } catch (Exception exception) { - // Error summary panel - if (settings.SimpleOutput) - { - Console.WriteLine("Generation failed!"); - Console.WriteLine(); - } - else - { - var errorPanel = new Panel("[bold red]❌ Generation failed![/]") - .BorderColor(Color.Red) - .RoundedBorder() - .Header("[bold red]🚨 Error[/]") - .HeaderAlignment(Justify.Center); - - AnsiConsole.Write(errorPanel); - AnsiConsole.WriteLine(); - } - - if (exception is OpenApiUnsupportedSpecVersionException unsupportedSpecVersionException) - { - if (settings.SimpleOutput) - { - Console.WriteLine($"Unsupported OpenAPI version: {unsupportedSpecVersionException.SpecificationVersion}"); - Console.WriteLine(); - } - else - { - var versionPanel = new Panel( - $"[bold red]🚫 Unsupported OpenAPI version: {unsupportedSpecVersionException.SpecificationVersion}[/]" - ) - .BorderColor(Color.Red) - .RoundedBorder(); - - AnsiConsole.Write(versionPanel); - AnsiConsole.WriteLine(); - } - } - - if (exception is not OpenApiValidationException) - { - if (settings.SimpleOutput) - { - Console.WriteLine("Exception Details:"); - Console.WriteLine(exception.ToString()); - Console.WriteLine(); - } - else - { - AnsiConsole.MarkupLine("[bold red]🐛 Exception Details:[/]"); - AnsiConsole.WriteException(exception); - AnsiConsole.WriteLine(); - } - } - - if (!settings.SkipValidation) - { - if (settings.SimpleOutput) - { - Console.WriteLine("Suggestion"); - Console.WriteLine("Try using the --skip-validation argument."); - Console.WriteLine(); - } - else - { - var suggestionPanel = new Panel( - "💡 Try using the --skip-validation argument." - ) - .BorderColor(Color.Yellow) - .RoundedBorder() - .Header("Suggestion"); - - AnsiConsole.Write(suggestionPanel); - AnsiConsole.WriteLine(); - } - } - - if (settings.SimpleOutput) - { - Console.WriteLine("Support"); - Console.WriteLine("Need Help?"); - Console.WriteLine(); - Console.WriteLine("Report an issue: https://github.com/christianhelle/refitter/issues"); - Console.WriteLine(); - } - else - { - var helpPanel = new Panel( - "🆘 Need Help?\n\n" + - "🐛 Report an issue: https://github.com/christianhelle/refitter/issues" - ) - .BorderColor(Color.Yellow) - .RoundedBorder() - .Header("Support") - .HeaderAlignment(Justify.Center); - - AnsiConsole.Write(helpPanel); - AnsiConsole.WriteLine(); - } - - await Analytics.LogError(exception, settings); - return exception.HResult; + return await HandleException(exception, settings); } } @@ -328,7 +138,7 @@ await AnsiConsole.Status() .SpinnerStyle(Style.Parse("green bold")) .StartAsync("[yellow]🔧 Generating code...[/]", async _ => { - await Task.Delay(100); // Brief delay to show spinner + await Task.Delay(SpinnerDelayMs); // Brief delay to show spinner }); } @@ -379,9 +189,9 @@ private static string FormatFileSize(long bytes) double size = bytes; int suffixIndex = 0; - while (size >= 1024 && suffixIndex < suffixes.Length - 1) + while (size >= FileSize1024 && suffixIndex < suffixes.Length - 1) { - size /= 1024; + size /= FileSize1024; suffixIndex++; } @@ -406,7 +216,7 @@ private async Task WriteMultipleFiles( .SpinnerStyle(Style.Parse("green bold")) .StartAsync("[yellow]🔧 Generating multiple files...[/]", async _ => { - await Task.Delay(100); // Brief delay to show spinner + await Task.Delay(SpinnerDelayMs); // Brief delay to show spinner return generator.GenerateMultipleFiles(); }); } @@ -419,8 +229,8 @@ private async Task WriteMultipleFiles( if (settings.SimpleOutput) { Console.WriteLine("Generated Output Files"); - Console.WriteLine($"{"File",-30} {"Size",-10} {"Lines",-10}"); - Console.WriteLine(new string('-', 55)); + Console.WriteLine($"{"File",-FileNameColumnWidth} {"Size",-FileSizeColumnWidth} {"Lines",-FileLinesColumnWidth}"); + Console.WriteLine(new string('-', TableWidth)); } else { @@ -459,7 +269,7 @@ private async Task WriteMultipleFiles( if (settings.SimpleOutput) { - Console.WriteLine($"{outputFile.Filename,-30} {sizeFormatted,-10} {lines,-10:N0}"); + Console.WriteLine($"{outputFile.Filename,-FileNameColumnWidth} {sizeFormatted,-FileSizeColumnWidth} {lines,-FileLinesColumnWidth:N0}"); } else { @@ -486,7 +296,7 @@ private async Task WriteMultipleFiles( if (settings.SimpleOutput) { - Console.WriteLine($"{outputFile.Filename,-30} {formattedSize,-10} {fileLines,-10:N0}"); + Console.WriteLine($"{outputFile.Filename,-FileNameColumnWidth} {formattedSize,-FileSizeColumnWidth} {fileLines,-FileLinesColumnWidth:N0}"); } else { @@ -510,8 +320,8 @@ private async Task WriteMultipleFiles( if (settings.SimpleOutput) { - Console.WriteLine(new string('-', 55)); - Console.WriteLine($"{"Total (" + generatorOutput.Files.Count + " files)",-30} {FormatFileSize(totalSize),-10} {totalLines,-10:N0}"); + Console.WriteLine(new string('-', TableWidth)); + Console.WriteLine($"{"Total (" + generatorOutput.Files.Count + " files)",-FileNameColumnWidth} {FormatFileSize(totalSize),-FileSizeColumnWidth} {totalLines,-FileLinesColumnWidth:N0}"); Console.WriteLine(); } else @@ -594,9 +404,9 @@ private static void DonationBanner() { var panel = new Panel( "[yellow]💖 [bold]Enjoying Refitter?[/] Consider supporting the project![/]\n\n" + - "[cyan]🎯 Sponsor:[/] [link]https://github.com/sponsors/christianhelle[/]\n" + - "[yellow]☕ Buy me a coffee:[/] [link]https://www.buymeacoffee.com/christianhelle[/]\n\n" + - "[red]🐛 Found an issue?[/] [link]https://github.com/christianhelle/refitter/issues[/]" + $"[cyan]🎯 Sponsor:[/] [link]{GitHubSponsorsUrl}[/]\n" + + $"[yellow]☕ Buy me a coffee:[/] [link]{BuyMeCoffeeUrl}[/]\n\n" + + $"[red]🐛 Found an issue?[/] [link]{GitHubIssuesUrl}[/]" ) .BorderColor(Color.Grey) .RoundedBorder() @@ -612,10 +422,10 @@ private static void SimpleDonationBanner() Console.WriteLine("Support"); Console.WriteLine("Enjoying Refitter? Consider supporting the project!"); Console.WriteLine(); - Console.WriteLine("Sponsor: https://github.com/sponsors/christianhelle"); - Console.WriteLine("Buy me a coffee: https://www.buymeacoffee.com/christianhelle"); + Console.WriteLine($"Sponsor: {GitHubSponsorsUrl}"); + Console.WriteLine($"Buy me a coffee: {BuyMeCoffeeUrl}"); Console.WriteLine(); - Console.WriteLine("Found an issue? https://github.com/christianhelle/refitter/issues"); + Console.WriteLine($"Found an issue? {GitHubIssuesUrl}"); Console.WriteLine(); } @@ -825,4 +635,243 @@ private static void TryWriteLine( Console.ForegroundColor = originalColor; } } + + private static void DisplayHeader(Settings settings) + { + var version = typeof(GenerateCommand).Assembly.GetName().Version!.ToString(); + if (version == "1.0.0.0") + version += LocalBuildVersion; + + const string asciiArt = +""" + ██████╗ ███████╗███████╗██╗████████╗████████╗███████╗██████╗ + ██╔══██╗██╔════╝██╔════╝██║╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ + ██████╔╝█████╗ █████╗ ██║ ██║ ██║ █████╗ ██████╔╝ + ██╔══██╗██╔══╝ ██╔══╝ ██║ ██║ ██║ ██╔══╝ ██╔══██╗ + ██║ ██║███████╗██║ ██║ ██║ ██║ ███████╗██║ ██║ + ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +"""; + + if (settings.SimpleOutput) + { + Console.WriteLine(); + Console.WriteLine($"Refitter v{version}"); + Console.WriteLine("OpenAPI to Refit Interface Generator"); + Console.WriteLine(); + } + else + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[bold cyan]{asciiArt}[/]"); + AnsiConsole.MarkupLine($"[bold cyan]╔═══════════════════════════════════════════════════════════════╗[/]"); + AnsiConsole.MarkupLine($"[bold cyan]║[/] [bold white]🚀 Refitter v{version,-48}[/] [bold cyan]║[/]"); + AnsiConsole.MarkupLine($"[bold cyan]║[/] [dim] OpenAPI to Refit Interface Generator[/]{new string(' ', 22)} [bold cyan]║[/]"); + AnsiConsole.MarkupLine($"[bold cyan]╚═══════════════════════════════════════════════════════════════╝[/]"); + AnsiConsole.WriteLine(); + } + } + + private static void DisplaySupportInfo(Settings settings) + { + var supportKey = settings.NoLogging + ? "Unavailable when logging is disabled" + : SupportInformation.GetSupportKey(); + + if (settings.SimpleOutput) + { + Console.WriteLine($"Support key: {supportKey}"); + Console.WriteLine(); + } + else + { + AnsiConsole.MarkupLine($"[dim]🔑 Support key: {supportKey}[/]"); + AnsiConsole.WriteLine(); + } + } + + private static bool IsVersionRequest(CommandContext context) + { + return context.Arguments.Any(a => a.Equals("--version", StringComparison.OrdinalIgnoreCase)) || + context.Arguments.Any(a => a.Equals("-v", StringComparison.OrdinalIgnoreCase)); + } + + private static async Task LoadSettingsFileIfProvided(Settings settings, RefitGeneratorSettings refitGeneratorSettings) + { + if (!string.IsNullOrWhiteSpace(settings.SettingsFilePath)) + { + var json = await File.ReadAllTextAsync(settings.SettingsFilePath); + refitGeneratorSettings = Serializer.Deserialize(json); + refitGeneratorSettings.OpenApiPath = settings.OpenApiPath!; + + if (!string.IsNullOrWhiteSpace(refitGeneratorSettings.ContractsOutputFolder)) + refitGeneratorSettings.GenerateMultipleFiles = true; + } + + return refitGeneratorSettings; + } + + private static void ShowPathFilteringWarnings(RefitGeneratorSettings refitGeneratorSettings, RefitGenerator generator) + { + if (refitGeneratorSettings.IncludePathMatches.Length > 0 && + generator.OpenApiDocument.Paths.Count == 0) + { + Console.WriteLine("⚠️ WARNING: All API paths were filtered out by --match-path patterns. ⚠️"); + Console.WriteLine($" Match Patterns used: {string.Join(", ", refitGeneratorSettings.IncludePathMatches)}"); + Console.WriteLine(); + Console.WriteLine(" This could indicate that:"); + Console.WriteLine(" 1. The regex patterns don't match any available paths"); + Console.WriteLine(" 2. There's a syntax error in the regex patterns"); + Console.WriteLine(" 3. The patterns were corrupted by command line interpretation"); + Console.WriteLine(); + Console.WriteLine(" This commonly happens when using the Windows Command Prompt (CMD) instead of PowerShell."); + Console.WriteLine(" The ^ character in regex patterns is interpreted as an escape character in CMD."); + Console.WriteLine(); + Console.WriteLine(" Solutions:"); + Console.WriteLine(" 1. Use PowerShell instead of CMD"); + Console.WriteLine(" 2. In CMD, escape the ^ character or use different quoting"); + Console.WriteLine(" 3. Use a .refitter settings file instead of command line arguments"); + Console.WriteLine(); + } + } + + private static void ShowSuccessSummary(Settings settings, RefitGeneratorSettings refitGeneratorSettings, Stopwatch stopwatch) + { + stopwatch.Stop(); + if (settings.SimpleOutput) + { + Console.WriteLine(GenerationSuccessMessage); + Console.WriteLine($"Duration: {stopwatch.Elapsed:mm\\:ss\\.ffff}"); + Console.WriteLine($"Performance: {(refitGeneratorSettings.GenerateMultipleFiles ? "Multi-file" : "Single-file")} generation"); + Console.WriteLine(); + } + else + { + var successPanel = new Panel( + $"[bold green]✅ {GenerationSuccessMessage}[/]\n\n" + + $"[dim]📊 Duration:[/] [green]{stopwatch.Elapsed:mm\\:ss\\.ffff}[/]\n" + + $"[dim]🚀 Performance:[/] [green]{(refitGeneratorSettings.GenerateMultipleFiles ? "Multi-file" : "Single-file")} generation[/]" + ) + .BorderColor(Color.Green) + .RoundedBorder() + .Header("[bold green]🎉 Success[/]") + .HeaderAlignment(Justify.Center); + + AnsiConsole.Write(successPanel); + AnsiConsole.WriteLine(); + } + } + + private static void ShowDonationBanner(Settings settings) + { + if (!settings.NoBanner) + { + if (settings.SimpleOutput) + SimpleDonationBanner(); + else + DonationBanner(); + } + } + + private static async Task HandleException(Exception exception, Settings settings) + { + // Error summary panel + if (settings.SimpleOutput) + { + Console.WriteLine(GenerationFailedMessage); + Console.WriteLine(); + } + else + { + var errorPanel = new Panel($"[bold red]❌ {GenerationFailedMessage}[/]") + .BorderColor(Color.Red) + .RoundedBorder() + .Header("[bold red]🚨 Error[/]") + .HeaderAlignment(Justify.Center); + + AnsiConsole.Write(errorPanel); + AnsiConsole.WriteLine(); + } + + if (exception is OpenApiUnsupportedSpecVersionException unsupportedSpecVersionException) + { + if (settings.SimpleOutput) + { + Console.WriteLine($"Unsupported OpenAPI version: {unsupportedSpecVersionException.SpecificationVersion}"); + Console.WriteLine(); + } + else + { + var versionPanel = new Panel( + $"[bold red]🚫 Unsupported OpenAPI version: {unsupportedSpecVersionException.SpecificationVersion}[/]" + ) + .BorderColor(Color.Red) + .RoundedBorder(); + + AnsiConsole.Write(versionPanel); + AnsiConsole.WriteLine(); + } + } + + if (exception is not OpenApiValidationException) + { + if (settings.SimpleOutput) + { + Console.WriteLine("Exception Details:"); + Console.WriteLine(exception.ToString()); + Console.WriteLine(); + } + else + { + AnsiConsole.MarkupLine("[bold red]🐛 Exception Details:[/]"); + AnsiConsole.WriteException(exception); + AnsiConsole.WriteLine(); + } + } + + if (settings.SkipValidation == false) + { + if (settings.SimpleOutput) + { + Console.WriteLine("Suggestion"); + Console.WriteLine("Try using the --skip-validation argument."); + Console.WriteLine(); + } + else + { + var suggestionPanel = new Panel(SkipValidationSuggestion) + .BorderColor(Color.Yellow) + .RoundedBorder() + .Header("Suggestion"); + + AnsiConsole.Write(suggestionPanel); + AnsiConsole.WriteLine(); + } + } + + if (settings.SimpleOutput) + { + Console.WriteLine("Support"); + Console.WriteLine("Need Help?"); + Console.WriteLine(); + Console.WriteLine($"Report an issue: {GitHubIssuesUrl}"); + Console.WriteLine(); + } + else + { + var helpPanel = new Panel( + $"🆘 Need Help?\n\n" + + $"🐛 Report an issue: {GitHubIssuesUrl}" + ) + .BorderColor(Color.Yellow) + .RoundedBorder() + .Header("Support") + .HeaderAlignment(Justify.Center); + + AnsiConsole.Write(helpPanel); + AnsiConsole.WriteLine(); + } + + await Analytics.LogError(exception, settings); + return exception.HResult; + } }