Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
</ItemGroup>

Expand Down
231 changes: 167 additions & 64 deletions src/Microsoft.OpenApi.Hidi/OpenApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
Expand All @@ -18,123 +22,201 @@

namespace Microsoft.OpenApi.Hidi
{
public static class OpenApiService
public class OpenApiService
{
public static void ProcessOpenApiDocument(
string input,
public static async void ProcessOpenApiDocument(
string openapi,
FileInfo output,
OpenApiSpecVersion? version,
OpenApiFormat? format,
string filterByOperationIds,
string filterByTags,
string filterByCollection,
LogLevel loglevel,
string filterbyoperationids,
string filterbytags,
string filterbycollection,
bool inline,
bool resolveExternal)
bool resolveexternal)
{
if (string.IsNullOrEmpty(input))
var logger = ConfigureLoggerInstance(loglevel);

try
{
throw new ArgumentNullException(nameof(input));
if (string.IsNullOrEmpty(openapi))
{
throw new ArgumentNullException(nameof(openapi));
}
}
if(output == null)
catch (ArgumentNullException ex)
{
throw new ArgumentException(nameof(output));
logger.LogError(ex.Message);
return;
}
if (output.Exists)
try
{
throw new IOException("The file you're writing to already exists. Please input a new output path.");
if(output == null)
{
throw new ArgumentException(nameof(output));
}
}
catch (ArgumentException ex)
{
logger.LogError(ex.Message);
return;
}
try
{
if (output.Exists)
{
throw new IOException("The file you're writing to already exists. Please input a new file path.");
}
}
catch (IOException ex)
{
logger.LogError(ex.Message);
return;
}

var stream = await GetStream(openapi, logger);

var stream = GetStream(input);
// Parsing OpenAPI file
var stopwatch = new Stopwatch();
stopwatch.Start();
logger.LogTrace("Parsing OpenApi file");
var result = new OpenApiStreamReader(new OpenApiReaderSettings
{
ReferenceResolution = resolveExternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
}
).ReadAsync(stream).GetAwaiter().GetResult();

var document = result.OpenApiDocument;
stopwatch.Stop();

var context = result.OpenApiDiagnostic;
if (context.Errors.Count > 0)
{
var errorReport = new StringBuilder();

foreach (var error in context.Errors)
{
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);
}

Func<string, OperationType?, OpenApiOperation, bool> predicate;

// Check if filter options are provided, then execute
if (!string.IsNullOrEmpty(filterByOperationIds) && !string.IsNullOrEmpty(filterByTags))
// 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))
if (!string.IsNullOrEmpty(filterbyoperationids))
{
predicate = OpenApiFilterService.CreatePredicate(operationIds: filterByOperationIds);
logger.LogTrace("Creating predicate based on the operationIds supplied.");
predicate = OpenApiFilterService.CreatePredicate(operationIds: filterbyoperationids);

logger.LogTrace("Creating subset OpenApi document.");
document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
}
if (!string.IsNullOrEmpty(filterByTags))
if (!string.IsNullOrEmpty(filterbytags))
{
predicate = OpenApiFilterService.CreatePredicate(tags: filterByTags);
document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
}
logger.LogTrace("Creating predicate based on the tags supplied.");
predicate = OpenApiFilterService.CreatePredicate(tags: filterbytags);

if (!string.IsNullOrEmpty(filterByCollection))
{
var fileStream = GetStream(filterByCollection);
var requestUrls = ParseJsonCollectionFile(fileStream);
predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source:document);
logger.LogTrace("Creating subset OpenApi document.");
document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
}

var context = result.OpenApiDiagnostic;

if (context.Errors.Count > 0)
if (!string.IsNullOrEmpty(filterbycollection))
{
var errorReport = new StringBuilder();
var fileStream = await GetStream(filterbycollection, logger);
var requestUrls = ParseJsonCollectionFile(fileStream, logger);

foreach (var error in context.Errors)
{
errorReport.AppendLine(error.ToString());
}
logger.LogTrace("Creating predicate based on the paths and Http methods defined in the Postman collection.");
predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source:document);

throw new ArgumentException(string.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray()));
logger.LogTrace("Creating subset OpenApi document.");
document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
}


logger.LogTrace("Creating a new file");
using var outputStream = output?.Create();

var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out;
var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out;

var settings = new OpenApiWriterSettings()
{
ReferenceInline = inline ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences
};

var openApiFormat = format ?? GetOpenApiFormat(input);
var openApiFormat = format ?? GetOpenApiFormat(openapi, logger);
var openApiVersion = version ?? result.OpenApiDiagnostic.SpecificationVersion;
IOpenApiWriter writer = openApiFormat switch
{
OpenApiFormat.Json => new OpenApiJsonWriter(textWriter, settings),
OpenApiFormat.Yaml => new OpenApiYamlWriter(textWriter, settings),
_ => throw new ArgumentException("Unknown format"),
};

logger.LogTrace("Serializing to OpenApi document using the provided spec version and writer");

stopwatch.Start();
document.Serialize(writer, openApiVersion);
stopwatch.Stop();

logger.LogTrace($"Finished serializing in {stopwatch.ElapsedMilliseconds}ms");

textWriter.Flush();
}

private static Stream GetStream(string input)
private static async Task<Stream> GetStream(string input, ILogger logger)
{
var stopwatch = new Stopwatch();
stopwatch.Start();

Stream stream;
if (input.StartsWith("http"))
{
var httpClient = new HttpClient(new HttpClientHandler()
try
{
SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
})
using var httpClientHandler = new HttpClientHandler()
{
SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
};
using var httpClient = new HttpClient(httpClientHandler)
{
DefaultRequestVersion = HttpVersion.Version20
};
stream = await httpClient.GetStreamAsync(input);
}
catch (HttpRequestException ex)
{
DefaultRequestVersion = HttpVersion.Version20
};
stream = httpClient.GetStreamAsync(input).Result;
logger.LogError($"Could not download the file at {input}, reason{ex}");
return null;
}
}
else
{
var fileInput = new FileInfo(input);
stream = fileInput.OpenRead();
try
{
var fileInput = new FileInfo(input);
stream = fileInput.OpenRead();
}
catch (Exception ex) when (ex is FileNotFoundException ||
ex is PathTooLongException ||
ex is DirectoryNotFoundException ||
ex is IOException ||
ex is UnauthorizedAccessException ||
ex is SecurityException ||
ex is NotSupportedException)
{
logger.LogError($"Could not open the file at {input}, reason: {ex.Message}");
return null;
}
}

stopwatch.Stop();
logger.LogTrace("{timestamp}ms: Read file {input}", stopwatch.ElapsedMilliseconds, input);
return stream;
}

Expand All @@ -143,11 +225,11 @@ private static Stream GetStream(string input)
/// </summary>
/// <param name="stream"> A file stream.</param>
/// <returns> A dictionary of request urls and http methods from a collection.</returns>
public static Dictionary<string, List<string>> ParseJsonCollectionFile(Stream stream)
public static Dictionary<string, List<string>> ParseJsonCollectionFile(Stream stream, ILogger logger)
{
var requestUrls = new Dictionary<string, List<string>>();

// Convert file to JsonDocument
logger.LogTrace("Parsing the json collection file into a JsonDocument");
using var document = JsonDocument.Parse(stream);
var root = document.RootElement;
var itemElement = root.GetProperty("item");
Expand All @@ -166,21 +248,21 @@ public static Dictionary<string, List<string>> ParseJsonCollectionFile(Stream st
requestUrls[path].Add(method);
}
}

logger.LogTrace("Finished fetching the list of paths and Http methods defined in the Postman collection.");
return requestUrls;
}

internal static void ValidateOpenApiDocument(string input)
internal static async void ValidateOpenApiDocument(string openapi, LogLevel loglevel)
{
if (input == null)
if (string.IsNullOrEmpty(openapi))
{
throw new ArgumentNullException("input");
throw new ArgumentNullException(nameof(openapi));
}

var stream = GetStream(input);
var logger = ConfigureLoggerInstance(loglevel);
var stream = await GetStream(openapi, logger);

OpenApiDocument document;

logger.LogTrace("Parsing the OpenApi file");
document = new OpenApiStreamReader(new OpenApiReaderSettings
{
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
Expand All @@ -199,12 +281,33 @@ internal static void ValidateOpenApiDocument(string input)
var walker = new OpenApiWalker(statsVisitor);
walker.Walk(document);

logger.LogTrace("Finished walking through the OpenApi document. Generating a statistics report..");
Console.WriteLine(statsVisitor.GetStatisticsReport());
}

private static OpenApiFormat GetOpenApiFormat(string input)
private static OpenApiFormat GetOpenApiFormat(string openapi, ILogger logger)
{
logger.LogTrace("Getting the OpenApi format");
return !openapi.StartsWith("http") && Path.GetExtension(openapi) == ".json" ? OpenApiFormat.Json : OpenApiFormat.Yaml;
}

private static ILogger ConfigureLoggerInstance(LogLevel loglevel)
{
return !input.StartsWith("http") && Path.GetExtension(input) == ".json" ? OpenApiFormat.Json : OpenApiFormat.Yaml;
// Configure logger options
#if DEBUG
loglevel = loglevel > LogLevel.Debug ? LogLevel.Debug : loglevel;
#endif

var logger = LoggerFactory.Create((builder) => {
builder
.AddConsole()
#if DEBUG
.AddDebug()
#endif
.SetMinimumLevel(loglevel);
}).CreateLogger<OpenApiService>();

return logger;
}
}
}
Loading