diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 1f86e3c06..2cf1b01ad 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -22,12 +22,13 @@ using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Validations; using Microsoft.OpenApi.Writers; +using System.Threading; namespace Microsoft.OpenApi.Hidi { public class OpenApiService { - public static async Task ProcessOpenApiDocument( + public static async Task ProcessOpenApiDocument( string openapi, string csdl, FileInfo output, @@ -38,7 +39,8 @@ public static async Task ProcessOpenApiDocument( bool resolveexternal, string filterbyoperationids, string filterbytags, - string filterbycollection + string filterbycollection, + CancellationToken cancellationToken ) { var logger = ConfigureLoggerInstance(loglevel); @@ -47,150 +49,143 @@ string filterbycollection { if (string.IsNullOrEmpty(openapi) && string.IsNullOrEmpty(csdl)) { - throw new ArgumentNullException("Please input a file path"); + throw new ArgumentException("Please input a file path"); } - } - catch (ArgumentNullException ex) - { - logger.LogError(ex.Message); - return; - } - try - { if(output == null) { - throw new ArgumentException(nameof(output)); + throw new ArgumentNullException(nameof(output)); } - } - catch (ArgumentException ex) - { - logger.LogError(ex.Message); - return; - } - try - { if (output.Exists) { - throw new IOException("The file you're writing to already exists. Please input a new file path."); + throw new IOException($"The file {output} already exists. Please input a new file path."); } - } - catch (IOException ex) - { - logger.LogError(ex.Message); - return; - } - - Stream stream; - OpenApiDocument document; - OpenApiFormat openApiFormat; - var stopwatch = new Stopwatch(); - if (!string.IsNullOrEmpty(csdl)) - { - // Default to yaml and OpenApiVersion 3 during csdl to OpenApi conversion - openApiFormat = format ?? GetOpenApiFormat(csdl, logger); - version ??= OpenApiSpecVersion.OpenApi3_0; + Stream stream; + OpenApiDocument document; + OpenApiFormat openApiFormat; + var stopwatch = new Stopwatch(); - stream = await GetStream(csdl, logger); - document = await ConvertCsdlToOpenApi(stream); - } - else - { - stream = await GetStream(openapi, logger); - - // Parsing OpenAPI file - stopwatch.Start(); - logger.LogTrace("Parsing OpenApi file"); - var result = new OpenApiStreamReader(new OpenApiReaderSettings + if (!string.IsNullOrEmpty(csdl)) { - ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences, - RuleSet = ValidationRuleSet.GetDefaultRuleSet() + // Default to yaml and OpenApiVersion 3 during csdl to OpenApi conversion + openApiFormat = format ?? GetOpenApiFormat(csdl, logger); + version ??= OpenApiSpecVersion.OpenApi3_0; + + stream = await GetStream(csdl, logger, cancellationToken); + document = await ConvertCsdlToOpenApi(stream); } - ).ReadAsync(stream).GetAwaiter().GetResult(); + else + { + stream = await GetStream(openapi, logger, cancellationToken); - document = result.OpenApiDocument; - stopwatch.Stop(); + // Parsing OpenAPI file + stopwatch.Start(); + logger.LogTrace("Parsing OpenApi file"); + var result = new OpenApiStreamReader(new OpenApiReaderSettings + { + ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences, + RuleSet = ValidationRuleSet.GetDefaultRuleSet() + } + ).ReadAsync(stream).GetAwaiter().GetResult(); - var context = result.OpenApiDiagnostic; - if (context.Errors.Count > 0) - { - var errorReport = new StringBuilder(); + document = result.OpenApiDocument; + stopwatch.Stop(); - foreach (var error in context.Errors) + var context = result.OpenApiDiagnostic; + if (context.Errors.Count > 0) { - errorReport.AppendLine(error.ToString()); + logger.LogTrace("{timestamp}ms: Parsed OpenAPI with errors. {count} paths found.", stopwatch.ElapsedMilliseconds, document.Paths.Count); + + var errorReport = new StringBuilder(); + + foreach (var error in context.Errors) + { + logger.LogError("OpenApi Parsing error: {message}", error.ToString()); + errorReport.AppendLine(error.ToString()); + } + logger.LogError($"{stopwatch.ElapsedMilliseconds}ms: OpenApi Parsing errors {string.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray())}"); } - logger.LogError($"{stopwatch.ElapsedMilliseconds}ms: OpenApi Parsing errors {string.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray())}"); + else + { + logger.LogTrace("{timestamp}ms: Parsed OpenApi successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, document.Paths.Count); + } + + openApiFormat = format ?? GetOpenApiFormat(openapi, logger); + version ??= result.OpenApiDiagnostic.SpecificationVersion; } - else + + Func predicate; + + // Check if filter options are provided, then slice the OpenAPI document + if (!string.IsNullOrEmpty(filterbyoperationids) && !string.IsNullOrEmpty(filterbytags)) { - logger.LogTrace("{timestamp}ms: Parsed OpenApi successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, document.Paths.Count); + throw new InvalidOperationException("Cannot filter by operationIds and tags at the same time."); } + if (!string.IsNullOrEmpty(filterbyoperationids)) + { + logger.LogTrace("Creating predicate based on the operationIds supplied."); + predicate = OpenApiFilterService.CreatePredicate(operationIds: filterbyoperationids); - openApiFormat = format ?? GetOpenApiFormat(openapi, logger); - version ??= result.OpenApiDiagnostic.SpecificationVersion; - } + logger.LogTrace("Creating subset OpenApi document."); + document = OpenApiFilterService.CreateFilteredDocument(document, predicate); + } + if (!string.IsNullOrEmpty(filterbytags)) + { + logger.LogTrace("Creating predicate based on the tags supplied."); + predicate = OpenApiFilterService.CreatePredicate(tags: filterbytags); - Func predicate; + logger.LogTrace("Creating subset OpenApi document."); + document = OpenApiFilterService.CreateFilteredDocument(document, predicate); + } + if (!string.IsNullOrEmpty(filterbycollection)) + { + var fileStream = await GetStream(filterbycollection, logger, cancellationToken); + var requestUrls = ParseJsonCollectionFile(fileStream, logger); - // Check if filter options are provided, then slice the OpenAPI document - if (!string.IsNullOrEmpty(filterbyoperationids) && !string.IsNullOrEmpty(filterbytags)) - { - throw new InvalidOperationException("Cannot filter by operationIds and tags at the same time."); - } - if (!string.IsNullOrEmpty(filterbyoperationids)) - { - logger.LogTrace("Creating predicate based on the operationIds supplied."); - predicate = OpenApiFilterService.CreatePredicate(operationIds: filterbyoperationids); + logger.LogTrace("Creating predicate based on the paths and Http methods defined in the Postman collection."); + predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source: document); - logger.LogTrace("Creating subset OpenApi document."); - document = OpenApiFilterService.CreateFilteredDocument(document, predicate); - } - if (!string.IsNullOrEmpty(filterbytags)) - { - logger.LogTrace("Creating predicate based on the tags supplied."); - predicate = OpenApiFilterService.CreatePredicate(tags: filterbytags); + logger.LogTrace("Creating subset OpenApi document."); + document = OpenApiFilterService.CreateFilteredDocument(document, predicate); + } - logger.LogTrace("Creating subset OpenApi document."); - document = OpenApiFilterService.CreateFilteredDocument(document, predicate); - } - if (!string.IsNullOrEmpty(filterbycollection)) - { - var fileStream = await GetStream(filterbycollection, logger); - var requestUrls = ParseJsonCollectionFile(fileStream, logger); + logger.LogTrace("Creating a new file"); + using var outputStream = output?.Create(); + var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out; - logger.LogTrace("Creating predicate based on the paths and Http methods defined in the Postman collection."); - predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source:document); + var settings = new OpenApiWriterSettings() + { + ReferenceInline = inline ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences + }; - logger.LogTrace("Creating subset OpenApi document."); - document = OpenApiFilterService.CreateFilteredDocument(document, predicate); - } - - logger.LogTrace("Creating a new file"); - using var outputStream = output?.Create(); - var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out; + IOpenApiWriter writer = openApiFormat switch + { + OpenApiFormat.Json => new OpenApiJsonWriter(textWriter, settings), + OpenApiFormat.Yaml => new OpenApiYamlWriter(textWriter, settings), + _ => throw new ArgumentException("Unknown format"), + }; - var settings = new OpenApiWriterSettings() - { - ReferenceInline = inline ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences - }; + logger.LogTrace("Serializing to OpenApi document using the provided spec version and writer"); - IOpenApiWriter writer = openApiFormat switch - { - OpenApiFormat.Json => new OpenApiJsonWriter(textWriter, settings), - OpenApiFormat.Yaml => new OpenApiYamlWriter(textWriter, settings), - _ => throw new ArgumentException("Unknown format"), - }; + stopwatch.Start(); + document.Serialize(writer, (OpenApiSpecVersion)version); + stopwatch.Stop(); - logger.LogTrace("Serializing to OpenApi document using the provided spec version and writer"); - - stopwatch.Start(); - document.Serialize(writer, (OpenApiSpecVersion)version); - stopwatch.Stop(); + logger.LogTrace($"Finished serializing in {stopwatch.ElapsedMilliseconds}ms"); - logger.LogTrace($"Finished serializing in {stopwatch.ElapsedMilliseconds}ms"); + textWriter.Flush(); - textWriter.Flush(); + return 0; + } + catch (Exception ex) + { +#if DEBUG + logger.LogCritical(ex, ex.Message); +#else + logger.LogCritical(ex.Message); +#endif + return 1; + } } /// @@ -245,7 +240,7 @@ public static OpenApiDocument FixReferences(OpenApiDocument document) return doc; } - private static async Task GetStream(string input, ILogger logger) + private static async Task GetStream(string input, ILogger logger, CancellationToken cancellationToken) { var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -263,12 +258,11 @@ private static async Task GetStream(string input, ILogger logger) { DefaultRequestVersion = HttpVersion.Version20 }; - stream = await httpClient.GetStreamAsync(input); + stream = await httpClient.GetStreamAsync(input, cancellationToken); } catch (HttpRequestException ex) { - logger.LogError($"Could not download the file at {input}, reason{ex}"); - return null; + throw new InvalidOperationException($"Could not download the file at {input}", ex); } } else @@ -286,8 +280,7 @@ ex is UnauthorizedAccessException || ex is SecurityException || ex is NotSupportedException) { - logger.LogError($"Could not open the file at {input}, reason: {ex.Message}"); - return null; + throw new InvalidOperationException($"Could not open the file at {input}", ex); } } stopwatch.Stop(); @@ -327,37 +320,53 @@ public static Dictionary> ParseJsonCollectionFile(Stream st return requestUrls; } - internal static async Task ValidateOpenApiDocument(string openapi, LogLevel loglevel) + internal static async Task ValidateOpenApiDocument(string openapi, LogLevel loglevel, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(openapi)) - { - throw new ArgumentNullException(nameof(openapi)); - } var logger = ConfigureLoggerInstance(loglevel); - var stream = await GetStream(openapi, logger); - OpenApiDocument document; - logger.LogTrace("Parsing the OpenApi file"); - document = new OpenApiStreamReader(new OpenApiReaderSettings + try { - RuleSet = ValidationRuleSet.GetDefaultRuleSet() - } - ).Read(stream, out var context); + if (string.IsNullOrEmpty(openapi)) + { + throw new ArgumentNullException(nameof(openapi)); + } + var stream = await GetStream(openapi, logger, cancellationToken); - if (context.Errors.Count != 0) - { - foreach (var error in context.Errors) + OpenApiDocument document; + logger.LogTrace("Parsing the OpenApi file"); + document = new OpenApiStreamReader(new OpenApiReaderSettings { - Console.WriteLine(error.ToString()); + RuleSet = ValidationRuleSet.GetDefaultRuleSet() } - } + ).Read(stream, out var context); - var statsVisitor = new StatsVisitor(); - var walker = new OpenApiWalker(statsVisitor); - walker.Walk(document); + if (context.Errors.Count != 0) + { + foreach (var error in context.Errors) + { + logger.LogError("OpenApi Parsing error: {message}", error.ToString()); + } + } + + var statsVisitor = new StatsVisitor(); + var walker = new OpenApiWalker(statsVisitor); + walker.Walk(document); + + logger.LogTrace("Finished walking through the OpenApi document. Generating a statistics report.."); + logger.LogInformation(statsVisitor.GetStatisticsReport()); + + return 0; + } + catch(Exception ex) + { +#if DEBUG + logger.LogCritical(ex, ex.Message); +#else + logger.LogCritical(ex.Message); +#endif + return 1; + } - logger.LogTrace("Finished walking through the OpenApi document. Generating a statistics report.."); - Console.WriteLine(statsVisitor.GetStatisticsReport()); } private static OpenApiFormat GetOpenApiFormat(string input, ILogger logger) @@ -369,16 +378,16 @@ private static OpenApiFormat GetOpenApiFormat(string input, ILogger logger) private static ILogger ConfigureLoggerInstance(LogLevel loglevel) { // Configure logger options - #if DEBUG +#if DEBUG loglevel = loglevel > LogLevel.Debug ? LogLevel.Debug : loglevel; - #endif +#endif var logger = LoggerFactory.Create((builder) => { builder .AddConsole() - #if DEBUG +#if DEBUG .AddDebug() - #endif +#endif .SetMinimumLevel(loglevel); }).CreateLogger(); diff --git a/src/Microsoft.OpenApi.Hidi/Program.cs b/src/Microsoft.OpenApi.Hidi/Program.cs index 95e6f63f2..f3d455e4a 100644 --- a/src/Microsoft.OpenApi.Hidi/Program.cs +++ b/src/Microsoft.OpenApi.Hidi/Program.cs @@ -3,6 +3,7 @@ using System.CommandLine; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -55,7 +56,7 @@ static async Task Main(string[] args) logLevelOption }; - validateCommand.SetHandler(OpenApiService.ValidateOpenApiDocument, descriptionOption, logLevelOption); + validateCommand.SetHandler(OpenApiService.ValidateOpenApiDocument, descriptionOption, logLevelOption); var transformCommand = new Command("transform") { @@ -72,7 +73,7 @@ static async Task Main(string[] args) resolveExternalOption, }; - transformCommand.SetHandler ( + transformCommand.SetHandler ( OpenApiService.ProcessOpenApiDocument, descriptionOption, csdlOption, outputOption, versionOption, formatOption, logLevelOption, inlineOption, resolveExternalOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption); rootCommand.Add(transformCommand);