From e5170530affd8494756f4e5f90ef7004c8b79135 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Thu, 22 Dec 2022 22:54:07 -0500 Subject: [PATCH 01/35] Added show command --- .../Handlers/ShowCommandHandler.cs | 51 ++++++++ src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 114 ++++++++++++++++++ src/Microsoft.OpenApi.Hidi/Program.cs | 16 +++ src/Microsoft.OpenApi.Hidi/readme.md | 32 +++-- 4 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs diff --git a/src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs b/src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs new file mode 100644 index 000000000..e6542c34a --- /dev/null +++ b/src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.OpenApi.Hidi.Handlers +{ + internal class ShowCommandHandler : ICommandHandler + { + public Option DescriptionOption { get; set; } + public Option OutputOption { get; set; } + public Option LogLevelOption { get; set; } + + public int Invoke(InvocationContext context) + { + return InvokeAsync(context).GetAwaiter().GetResult(); + } + public async Task InvokeAsync(InvocationContext context) + { + string openapi = context.ParseResult.GetValueForOption(DescriptionOption); + FileInfo output = context.ParseResult.GetValueForOption(OutputOption); + LogLevel logLevel = context.ParseResult.GetValueForOption(LogLevelOption); + CancellationToken cancellationToken = (CancellationToken)context.BindingContext.GetService(typeof(CancellationToken)); + + using var loggerFactory = Logger.ConfigureLogger(logLevel); + var logger = loggerFactory.CreateLogger(); + try + { + await OpenApiService.ShowOpenApiDocument(openapi, output, logLevel, cancellationToken); + + return 0; + } + catch (Exception ex) + { +#if DEBUG + logger.LogCritical(ex, ex.Message); + throw; // so debug tools go straight to the source of the exception when attached +#else + logger.LogCritical( ex.Message); + return 1; +#endif + } + } + } +} diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 60bba4aef..a1f95a63d 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -535,5 +535,119 @@ private static ILoggerFactory ConfigureLoggerInstance(LogLevel loglevel) .SetMinimumLevel(loglevel); }); } + + internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, LogLevel logLevel, CancellationToken cancellationToken) + { + using var loggerFactory = Logger.ConfigureLogger(logLevel); + var logger = loggerFactory.CreateLogger(); + try + { + if (string.IsNullOrEmpty(openapi)) + { + throw new ArgumentNullException(nameof(openapi)); + } + var stream = await GetStream(openapi, logger, cancellationToken); + + OpenApiDocument document; + Stopwatch stopwatch = Stopwatch.StartNew(); + using (logger.BeginScope($"Parsing OpenAPI: {openapi}", openapi)) + { + stopwatch.Start(); + + var result = await new OpenApiStreamReader(new OpenApiReaderSettings + { + RuleSet = ValidationRuleSet.GetDefaultRuleSet() + } + ).ReadAsync(stream); + + logger.LogTrace("{timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds); + + document = result.OpenApiDocument; + var context = result.OpenApiDiagnostic; + if (context.Errors.Count != 0) + { + using (logger.BeginScope("Detected errors")) + { + foreach (var error in context.Errors) + { + logger.LogError(error.ToString()); + } + } + } + stopwatch.Stop(); + } + + using (logger.BeginScope("Creating diagram")) + { + // Create OpenApiUrlTree from document + + using var file = new FileStream(output.FullName, FileMode.Create); + var writer = new StreamWriter(file); + WriteTreeDocument(openapi, document, writer); + writer.Flush(); + + logger.LogTrace("Finished walking through the OpenApi document. "); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Could not generate the document, reason: {ex.Message}", ex); + } + } + + private static void WriteTreeDocument(string openapi, OpenApiDocument document, StreamWriter writer) + { + var rootNode = OpenApiUrlTreeNode.Create(document, "main"); + + writer.WriteLine("# " + document.Info.Title); + writer.WriteLine(); + writer.WriteLine("OpenAPI: " + openapi); + writer.Write(@"
+GET +POST +GET POST +GET PATCH DELETE +GET PUT DELETE +GET DELETE +DELETE +
+"); + writer.WriteLine(); + writer.WriteLine("```mermaid"); + writer.WriteLine("graph LR"); + writer.WriteLine("classDef GET fill:lightSteelBlue,stroke:#333,stroke-width:2px;"); + writer.WriteLine("classDef POST fill:SteelBlue,stroke:#333,stroke-width:2px;"); + writer.WriteLine("classDef GETPOST fill:forestGreen,stroke:#333,stroke-width:2px;"); + writer.WriteLine("classDef DELETEGETPATCH fill:yellowGreen,stroke:#333,stroke-width:2px;"); + writer.WriteLine("classDef DELETEGETPUT fill:olive,stroke:#333,stroke-width:2px;"); + writer.WriteLine("classDef DELETEGET fill:DarkSeaGreen,stroke:#333,stroke-width:2px;"); + writer.WriteLine("classDef DELETE fill:tomato,stroke:#333,stroke-width:2px;"); + writer.WriteLine("classDef OTHER fill:white,stroke:#333,stroke-width:2px;"); + + ProcessNode(rootNode, writer); + writer.WriteLine("```"); + } + + private static void ProcessNode(OpenApiUrlTreeNode node, StreamWriter writer) + { + var path = string.IsNullOrEmpty(node.Path) ? "/" : Sanitize(node.Path); + foreach (var child in node.Children) + { + writer.WriteLine($"{Sanitize(path)} --> {Sanitize(child.Value.Path)}[{Sanitize(child.Key)}]"); + ProcessNode(child.Value, writer); + } + var methods = String.Join("", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) + .Distinct() + .Select(o => o.ToString().ToUpper()) + .OrderBy(o => o) + .ToList()); + if (String.IsNullOrEmpty(methods)) methods = "OTHER"; + writer.WriteLine($"class {path} {methods}"); + } + + private static string Sanitize(string token) + { + return token.Replace("\\", "/").Replace("{", ":").Replace("}", ""); + } } } diff --git a/src/Microsoft.OpenApi.Hidi/Program.cs b/src/Microsoft.OpenApi.Hidi/Program.cs index e9246eb6c..b9db1229f 100644 --- a/src/Microsoft.OpenApi.Hidi/Program.cs +++ b/src/Microsoft.OpenApi.Hidi/Program.cs @@ -115,6 +115,22 @@ static async Task Main(string[] args) InlineExternalOption = inlineExternalOption }; + var showCommand = new Command("show") + { + descriptionOption, + logLevelOption, + outputOption, + cleanOutputOption + }; + + showCommand.Handler = new ShowCommandHandler + { + DescriptionOption = descriptionOption, + OutputOption = outputOption, + LogLevelOption = logLevelOption + }; + + rootCommand.Add(showCommand); rootCommand.Add(transformCommand); rootCommand.Add(validateCommand); diff --git a/src/Microsoft.OpenApi.Hidi/readme.md b/src/Microsoft.OpenApi.Hidi/readme.md index 6295c5c99..a6283817e 100644 --- a/src/Microsoft.OpenApi.Hidi/readme.md +++ b/src/Microsoft.OpenApi.Hidi/readme.md @@ -1,24 +1,26 @@ -# Overview +# Overview Hidi is a command line tool that makes it easy to work with and transform OpenAPI documents. The tool enables you validate and apply transformations to and from different file formats using various commands to do different actions on the files. ## Capabilities + Hidi has these key capabilities that enable you to build different scenarios off the tool • Validation of OpenAPI files • Conversion of OpenAPI files into different file formats: convert files from JSON to YAML, YAML to JSON • Slice or filter OpenAPI documents to smaller subsets using operationIDs and tags + • Generate a Mermaid diagram of the API from an OpenAPI document - -## Installation +## Installation Install [Microsoft.OpenApi.Hidi](https://www.nuget.org/packages/Microsoft.OpenApi.Hidi/1.0.0-preview4) package from NuGet by running the following command: -### .NET CLI(Global) +### .NET CLI(Global) + 1. dotnet tool install --global Microsoft.OpenApi.Hidi --prerelease -### .NET CLI(local) +### .NET CLI(local) 1. dotnet new tool-manifest #if you are setting up the OpenAPI.NET repo 2. dotnet tool install --local Microsoft.OpenApi.Hidi --prerelease @@ -27,14 +29,17 @@ Install [Microsoft.OpenApi.Hidi](https://www.nuget.org/packages/Microsoft.OpenAp ## How to use Hidi + Once you've installed the package locally, you can invoke the Hidi by running: hidi [command]. You can access the list of command options we have by running hidi -h The tool avails the following commands: • Validate • Transform + • Show -### Validate +### Validate + This command option accepts an OpenAPI document as an input parameter, visits multiple OpenAPI elements within the document and returns statistics count report on the following elements: • Path Items @@ -54,9 +59,10 @@ It accepts the following command: **Example:** `hidi.exe validate --openapi C:\OpenApidocs\Mail.yml --loglevel trace` -Run validate -h to see the options available. - -### Transform +Run validate -h to see the options available. + +### Transform + Used to convert file formats from JSON to YAML and vice versa and performs slicing of OpenAPI documents. This command accepts the following parameters: @@ -90,3 +96,11 @@ This command accepts the following parameters: hidi transform -cs dataverse.csdl --csdlFilter "appointments,opportunities" -o appointmentsAndOpportunities.yaml -ll trace Run transform -h to see all the available usage options. + +### Show + +This command accepts an OpenAPI document as an input parameter and generates a Markdown file that contains a diagram of the API using Mermaid syntax. + +**Examples:** + + 1. hidi show -d files\People.yml -o People.md -ll trace From 49a12f384632a08c2cb663c7a9c8350a62953294 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Fri, 23 Dec 2022 12:42:12 -0500 Subject: [PATCH 02/35] Moved mermaid writer into OpenApiUrlTreeNode and fixed more sanitization issues --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 52 +++------------- .../Services/OpenApiUrlTreeNode.cs | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index a1f95a63d..881fda1e1 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -602,52 +602,18 @@ private static void WriteTreeDocument(string openapi, OpenApiDocument document, writer.WriteLine("# " + document.Info.Title); writer.WriteLine(); writer.WriteLine("OpenAPI: " + openapi); - writer.Write(@"
-GET -POST -GET POST -GET PATCH DELETE -GET PUT DELETE -GET DELETE -DELETE -
-"); - writer.WriteLine(); - writer.WriteLine("```mermaid"); - writer.WriteLine("graph LR"); - writer.WriteLine("classDef GET fill:lightSteelBlue,stroke:#333,stroke-width:2px;"); - writer.WriteLine("classDef POST fill:SteelBlue,stroke:#333,stroke-width:2px;"); - writer.WriteLine("classDef GETPOST fill:forestGreen,stroke:#333,stroke-width:2px;"); - writer.WriteLine("classDef DELETEGETPATCH fill:yellowGreen,stroke:#333,stroke-width:2px;"); - writer.WriteLine("classDef DELETEGETPUT fill:olive,stroke:#333,stroke-width:2px;"); - writer.WriteLine("classDef DELETEGET fill:DarkSeaGreen,stroke:#333,stroke-width:2px;"); - writer.WriteLine("classDef DELETE fill:tomato,stroke:#333,stroke-width:2px;"); - writer.WriteLine("classDef OTHER fill:white,stroke:#333,stroke-width:2px;"); - - ProcessNode(rootNode, writer); - writer.WriteLine("```"); - } - private static void ProcessNode(OpenApiUrlTreeNode node, StreamWriter writer) - { - var path = string.IsNullOrEmpty(node.Path) ? "/" : Sanitize(node.Path); - foreach (var child in node.Children) + writer.WriteLine(@"
"); + // write a span for each mermaidcolorscheme + foreach (var color in OpenApiUrlTreeNode.MermaidColorScheme) { - writer.WriteLine($"{Sanitize(path)} --> {Sanitize(child.Value.Path)}[{Sanitize(child.Key)}]"); - ProcessNode(child.Value, writer); + writer.WriteLine($"{color.Key.Replace("_"," ")}"); } - var methods = String.Join("", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) - .Distinct() - .Select(o => o.ToString().ToUpper()) - .OrderBy(o => o) - .ToList()); - if (String.IsNullOrEmpty(methods)) methods = "OTHER"; - writer.WriteLine($"class {path} {methods}"); - } - - private static string Sanitize(string token) - { - return token.Replace("\\", "/").Replace("{", ":").Replace("}", ""); + writer.WriteLine("/div"); + writer.WriteLine(); + writer.WriteLine("```mermaid"); + rootNode.WriteMermaid(writer); + writer.WriteLine("```"); } } } diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 30a47bdd7..81c66f120 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.OpenApi.Models; @@ -235,5 +236,63 @@ public void AddAdditionalData(Dictionary> additionalData) } } } + + /// + /// Write tree as Mermaid syntax + /// + /// StreamWriter to write the Mermaid content to + public void WriteMermaid(StreamWriter writer) + { + writer.WriteLine("graph LR"); + foreach (var color in MermaidColorScheme) + { + writer.WriteLine($"classDef {color.Key} fill:{color.Value},stroke:#333,stroke-width:4px"); + } + + ProcessNode(this, writer); + } + + /// + /// Dictionary that maps a set of HTTP methods to HTML color. Keys are sorted, uppercased, concatenated HTTP methods. + /// + public static Dictionary MermaidColorScheme = new Dictionary + { + { "GET", "lightSteelBlue" }, + { "POST", "SteelBlue" }, + { "GET_POST", "forestGreen" }, + { "DELETE_GET_PATCH", "yellowGreen" }, + { "DELETE_GET_PUT", "olive" }, + { "DELETE_GET", "DarkSeaGreen" }, + { "DELETE", "tomato" }, + { "OTHER", "white" } + }; + + private static void ProcessNode(OpenApiUrlTreeNode node, StreamWriter writer) + { + var path = string.IsNullOrEmpty(node.Path) ? "/" : SanitizeMermaidNode(node.Path); + foreach (var child in node.Children) + { + writer.WriteLine($"{path} --> {SanitizeMermaidNode(child.Value.Path)}[\"{child.Key}\"]"); + ProcessNode(child.Value, writer); + } + var methods = String.Join("_", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) + .Distinct() + .Select(o => o.ToString().ToUpper()) + .OrderBy(o => o) + .ToList()); + if (String.IsNullOrEmpty(methods)) methods = "OTHER"; + writer.WriteLine($"class {path} {methods}"); + } + + private static string SanitizeMermaidNode(string token) + { + return token.Replace("\\", "/") + .Replace("{", ":") + .Replace("}", "") + .Replace(".", "_") + .Replace(";", "_") + .Replace("-", "_") + .Replace("default", "def_ault"); // default is a reserved word for classes + } } } From 87818841f1739220145dfab79053452456b515f9 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 24 Dec 2022 18:35:42 -0500 Subject: [PATCH 03/35] Added shapes for better accessibility --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 6 +- .../Services/OpenApiUrlTreeNode.cs | 119 +++++++++++++++--- 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 881fda1e1..fbbacb140 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -605,11 +605,11 @@ private static void WriteTreeDocument(string openapi, OpenApiDocument document, writer.WriteLine(@"
"); // write a span for each mermaidcolorscheme - foreach (var color in OpenApiUrlTreeNode.MermaidColorScheme) + foreach (var style in OpenApiUrlTreeNode.MermaidNodeStyles) { - writer.WriteLine($"{color.Key.Replace("_"," ")}"); + writer.WriteLine($"{style.Key.Replace("_"," ")}"); } - writer.WriteLine("/div"); + writer.WriteLine("
"); writer.WriteLine(); writer.WriteLine("```mermaid"); rootNode.WriteMermaid(writer); diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 81c66f120..5b07cd8c7 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -244,9 +244,9 @@ public void AddAdditionalData(Dictionary> additionalData) public void WriteMermaid(StreamWriter writer) { writer.WriteLine("graph LR"); - foreach (var color in MermaidColorScheme) + foreach (var style in MermaidNodeStyles) { - writer.WriteLine($"classDef {color.Key} fill:{color.Value},stroke:#333,stroke-width:4px"); + writer.WriteLine($"classDef {style.Key} fill:{style.Value.Color},stroke:#333,stroke-width:2px"); } ProcessNode(this, writer); @@ -255,35 +255,71 @@ public void WriteMermaid(StreamWriter writer) /// /// Dictionary that maps a set of HTTP methods to HTML color. Keys are sorted, uppercased, concatenated HTTP methods. /// - public static Dictionary MermaidColorScheme = new Dictionary + public static Dictionary MermaidNodeStyles = new Dictionary { - { "GET", "lightSteelBlue" }, - { "POST", "SteelBlue" }, - { "GET_POST", "forestGreen" }, - { "DELETE_GET_PATCH", "yellowGreen" }, - { "DELETE_GET_PUT", "olive" }, - { "DELETE_GET", "DarkSeaGreen" }, - { "DELETE", "tomato" }, - { "OTHER", "white" } + { "GET", new MermaidNodeStyle("lightSteelBlue", MermaidNodeShape.SquareCornerRectangle) }, + { "POST", new MermaidNodeStyle("Lightcoral", MermaidNodeShape.OddShape) }, + { "GET_POST", new MermaidNodeStyle("forestGreen", MermaidNodeShape.RoundedCornerRectangle) }, + { "DELETE_GET_PATCH", new MermaidNodeStyle("yellowGreen", MermaidNodeShape.Circle) }, + { "DELETE_GET_PATCH_PUT", new MermaidNodeStyle("oliveDrab", MermaidNodeShape.Circle) }, + { "DELETE_GET_PUT", new MermaidNodeStyle("olive", MermaidNodeShape.Circle) }, + { "DELETE_GET", new MermaidNodeStyle("DarkSeaGreen", MermaidNodeShape.Circle) }, + { "DELETE", new MermaidNodeStyle("Tomato", MermaidNodeShape.Rhombus) }, + { "OTHER", new MermaidNodeStyle("White", MermaidNodeShape.SquareCornerRectangle) }, }; private static void ProcessNode(OpenApiUrlTreeNode node, StreamWriter writer) { var path = string.IsNullOrEmpty(node.Path) ? "/" : SanitizeMermaidNode(node.Path); + var methods = GetMethods(node); + var (startChar, endChar) = GetShapeDelimiters(methods); foreach (var child in node.Children) { - writer.WriteLine($"{path} --> {SanitizeMermaidNode(child.Value.Path)}[\"{child.Key}\"]"); + var childMethods = GetMethods(child.Value); + var (childStartChar, childEndChar) = GetShapeDelimiters(childMethods); + writer.WriteLine($"{path}{startChar}\"{node.Segment}\"{endChar} --> {SanitizeMermaidNode(child.Value.Path)}{childStartChar}\"{child.Key}\"{childEndChar}"); ProcessNode(child.Value, writer); } - var methods = String.Join("_", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) + if (String.IsNullOrEmpty(methods)) methods = "OTHER"; + writer.WriteLine($"class {path} {methods}"); + } + + private static string GetMethods(OpenApiUrlTreeNode node) + { + return String.Join("_", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) .Distinct() .Select(o => o.ToString().ToUpper()) .OrderBy(o => o) .ToList()); - if (String.IsNullOrEmpty(methods)) methods = "OTHER"; - writer.WriteLine($"class {path} {methods}"); } + private static (string, string) GetShapeDelimiters(string methods) + { + + if (MermaidNodeStyles.ContainsKey(methods)) + { + //switch on shape + switch (MermaidNodeStyles[methods].Shape) + { + case MermaidNodeShape.Circle: + return ("((", "))"); + case MermaidNodeShape.RoundedCornerRectangle: + return ("(", ")"); + case MermaidNodeShape.Rhombus: + return ("{", "}"); + case MermaidNodeShape.SquareCornerRectangle: + return ("[", "]"); + case MermaidNodeShape.OddShape: + return (">", "]"); + default: + return ("[", "]"); + } + } + else + { + return ("[", "]"); + } + } private static string SanitizeMermaidNode(string token) { return token.Replace("\\", "/") @@ -295,4 +331,57 @@ private static string SanitizeMermaidNode(string token) .Replace("default", "def_ault"); // default is a reserved word for classes } } + /// + /// Defines the color and shape of a node in a Mermaid graph diagram + /// + public class MermaidNodeStyle + { + /// + /// + /// + /// + /// + public MermaidNodeStyle(string color, MermaidNodeShape shape) + { + Color = color; + Shape = shape; + } + + /// + /// + /// + public string Color { get; } + + /// + /// + /// + public MermaidNodeShape Shape { get; } + } + + /// + /// + /// + public enum MermaidNodeShape + { + /// + /// Rectangle with square corners + /// + SquareCornerRectangle, + /// + /// Rectangle with rounded corners + /// + RoundedCornerRectangle, + /// + /// Circle + /// + Circle, + /// + /// Rhombus + /// + Rhombus, + /// + /// Odd shape + /// + OddShape + } } From 0bc172675c9d65cabcb1649c08a388a3732372f8 Mon Sep 17 00:00:00 2001 From: Darrel Date: Sat, 24 Dec 2022 19:06:22 -0500 Subject: [PATCH 04/35] Update to do a unnecessary using Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index fbbacb140..0df940d03 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -582,7 +582,7 @@ internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, // Create OpenApiUrlTree from document using var file = new FileStream(output.FullName, FileMode.Create); - var writer = new StreamWriter(file); + using var writer = new StreamWriter(file); WriteTreeDocument(openapi, document, writer); writer.Flush(); From e8061ac95c051c05c25681d96d92d2b13db5f3ae Mon Sep 17 00:00:00 2001 From: Darrel Date: Sat, 24 Dec 2022 19:14:45 -0500 Subject: [PATCH 05/35] Update src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 5b07cd8c7..205bb8cdd 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -289,7 +289,7 @@ private static string GetMethods(OpenApiUrlTreeNode node) return String.Join("_", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) .Distinct() .Select(o => o.ToString().ToUpper()) - .OrderBy(o => o) + .Order() .ToList()); } From 5a7146c09799b395b548e256f1a9c4aedca5a927 Mon Sep 17 00:00:00 2001 From: Darrel Date: Sat, 24 Dec 2022 19:15:08 -0500 Subject: [PATCH 06/35] Update src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 205bb8cdd..4b211349a 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -288,7 +288,7 @@ private static string GetMethods(OpenApiUrlTreeNode node) { return String.Join("_", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) .Distinct() - .Select(o => o.ToString().ToUpper()) + .Select(static o => o.ToString().ToUpper()) .Order() .ToList()); } From fc3ba5e36d2b8d7779e9cd7837b218c5b16c8429 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 24 Dec 2022 19:15:57 -0500 Subject: [PATCH 07/35] Added a bunch of usings and removed an unnecessary flush to address comments --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 0df940d03..52d2e4fc9 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -265,7 +265,7 @@ public static async Task ValidateOpenApiDocument( { throw new ArgumentNullException(nameof(openapi)); } - var stream = await GetStream(openapi, logger, cancellationToken); + using var stream = await GetStream(openapi, logger, cancellationToken); OpenApiDocument document; Stopwatch stopwatch = Stopwatch.StartNew(); @@ -546,7 +546,7 @@ internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, { throw new ArgumentNullException(nameof(openapi)); } - var stream = await GetStream(openapi, logger, cancellationToken); + using var stream = await GetStream(openapi, logger, cancellationToken); OpenApiDocument document; Stopwatch stopwatch = Stopwatch.StartNew(); @@ -584,7 +584,6 @@ internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, using var file = new FileStream(output.FullName, FileMode.Create); using var writer = new StreamWriter(file); WriteTreeDocument(openapi, document, writer); - writer.Flush(); logger.LogTrace("Finished walking through the OpenApi document. "); } From 6c57e8da3b8820ec7f82500f4fcfb098de86eac9 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 24 Dec 2022 19:22:48 -0500 Subject: [PATCH 08/35] Fixed broken order method --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 4b211349a..5b07cd8c7 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -288,8 +288,8 @@ private static string GetMethods(OpenApiUrlTreeNode node) { return String.Join("_", node.PathItems.SelectMany(p => p.Value.Operations.Select(o => o.Key)) .Distinct() - .Select(static o => o.ToString().ToUpper()) - .Order() + .Select(o => o.ToString().ToUpper()) + .OrderBy(o => o) .ToList()); } From 9e2ff5161cb0c8720cf14a98929704240410b11a Mon Sep 17 00:00:00 2001 From: Darrel Date: Wed, 28 Dec 2022 09:18:14 -0500 Subject: [PATCH 09/35] Update src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 5b07cd8c7..70b325712 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -296,7 +296,7 @@ private static string GetMethods(OpenApiUrlTreeNode node) private static (string, string) GetShapeDelimiters(string methods) { - if (MermaidNodeStyles.ContainsKey(methods)) + if (MermaidNodeStyles.TryGetValue(methods, out var style)) { //switch on shape switch (MermaidNodeStyles[methods].Shape) From efeeca7c5a76ec48eb99916ce161306576cf2189 Mon Sep 17 00:00:00 2001 From: Darrel Date: Wed, 28 Dec 2022 17:05:52 -0500 Subject: [PATCH 10/35] Update src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 70b325712..01634d94e 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -299,7 +299,7 @@ private static (string, string) GetShapeDelimiters(string methods) if (MermaidNodeStyles.TryGetValue(methods, out var style)) { //switch on shape - switch (MermaidNodeStyles[methods].Shape) + switch (style.Shape) { case MermaidNodeShape.Circle: return ("((", "))"); From 5d87820d686424d50136e3de330570b97cfbaaba Mon Sep 17 00:00:00 2001 From: Darrel Date: Wed, 28 Dec 2022 17:06:21 -0500 Subject: [PATCH 11/35] Update src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 01634d94e..556b54a9f 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -255,7 +255,7 @@ public void WriteMermaid(StreamWriter writer) /// /// Dictionary that maps a set of HTTP methods to HTML color. Keys are sorted, uppercased, concatenated HTTP methods. /// - public static Dictionary MermaidNodeStyles = new Dictionary + public static Dictionary MermaidNodeStyles = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "GET", new MermaidNodeStyle("lightSteelBlue", MermaidNodeShape.SquareCornerRectangle) }, { "POST", new MermaidNodeStyle("Lightcoral", MermaidNodeShape.OddShape) }, From 674fe14b22d37c82380814b72150905cecee1c14 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Tue, 3 Jan 2023 17:57:55 -0500 Subject: [PATCH 12/35] Changed mermaid styles to make them readonly --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 556b54a9f..d28b9d53f 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using Microsoft.OpenApi.Models; @@ -255,7 +256,7 @@ public void WriteMermaid(StreamWriter writer) /// /// Dictionary that maps a set of HTTP methods to HTML color. Keys are sorted, uppercased, concatenated HTTP methods. /// - public static Dictionary MermaidNodeStyles = new Dictionary(StringComparer.OrdinalIgnoreCase) + public readonly static IReadOnlyDictionary MermaidNodeStyles = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "GET", new MermaidNodeStyle("lightSteelBlue", MermaidNodeShape.SquareCornerRectangle) }, { "POST", new MermaidNodeStyle("Lightcoral", MermaidNodeShape.OddShape) }, @@ -341,7 +342,7 @@ public class MermaidNodeStyle /// /// /// - public MermaidNodeStyle(string color, MermaidNodeShape shape) + internal MermaidNodeStyle(string color, MermaidNodeShape shape) { Color = color; Shape = shape; From 931270fcfe2f930940499138ed045e8d5b976cb7 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Tue, 3 Jan 2023 23:08:02 -0500 Subject: [PATCH 13/35] Fixed data in broken test --- .../Services/OpenApiServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index 3d764b4fb..a080db11a 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -30,7 +30,7 @@ public async Task ReturnConvertedCSDLFile() } [Theory] - [InlineData("Todos.Todo.UpdateTodo",null, 1)] + [InlineData("Todos.Todo.UpdateTodoById",null, 1)] [InlineData("Todos.Todo.ListTodo",null, 1)] [InlineData(null, "Todos.Todo", 4)] public async Task ReturnFilteredOpenApiDocBasedOnOperationIdsAndInputCsdlDocument(string operationIds, string tags, int expectedPathCount) From 079da0f22f4c2b3eb4a9acb1f20b531a49a609e8 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Tue, 3 Jan 2023 23:08:31 -0500 Subject: [PATCH 14/35] Updated public API --- .../PublicApi/PublicApi.approved.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index db3a3ecf7..cc3378c4c 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1062,6 +1062,19 @@ namespace Microsoft.OpenApi.Services public string Response { get; set; } public string ServerVariable { get; } } + public enum MermaidNodeShape + { + SquareCornerRectangle = 0, + RoundedCornerRectangle = 1, + Circle = 2, + Rhombus = 3, + OddShape = 4, + } + public class MermaidNodeStyle + { + public string Color { get; } + public Microsoft.OpenApi.Services.MermaidNodeShape Shape { get; } + } public static class OpenApiFilterService { public static Microsoft.OpenApi.Models.OpenApiDocument CreateFilteredDocument(Microsoft.OpenApi.Models.OpenApiDocument source, System.Func predicate) { } @@ -1094,6 +1107,7 @@ namespace Microsoft.OpenApi.Services } public class OpenApiUrlTreeNode { + public static readonly System.Collections.Generic.IReadOnlyDictionary MermaidNodeStyles; public System.Collections.Generic.IDictionary> AdditionalData { get; set; } public System.Collections.Generic.IDictionary Children { get; } public bool IsParameter { get; } @@ -1104,6 +1118,7 @@ namespace Microsoft.OpenApi.Services public void Attach(Microsoft.OpenApi.Models.OpenApiDocument doc, string label) { } public Microsoft.OpenApi.Services.OpenApiUrlTreeNode Attach(string path, Microsoft.OpenApi.Models.OpenApiPathItem pathItem, string label) { } public bool HasOperations(string label) { } + public void WriteMermaid(System.IO.StreamWriter writer) { } public static Microsoft.OpenApi.Services.OpenApiUrlTreeNode Create() { } public static Microsoft.OpenApi.Services.OpenApiUrlTreeNode Create(Microsoft.OpenApi.Models.OpenApiDocument doc, string label) { } } From 3f5978410e5e0cb9deb024f97f7ed26b08a61c1c Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Thu, 5 Jan 2023 14:27:41 -0500 Subject: [PATCH 15/35] Refactored OpenAPIService to remove duplicate code --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 145 +++++++------------ 1 file changed, 50 insertions(+), 95 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 52d2e4fc9..d2eb2e229 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -29,6 +29,7 @@ using System.Reflection; using Microsoft.Extensions.Configuration; using System.Runtime.CompilerServices; +using System.Reflection.Metadata; namespace Microsoft.OpenApi.Hidi { @@ -110,43 +111,13 @@ CancellationToken cancellationToken else { stream = await GetStream(openapi, logger, cancellationToken); - - using (logger.BeginScope($"Parse OpenAPI: {openapi}",openapi)) - { - stopwatch.Restart(); - var result = await new OpenApiStreamReader(new OpenApiReaderSettings - { - RuleSet = ValidationRuleSet.GetDefaultRuleSet(), - LoadExternalRefs = inlineExternal, - BaseUrl = openapi.StartsWith("http") ? new Uri(openapi) : new Uri("file:" + new FileInfo(openapi).DirectoryName + "\\") - } - ).ReadAsync(stream); - - document = result.OpenApiDocument; - - var context = result.OpenApiDiagnostic; - if (context.Errors.Count > 0) - { - 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())}"); - } - else - { - logger.LogTrace("{timestamp}ms: Parsed OpenApi successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, document.Paths.Count); - } - - openApiFormat = format ?? GetOpenApiFormat(openapi, logger); - openApiVersion = version != null ? TryParseOpenApiSpecVersion(version) : result.OpenApiDiagnostic.SpecificationVersion; - stopwatch.Stop(); - } + stopwatch.Restart(); + var result = await ParseOpenApi(openapi, logger, stream); + document = result.OpenApiDocument; + + openApiFormat = format ?? GetOpenApiFormat(openapi, logger); + openApiVersion = version != null ? TryParseOpenApiSpecVersion(version) : result.OpenApiDiagnostic.SpecificationVersion; + stopwatch.Stop(); } using (logger.BeginScope("Filter")) @@ -267,40 +238,13 @@ public static async Task ValidateOpenApiDocument( } using var stream = await GetStream(openapi, logger, cancellationToken); - OpenApiDocument document; - Stopwatch stopwatch = Stopwatch.StartNew(); - using (logger.BeginScope($"Parsing OpenAPI: {openapi}", openapi)) - { - stopwatch.Start(); - - var result = await new OpenApiStreamReader(new OpenApiReaderSettings - { - RuleSet = ValidationRuleSet.GetDefaultRuleSet() - } - ).ReadAsync(stream); - - logger.LogTrace("{timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds); - - document = result.OpenApiDocument; - var context = result.OpenApiDiagnostic; - if (context.Errors.Count != 0) - { - using (logger.BeginScope("Detected errors")) - { - foreach (var error in context.Errors) - { - logger.LogError(error.ToString()); - } - } - } - stopwatch.Stop(); - } + var result = await ParseOpenApi(openapi, logger, stream); using (logger.BeginScope("Calculating statistics")) { var statsVisitor = new StatsVisitor(); var walker = new OpenApiWalker(statsVisitor); - walker.Walk(document); + walker.Walk(result.OpenApiDocument); logger.LogTrace("Finished walking through the OpenApi document. Generating a statistics report.."); logger.LogInformation(statsVisitor.GetStatisticsReport()); @@ -312,6 +256,29 @@ public static async Task ValidateOpenApiDocument( } } + private static async Task ParseOpenApi(string openApiFile, ILogger logger, Stream stream) + { + ReadResult result; + Stopwatch stopwatch = Stopwatch.StartNew(); + using (logger.BeginScope($"Parsing OpenAPI: {openApiFile}", openApiFile)) + { + stopwatch.Start(); + + result = await new OpenApiStreamReader(new OpenApiReaderSettings + { + RuleSet = ValidationRuleSet.GetDefaultRuleSet() + } + ).ReadAsync(stream); + + logger.LogTrace("{timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds); + + LogErrors(logger, result); + stopwatch.Stop(); + } + + return result; + } + internal static IConfiguration GetConfiguration(string settingsFile) { settingsFile ??= "appsettings.json"; @@ -548,34 +515,7 @@ internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, } using var stream = await GetStream(openapi, logger, cancellationToken); - OpenApiDocument document; - Stopwatch stopwatch = Stopwatch.StartNew(); - using (logger.BeginScope($"Parsing OpenAPI: {openapi}", openapi)) - { - stopwatch.Start(); - - var result = await new OpenApiStreamReader(new OpenApiReaderSettings - { - RuleSet = ValidationRuleSet.GetDefaultRuleSet() - } - ).ReadAsync(stream); - - logger.LogTrace("{timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds); - - document = result.OpenApiDocument; - var context = result.OpenApiDiagnostic; - if (context.Errors.Count != 0) - { - using (logger.BeginScope("Detected errors")) - { - foreach (var error in context.Errors) - { - logger.LogError(error.ToString()); - } - } - } - stopwatch.Stop(); - } + var result = await ParseOpenApi(openapi, logger, stream); using (logger.BeginScope("Creating diagram")) { @@ -583,7 +523,7 @@ internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, using var file = new FileStream(output.FullName, FileMode.Create); using var writer = new StreamWriter(file); - WriteTreeDocument(openapi, document, writer); + WriteTreeDocument(openapi, result.OpenApiDocument, writer); logger.LogTrace("Finished walking through the OpenApi document. "); } @@ -594,6 +534,21 @@ internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, } } + private static void LogErrors(ILogger logger, ReadResult result) + { + var context = result.OpenApiDiagnostic; + if (context.Errors.Count != 0) + { + using (logger.BeginScope("Detected errors")) + { + foreach (var error in context.Errors) + { + logger.LogError(error.ToString()); + } + } + } + } + private static void WriteTreeDocument(string openapi, OpenApiDocument document, StreamWriter writer) { var rootNode = OpenApiUrlTreeNode.Create(document, "main"); From e0d08f80c3f289bcb199180d2c0053150beb9af9 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 8 Jan 2023 10:30:07 -0500 Subject: [PATCH 16/35] Added tests for mermaid diagrams --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 4 ++-- .../Services/OpenApiUrlTreeNode.cs | 4 ++-- .../Services/OpenApiServiceTests.cs | 23 +++++++++++++++++++ .../PublicApi/PublicApi.approved.txt | 2 +- ...erifyDiagramFromSampleOpenAPI.verified.txt | 15 ++++++++++++ .../Services/OpenApiUrlTreeNodeTests.cs | 20 ++++++++++++++++ 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index d2eb2e229..73c9fc336 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -549,13 +549,13 @@ private static void LogErrors(ILogger logger, ReadResult result) } } - private static void WriteTreeDocument(string openapi, OpenApiDocument document, StreamWriter writer) + internal static void WriteTreeDocument(string openapiUrl, OpenApiDocument document, StreamWriter writer) { var rootNode = OpenApiUrlTreeNode.Create(document, "main"); writer.WriteLine("# " + document.Info.Title); writer.WriteLine(); - writer.WriteLine("OpenAPI: " + openapi); + writer.WriteLine("OpenAPI: " + openapiUrl); writer.WriteLine(@"
"); // write a span for each mermaidcolorscheme diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index d28b9d53f..d8d4240f8 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -242,7 +242,7 @@ public void AddAdditionalData(Dictionary> additionalData) /// Write tree as Mermaid syntax /// /// StreamWriter to write the Mermaid content to - public void WriteMermaid(StreamWriter writer) + public void WriteMermaid(TextWriter writer) { writer.WriteLine("graph LR"); foreach (var style in MermaidNodeStyles) @@ -269,7 +269,7 @@ public void WriteMermaid(StreamWriter writer) { "OTHER", new MermaidNodeStyle("White", MermaidNodeShape.SquareCornerRectangle) }, }; - private static void ProcessNode(OpenApiUrlTreeNode node, StreamWriter writer) + private static void ProcessNode(OpenApiUrlTreeNode node, TextWriter writer) { var path = string.IsNullOrEmpty(node.Path) ? "/" : SanitizeMermaidNode(node.Path); var methods = GetMethods(node); diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index a080db11a..eb0872b3b 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -1,10 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Text; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Hidi; +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.OData; using Microsoft.OpenApi.Services; +using Microsoft.VisualStudio.TestPlatform.Utilities; using Xunit; namespace Microsoft.OpenApi.Tests.Services @@ -71,5 +75,24 @@ public void ReturnOpenApiConvertSettingsWhenSettingsFileIsProvided(string filePa Assert.NotNull(settings); } } + + [Fact] + public void ShowCommandGeneratesMermaidDiagram() + { + var openApiDoc = new OpenApiDocument(); + openApiDoc.Info = new OpenApiInfo + { + Title = "Test", + Version = "1.0.0" + }; + var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + OpenApiService.WriteTreeDocument("https://example.org/openapi.json", openApiDoc, writer); + writer.Flush(); + stream.Position = 0; + using var reader = new StreamReader(stream); + var output = reader.ReadToEnd(); + Assert.Contains("graph LR", output); + } } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index cc3378c4c..63cd0f535 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1118,7 +1118,7 @@ namespace Microsoft.OpenApi.Services public void Attach(Microsoft.OpenApi.Models.OpenApiDocument doc, string label) { } public Microsoft.OpenApi.Services.OpenApiUrlTreeNode Attach(string path, Microsoft.OpenApi.Models.OpenApiPathItem pathItem, string label) { } public bool HasOperations(string label) { } - public void WriteMermaid(System.IO.StreamWriter writer) { } + public void WriteMermaid(System.IO.TextWriter writer) { } public static Microsoft.OpenApi.Services.OpenApiUrlTreeNode Create() { } public static Microsoft.OpenApi.Services.OpenApiUrlTreeNode Create(Microsoft.OpenApi.Models.OpenApiDocument doc, string label) { } } diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt new file mode 100644 index 000000000..19596aff5 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt @@ -0,0 +1,15 @@ +graph LR +classDef GET fill:lightSteelBlue,stroke:#333,stroke-width:2px +classDef POST fill:Lightcoral,stroke:#333,stroke-width:2px +classDef GET_POST fill:forestGreen,stroke:#333,stroke-width:2px +classDef DELETE_GET_PATCH fill:yellowGreen,stroke:#333,stroke-width:2px +classDef DELETE_GET_PATCH_PUT fill:oliveDrab,stroke:#333,stroke-width:2px +classDef DELETE_GET_PUT fill:olive,stroke:#333,stroke-width:2px +classDef DELETE_GET fill:DarkSeaGreen,stroke:#333,stroke-width:2px +classDef DELETE fill:Tomato,stroke:#333,stroke-width:2px +classDef OTHER fill:White,stroke:#333,stroke-width:2px +/["/"] --> /houses["houses"] +class /houses OTHER +/["/"] --> /cars["cars"] +class /cars OTHER +class / OTHER diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs index 944e6c830..6d0278415 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs @@ -3,12 +3,16 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; +using VerifyXunit; using Xunit; namespace Microsoft.OpenApi.Tests.Services { + [UsesVerify] public class OpenApiUrlTreeNodeTests { private OpenApiDocument OpenApiDocumentSample_1 => new OpenApiDocument() @@ -443,5 +447,21 @@ public void ThrowsArgumentNullExceptionForNullArgumentInAddAdditionalDataMethod( Assert.Throws(() => rootNode.AddAdditionalData(null)); } + + [Fact] + public async Task VerifyDiagramFromSampleOpenAPI() + { + var doc1 = OpenApiDocumentSample_1; + + var label1 = "personal"; + var rootNode = OpenApiUrlTreeNode.Create(doc1, label1); + + var writer = new StringWriter(); + rootNode.WriteMermaid(writer); + writer.Flush(); + var diagram = writer.GetStringBuilder().ToString(); + + await Verifier.Verify(diagram); + } } } From b61edcf71279d11728c8a8359bf640bd44e9e5c9 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 8 Jan 2023 19:30:44 -0500 Subject: [PATCH 17/35] Updated diagram test to cover more scenarios --- ...erifyDiagramFromSampleOpenAPI.verified.txt | 10 ++++----- .../Services/OpenApiUrlTreeNodeTests.cs | 22 +++++++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt index 19596aff5..c24dd943d 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.VerifyDiagramFromSampleOpenAPI.verified.txt @@ -8,8 +8,8 @@ classDef DELETE_GET_PUT fill:olive,stroke:#333,stroke-width:2px classDef DELETE_GET fill:DarkSeaGreen,stroke:#333,stroke-width:2px classDef DELETE fill:Tomato,stroke:#333,stroke-width:2px classDef OTHER fill:White,stroke:#333,stroke-width:2px -/["/"] --> /houses["houses"] -class /houses OTHER -/["/"] --> /cars["cars"] -class /cars OTHER -class / OTHER +/["/"] --> /houses("houses") +class /houses GET_POST +/["/"] --> /cars>"cars"] +class /cars POST +class / GET diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs index 6d0278415..d251c99c1 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs @@ -19,9 +19,27 @@ public class OpenApiUrlTreeNodeTests { Paths = new OpenApiPaths() { - ["/"] = new OpenApiPathItem(), - ["/houses"] = new OpenApiPathItem(), + ["/"] = new OpenApiPathItem() { + Operations = new Dictionary() + { + [OperationType.Get] = new OpenApiOperation(), + } + }, + ["/houses"] = new OpenApiPathItem() + { + Operations = new Dictionary() + { + [OperationType.Get] = new OpenApiOperation(), + [OperationType.Post] = new OpenApiOperation() + } + }, ["/cars"] = new OpenApiPathItem() + { + Operations = new Dictionary() + { + [OperationType.Post] = new OpenApiOperation() + } + } } }; From 8a9305b52ec4b6c99904ce3cab7c7336a8f7a765 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Wed, 11 Jan 2023 17:44:02 -0500 Subject: [PATCH 18/35] Added test for show command --- .../Microsoft.OpenApi.Hidi.Tests.csproj | 3 +++ .../Services/OpenApiServiceTests.cs | 10 ++++++++++ .../UtilityFiles/SampleOpenApi.yml | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/SampleOpenApi.yml diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 578cdc9e3..aaaa66cba 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -53,6 +53,9 @@ Always + + PreserveNewest + Always diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index eb0872b3b..fd1ea0d59 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -94,5 +94,15 @@ public void ShowCommandGeneratesMermaidDiagram() var output = reader.ReadToEnd(); Assert.Contains("graph LR", output); } + + [Fact] + public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() + { + var fileinfo = new FileInfo("sample.md"); + await OpenApiService.ShowOpenApiDocument("UtilityFiles\\SampleOpenApi.yml", fileinfo, LogLevel.Information, new CancellationToken()); + + var output = File.ReadAllText(fileinfo.FullName); + Assert.Contains("graph LR", output); + } } } diff --git a/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/SampleOpenApi.yml b/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/SampleOpenApi.yml new file mode 100644 index 000000000..c4fb2e62f --- /dev/null +++ b/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/SampleOpenApi.yml @@ -0,0 +1,19 @@ +openapi: 3.0.0 +info: + title: Sample OpenApi + version: 1.0.0 +paths: + /api/editresource: + get: + responses: + '200': + description: OK + patch: + responses: + '200': + description: OK + /api/viewresource: + get: + responses: + '200': + description: OK \ No newline at end of file From b0af5268256ec57ced75adb98163a85e15dbcf20 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Wed, 11 Jan 2023 21:29:39 -0500 Subject: [PATCH 19/35] Refactored to improve test coverage --- src/Microsoft.OpenApi.Hidi/Program.cs | 32 +++++++++++-------- .../Services/OpenApiServiceTests.cs | 27 ++++++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/Program.cs b/src/Microsoft.OpenApi.Hidi/Program.cs index b9db1229f..03aac121d 100644 --- a/src/Microsoft.OpenApi.Hidi/Program.cs +++ b/src/Microsoft.OpenApi.Hidi/Program.cs @@ -16,8 +16,19 @@ namespace Microsoft.OpenApi.Hidi static class Program { static async Task Main(string[] args) - { - var rootCommand = new RootCommand() {}; + { + var rootCommand = CreateRootCommand(); + + // Parse the incoming args and invoke the handler + await rootCommand.InvokeAsync(args); + + //// Wait for logger to write messages to the console before exiting + await Task.Delay(10); + } + + internal static RootCommand CreateRootCommand() + { + var rootCommand = new RootCommand() { }; // command option parameters and aliases var descriptionOption = new Option("--openapi", "Input OpenAPI description file path or URL"); @@ -46,7 +57,7 @@ static async Task Main(string[] args) var settingsFileOption = new Option("--settings-path", "The configuration file with CSDL conversion settings."); settingsFileOption.AddAlias("--sp"); - + var logLevelOption = new Option("--log-level", () => LogLevel.Information, "The log level to use when logging messages to the main output."); logLevelOption.AddAlias("--ll"); @@ -71,7 +82,7 @@ static async Task Main(string[] args) logLevelOption }; - validateCommand.Handler = new ValidateCommandHandler + validateCommand.Handler = new ValidateCommandHandler { DescriptionOption = descriptionOption, LogLevelOption = logLevelOption @@ -88,7 +99,7 @@ static async Task Main(string[] args) formatOption, terseOutputOption, settingsFileOption, - logLevelOption, + logLevelOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption, @@ -123,7 +134,7 @@ static async Task Main(string[] args) cleanOutputOption }; - showCommand.Handler = new ShowCommandHandler + showCommand.Handler = new ShowCommandHandler { DescriptionOption = descriptionOption, OutputOption = outputOption, @@ -133,12 +144,7 @@ static async Task Main(string[] args) rootCommand.Add(showCommand); rootCommand.Add(transformCommand); rootCommand.Add(validateCommand); - - // Parse the incoming args and invoke the handler - await rootCommand.InvokeAsync(args); - - //// Wait for logger to write messages to the console before exiting - await Task.Delay(10); - } + return rootCommand; + } } } diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index fd1ea0d59..020f0db9c 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.CommandLine; +using System.CommandLine.Invocation; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Hidi; +using Microsoft.OpenApi.Hidi.Handlers; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.OData; using Microsoft.OpenApi.Services; @@ -104,5 +107,29 @@ public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() var output = File.ReadAllText(fileinfo.FullName); Assert.Contains("graph LR", output); } + + [Fact] + public async Task InvokeShowCommand() + { + var rootCommand = Program.CreateRootCommand(); + var args = new string[] { "show", "-d", ".\\UtilityFiles\\SampleOpenApi.yml", "-o", "sample.md" }; + var parseResult = rootCommand.Parse(args); + var handler = rootCommand.Subcommands.Where(c => c.Name == "show").First().Handler; + var context = new InvocationContext(parseResult); + + await handler.InvokeAsync(context); + + var output = File.ReadAllText("sample.md"); + Assert.Contains("graph LR", output); + } + + + // Relatively useless test to keep the code coverage metrics happy + [Fact] + public void CreateRootCommand() + { + var rootCommand = Program.CreateRootCommand(); + Assert.NotNull(rootCommand); + } } } From 776e98faa90ba6f6ad90e8e4d37d357c671ed8b5 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Wed, 11 Jan 2023 21:43:14 -0500 Subject: [PATCH 20/35] Change test to call sync invoke --- .../Services/OpenApiServiceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index 020f0db9c..db30d2eff 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -109,7 +109,7 @@ public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() } [Fact] - public async Task InvokeShowCommand() + public void InvokeShowCommand() { var rootCommand = Program.CreateRootCommand(); var args = new string[] { "show", "-d", ".\\UtilityFiles\\SampleOpenApi.yml", "-o", "sample.md" }; @@ -117,7 +117,7 @@ public async Task InvokeShowCommand() var handler = rootCommand.Subcommands.Where(c => c.Name == "show").First().Handler; var context = new InvocationContext(parseResult); - await handler.InvokeAsync(context); + handler.Invoke(context); var output = File.ReadAllText("sample.md"); Assert.Contains("graph LR", output); From 5bc0bd4dcce47830964714677cbd87e0b7e60f1f Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Wed, 11 Jan 2023 22:18:10 -0500 Subject: [PATCH 21/35] Added back missing parameter config options in parseopenapi --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 73c9fc336..dfd1886a4 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -112,7 +112,7 @@ CancellationToken cancellationToken { stream = await GetStream(openapi, logger, cancellationToken); stopwatch.Restart(); - var result = await ParseOpenApi(openapi, logger, stream); + var result = await ParseOpenApi(openapi, inlineExternal, logger, stream); document = result.OpenApiDocument; openApiFormat = format ?? GetOpenApiFormat(openapi, logger); @@ -238,7 +238,7 @@ public static async Task ValidateOpenApiDocument( } using var stream = await GetStream(openapi, logger, cancellationToken); - var result = await ParseOpenApi(openapi, logger, stream); + var result = await ParseOpenApi(openapi, false, logger, stream); using (logger.BeginScope("Calculating statistics")) { @@ -256,7 +256,7 @@ public static async Task ValidateOpenApiDocument( } } - private static async Task ParseOpenApi(string openApiFile, ILogger logger, Stream stream) + private static async Task ParseOpenApi(string openApiFile, bool inlineExternal, ILogger logger, Stream stream) { ReadResult result; Stopwatch stopwatch = Stopwatch.StartNew(); @@ -266,7 +266,9 @@ private static async Task ParseOpenApi(string openApiFile, ILogger Date: Sat, 14 Jan 2023 20:31:28 -0500 Subject: [PATCH 22/35] Updated SanitizeMermaidNode to handle cases found in Microsoft Graph and GitHub APIs --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index d8d4240f8..870fb36d9 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -327,8 +327,11 @@ private static string SanitizeMermaidNode(string token) .Replace("{", ":") .Replace("}", "") .Replace(".", "_") - .Replace(";", "_") + .Replace("(", "_") + .Replace(")", "_") + .Replace(";", "_") .Replace("-", "_") + .Replace("graph", "gra_ph") // graph is a reserved word .Replace("default", "def_ault"); // default is a reserved word for classes } } From a7c4983e38ab16f4e7bd02f6edb28097354a6130 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 14 Jan 2023 20:33:05 -0500 Subject: [PATCH 23/35] Removed Task.Delay as no longer necessary. #1127 --- src/Microsoft.OpenApi.Hidi/Program.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/Program.cs b/src/Microsoft.OpenApi.Hidi/Program.cs index 03aac121d..056da9ab2 100644 --- a/src/Microsoft.OpenApi.Hidi/Program.cs +++ b/src/Microsoft.OpenApi.Hidi/Program.cs @@ -22,8 +22,6 @@ static async Task Main(string[] args) // Parse the incoming args and invoke the handler await rootCommand.InvokeAsync(args); - //// Wait for logger to write messages to the console before exiting - await Task.Delay(10); } internal static RootCommand CreateRootCommand() @@ -129,6 +127,8 @@ internal static RootCommand CreateRootCommand() var showCommand = new Command("show") { descriptionOption, + csdlOption, + csdlFilterOption, logLevelOption, outputOption, cleanOutputOption @@ -137,6 +137,8 @@ internal static RootCommand CreateRootCommand() showCommand.Handler = new ShowCommandHandler { DescriptionOption = descriptionOption, + CsdlOption = csdlOption, + CsdlFilterOption = csdlFilterOption, OutputOption = outputOption, LogLevelOption = logLevelOption }; From 7aac03ffd6f5e1fbe6d6c8cb07a280c1bacc47c4 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 14 Jan 2023 20:33:59 -0500 Subject: [PATCH 24/35] Updated commands to enable reading from CSDL url for both transform and show commands --- .../Handlers/ShowCommandHandler.cs | 7 +- .../Handlers/TransformCommandHandler.cs | 2 +- .../Handlers/ValidateCommandHandler.cs | 2 +- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 345 +++++++++++------- .../Services/OpenApiServiceTests.cs | 44 ++- 5 files changed, 254 insertions(+), 146 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs b/src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs index e6542c34a..6974e76dd 100644 --- a/src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs +++ b/src/Microsoft.OpenApi.Hidi/Handlers/ShowCommandHandler.cs @@ -16,6 +16,9 @@ internal class ShowCommandHandler : ICommandHandler public Option DescriptionOption { get; set; } public Option OutputOption { get; set; } public Option LogLevelOption { get; set; } + public Option CsdlOption { get; set; } + public Option CsdlFilterOption { get; set; } + public int Invoke(InvocationContext context) { @@ -26,13 +29,15 @@ public async Task InvokeAsync(InvocationContext context) string openapi = context.ParseResult.GetValueForOption(DescriptionOption); FileInfo output = context.ParseResult.GetValueForOption(OutputOption); LogLevel logLevel = context.ParseResult.GetValueForOption(LogLevelOption); + string csdlFilter = context.ParseResult.GetValueForOption(CsdlFilterOption); + string csdl = context.ParseResult.GetValueForOption(CsdlOption); CancellationToken cancellationToken = (CancellationToken)context.BindingContext.GetService(typeof(CancellationToken)); using var loggerFactory = Logger.ConfigureLogger(logLevel); var logger = loggerFactory.CreateLogger(); try { - await OpenApiService.ShowOpenApiDocument(openapi, output, logLevel, cancellationToken); + await OpenApiService.ShowOpenApiDocument(openapi, csdl, csdlFilter, output, logger, cancellationToken); return 0; } diff --git a/src/Microsoft.OpenApi.Hidi/Handlers/TransformCommandHandler.cs b/src/Microsoft.OpenApi.Hidi/Handlers/TransformCommandHandler.cs index e46b34340..d0a49c209 100644 --- a/src/Microsoft.OpenApi.Hidi/Handlers/TransformCommandHandler.cs +++ b/src/Microsoft.OpenApi.Hidi/Handlers/TransformCommandHandler.cs @@ -57,7 +57,7 @@ public async Task InvokeAsync(InvocationContext context) var logger = loggerFactory.CreateLogger(); try { - await OpenApiService.TransformOpenApiDocument(openapi, csdl, csdlFilter, output, cleanOutput, version, format, terseOutput, settingsFile, logLevel, inlineLocal, inlineExternal, filterbyoperationids, filterbytags, filterbycollection, cancellationToken); + await OpenApiService.TransformOpenApiDocument(openapi, csdl, csdlFilter, output, cleanOutput, version, format, terseOutput, settingsFile, inlineLocal, inlineExternal, filterbyoperationids, filterbytags, filterbycollection, logger, cancellationToken); return 0; } diff --git a/src/Microsoft.OpenApi.Hidi/Handlers/ValidateCommandHandler.cs b/src/Microsoft.OpenApi.Hidi/Handlers/ValidateCommandHandler.cs index 2faa771ea..416471d9e 100644 --- a/src/Microsoft.OpenApi.Hidi/Handlers/ValidateCommandHandler.cs +++ b/src/Microsoft.OpenApi.Hidi/Handlers/ValidateCommandHandler.cs @@ -30,7 +30,7 @@ public async Task InvokeAsync(InvocationContext context) var logger = loggerFactory.CreateLogger(); try { - await OpenApiService.ValidateOpenApiDocument(openapi, logLevel, cancellationToken); + await OpenApiService.ValidateOpenApiDocument(openapi, logger, cancellationToken); return 0; } catch (Exception ex) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index dfd1886a4..c54b65db5 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -28,8 +28,6 @@ using System.Xml; using System.Reflection; using Microsoft.Extensions.Configuration; -using System.Runtime.CompilerServices; -using System.Reflection.Metadata; namespace Microsoft.OpenApi.Hidi { @@ -48,22 +46,21 @@ public static async Task TransformOpenApiDocument( OpenApiFormat? format, bool terseOutput, string settingsFile, - LogLevel logLevel, bool inlineLocal, bool inlineExternal, string filterbyoperationids, string filterbytags, string filterbycollection, + ILogger logger, CancellationToken cancellationToken ) { - using var loggerFactory = Logger.ConfigureLogger(logLevel); - var logger = loggerFactory.CreateLogger(); + try { if (string.IsNullOrEmpty(openapi) && string.IsNullOrEmpty(csdl)) { - throw new ArgumentException("Please input a file path"); + throw new ArgumentException("Please input a file path or URL"); } if (output == null) { @@ -79,122 +76,136 @@ CancellationToken cancellationToken throw new IOException($"The file {output} already exists. Please input a new file path."); } - Stream stream; - OpenApiDocument document; - OpenApiFormat openApiFormat; - OpenApiSpecVersion openApiVersion; - var stopwatch = new Stopwatch(); + // Default to yaml and OpenApiVersion 3 during csdl to OpenApi conversion + OpenApiFormat openApiFormat = format ?? (!string.IsNullOrEmpty(openapi) ? GetOpenApiFormat(openapi, logger) : OpenApiFormat.Yaml); + OpenApiSpecVersion openApiVersion = version != null ? TryParseOpenApiSpecVersion(version) : OpenApiSpecVersion.OpenApi3_0; - if (!string.IsNullOrEmpty(csdl)) - { - using (logger.BeginScope($"Convert CSDL: {csdl}", csdl)) - { - stopwatch.Start(); - // Default to yaml and OpenApiVersion 3 during csdl to OpenApi conversion - openApiFormat = format ?? GetOpenApiFormat(csdl, logger); - openApiVersion = version != null ? TryParseOpenApiSpecVersion(version) : OpenApiSpecVersion.OpenApi3_0; - - stream = await GetStream(csdl, logger, cancellationToken); + OpenApiDocument document = await GetOpenApi(openapi, csdl, csdlFilter, settingsFile, inlineExternal, logger, cancellationToken); + document = await FilterOpenApiDocument(filterbyoperationids, filterbytags, filterbycollection, document, logger, cancellationToken); + WriteOpenApi(output, terseOutput, inlineLocal, inlineExternal, openApiFormat, openApiVersion, document, logger); + } + catch (TaskCanceledException) + { + Console.Error.WriteLine("CTRL+C pressed, aborting the operation."); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Could not transform the document, reason: {ex.Message}", ex); + } + } - if (!string.IsNullOrEmpty(csdlFilter)) - { - XslCompiledTransform transform = GetFilterTransform(); - stream = ApplyFilter(csdl, csdlFilter, transform); - stream.Position = 0; - } + private static void WriteOpenApi(FileInfo output, bool terseOutput, bool inlineLocal, bool inlineExternal, OpenApiFormat openApiFormat, OpenApiSpecVersion openApiVersion, OpenApiDocument document, ILogger logger) + { + using (logger.BeginScope("Output")) + { + using var outputStream = output?.Create(); + var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out; - document = await ConvertCsdlToOpenApi(stream, settingsFile, cancellationToken); - stopwatch.Stop(); - logger.LogTrace("{timestamp}ms: Generated OpenAPI with {paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count); - } - } - else + var settings = new OpenApiWriterSettings() { - stream = await GetStream(openapi, logger, cancellationToken); - stopwatch.Restart(); - var result = await ParseOpenApi(openapi, inlineExternal, logger, stream); - document = result.OpenApiDocument; - - openApiFormat = format ?? GetOpenApiFormat(openapi, logger); - openApiVersion = version != null ? TryParseOpenApiSpecVersion(version) : result.OpenApiDiagnostic.SpecificationVersion; - stopwatch.Stop(); - } + InlineLocalReferences = inlineLocal, + InlineExternalReferences = inlineExternal + }; - using (logger.BeginScope("Filter")) + IOpenApiWriter writer = openApiFormat switch { - Func predicate = null; + OpenApiFormat.Json => terseOutput ? new OpenApiJsonWriter(textWriter, settings, terseOutput) : new OpenApiJsonWriter(textWriter, settings, false), + OpenApiFormat.Yaml => new OpenApiYamlWriter(textWriter, settings), + _ => throw new ArgumentException("Unknown format"), + }; - // 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("Serializing to OpenApi document using the provided spec version and writer"); - } - if (!string.IsNullOrEmpty(filterbytags)) - { - logger.LogTrace("Creating predicate based on the tags supplied."); - predicate = OpenApiFilterService.CreatePredicate(tags: filterbytags); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + document.Serialize(writer, openApiVersion); + stopwatch.Stop(); - } - if (!string.IsNullOrEmpty(filterbycollection)) - { - var fileStream = await GetStream(filterbycollection, logger, cancellationToken); - var requestUrls = ParseJsonCollectionFile(fileStream, logger); + logger.LogTrace($"Finished serializing in {stopwatch.ElapsedMilliseconds}ms"); + textWriter.Flush(); + } + } - logger.LogTrace("Creating predicate based on the paths and Http methods defined in the Postman collection."); - predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source: document); - } - if (predicate != null) + // Get OpenAPI document either from OpenAPI or CSDL + private static async Task GetOpenApi(string openapi, string csdl, string csdlFilter, string settingsFile, bool inlineExternal, ILogger logger, CancellationToken cancellationToken) + { + OpenApiDocument document; + Stream stream; + + if (!string.IsNullOrEmpty(csdl)) + { + var stopwatch = new Stopwatch(); + using (logger.BeginScope($"Convert CSDL: {csdl}", csdl)) + { + stopwatch.Start(); + stream = await GetStream(csdl, logger, cancellationToken); + Stream filteredStream = null; + if (!string.IsNullOrEmpty(csdlFilter)) { - stopwatch.Restart(); - document = OpenApiFilterService.CreateFilteredDocument(document, predicate); - stopwatch.Stop(); - logger.LogTrace("{timestamp}ms: Creating filtered OpenApi document with {paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count); + XslCompiledTransform transform = GetFilterTransform(); + filteredStream = ApplyFilterToCsdl(stream, csdlFilter, transform); + filteredStream.Position = 0; + stream.Dispose(); + stream = null; } + + document = await ConvertCsdlToOpenApi(filteredStream ?? stream, settingsFile, cancellationToken); + stopwatch.Stop(); + logger.LogTrace("{timestamp}ms: Generated OpenAPI with {paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count); } + } + else + { + stream = await GetStream(openapi, logger, cancellationToken); + var result = await ParseOpenApi(openapi, inlineExternal, logger, stream); + document = result.OpenApiDocument; + } - using (logger.BeginScope("Output")) - { - ; - using var outputStream = output?.Create(); - var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out; + return document; + } - var settings = new OpenApiWriterSettings() - { - InlineLocalReferences = inlineLocal, - InlineExternalReferences = inlineExternal - }; + private static async Task FilterOpenApiDocument(string filterbyoperationids, string filterbytags, string filterbycollection, OpenApiDocument document, ILogger logger, CancellationToken cancellationToken) + { + using (logger.BeginScope("Filter")) + { + Func predicate = null; - IOpenApiWriter writer = openApiFormat switch - { - OpenApiFormat.Json => terseOutput ? new OpenApiJsonWriter(textWriter, settings, terseOutput) : new OpenApiJsonWriter(textWriter, settings, false), - OpenApiFormat.Yaml => new OpenApiYamlWriter(textWriter, settings), - _ => throw new ArgumentException("Unknown format"), - }; + // 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); + + } + if (!string.IsNullOrEmpty(filterbytags)) + { + logger.LogTrace("Creating predicate based on the tags supplied."); + predicate = OpenApiFilterService.CreatePredicate(tags: filterbytags); - logger.LogTrace("Serializing to OpenApi document using the provided spec version and writer"); + } + if (!string.IsNullOrEmpty(filterbycollection)) + { + var fileStream = await GetStream(filterbycollection, logger, cancellationToken); + var requestUrls = ParseJsonCollectionFile(fileStream, logger); + logger.LogTrace("Creating predicate based on the paths and Http methods defined in the Postman collection."); + predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source: document); + } + if (predicate != null) + { + var stopwatch = new Stopwatch(); stopwatch.Start(); - document.Serialize(writer, openApiVersion); + document = OpenApiFilterService.CreateFilteredDocument(document, predicate); stopwatch.Stop(); - - logger.LogTrace($"Finished serializing in {stopwatch.ElapsedMilliseconds}ms"); - textWriter.Flush(); + logger.LogTrace("{timestamp}ms: Creating filtered OpenApi document with {paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count); } } - catch(TaskCanceledException) - { - Console.Error.WriteLine("CTRL+C pressed, aborting the operation."); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Could not transform the document, reason: {ex.Message}", ex); - } + + return document; } private static XslCompiledTransform GetFilterTransform() @@ -206,10 +217,10 @@ private static XslCompiledTransform GetFilterTransform() return transform; } - private static Stream ApplyFilter(string csdl, string entitySetOrSingleton, XslCompiledTransform transform) + private static Stream ApplyFilterToCsdl(Stream csdlStream, string entitySetOrSingleton, XslCompiledTransform transform) { Stream stream; - StreamReader inputReader = new(csdl); + StreamReader inputReader = new(csdlStream); XmlReader inputXmlReader = XmlReader.Create(inputReader); MemoryStream filteredStream = new(); StreamWriter writer = new(filteredStream); @@ -225,11 +236,9 @@ private static Stream ApplyFilter(string csdl, string entitySetOrSingleton, XslC /// public static async Task ValidateOpenApiDocument( string openapi, - LogLevel logLevel, + ILogger logger, CancellationToken cancellationToken) { - using var loggerFactory = Logger.ConfigureLogger(logLevel); - var logger = loggerFactory.CreateLogger(); try { if (string.IsNullOrEmpty(openapi)) @@ -250,13 +259,17 @@ public static async Task ValidateOpenApiDocument( logger.LogInformation(statsVisitor.GetStatisticsReport()); } } + catch (TaskCanceledException) + { + Console.Error.WriteLine("CTRL+C pressed, aborting the operation."); + } catch (Exception ex) { throw new InvalidOperationException($"Could not validate the document, reason: {ex.Message}", ex); } } - private static async Task ParseOpenApi(string openApiFile, bool inlineExternal, ILogger logger, Stream stream) + private static async Task ParseOpenApi(string openApiFile, bool inlineExternal, ILogger logger, Stream stream) { ReadResult result; Stopwatch stopwatch = Stopwatch.StartNew(); @@ -486,57 +499,60 @@ private static string GetInputPathExtension(string openapi = null, string csdl = return extension; } - private static ILoggerFactory ConfigureLoggerInstance(LogLevel loglevel) + internal static async Task ShowOpenApiDocument(string openapi, string csdl, string csdlFilter, FileInfo output, ILogger logger, CancellationToken cancellationToken) { - // Configure logger options -#if DEBUG - loglevel = loglevel > LogLevel.Debug ? LogLevel.Debug : loglevel; -#endif - - return Microsoft.Extensions.Logging.LoggerFactory.Create((builder) => { - builder - .AddSimpleConsole(c => { - c.IncludeScopes = true; - }) -#if DEBUG - .AddDebug() -#endif - .SetMinimumLevel(loglevel); - }); - } - - internal static async Task ShowOpenApiDocument(string openapi, FileInfo output, LogLevel logLevel, CancellationToken cancellationToken) - { - using var loggerFactory = Logger.ConfigureLogger(logLevel); - var logger = loggerFactory.CreateLogger(); try { - if (string.IsNullOrEmpty(openapi)) + if (string.IsNullOrEmpty(openapi) && string.IsNullOrEmpty(csdl)) { - throw new ArgumentNullException(nameof(openapi)); + throw new ArgumentException("Please input a file path or URL"); } - using var stream = await GetStream(openapi, logger, cancellationToken); - var result = await ParseOpenApi(openapi, false, logger, stream); + var document = await GetOpenApi(openapi, csdl, csdlFilter, null, false, logger, cancellationToken); using (logger.BeginScope("Creating diagram")) { - // Create OpenApiUrlTree from document + // If output is null, create a HTML file in the user's temporary directory + if (output == null) + { + var tempPath = Path.GetTempPath(); - using var file = new FileStream(output.FullName, FileMode.Create); - using var writer = new StreamWriter(file); - WriteTreeDocument(openapi, result.OpenApiDocument, writer); + output = new FileInfo(Path.Combine(tempPath, "apitree.html")); + using (var file = new FileStream(output.FullName, FileMode.Create)) + { + using var writer = new StreamWriter(file); + WriteTreeDocumentAsHtml(openapi ?? csdl, document, writer); + } + logger.LogTrace("Created Html document with diagram "); - logger.LogTrace("Finished walking through the OpenApi document. "); + // Launch a browser to display the output html file + var process = new Process(); + process.StartInfo.FileName = output.FullName; + process.StartInfo.UseShellExecute = true; + process.Start(); + } + else // Write diagram as Markdown document to output file + { + using (var file = new FileStream(output.FullName, FileMode.Create)) + { + using var writer = new StreamWriter(file); + WriteTreeDocumentAsMarkdown(openapi ?? csdl, document, writer); + } + logger.LogTrace("Created markdown document with diagram "); + } } } + catch (TaskCanceledException) + { + Console.Error.WriteLine("CTRL+C pressed, aborting the operation."); + } catch (Exception ex) { throw new InvalidOperationException($"Could not generate the document, reason: {ex.Message}", ex); } } - private static void LogErrors(ILogger logger, ReadResult result) + private static void LogErrors(ILogger logger, ReadResult result) { var context = result.OpenApiDiagnostic; if (context.Errors.Count != 0) @@ -551,13 +567,13 @@ private static void LogErrors(ILogger logger, ReadResult result) } } - internal static void WriteTreeDocument(string openapiUrl, OpenApiDocument document, StreamWriter writer) + internal static void WriteTreeDocumentAsMarkdown(string openapiUrl, OpenApiDocument document, StreamWriter writer) { var rootNode = OpenApiUrlTreeNode.Create(document, "main"); writer.WriteLine("# " + document.Info.Title); writer.WriteLine(); - writer.WriteLine("OpenAPI: " + openapiUrl); + writer.WriteLine("API Description: " + openapiUrl); writer.WriteLine(@"
"); // write a span for each mermaidcolorscheme @@ -571,5 +587,54 @@ internal static void WriteTreeDocument(string openapiUrl, OpenApiDocument docume rootNode.WriteMermaid(writer); writer.WriteLine("```"); } + + internal static void WriteTreeDocumentAsHtml(string sourceUrl, OpenApiDocument document, StreamWriter writer, bool asHtmlFile = false) + { + var rootNode = OpenApiUrlTreeNode.Create(document, "main"); + + writer.WriteLine(@" + + + + + + +"); + writer.WriteLine("

" + document.Info.Title + "

"); + writer.WriteLine(); + writer.WriteLine($"

API Description: {sourceUrl}

"); + + writer.WriteLine(@"
"); + // write a span for each mermaidcolorscheme + foreach (var style in OpenApiUrlTreeNode.MermaidNodeStyles) + { + writer.WriteLine($"{style.Key.Replace("_", " ")}"); + } + writer.WriteLine("
"); + writer.WriteLine("
"); + writer.WriteLine(""); + rootNode.WriteMermaid(writer); + writer.WriteLine(""); + + // Write script tag to include JS library for rendering markdown + writer.WriteLine(@""); + // Write script tag to include JS library for rendering mermaid + writer.WriteLine("(new LoggerFactory()), new CancellationToken()); var output = File.ReadAllText(fileinfo.FullName); Assert.Contains("graph LR", output); @@ -124,6 +146,22 @@ public void InvokeShowCommand() } + [Fact] + public void InvokeShowCommandWithoutOutput() + { + var rootCommand = Program.CreateRootCommand(); + var args = new string[] { "show", "-d", ".\\UtilityFiles\\SampleOpenApi.yml" }; + var parseResult = rootCommand.Parse(args); + var handler = rootCommand.Subcommands.Where(c => c.Name == "show").First().Handler; + var context = new InvocationContext(parseResult); + + handler.Invoke(context); + + var output = File.ReadAllText(Path.Combine(Path.GetTempPath(), "apitree.html")); + Assert.Contains("graph LR", output); + } + + // Relatively useless test to keep the code coverage metrics happy [Fact] public void CreateRootCommand() From 8e2d470b7b544061d0171e820809f4ac6e4b3eeb Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 14 Jan 2023 21:03:57 -0500 Subject: [PATCH 25/35] Used random file in a hidi folder to address security concerns. --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 12 +++++++++--- .../Services/OpenApiServiceTests.cs | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index c54b65db5..2cc188865 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -515,9 +515,15 @@ internal static async Task ShowOpenApiDocument(string openapi, string csdl, stri // If output is null, create a HTML file in the user's temporary directory if (output == null) { - var tempPath = Path.GetTempPath(); + var tempPath = Path.GetTempPath() + "/hidi/"; + if(!File.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + + var fileName = Path.GetRandomFileName(); - output = new FileInfo(Path.Combine(tempPath, "apitree.html")); + output = new FileInfo(Path.Combine(tempPath, fileName + ".html")); using (var file = new FileStream(output.FullName, FileMode.Create)) { using var writer = new StreamWriter(file); @@ -526,7 +532,7 @@ internal static async Task ShowOpenApiDocument(string openapi, string csdl, stri logger.LogTrace("Created Html document with diagram "); // Launch a browser to display the output html file - var process = new Process(); + using var process = new Process(); process.StartInfo.FileName = output.FullName; process.StartInfo.UseShellExecute = true; process.Start(); diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index 09ac6fb04..aa49ff520 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -80,6 +80,7 @@ public void ReturnOpenApiConvertSettingsWhenSettingsFileIsProvided(string filePa } } + [Fact] public void ShowCommandGeneratesMermaidDiagramAsMarkdown() { @@ -130,6 +131,22 @@ public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() Assert.Contains("graph LR", output); } + [Fact] + public void InvokeTransformCommand() + { + var rootCommand = Program.CreateRootCommand(); + var args = new string[] { "transform", "-d", ".\\UtilityFiles\\SampleOpenApi.yml", "-o", "sample.json" }; + var parseResult = rootCommand.Parse(args); + var handler = rootCommand.Subcommands.Where(c => c.Name == "transform").First().Handler; + var context = new InvocationContext(parseResult); + + handler.Invoke(context); + + var output = File.ReadAllText("sample.json"); + Assert.NotEmpty(output); + } + + [Fact] public void InvokeShowCommand() { From c23694cfe43478d77dc2128452e46e21f61fdb03 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 15 Jan 2023 11:11:21 -0500 Subject: [PATCH 26/35] Fixed code smell relating to LogError --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 2cc188865..8d7ea774e 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -567,7 +567,7 @@ private static void LogErrors(ILogger logger, ReadResult result) { foreach (var error in context.Errors) { - logger.LogError(error.ToString()); + logger.LogError($"Detected error during parsing: {error}",error.ToString()); } } } From 6a3dd015694bd07f883fe4e00f6970f9337896a7 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 15 Jan 2023 11:11:48 -0500 Subject: [PATCH 27/35] Added test to call Transform command directly so that code coverage will actually see it. --- .../Services/OpenApiServiceTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index aa49ff520..995ce1f02 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -131,6 +131,18 @@ public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() Assert.Contains("graph LR", output); } + + [Fact] + public async Task TransformCommandConvertsOpenApi() + { + var fileinfo = new FileInfo("sample.json"); + // create a dummy ILogger instance for testing + await OpenApiService.TransformOpenApiDocument("UtilityFiles\\SampleOpenApi.yml",null, null, fileinfo, true, null, null,false,null,false,false,null,null,null,new Logger(new LoggerFactory()), new CancellationToken()); + + var output = File.ReadAllText("sample.json"); + Assert.NotEmpty(output); + } + [Fact] public void InvokeTransformCommand() { From 8ff70a18b7f6b3e5568e5501f629c172f301f0d1 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 13:18:00 -0500 Subject: [PATCH 28/35] Removed unnecessary test that was breaking --- .../Services/OpenApiServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index 995ce1f02..bdb5827b1 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -147,7 +147,7 @@ public async Task TransformCommandConvertsOpenApi() public void InvokeTransformCommand() { var rootCommand = Program.CreateRootCommand(); - var args = new string[] { "transform", "-d", ".\\UtilityFiles\\SampleOpenApi.yml", "-o", "sample.json" }; + var args = new string[] { "transform", "-d", ".\\UtilityFiles\\SampleOpenApi.yml", "-o", "sample.json","--co" }; var parseResult = rootCommand.Parse(args); var handler = rootCommand.Subcommands.Where(c => c.Name == "transform").First().Handler; var context = new InvocationContext(parseResult); From b95cda39f2a80f50889ded12fc85b3eb23805866 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 13:51:39 -0500 Subject: [PATCH 29/35] This time I included the change --- .../Services/OpenApiServiceTests.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index bdb5827b1..be1ca18e8 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -175,20 +175,6 @@ public void InvokeShowCommand() } - [Fact] - public void InvokeShowCommandWithoutOutput() - { - var rootCommand = Program.CreateRootCommand(); - var args = new string[] { "show", "-d", ".\\UtilityFiles\\SampleOpenApi.yml" }; - var parseResult = rootCommand.Parse(args); - var handler = rootCommand.Subcommands.Where(c => c.Name == "show").First().Handler; - var context = new InvocationContext(parseResult); - - handler.Invoke(context); - - var output = File.ReadAllText(Path.Combine(Path.GetTempPath(), "apitree.html")); - Assert.Contains("graph LR", output); - } // Relatively useless test to keep the code coverage metrics happy From 230af2f1009769756ade41e855be6f74e4985898 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 13:57:18 -0500 Subject: [PATCH 30/35] Added missing comments for public APIs --- src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 870fb36d9..c8e2da03f 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -341,7 +341,7 @@ private static string SanitizeMermaidNode(string token) public class MermaidNodeStyle { /// - /// + /// Create a style that defines the color and shape of a diagram element /// /// /// @@ -352,18 +352,18 @@ internal MermaidNodeStyle(string color, MermaidNodeShape shape) } /// - /// + /// The CSS color name of the diagram element /// public string Color { get; } /// - /// + /// The shape of the diagram element /// public MermaidNodeShape Shape { get; } } /// - /// + /// Shapes supported by Mermaid diagrams /// public enum MermaidNodeShape { From 2b82a74b805be0a4a9971b3fc0cbc6cd11fa2518 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 14:47:07 -0500 Subject: [PATCH 31/35] Added more tests to meet the coverage gods --- .../Services/OpenApiServiceTests.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index be1ca18e8..dbfbce221 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -131,6 +131,30 @@ public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() Assert.Contains("graph LR", output); } + [Fact] + public async Task ShowCommandGeneratesMermaidMarkdownFileFromCsdlWithMermaidDiagram() + { + var fileinfo = new FileInfo("sample.md"); + // create a dummy ILogger instance for testing + await OpenApiService.ShowOpenApiDocument(null, "UtilityFiles\\Todo.xml", "todos", fileinfo, new Logger(new LoggerFactory()), new CancellationToken()); + + var output = File.ReadAllText(fileinfo.FullName); + Assert.Contains("graph LR", output); + } + + [Fact] + public async Task ThrowIfURLIsNotResolvableWhenValidating() + { + var message = Assert.ThrowsAsync(async () => + await OpenApiService.ValidateOpenApiDocument("https://example.org/itdoesnmatter", new Logger(new LoggerFactory()), new CancellationToken())); + } + + [Fact] + public async Task ThrowIfFileDoesNotExistWhenValidating() + { + var message = Assert.ThrowsAsync(async () => + await OpenApiService.ValidateOpenApiDocument("aFileThatBetterNotExist.fake", new Logger(new LoggerFactory()), new CancellationToken())); + } [Fact] public async Task TransformCommandConvertsOpenApi() @@ -175,8 +199,6 @@ public void InvokeShowCommand() } - - // Relatively useless test to keep the code coverage metrics happy [Fact] public void CreateRootCommand() From 45372397dfe9514ac55c1f873919c7587807a599 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 15:07:01 -0500 Subject: [PATCH 32/35] More sacrifices made --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 24 +++++++++------ .../Services/OpenApiServiceTests.cs | 30 +++++++++++++++++-- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 8d7ea774e..c4653353a 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -55,18 +55,19 @@ public static async Task TransformOpenApiDocument( CancellationToken cancellationToken ) { + if (string.IsNullOrEmpty(openapi) && string.IsNullOrEmpty(csdl)) + { + throw new ArgumentException("Please input a file path or URL"); + } try { - if (string.IsNullOrEmpty(openapi) && string.IsNullOrEmpty(csdl)) - { - throw new ArgumentException("Please input a file path or URL"); - } if (output == null) { var inputExtension = GetInputPathExtension(openapi, csdl); output = new FileInfo($"./output{inputExtension}"); }; + if (cleanoutput && output.Exists) { output.Delete(); @@ -87,7 +88,11 @@ CancellationToken cancellationToken catch (TaskCanceledException) { Console.Error.WriteLine("CTRL+C pressed, aborting the operation."); - } + } + catch (IOException) + { + throw; + } catch (Exception ex) { throw new InvalidOperationException($"Could not transform the document, reason: {ex.Message}", ex); @@ -239,12 +244,13 @@ public static async Task ValidateOpenApiDocument( ILogger logger, CancellationToken cancellationToken) { + if (string.IsNullOrEmpty(openapi)) + { + throw new ArgumentNullException(nameof(openapi)); + } + try { - if (string.IsNullOrEmpty(openapi)) - { - throw new ArgumentNullException(nameof(openapi)); - } using var stream = await GetStream(openapi, logger, cancellationToken); var result = await ParseOpenApi(openapi, false, logger, stream); diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index dbfbce221..d397e4163 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -142,20 +142,38 @@ public async Task ShowCommandGeneratesMermaidMarkdownFileFromCsdlWithMermaidDiag Assert.Contains("graph LR", output); } + [Fact] + public async Task ThrowIfOpenApiUrlIsNotProvidedWhenValidating() + { + await Assert.ThrowsAsync(async () => + await OpenApiService.ValidateOpenApiDocument("", new Logger(new LoggerFactory()), new CancellationToken())); + } + + [Fact] public async Task ThrowIfURLIsNotResolvableWhenValidating() { - var message = Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await OpenApiService.ValidateOpenApiDocument("https://example.org/itdoesnmatter", new Logger(new LoggerFactory()), new CancellationToken())); } [Fact] public async Task ThrowIfFileDoesNotExistWhenValidating() { - var message = Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await OpenApiService.ValidateOpenApiDocument("aFileThatBetterNotExist.fake", new Logger(new LoggerFactory()), new CancellationToken())); } + [Fact] + public async Task ValidateCommandProcessesOpenApi() + { + // create a dummy ILogger instance for testing + await OpenApiService.ValidateOpenApiDocument("UtilityFiles\\SampleOpenApi.yml", new Logger(new LoggerFactory()), new CancellationToken()); + + Assert.True(true); + } + + [Fact] public async Task TransformCommandConvertsOpenApi() { @@ -167,6 +185,14 @@ public async Task TransformCommandConvertsOpenApi() Assert.NotEmpty(output); } + [Fact] + public async Task ThrowTransformCommandIfOpenApiAndCsdlAreEmpty() + { + await Assert.ThrowsAsync(async () => + await OpenApiService.TransformOpenApiDocument(null, null, null, null, true, null, null, false, null, false, false, null, null, null, new Logger(new LoggerFactory()), new CancellationToken())); + + } + [Fact] public void InvokeTransformCommand() { From 96fae889d55f4c12ea176c4f8d338d6f89d9fdb5 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 16:39:59 -0500 Subject: [PATCH 33/35] Will these be the tests that achieve the magical goal? --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 4 ++-- .../Services/OpenApiServiceTests.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index c4653353a..a952f414b 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -103,8 +103,8 @@ private static void WriteOpenApi(FileInfo output, bool terseOutput, bool inlineL { using (logger.BeginScope("Output")) { - using var outputStream = output?.Create(); - var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out; + using var outputStream = output.Create(); + var textWriter = new StreamWriter(outputStream); var settings = new OpenApiWriterSettings() { diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index d397e4163..11b5bc4f3 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -185,6 +185,26 @@ public async Task TransformCommandConvertsOpenApi() Assert.NotEmpty(output); } + [Fact] + public async Task TransformCommandConvertsOpenApiWithDefaultOutputname() + { + // create a dummy ILogger instance for testing + await OpenApiService.TransformOpenApiDocument("UtilityFiles\\SampleOpenApi.yml", null, null, null, true, null, null, false, null, false, false, null, null, null, new Logger(new LoggerFactory()), new CancellationToken()); + + var output = File.ReadAllText("output.yml"); + Assert.NotEmpty(output); + } + + [Fact] + public async Task TransformCommandConvertsOpenApiWithDefaultOutputnameAndSwitchFormat() + { + // create a dummy ILogger instance for testing + await OpenApiService.TransformOpenApiDocument("UtilityFiles\\SampleOpenApi.yml", null, null, null, true, "3.0", OpenApiFormat.Yaml, false, null, false, false, null, null, null, new Logger(new LoggerFactory()), new CancellationToken()); + + var output = File.ReadAllText("output.yml"); + Assert.NotEmpty(output); + } + [Fact] public async Task ThrowTransformCommandIfOpenApiAndCsdlAreEmpty() { From 7638805bf8d2c1a5d169bc1900b32e167c1c8b87 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 17:09:37 -0500 Subject: [PATCH 34/35] I am confidence I have enough tests now --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 6 ++- .../Services/OpenApiServiceTests.cs | 42 ++++++++++++++----- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index a952f414b..64a23dff3 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -505,7 +505,7 @@ private static string GetInputPathExtension(string openapi = null, string csdl = return extension; } - internal static async Task ShowOpenApiDocument(string openapi, string csdl, string csdlFilter, FileInfo output, ILogger logger, CancellationToken cancellationToken) + internal static async Task ShowOpenApiDocument(string openapi, string csdl, string csdlFilter, FileInfo output, ILogger logger, CancellationToken cancellationToken) { try { @@ -542,6 +542,8 @@ internal static async Task ShowOpenApiDocument(string openapi, string csdl, stri process.StartInfo.FileName = output.FullName; process.StartInfo.UseShellExecute = true; process.Start(); + + return output.FullName; } else // Write diagram as Markdown document to output file { @@ -551,6 +553,7 @@ internal static async Task ShowOpenApiDocument(string openapi, string csdl, stri WriteTreeDocumentAsMarkdown(openapi ?? csdl, document, writer); } logger.LogTrace("Created markdown document with diagram "); + return output.FullName; } } } @@ -562,6 +565,7 @@ internal static async Task ShowOpenApiDocument(string openapi, string csdl, stri { throw new InvalidOperationException($"Could not generate the document, reason: {ex.Message}", ex); } + return null; } private static void LogErrors(ILogger logger, ReadResult result) diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs index 11b5bc4f3..ac2048ad1 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs @@ -84,11 +84,13 @@ public void ReturnOpenApiConvertSettingsWhenSettingsFileIsProvided(string filePa [Fact] public void ShowCommandGeneratesMermaidDiagramAsMarkdown() { - var openApiDoc = new OpenApiDocument(); - openApiDoc.Info = new OpenApiInfo + var openApiDoc = new OpenApiDocument { - Title = "Test", - Version = "1.0.0" + Info = new OpenApiInfo + { + Title = "Test", + Version = "1.0.0" + } }; var stream = new MemoryStream(); using var writer = new StreamWriter(stream); @@ -101,13 +103,15 @@ public void ShowCommandGeneratesMermaidDiagramAsMarkdown() } [Fact] - public void ShowCommandGeneratesMermaidDiagramAsHtml () + public void ShowCommandGeneratesMermaidDiagramAsHtml() { - var openApiDoc = new OpenApiDocument(); - openApiDoc.Info = new OpenApiInfo + var openApiDoc = new OpenApiDocument { - Title = "Test", - Version = "1.0.0" + Info = new OpenApiInfo + { + Title = "Test", + Version = "1.0.0" + } }; var stream = new MemoryStream(); using var writer = new StreamWriter(stream); @@ -118,7 +122,7 @@ public void ShowCommandGeneratesMermaidDiagramAsHtml () var output = reader.ReadToEnd(); Assert.Contains("graph LR", output); } - + [Fact] public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() @@ -131,6 +135,13 @@ public async Task ShowCommandGeneratesMermaidMarkdownFileWithMermaidDiagram() Assert.Contains("graph LR", output); } + [Fact] + public async Task ShowCommandGeneratesMermaidHtmlFileWithMermaidDiagram() + { + var filePath = await OpenApiService.ShowOpenApiDocument("UtilityFiles\\SampleOpenApi.yml", null, null, null, new Logger(new LoggerFactory()), new CancellationToken()); + Assert.True(File.Exists(filePath)); + } + [Fact] public async Task ShowCommandGeneratesMermaidMarkdownFileFromCsdlWithMermaidDiagram() { @@ -185,6 +196,7 @@ public async Task TransformCommandConvertsOpenApi() Assert.NotEmpty(output); } + [Fact] public async Task TransformCommandConvertsOpenApiWithDefaultOutputname() { @@ -195,6 +207,16 @@ public async Task TransformCommandConvertsOpenApiWithDefaultOutputname() Assert.NotEmpty(output); } + [Fact] + public async Task TransformCommandConvertsCsdlWithDefaultOutputname() + { + // create a dummy ILogger instance for testing + await OpenApiService.TransformOpenApiDocument(null, "UtilityFiles\\Todo.xml", null, null, true, null, null, false, null, false, false, null, null, null, new Logger(new LoggerFactory()), new CancellationToken()); + + var output = File.ReadAllText("output.yml"); + Assert.NotEmpty(output); + } + [Fact] public async Task TransformCommandConvertsOpenApiWithDefaultOutputnameAndSwitchFormat() { From a8a693d5d34193a86299946bb6fdf8cee8aa3d64 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 16 Jan 2023 17:43:45 -0500 Subject: [PATCH 35/35] Added a using to dispose a StreamReader --- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 64a23dff3..e63a2b9ba 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -225,7 +225,7 @@ private static XslCompiledTransform GetFilterTransform() private static Stream ApplyFilterToCsdl(Stream csdlStream, string entitySetOrSingleton, XslCompiledTransform transform) { Stream stream; - StreamReader inputReader = new(csdlStream); + using StreamReader inputReader = new(csdlStream, leaveOpen: true); XmlReader inputXmlReader = XmlReader.Create(inputReader); MemoryStream filteredStream = new(); StreamWriter writer = new(filteredStream);