From d874823e5f0979493211070dd7679177d434178d Mon Sep 17 00:00:00 2001 From: Nam Vu Date: Wed, 17 Jul 2024 15:42:05 +0200 Subject: [PATCH] feat: show parsing errors in stderr Example: ``` Errors occurred while parsing the OpenAPI specifications: [old] The field 'version' in 'info' object is REQUIRED. [#/info/version] [new] The field 'title' in 'info' object is REQUIRED. [#/info/title] ``` Also write the other occuring CLI errors in stderr instead of stdout. --- src/Criteo.OpenApi.Comparator.Cli/Program.cs | 21 +++++++--- .../OpenApiParserTests.cs | 4 +- .../OpenApiSpecificationsCompareTests.cs | 2 +- .../OpenApiComparator.cs | 40 +++++++++++++++++-- .../Parser/OpenApiParser.cs | 5 ++- 5 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/Criteo.OpenApi.Comparator.Cli/Program.cs b/src/Criteo.OpenApi.Comparator.Cli/Program.cs index ff743be..d4a883a 100644 --- a/src/Criteo.OpenApi.Comparator.Cli/Program.cs +++ b/src/Criteo.OpenApi.Comparator.Cli/Program.cs @@ -36,12 +36,21 @@ public static int Main(string[] args) if (!oldFileFound || !newFileFound) { - Console.WriteLine("Exiting."); + Console.Error.WriteLine("Exiting."); return 1; } var differences = OpenApiComparator.Compare( - oldOpenApiSpecification, newOpenApiSpecification, options.StrictMode); + oldOpenApiSpecification, newOpenApiSpecification, out var parsingErrors, options.StrictMode); + + if (parsingErrors.Any()) + { + Console.Error.WriteLine("Errors occurred while parsing the OpenAPI specifications:"); + foreach (var error in parsingErrors) + { + Console.Error.WriteLine(error); + } + } DisplayOutput(differences, options.OutputFormat); @@ -66,7 +75,7 @@ private static bool TryReadFile(string path, out string fileContent) } catch (UriFormatException) { - Console.WriteLine($"Failed to interpret the provided URI: {path}."); + Console.Error.WriteLine($"Failed to interpret the provided URI: {path}."); fileContent = null; } return false; @@ -81,7 +90,7 @@ private static bool TryReadLocalFile(string path, out string fileContent) } catch (FileNotFoundException) { - Console.WriteLine($"File not found for: {path}."); + Console.Error.WriteLine($"File not found for: {path}."); fileContent = null; return false; } @@ -97,7 +106,7 @@ private static bool TryReadRemoteFile(string path, out string fileContent) } catch (HttpRequestException exception) { - Console.WriteLine($"Http request failed on {path}: {exception.Message}"); + Console.Error.WriteLine($"Http request failed on {path}: {exception.Message}"); } catch (AggregateException exception) { @@ -108,7 +117,7 @@ private static bool TryReadRemoteFile(string path, out string fileContent) stringWriter.Write($"- {innerException.GetType()}: {exception.Message}"); return true; }); - Console.WriteLine(stringWriter.ToString()); + Console.Error.WriteLine(stringWriter.ToString()); } fileContent = null; return false; diff --git a/src/Criteo.OpenApi.Comparator.UTest/OpenApiParserTests.cs b/src/Criteo.OpenApi.Comparator.UTest/OpenApiParserTests.cs index 43dbbee..72b74ea 100644 --- a/src/Criteo.OpenApi.Comparator.UTest/OpenApiParserTests.cs +++ b/src/Criteo.OpenApi.Comparator.UTest/OpenApiParserTests.cs @@ -29,7 +29,7 @@ public void OpenApiParser_Should_Throw_Exception_When_Invalid_Json() { const string fileName = "invalid_json_file.txt"; var documentAsString = ReadOpenApiFile(fileName); - Assert.Throws(() => OpenApiParser.Parse(documentAsString)); + Assert.Throws(() => OpenApiParser.Parse(documentAsString, out _)); } /// @@ -40,7 +40,7 @@ public void OpenApiParser_Should_Throw_Exception_When_Invalid_Json() public void OpenApiParser_Should_Return_Valid_OpenApi_Document_Object(string fileName) { var documentAsString = ReadOpenApiFile(fileName); - var validOpenApiDocument = OpenApiParser.Parse(documentAsString); + var validOpenApiDocument = OpenApiParser.Parse(documentAsString, out _); Assert.IsInstanceOf>(validOpenApiDocument); } } diff --git a/src/Criteo.OpenApi.Comparator.UTest/OpenApiSpecificationsCompareTests.cs b/src/Criteo.OpenApi.Comparator.UTest/OpenApiSpecificationsCompareTests.cs index 791b678..241b213 100644 --- a/src/Criteo.OpenApi.Comparator.UTest/OpenApiSpecificationsCompareTests.cs +++ b/src/Criteo.OpenApi.Comparator.UTest/OpenApiSpecificationsCompareTests.cs @@ -44,7 +44,7 @@ public void CompareOAS(string testcase) var diffFileName = Path.Combine(resourceDirectory, testcase, "diff.json"); var differences = OpenApiComparator - .Compare(File.ReadAllText(oldFileName), File.ReadAllText(newFileName)); + .Compare(File.ReadAllText(oldFileName), File.ReadAllText(newFileName), out _); var expectedDifferencesText = File.ReadAllText(diffFileName); var expectedDifferences = JsonSerializer diff --git a/src/Criteo.OpenApi.Comparator/OpenApiComparator.cs b/src/Criteo.OpenApi.Comparator/OpenApiComparator.cs index 1f6e546..1323ffb 100644 --- a/src/Criteo.OpenApi.Comparator/OpenApiComparator.cs +++ b/src/Criteo.OpenApi.Comparator/OpenApiComparator.cs @@ -2,8 +2,10 @@ // Licensed under the Apache 2.0 License. See LICENSE in the project root for license information. using System.Collections.Generic; +using System.Linq; using Criteo.OpenApi.Comparator.Comparators; using Criteo.OpenApi.Comparator.Parser; +using Microsoft.OpenApi.Models; namespace Criteo.OpenApi.Comparator { @@ -17,11 +19,20 @@ public static class OpenApiComparator /// /// The content of the old OpenAPI Specification /// The content of the new OpenAPI Specification + /// Parsing errors /// If true, then breaking changes are errors instead of warnings. - public static IEnumerable Compare(string oldOpenApiSpec, string newOpenApiSpec, bool strict = false) + public static IEnumerable Compare( + string oldOpenApiSpec, + string newOpenApiSpec, + out IEnumerable parsingErrors, + bool strict = false) { - var oldOpenApiDocument = OpenApiParser.Parse(oldOpenApiSpec); - var newOpenApiDocument = OpenApiParser.Parse(newOpenApiSpec); + var oldOpenApiDocument = OpenApiParser.Parse(oldOpenApiSpec, out var oldSpecDiagnostic); + var newOpenApiDocument = OpenApiParser.Parse(newOpenApiSpec, out var newSpecDiagnostic); + + parsingErrors = oldSpecDiagnostic.Errors + .Select(e => new ParsingError("old", e)) + .Concat(newSpecDiagnostic.Errors.Select(e => new ParsingError("new", e))); var context = new ComparisonContext(oldOpenApiDocument, newOpenApiDocument) { Strict = strict }; @@ -31,4 +42,27 @@ public static IEnumerable Compare(string oldOpenApiSpec, stri return comparisonMessages; } } + + /// + /// Represents an error that occurred while parsing an OpenAPI document. + /// + public class ParsingError + { + private readonly string _documentName; + private readonly OpenApiError _error; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ParsingError(string documentName, OpenApiError error) + { + _documentName = documentName; + _error = error; + } + + /// + public override string ToString() => $"[{_documentName}] {_error}"; + } } diff --git a/src/Criteo.OpenApi.Comparator/Parser/OpenApiParser.cs b/src/Criteo.OpenApi.Comparator/Parser/OpenApiParser.cs index 386b282..e395fd4 100644 --- a/src/Criteo.OpenApi.Comparator/Parser/OpenApiParser.cs +++ b/src/Criteo.OpenApi.Comparator/Parser/OpenApiParser.cs @@ -15,14 +15,15 @@ namespace Criteo.OpenApi.Comparator.Parser internal static class OpenApiParser { /// Swagger as string - internal static JsonDocument Parse(string openApiDocumentAsString) + /// + internal static JsonDocument Parse(string openApiDocumentAsString, out OpenApiDiagnostic diagnostic) { var openApiReaderSettings = new OpenApiReaderSettings { ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences }; var openApiReader = new OpenApiStringReader(openApiReaderSettings); - var openApiDocument = openApiReader.Read(openApiDocumentAsString, out _); + var openApiDocument = openApiReader.Read(openApiDocumentAsString, out diagnostic); var textWriter = new StringWriter(); var openApiWriter = new OpenApiJsonWriter(textWriter);