From 64797997c39ff4c0008910c48e1da6da1fec843d Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Tue, 5 Sep 2023 16:14:39 +0200 Subject: [PATCH 1/6] Add Microsoft OpenApi packages to Refitter New packages Microsoft.OpenApi.OData and Microsoft.OpenApi.Readers were added to the csproj file to support OData and improved API documentation efforts. They were chosen for their comprehensive features and broad compatibility. --- src/Refitter/Refitter.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Refitter/Refitter.csproj b/src/Refitter/Refitter.csproj index b0e3d7aa..c403bbc0 100644 --- a/src/Refitter/Refitter.csproj +++ b/src/Refitter/Refitter.csproj @@ -18,6 +18,8 @@ + + From 2e052dcbcde2f825dc8c38fabe53b3cad2dcb83c Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Tue, 5 Sep 2023 16:16:38 +0200 Subject: [PATCH 2/6] Implement OpenAPI validation Implemented new classes for adding OpenAPI validation. The included changes allow us to validate the correctness and adherence of a given OpenAPI schema and report statistics about the schema elements such as the number of parameters, operations, headers, etc. The changes also handle exceptions encountered during validation and provide diagnostic details about the encountered issues. --- src/Refitter/Validation/OpenApiStats.cs | 82 +++++++++++++++++++ .../Validation/OpenApiValidationException.cs | 22 +++++ .../Validation/OpenApiValidationResult.cs | 16 ++++ src/Refitter/Validation/OpenApiValidator.cs | 78 ++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 src/Refitter/Validation/OpenApiStats.cs create mode 100644 src/Refitter/Validation/OpenApiValidationException.cs create mode 100644 src/Refitter/Validation/OpenApiValidationResult.cs create mode 100644 src/Refitter/Validation/OpenApiValidator.cs diff --git a/src/Refitter/Validation/OpenApiStats.cs b/src/Refitter/Validation/OpenApiStats.cs new file mode 100644 index 00000000..943dd743 --- /dev/null +++ b/src/Refitter/Validation/OpenApiStats.cs @@ -0,0 +1,82 @@ +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; + +namespace Refitter.Validation; + +public class OpenApiStats : OpenApiVisitorBase +{ + public int ParameterCount { get; set; } = 0; + public int SchemaCount { get; set; } = 0; + public int HeaderCount { get; set; } = 0; + public int PathItemCount { get; set; } = 0; + public int RequestBodyCount { get; set; } = 0; + public int ResponseCount { get; set; } = 0; + public int OperationCount { get; set; } = 0; + public int LinkCount { get; set; } = 0; + public int CallbackCount { get; set; } = 0; + + public override void Visit(OpenApiParameter parameter) + { + ParameterCount++; + } + + public override void Visit(OpenApiSchema schema) + { + SchemaCount++; + } + + + public override void Visit(IDictionary headers) + { + HeaderCount++; + } + + + public override void Visit(OpenApiPathItem pathItem) + { + PathItemCount++; + } + + + public override void Visit(OpenApiRequestBody requestBody) + { + RequestBodyCount++; + } + + + public override void Visit(OpenApiResponses response) + { + ResponseCount++; + } + + + public override void Visit(OpenApiOperation operation) + { + OperationCount++; + } + + + public override void Visit(OpenApiLink link) + { + LinkCount++; + } + + public override void Visit(OpenApiCallback callback) + { + CallbackCount++; + } + + public override string ToString() + { + return $""" + Path Items: {PathItemCount} + Operations: {OperationCount} + Parameters: {ParameterCount} + Request Bodies: {RequestBodyCount} + Responses: {ResponseCount} + Links: {LinkCount} + Callbacks: {CallbackCount} + Schemas: {SchemaCount} + """; + } +} \ No newline at end of file diff --git a/src/Refitter/Validation/OpenApiValidationException.cs b/src/Refitter/Validation/OpenApiValidationException.cs new file mode 100644 index 00000000..bfe263b3 --- /dev/null +++ b/src/Refitter/Validation/OpenApiValidationException.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Refitter.Validation; + +[Serializable] +public class OpenApiValidationException : Exception +{ + public OpenApiValidationResult ValidationResult { get; } = null!; + + public OpenApiValidationException( + OpenApiValidationResult validationResult) + : base("OpenAPI validation failed") + { + ValidationResult = validationResult; + } + + protected OpenApiValidationException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/src/Refitter/Validation/OpenApiValidationResult.cs b/src/Refitter/Validation/OpenApiValidationResult.cs new file mode 100644 index 00000000..1a8c2702 --- /dev/null +++ b/src/Refitter/Validation/OpenApiValidationResult.cs @@ -0,0 +1,16 @@ +using Microsoft.OpenApi.Readers; + +namespace Refitter.Validation; + +public record OpenApiValidationResult( + OpenApiDiagnostic Diagnostics, + OpenApiStats Statistics) +{ + public bool IsValid => Diagnostics.Errors.Count == 0; + + public void ThrowIfInvalid() + { + if (!IsValid) + throw new OpenApiValidationException(this); + } +} \ No newline at end of file diff --git a/src/Refitter/Validation/OpenApiValidator.cs b/src/Refitter/Validation/OpenApiValidator.cs new file mode 100644 index 00000000..b735f504 --- /dev/null +++ b/src/Refitter/Validation/OpenApiValidator.cs @@ -0,0 +1,78 @@ +using System.Net; +using System.Security; + +using Microsoft.OpenApi.Readers; +using Microsoft.OpenApi.Services; + +namespace Refitter.Validation; + +public static class OpenApiValidator +{ + public static async Task Validate(string openApiPath) + { + var result = await ParseOpenApi(openApiPath); + + var statsVisitor = new OpenApiStats(); + var walker = new OpenApiWalker(statsVisitor); + walker.Walk(result.OpenApiDocument); + + return new( + result.OpenApiDiagnostic, + statsVisitor); + } + + private static async Task GetStream( + string input, + CancellationToken cancellationToken) + { + if (input.StartsWith("http")) + { + try + { + var httpClientHandler = new HttpClientHandler() + { + SslProtocols = System.Security.Authentication.SslProtocols.Tls12, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + using var httpClient = new HttpClient(httpClientHandler); + httpClient.DefaultRequestVersion = HttpVersion.Version20; + return await httpClient.GetStreamAsync(input, cancellationToken); + } + catch (HttpRequestException ex) + { + throw new InvalidOperationException($"Could not download the file at {input}", ex); + } + } + + try + { + var fileInput = new FileInfo(input); + return fileInput.OpenRead(); + } + catch (Exception ex) when (ex is FileNotFoundException || + ex is PathTooLongException || + ex is DirectoryNotFoundException || + ex is IOException || + ex is UnauthorizedAccessException || + ex is SecurityException || + ex is NotSupportedException) + { + throw new InvalidOperationException($"Could not open the file at {input}", ex); + } + } + + private static async Task ParseOpenApi(string openApiFile) + { + var directoryName = new FileInfo(openApiFile).DirectoryName; + var openApiReaderSettings = new OpenApiReaderSettings + { + BaseUrl = openApiFile.StartsWith("http", StringComparison.OrdinalIgnoreCase) + ? new Uri(openApiFile) + : new Uri($"file://{directoryName}{Path.DirectorySeparatorChar}") + }; + + await using var stream = await GetStream(openApiFile, CancellationToken.None); + var reader = new OpenApiStreamReader(openApiReaderSettings); + return await reader.ReadAsync(stream, CancellationToken.None); + } +} \ No newline at end of file From 06a93917196c7579fd18a504c2d3c450e327baaa Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Tue, 5 Sep 2023 16:20:28 +0200 Subject: [PATCH 3/6] Enable OpenAPI spec validation In this commit, I have introduced the OpenAPI specification validation to the 'GenerateCommand' execution command. This will enable the application to validate the OpenAPI specifications before trying to generate any code, thus adding a layer of protection against invalid specifications. I have used the OpenApiValidator to perform the validation. I have also refined the error display, replacing generic Exception handling with specialized handling for OpenApiValidationException to improve error diagnosis. --- src/Refitter/GenerateCommand.cs | 58 ++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/src/Refitter/GenerateCommand.cs b/src/Refitter/GenerateCommand.cs index 6aff904a..4bde335d 100644 --- a/src/Refitter/GenerateCommand.cs +++ b/src/Refitter/GenerateCommand.cs @@ -3,11 +3,15 @@ using Spectre.Console.Cli; using System.Diagnostics; using System.Text.Json; +using Microsoft.OpenApi.Models; +using Refitter.Validation; namespace Refitter; public sealed class GenerateCommand : AsyncCommand { + private static readonly string Crlf = Environment.NewLine; + public override ValidationResult Validate(CommandContext context, Settings settings) { if (string.IsNullOrWhiteSpace(settings.OpenApiPath)) @@ -43,13 +47,14 @@ public override async Task ExecuteAsync(CommandContext context, Settings se IncludeTags = settings.Tags ?? Array.Empty(), }; - var crlf = Environment.NewLine; try { var stopwatch = Stopwatch.StartNew(); AnsiConsole.MarkupLine($"[green]Refitter v{GetType().Assembly.GetName().Version!}[/]"); AnsiConsole.MarkupLine($"[green]Support key: {SupportInformation.GetSupportKey()}[/]"); - + + await ValidateOpenApiSpec(settings); + if (!string.IsNullOrWhiteSpace(settings.SettingsFilePath)) { var json = await File.ReadAllTextAsync(settings.SettingsFilePath); @@ -73,18 +78,61 @@ public override async Task ExecuteAsync(CommandContext context, Settings se await File.WriteAllTextAsync(outputPath, code); await Analytics.LogFeatureUsage(settings); - AnsiConsole.MarkupLine($"[green]Duration: {stopwatch.Elapsed}{crlf}[/]"); + AnsiConsole.MarkupLine($"[green]Duration: {stopwatch.Elapsed}{Crlf}[/]"); return 0; } catch (Exception exception) { - AnsiConsole.MarkupLine($"[red]Error:{crlf}{exception.Message}[/]"); - AnsiConsole.MarkupLine($"[yellow]Stack Trace:{crlf}{exception.StackTrace}[/]"); + if (exception is not OpenApiValidationException) + { + AnsiConsole.MarkupLine($"[red]Error:{Crlf}{exception.Message}[/]"); + AnsiConsole.MarkupLine($"[red]Exception:{Crlf}{exception.GetType()}[/]"); + AnsiConsole.MarkupLine($"[yellow]Stack Trace:{Crlf}{exception.StackTrace}[/]"); + } + await Analytics.LogError(exception, settings); return exception.HResult; } } + private static async Task ValidateOpenApiSpec(Settings settings) + { + var validationResult = await OpenApiValidator.Validate(settings.OpenApiPath!); + if (!validationResult.IsValid) + { + AnsiConsole.MarkupLine($"[red]{Crlf}OpenAPI validation failed:{Crlf}[/]"); + + foreach (var error in validationResult.Diagnostics.Errors) + { + TryWriteLine(error, "red", "Error"); + } + + foreach (var warning in validationResult.Diagnostics.Warnings) + { + TryWriteLine(warning, "yellow", "Warning"); + } + + validationResult.ThrowIfInvalid(); + } + + AnsiConsole.MarkupLine($"[green]{Crlf}OpenAPI statistics:{Crlf}{validationResult.Statistics}{Crlf}[/]"); + } + + private static void TryWriteLine( + OpenApiError error, + string color, + string label) + { + try + { + AnsiConsole.MarkupLine($"[{color}]{label}:{Crlf}{error}{Crlf}[/]"); + } + catch + { + // ignored + } + } + private static bool IsUrl(string openApiPath) { return Uri.TryCreate(openApiPath, UriKind.Absolute, out var uriResult) && From 339fb985eb44fdc35b3cbd9f216e085fddb1397a Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Tue, 5 Sep 2023 16:24:29 +0200 Subject: [PATCH 4/6] Add option to skip OpenAPI spec validation A new option '--skip-validation' was introduced in the Settings class, allowing users to bypass the OpenAPI specification validation. This change was implemented to give users more flexibility. In the GenerateCommand class, a conditional statement checking this setting was also added to alter the execution flow based on the user's preference. --- src/Refitter/GenerateCommand.cs | 5 ++++- src/Refitter/Settings.cs | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Refitter/GenerateCommand.cs b/src/Refitter/GenerateCommand.cs index 4bde335d..b1bd4de0 100644 --- a/src/Refitter/GenerateCommand.cs +++ b/src/Refitter/GenerateCommand.cs @@ -53,7 +53,10 @@ public override async Task ExecuteAsync(CommandContext context, Settings se AnsiConsole.MarkupLine($"[green]Refitter v{GetType().Assembly.GetName().Version!}[/]"); AnsiConsole.MarkupLine($"[green]Support key: {SupportInformation.GetSupportKey()}[/]"); - await ValidateOpenApiSpec(settings); + if (!settings.SkipValidation) + { + await ValidateOpenApiSpec(settings); + } if (!string.IsNullOrWhiteSpace(settings.SettingsFilePath)) { diff --git a/src/Refitter/Settings.cs b/src/Refitter/Settings.cs index 5b5d766f..ad595f97 100644 --- a/src/Refitter/Settings.cs +++ b/src/Refitter/Settings.cs @@ -88,4 +88,8 @@ public sealed class Settings : CommandSettings [CommandOption("--tag")] [DefaultValue(new string[0])] public string[]? Tags { get; set; } + + [Description("Skip validation of the OpenAPI specification")] + [CommandOption("--skip-validation")] + public bool SkipValidation { get; set; } } \ No newline at end of file From db466b4bd3bbf4f1f78aae903a6213f1db1b3366 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Tue, 5 Sep 2023 18:50:25 +0200 Subject: [PATCH 5/6] Add skip validation flag in ReadMe files Included a new command line flag to README files, '--skip-validation', to allow users to skip validation of the OpenAPI specification. This is particularly useful when users want to expedite the testing process without validating the OpenAPI specification, wherein the tool will trust the input spec and won't raise any validation errors. --- README.md | 1 + src/Refitter/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index af611f99..881d1790 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ OPTIONS: --multiple-interfaces Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag --match-path Only include Paths that match the provided regular expression. May be set multiple times --tag Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation + --skip-validation Skip validation of the OpenAPI specification ``` To generate code from an OpenAPI specifications file, run the following: diff --git a/src/Refitter/README.md b/src/Refitter/README.md index 744c1cf4..19512101 100644 --- a/src/Refitter/README.md +++ b/src/Refitter/README.md @@ -58,6 +58,7 @@ OPTIONS: --multiple-interfaces Generate a Refit interface for each endpoint. May be one of ByEndpoint, ByTag --match-path Only include Paths that match the provided regular expression. May be set multiple times --tag Only include Endpoints that contain this tag. May be set multiple times and result in OR'ed evaluation + --skip-validation Skip validation of the OpenAPI specification ``` To generate code from an OpenAPI specifications file, run the following: From 0802d7bef925ccf7f7c7c32cf83c3c4e6557b3db Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Tue, 5 Sep 2023 19:20:36 +0200 Subject: [PATCH 6/6] Indent OpenAPI statistics --- src/Refitter/Validation/OpenApiStats.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Refitter/Validation/OpenApiStats.cs b/src/Refitter/Validation/OpenApiStats.cs index 943dd743..243f70ef 100644 --- a/src/Refitter/Validation/OpenApiStats.cs +++ b/src/Refitter/Validation/OpenApiStats.cs @@ -69,14 +69,14 @@ public override void Visit(OpenApiCallback callback) public override string ToString() { return $""" - Path Items: {PathItemCount} - Operations: {OperationCount} - Parameters: {ParameterCount} - Request Bodies: {RequestBodyCount} - Responses: {ResponseCount} - Links: {LinkCount} - Callbacks: {CallbackCount} - Schemas: {SchemaCount} + - Path Items: {PathItemCount} + - Operations: {OperationCount} + - Parameters: {ParameterCount} + - Request Bodies: {RequestBodyCount} + - Responses: {ResponseCount} + - Links: {LinkCount} + - Callbacks: {CallbackCount} + - Schemas: {SchemaCount} """; } } \ No newline at end of file