diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7ba18b1f..5565a864 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -137,11 +137,20 @@ dotnet run --project src/Refitter --configuration Release --framework net9.0 -- ## Common Development Tasks ### Adding New CLI Options -1. Update `src/Refitter/GenerateCommand.cs` with new command-line arguments -2. Add corresponding property to `src/Refitter.Core/Settings/RefitGeneratorSettings.cs` -3. Implement logic in `src/Refitter.Core/RefitInterfaceGenerator.cs` -4. Add unit tests in `src/Refitter.Tests/Examples/` -5. Update README.md documentation +1. Add new property to `src/Refitter/Settings.cs` with `[CommandOption("--option-name")]` attribute +2. Update `CreateRefitGeneratorSettings()` method in `src/Refitter/GenerateCommand.cs` to map the setting +3. Add corresponding property to `src/Refitter.Core/Settings/RefitGeneratorSettings.cs` +4. Implement logic in `src/Refitter.Core/RefitInterfaceGenerator.cs` or related generator classes +5. Add unit tests in `src/Refitter.Tests/Examples/` following the established pattern +6. Update README.md documentation with the new option + +### Recent CLI Options Added +- `--use-apizr`: Integration with Apizr library for request options +- `--use-dynamic-querystring-parameters`: Enable dynamic query string parameter wrapping +- `--use-polymorphic-serialization`: Use System.Text.Json polymorphic serialization +- `--disposable`: Generate IDisposable clients +- `--collection-format`: Control query parameter collection formatting (Multi/Csv/Ssv/Tsv/Pipes) +- `--no-banner`: Hide donation banner in CLI output ### Working with OpenAPI Specifications - Test resources are located in `src/Refitter.Tests/Resources/V2/` and `src/Refitter.Tests/Resources/V3/` @@ -155,14 +164,25 @@ dotnet run --project src/Refitter --configuration Release --framework net9.0 -- - Use meaningful variable and method names - Keep methods focused with single responsibility +### UI and Console Output +- CLI uses **Spectre.Console** for rich terminal output with colors, tables, and panels +- ASCII art banner is displayed on startup (can be disabled with `--no-banner`) +- Progress indicators and formatted tables show generation status and file information +- Error handling includes styled panels and structured exception display +- Support key display for troubleshooting (disabled when `--no-logging` is used) + ## Build and CI/CD Information ### GitHub Workflows - **build.yml**: Main build workflow (builds and tests) - **smoke-tests.yml**: Quick validation tests - **release.yml**: Automated releases +- **release-preview.yml**: Preview releases - **docfx.yml**: Documentation generation - **msbuild.yml**: MSBuild-specific testing +- **regression-tests.yml**: Regression testing against known scenarios +- **production-tests.yml**: Production environment testing +- **codecov.yml**: Code coverage reporting ### Expected Build Times - Package restore: ~65 seconds diff --git a/README.md b/README.md index 59b9a853..13bff257 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ EXAMPLES: refitter ./openapi.json --optional-nullable-parameters refitter ./openapi.json --use-polymorphic-serialization refitter ./openapi.json --collection-format Csv + refitter ./openapi.json --simple-output ARGUMENTS: [URL or input file] URL or file path to OpenAPI Specification file @@ -103,6 +104,7 @@ OPTIONS: --keep-schema Force to keep matching schema, uses regular expressions. Use together with "--trim-unused-schema". Can be set multiple times --include-inheritance-hierarchy Keep all possible inherited types/union types even if they are not directly used --no-banner Don't show donation banner + --simple-output Generate simple, plain-text console output without ASCII art, tables, emojis, or color formatting (suitable for IDE output windows) --skip-default-additional-properties Set to true to skip default additional properties --collection-format Multi Determines the format of collection parameters. May be one of Multi, Csv, Ssv, Tsv, Pipes --operation-name-generator Default The NSwag IOperationNameGenerator implementation to use. @@ -146,6 +148,28 @@ Here's what the console output looks like when running the Refitter CLI tool: ![Console Output](images/console-output.png) +### Simple Output Mode + +For integration with IDEs and build tools that don't support rich console formatting, Refitter provides a `--simple-output` option: + +```shell +refitter ./openapi.json --simple-output +``` + +This mode generates plain text output without: + +- ASCII art and banners +- Colored text and rich formatting +- Unicode characters and emojis +- Tables and panels + +This is particularly useful when: + +- Running Refitter from Visual Studio extensions +- Integrating with build systems that capture console output +- Using tools that don't support ANSI escape sequences +- Running in environments with limited console capabilities + ## Source Generator Refitter is available as a C# Source Generator that uses the [Refitter.Core](https://github.com/christianhelle/refitter/tree/main/src/Refitter.Core) library for generating a REST API Client using the [Refit](https://github.com/reactiveui/refit) library. Refitter can generate the Refit interface from OpenAPI specifications. Refitter could format the generated Refit interface to be managed by [Apizr](https://www.apizr.net) and generate some registration helpers too. diff --git a/docs/docfx_project/articles/cli-tool.md b/docs/docfx_project/articles/cli-tool.md index a01c9c92..e3e893a8 100644 --- a/docs/docfx_project/articles/cli-tool.md +++ b/docs/docfx_project/articles/cli-tool.md @@ -40,6 +40,7 @@ EXAMPLES: refitter ./openapi.json --optional-nullable-parameters refitter ./openapi.json --use-polymorphic-serialization refitter ./openapi.json --collection-format Csv + refitter ./openapi.json --simple-output ARGUMENTS: [URL or input file] URL or file path to OpenAPI Specification file @@ -83,6 +84,7 @@ OPTIONS: --include-inheritance-hierarchy Keep all possible inherited types/union types even if they are not directly used --no-banner Don't show donation banner --skip-default-additional-properties Set to true to skip default additional properties + --simple-output Generate output with no color, no formatting, no emojis, and no banners suitable for terminal or IDE output --collection-format Multi Determines the format of collection parameters. May be one of: - Multi (separate parameter instances for each array item) - Csv (comma separated values) diff --git a/src/Refitter/GenerateCommand.cs b/src/Refitter/GenerateCommand.cs index cf5dfc08..d10e7e45 100644 --- a/src/Refitter/GenerateCommand.cs +++ b/src/Refitter/GenerateCommand.cs @@ -45,20 +45,39 @@ public override async Task ExecuteAsync(CommandContext context, Settings se """; // Header with branding - 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(); + 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(); - AnsiConsole.MarkupLine($"[dim]🔑 Support key: {supportKey}[/]"); - AnsiConsole.WriteLine(); + + if (settings.SimpleOutput) + { + Console.WriteLine($"Support key: {supportKey}"); + Console.WriteLine(); + } + else + { + AnsiConsole.MarkupLine($"[dim]🔑 Support key: {supportKey}[/]"); + AnsiConsole.WriteLine(); + } if (context.Arguments.Any(a => a.Equals("--version", StringComparison.OrdinalIgnoreCase)) || context.Arguments.Any(a => a.Equals("-v", StringComparison.OrdinalIgnoreCase))) @@ -78,7 +97,7 @@ public override async Task ExecuteAsync(CommandContext context, Settings se var generator = await RefitGenerator.CreateAsync(refitGeneratorSettings); if (!settings.SkipValidation) - await ValidateOpenApiSpec(refitGeneratorSettings.OpenApiPath); + await ValidateOpenApiSpec(refitGeneratorSettings.OpenApiPath, settings); await (refitGeneratorSettings.GenerateMultipleFiles ? WriteMultipleFiles(generator, settings, refitGeneratorSettings) @@ -109,79 +128,140 @@ public override async Task ExecuteAsync(CommandContext context, Settings se // Success summary with performance metrics stopwatch.Stop(); - 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); + 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(); + AnsiConsole.Write(successPanel); + AnsiConsole.WriteLine(); + } if (!settings.NoBanner) - DonationBanner(); + { + if (settings.SimpleOutput) + SimpleDonationBanner(); + else + DonationBanner(); + } - ShowWarnings(refitGeneratorSettings); + ShowWarnings(refitGeneratorSettings, settings); return 0; } catch (Exception exception) { // Error summary panel - var errorPanel = new Panel("[bold red]❌ Generation failed![/]") - .BorderColor(Color.Red) - .RoundedBorder() - .Header("[bold red]🚨 Error[/]") - .HeaderAlignment(Justify.Center); + 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(); + AnsiConsole.Write(errorPanel); + AnsiConsole.WriteLine(); + } if (exception is OpenApiUnsupportedSpecVersionException unsupportedSpecVersionException) { - var versionPanel = new Panel( - $"[bold red]🚫 Unsupported OpenAPI version: {unsupportedSpecVersionException.SpecificationVersion}[/]" - ) - .BorderColor(Color.Red) - .RoundedBorder(); - - AnsiConsole.Write(versionPanel); - AnsiConsole.WriteLine(); + 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) { - AnsiConsole.MarkupLine("[bold red]🐛 Exception Details:[/]"); - AnsiConsole.WriteException(exception); - AnsiConsole.WriteLine(); + 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) { - var suggestionPanel = new Panel( - "💡 Try using the --skip-validation argument." + 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("Suggestion"); + .Header("Support") + .HeaderAlignment(Justify.Center); - AnsiConsole.Write(suggestionPanel); + AnsiConsole.Write(helpPanel); AnsiConsole.WriteLine(); } - 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; @@ -237,35 +317,54 @@ private static async Task WriteSingleFile( RefitGeneratorSettings refitGeneratorSettings) { // Show progress while generating - await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots) - .SpinnerStyle(Style.Parse("green bold")) - .StartAsync("[yellow]🔧 Generating code...[/]", async _ => - { - await Task.Delay(100); // Brief delay to show spinner - }); + if (settings.SimpleOutput) + { + Console.WriteLine("Generating code..."); + } + else + { + await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .SpinnerStyle(Style.Parse("green bold")) + .StartAsync("[yellow]🔧 Generating code...[/]", async _ => + { + await Task.Delay(100); // Brief delay to show spinner + }); + } var code = generator.Generate().ReplaceLineEndings(); var outputPath = GetOutputPath(settings, refitGeneratorSettings); - // Create a table for better formatting - var table = new Table() - .RoundedBorder() - .BorderColor(Color.Yellow) - .AddColumn(new TableColumn("[bold yellow]📁 Generated Output[/]").Centered()); - var fileName = Path.GetFileName(outputPath); var directory = Path.GetDirectoryName(outputPath) ?? ""; var sizeFormatted = FormatFileSize(code.Length); var lines = code.Split('\n').Length; - table.AddRow($"[bold white]📄 File:[/] [cyan]{fileName}[/]"); - table.AddRow($"[bold white]📂 Directory:[/] [dim]{directory}[/]"); - table.AddRow($"[bold white]📊 Size:[/] [green]{sizeFormatted}[/]"); - table.AddRow($"[bold white]📝 Lines:[/] [green]{lines:N0}[/]"); + if (settings.SimpleOutput) + { + Console.WriteLine("Generated Output"); + Console.WriteLine($"File: {fileName}"); + Console.WriteLine($"Directory: {directory}"); + Console.WriteLine($"Size: {sizeFormatted}"); + Console.WriteLine($"Lines: {lines:N0}"); + Console.WriteLine(); + } + else + { + // Create a table for better formatting + var table = new Table() + .RoundedBorder() + .BorderColor(Color.Yellow) + .AddColumn(new TableColumn("[bold yellow]📁 Generated Output[/]").Centered()); - AnsiConsole.Write(table); - AnsiConsole.WriteLine(); + table.AddRow($"[bold white]📄 File:[/] [cyan]{fileName}[/]"); + table.AddRow($"[bold white]📂 Directory:[/] [dim]{directory}[/]"); + table.AddRow($"[bold white]📊 Size:[/] [green]{sizeFormatted}[/]"); + table.AddRow($"[bold white]📝 Lines:[/] [green]{lines:N0}[/]"); + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } var outputDirectory = Path.GetDirectoryName(outputPath); if (!string.IsNullOrWhiteSpace(outputDirectory) && !Directory.Exists(outputDirectory)) @@ -294,28 +393,48 @@ private async Task WriteMultipleFiles( RefitGeneratorSettings refitGeneratorSettings) { // Show progress while generating - var generatorOutput = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots) - .SpinnerStyle(Style.Parse("green bold")) - .StartAsync("[yellow]🔧 Generating multiple files...[/]", async _ => - { - await Task.Delay(100); // Brief delay to show spinner - return generator.GenerateMultipleFiles(); - }); - - // Create a table for better formatting - var table = new Table() - .RoundedBorder() - .BorderColor(Color.Yellow) - .AddColumn(new TableColumn("[bold white]📄 File[/]").LeftAligned()) - .AddColumn(new TableColumn("[bold white]📂 Directory[/]").LeftAligned()) - .AddColumn(new TableColumn("[bold white]📊 Size[/]").RightAligned()) - .AddColumn(new TableColumn("[bold white]📝 Lines[/]").RightAligned()); - - table.Title = new TableTitle("[bold yellow]📁 Generated Output Files[/]"); + GeneratorOutput generatorOutput; + if (settings.SimpleOutput) + { + Console.WriteLine("Generating multiple files..."); + generatorOutput = generator.GenerateMultipleFiles(); + } + else + { + generatorOutput = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .SpinnerStyle(Style.Parse("green bold")) + .StartAsync("[yellow]🔧 Generating multiple files...[/]", async _ => + { + await Task.Delay(100); // Brief delay to show spinner + return generator.GenerateMultipleFiles(); + }); + } var totalSize = 0L; var totalLines = 0; + + Table? table = null; + + if (settings.SimpleOutput) + { + Console.WriteLine("Generated Output Files"); + Console.WriteLine($"{"File",-30} {"Size",-10} {"Lines",-10}"); + Console.WriteLine(new string('-', 55)); + } + else + { + // Create a table for better formatting + table = new Table() + .RoundedBorder() + .BorderColor(Color.Yellow) + .AddColumn(new TableColumn("[bold white]📄 File[/]").LeftAligned()) + .AddColumn(new TableColumn("[bold white]📂 Directory[/]").LeftAligned()) + .AddColumn(new TableColumn("[bold white]📊 Size[/]").RightAligned()) + .AddColumn(new TableColumn("[bold white]📝 Lines[/]").RightAligned()); + + table.Title = new TableTitle("[bold yellow]📁 Generated Output Files[/]"); + } foreach (var outputFile in generatorOutput.Files) { @@ -338,12 +457,19 @@ private async Task WriteMultipleFiles( var contractsDir = Path.GetDirectoryName(contractsFile) ?? ""; var lines = outputFile.Content.Split('\n').Length; - table.AddRow( - $"[cyan]{outputFile.Filename}[/]", - $"[dim]{contractsDir}[/]", - $"[green]{sizeFormatted}[/]", - $"[green]{lines:N0}[/]" - ); + if (settings.SimpleOutput) + { + Console.WriteLine($"{outputFile.Filename,-30} {sizeFormatted,-10} {lines,-10:N0}"); + } + else + { + table?.AddRow( + $"[cyan]{outputFile.Filename}[/]", + $"[dim]{contractsDir}[/]", + $"[green]{sizeFormatted}[/]", + $"[green]{lines:N0}[/]" + ); + } totalSize += outputFile.Content.Length; totalLines += lines; @@ -358,12 +484,19 @@ private async Task WriteMultipleFiles( var outputDirectory = Path.GetDirectoryName(outputPath) ?? ""; var fileLines = code.Split('\n').Length; - table.AddRow( - $"[cyan]{outputFile.Filename}[/]", - $"[dim]{outputDirectory}[/]", - $"[green]{formattedSize}[/]", - $"[green]{fileLines:N0}[/]" - ); + if (settings.SimpleOutput) + { + Console.WriteLine($"{outputFile.Filename,-30} {formattedSize,-10} {fileLines,-10:N0}"); + } + else + { + table?.AddRow( + $"[cyan]{outputFile.Filename}[/]", + $"[dim]{outputDirectory}[/]", + $"[green]{formattedSize}[/]", + $"[green]{fileLines:N0}[/]" + ); + } totalSize += code.Length; totalLines += fileLines; @@ -375,19 +508,31 @@ private async Task WriteMultipleFiles( await File.WriteAllTextAsync(outputPath, code); } - // Add summary row - table.AddEmptyRow(); - table.AddRow( - $"[bold yellow]📊 Total ({generatorOutput.Files.Count} files)[/]", - "[dim]---[/]", - $"[bold green]{FormatFileSize(totalSize)}[/]", - $"[bold green]{totalLines:N0}[/]" - ); + if (settings.SimpleOutput) + { + Console.WriteLine(new string('-', 55)); + Console.WriteLine($"{"Total (" + generatorOutput.Files.Count + " files)",-30} {FormatFileSize(totalSize),-10} {totalLines,-10:N0}"); + Console.WriteLine(); + } + else + { + // Add summary row + table?.AddEmptyRow(); + table?.AddRow( + $"[bold yellow]📊 Total ({generatorOutput.Files.Count} files)[/]", + "[dim]---[/]", + $"[bold green]{FormatFileSize(totalSize)}[/]", + $"[bold green]{totalLines:N0}[/]" + ); - AnsiConsole.Write(table); - AnsiConsole.WriteLine(); + if (table != null) + { + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } + } } - private static void ShowWarnings(RefitGeneratorSettings refitGeneratorSettings) + private static void ShowWarnings(RefitGeneratorSettings refitGeneratorSettings, Settings settings) { var warnings = new List<(string title, string description)>(); @@ -412,24 +557,37 @@ private static void ShowWarnings(RefitGeneratorSettings refitGeneratorSettings) if (warnings.Any()) { - var table = new Table() - .RoundedBorder() - .BorderColor(Color.Orange3) - .AddColumn(new TableColumn("[bold white]Warning[/]").LeftAligned()) - .AddColumn(new TableColumn("[bold white]Description[/]").LeftAligned()); - - table.Title = new TableTitle("[bold yellow]⚠️ Configuration Warnings[/]"); - - foreach (var (title, description) in warnings) + if (settings.SimpleOutput) { - table.AddRow( - $"[bold orange3]{title}[/]", - $"[orange3]{description}[/]" - ); + Console.WriteLine("Configuration Warnings"); + foreach (var (title, description) in warnings) + { + Console.WriteLine($"Warning: {title}"); + Console.WriteLine($"Description: {description}"); + Console.WriteLine(); + } } + else + { + var table = new Table() + .RoundedBorder() + .BorderColor(Color.Orange3) + .AddColumn(new TableColumn("[bold white]Warning[/]").LeftAligned()) + .AddColumn(new TableColumn("[bold white]Description[/]").LeftAligned()); - AnsiConsole.Write(table); - AnsiConsole.WriteLine(); + table.Title = new TableTitle("[bold yellow]⚠️ Configuration Warnings[/]"); + + foreach (var (title, description) in warnings) + { + table.AddRow( + $"[bold orange3]{title}[/]", + $"[orange3]{description}[/]" + ); + } + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } } } private static void DonationBanner() @@ -449,6 +607,18 @@ private static void DonationBanner() AnsiConsole.WriteLine(); } + 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(); + Console.WriteLine("Found an issue? https://github.com/christianhelle/refitter/issues"); + Console.WriteLine(); + } + private static string GetOutputPath(Settings settings, RefitGeneratorSettings refitGeneratorSettings) { var outputPath = settings.OutputPath != Settings.DefaultOutputPath && @@ -484,72 +654,113 @@ private string GetOutputPath( ? Path.Combine(root, settings.OutputPath, outputFile.Filename) : outputFile.Filename; } - private static async Task ValidateOpenApiSpec(string openApiPath) + private static async Task ValidateOpenApiSpec(string openApiPath, Settings settings) { - var validationResult = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots) - .SpinnerStyle(Style.Parse("cyan bold")) - .StartAsync("[cyan]🔍 Validating OpenAPI specification...[/]", async _ => - { - return await OpenApiValidator.Validate(openApiPath); - }); + OpenApiValidationResult validationResult; + if (settings.SimpleOutput) + { + Console.WriteLine("Validating OpenAPI specification..."); + validationResult = await OpenApiValidator.Validate(openApiPath); + } + else + { + validationResult = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .SpinnerStyle(Style.Parse("cyan bold")) + .StartAsync("[cyan]🔍 Validating OpenAPI specification...[/]", async _ => + { + return await OpenApiValidator.Validate(openApiPath); + }); + } if (!validationResult.IsValid) { - AnsiConsole.WriteLine(); - var errorPanel = new Panel("[red]❌ OpenAPI validation failed![/]") - .BorderColor(Color.Red) - .RoundedBorder(); - AnsiConsole.Write(errorPanel); - AnsiConsole.WriteLine(); + if (settings.SimpleOutput) + { + Console.WriteLine(); + Console.WriteLine("OpenAPI validation failed!"); + Console.WriteLine(); + } + else + { + AnsiConsole.WriteLine(); + var errorPanel = new Panel("[red]❌ OpenAPI validation failed![/]") + .BorderColor(Color.Red) + .RoundedBorder(); + AnsiConsole.Write(errorPanel); + AnsiConsole.WriteLine(); + } foreach (var error in validationResult.Diagnostics.Errors) { - TryWriteLine(error, "red", "Error"); + TryWriteLine(error, "red", "Error", settings.SimpleOutput); } foreach (var warning in validationResult.Diagnostics.Warnings) { - TryWriteLine(warning, "yellow", "Warning"); + TryWriteLine(warning, "yellow", "Warning", settings.SimpleOutput); } validationResult.ThrowIfInvalid(); } - // Create statistics table - var table = new Table() - .RoundedBorder() - .BorderColor(Color.Blue) - .AddColumn(new TableColumn("[bold white]📊 Metric[/]").LeftAligned()) - .AddColumn(new TableColumn("[bold white]📈 Count[/]").RightAligned()) - .AddColumn(new TableColumn("[bold white]📝 Details[/]").LeftAligned()); - - table.Title = new TableTitle("[bold cyan]📊 OpenAPI Analysis Results[/]"); - - var stats = validationResult.Statistics.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in stats) + if (settings.SimpleOutput) { - if (line.Trim().StartsWith("-")) + Console.WriteLine("OpenAPI Analysis Results"); + var stats = validationResult.Statistics.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in stats) { - var parts = line.Trim().TrimStart('-').Trim().Split(':', 2); - if (parts.Length == 2) + if (line.Trim().StartsWith("-")) { - var label = parts[0].Trim(); - var value = parts[1].Trim(); - var icon = GetStatsIcon(label); - var description = GetStatsDescription(label); - - table.AddRow( - $"{icon} [bold]{label}[/]", - $"[green]{value}[/]", - $"[dim]{description}[/]" - ); + var parts = line.Trim().TrimStart('-').Trim().Split(':', 2); + if (parts.Length == 2) + { + var label = parts[0].Trim(); + var value = parts[1].Trim(); + var description = GetStatsDescription(label); + Console.WriteLine($"{label}: {value} - {description}"); + } } } + Console.WriteLine(); } + else + { + // Create statistics table + var table = new Table() + .RoundedBorder() + .BorderColor(Color.Blue) + .AddColumn(new TableColumn("[bold white]📊 Metric[/]").LeftAligned()) + .AddColumn(new TableColumn("[bold white]📈 Count[/]").RightAligned()) + .AddColumn(new TableColumn("[bold white]📝 Details[/]").LeftAligned()); - AnsiConsole.Write(table); - AnsiConsole.WriteLine(); + table.Title = new TableTitle("[bold cyan]📊 OpenAPI Analysis Results[/]"); + + var stats = validationResult.Statistics.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in stats) + { + if (line.Trim().StartsWith("-")) + { + var parts = line.Trim().TrimStart('-').Trim().Split(':', 2); + if (parts.Length == 2) + { + var label = parts[0].Trim(); + var value = parts[1].Trim(); + var icon = GetStatsIcon(label); + var description = GetStatsDescription(label); + + table.AddRow( + $"{icon} [bold]{label}[/]", + $"[green]{value}[/]", + $"[dim]{description}[/]" + ); + } + } + } + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } } private static string GetStatsIcon(string label) { @@ -586,8 +797,15 @@ var l when l.Contains("schema") => "Data schemas defined", private static void TryWriteLine( OpenApiError error, string color, - string label) + string label, + bool simpleOutput = false) { + if (simpleOutput) + { + Console.WriteLine($"{label}:{Crlf}{error}{Crlf}"); + return; + } + try { AnsiConsole.MarkupLine($"[{color}]{label}:{Crlf}{error}{Crlf}[/]"); diff --git a/src/Refitter/Settings.cs b/src/Refitter/Settings.cs index 49e430db..9af149b6 100644 --- a/src/Refitter/Settings.cs +++ b/src/Refitter/Settings.cs @@ -248,4 +248,9 @@ payloads with (yet) unknown types are offered by newer versions of an API [CommandOption("--collection-format")] [DefaultValue(CollectionFormat.Multi)] public CollectionFormat CollectionFormat { get; set; } = CollectionFormat.Multi; + + [Description("Generate simple, plain-text console output without ASCII art, tables, emojis, or color formatting (suitable for IDE output windows)")] + [CommandOption("--simple-output")] + [DefaultValue(false)] + public bool SimpleOutput { get; set; } }