diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj index a210a5c94..70a39df3a 100644 --- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj +++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj @@ -5,6 +5,7 @@ netcoreapp3.1 9.0 true + http://go.microsoft.com/fwlink/?LinkID=288890 https://github.com/Microsoft/OpenAPI.NET MIT true @@ -15,6 +16,7 @@ hidi ./../../artifacts 0.5.0-preview4 + OpenAPI.NET CLI tool for slicing OpenAPI documents © Microsoft Corporation. All rights reserved. OpenAPI .NET https://github.com/Microsoft/OpenAPI.NET @@ -34,6 +36,8 @@ + + diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 3c9fdb7d5..964329aaf 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -13,8 +13,11 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using System.Xml.Linq; +using Microsoft.OData.Edm.Csdl; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.OData; using Microsoft.OpenApi.Readers; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Validations; @@ -26,6 +29,7 @@ public class OpenApiService { public static async void ProcessOpenApiDocument( string openapi, + string csdl, FileInfo output, OpenApiSpecVersion? version, OpenApiFormat? format, @@ -41,9 +45,9 @@ string filterbycollection try { - if (string.IsNullOrEmpty(openapi)) + if (string.IsNullOrEmpty(openapi) && string.IsNullOrEmpty(csdl)) { - throw new ArgumentNullException(nameof(openapi)); + throw new ArgumentNullException("Please input a file path"); } } catch (ArgumentNullException ex) @@ -75,36 +79,56 @@ string filterbycollection logger.LogError(ex.Message); return; } - - var stream = await GetStream(openapi, logger); - // Parsing OpenAPI file + Stream stream; + OpenApiDocument document; + OpenApiFormat openApiFormat; var stopwatch = new Stopwatch(); - stopwatch.Start(); - logger.LogTrace("Parsing OpenApi file"); - var result = new OpenApiStreamReader(new OpenApiReaderSettings - { - ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences, - RuleSet = ValidationRuleSet.GetDefaultRuleSet() - } - ).ReadAsync(stream).GetAwaiter().GetResult(); - var document = result.OpenApiDocument; - stopwatch.Stop(); - var context = result.OpenApiDiagnostic; - if (context.Errors.Count > 0) + if (!string.IsNullOrEmpty(csdl)) { - var errorReport = new StringBuilder(); + // Default to yaml and OpenApiVersion 3 during csdl to OpenApi conversion + openApiFormat = format ?? GetOpenApiFormat(csdl, logger); + version ??= OpenApiSpecVersion.OpenApi3_0; - 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())}"); + stream = await GetStream(csdl, logger); + document = ConvertCsdlToOpenApi(stream); } else { - logger.LogTrace("{timestamp}ms: Parsed OpenApi successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, document.Paths.Count); + stream = await GetStream(openapi, logger); + + // Parsing OpenAPI file + stopwatch.Start(); + logger.LogTrace("Parsing OpenApi file"); + var result = new OpenApiStreamReader(new OpenApiReaderSettings + { + ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences, + RuleSet = ValidationRuleSet.GetDefaultRuleSet() + } + ).ReadAsync(stream).GetAwaiter().GetResult(); + + 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); + } + + openApiFormat = format ?? GetOpenApiFormat(openapi, logger); + version ??= result.OpenApiDiagnostic.SpecificationVersion; } Func predicate; @@ -151,8 +175,6 @@ string filterbycollection ReferenceInline = inline ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences }; - var openApiFormat = format ?? GetOpenApiFormat(openapi, logger); - var openApiVersion = version ?? result.OpenApiDiagnostic.SpecificationVersion; IOpenApiWriter writer = openApiFormat switch { OpenApiFormat.Json => new OpenApiJsonWriter(textWriter, settings), @@ -163,7 +185,7 @@ string filterbycollection logger.LogTrace("Serializing to OpenApi document using the provided spec version and writer"); stopwatch.Start(); - document.Serialize(writer, openApiVersion); + document.Serialize(writer, (OpenApiSpecVersion)version); stopwatch.Stop(); logger.LogTrace($"Finished serializing in {stopwatch.ElapsedMilliseconds}ms"); @@ -171,6 +193,54 @@ string filterbycollection textWriter.Flush(); } + /// + /// Converts CSDL to OpenAPI + /// + /// The CSDL stream. + /// An OpenAPI document. + public static OpenApiDocument ConvertCsdlToOpenApi(Stream csdl) + { + using var reader = new StreamReader(csdl); + var csdlText = reader.ReadToEndAsync().GetAwaiter().GetResult(); + var edmModel = CsdlReader.Parse(XElement.Parse(csdlText).CreateReader()); + + var settings = new OpenApiConvertSettings() + { + EnableKeyAsSegment = true, + EnableOperationId = true, + PrefixEntityTypeNameBeforeKey = true, + TagDepth = 2, + EnablePagination = true, + EnableDiscriminatorValue = false, + EnableDerivedTypesReferencesForRequestBody = false, + EnableDerivedTypesReferencesForResponses = false, + ShowRootPath = true, + ShowLinks = true + }; + OpenApiDocument document = edmModel.ConvertToOpenApi(settings); + + document = FixReferences(document); + + return document; + } + + /// + /// Fixes the references in the resulting OpenApiDocument. + /// + /// The converted OpenApiDocument. + /// A valid OpenApiDocument instance. + public static OpenApiDocument FixReferences(OpenApiDocument document) + { + // This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance. + // So we write it out, and read it back in again to fix it up. + + var sb = new StringBuilder(); + document.SerializeAsV3(new OpenApiYamlWriter(new StringWriter(sb))); + var doc = new OpenApiStringReader().Read(sb.ToString(), out _); + + return doc; + } + private static async Task GetStream(string input, ILogger logger) { var stopwatch = new Stopwatch(); @@ -286,10 +356,10 @@ internal static async void ValidateOpenApiDocument(string openapi, LogLevel logl Console.WriteLine(statsVisitor.GetStatisticsReport()); } - private static OpenApiFormat GetOpenApiFormat(string openapi, ILogger logger) + private static OpenApiFormat GetOpenApiFormat(string input, ILogger logger) { logger.LogTrace("Getting the OpenApi format"); - return !openapi.StartsWith("http") && Path.GetExtension(openapi) == ".json" ? OpenApiFormat.Json : OpenApiFormat.Yaml; + return !input.StartsWith("http") && Path.GetExtension(input) == ".json" ? OpenApiFormat.Json : OpenApiFormat.Yaml; } private static ILogger ConfigureLoggerInstance(LogLevel loglevel) diff --git a/src/Microsoft.OpenApi.Hidi/Program.cs b/src/Microsoft.OpenApi.Hidi/Program.cs index 841c710e5..95e6f63f2 100644 --- a/src/Microsoft.OpenApi.Hidi/Program.cs +++ b/src/Microsoft.OpenApi.Hidi/Program.cs @@ -19,6 +19,9 @@ static async Task Main(string[] args) var descriptionOption = new Option("--openapi", "Input OpenAPI description file path or URL"); descriptionOption.AddAlias("-d"); + var csdlOption = new Option("--csdl", "Input CSDL file path or URL"); + csdlOption.AddAlias("-cs"); + var outputOption = new Option("--output", () => new FileInfo("./output"), "The output directory path for the generated file.") { Arity = ArgumentArity.ZeroOrOne }; outputOption.AddAlias("-o"); @@ -57,6 +60,7 @@ static async Task Main(string[] args) var transformCommand = new Command("transform") { descriptionOption, + csdlOption, outputOption, versionOption, formatOption, @@ -68,8 +72,8 @@ static async Task Main(string[] args) resolveExternalOption, }; - transformCommand.SetHandler ( - OpenApiService.ProcessOpenApiDocument, descriptionOption, outputOption, versionOption, formatOption, logLevelOption, inlineOption, resolveExternalOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption); + transformCommand.SetHandler ( + OpenApiService.ProcessOpenApiDocument, descriptionOption, csdlOption, outputOption, versionOption, formatOption, logLevelOption, inlineOption, resolveExternalOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption); rootCommand.Add(transformCommand); rootCommand.Add(validateCommand); diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index d8fd47fd1..695d4ef23 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -44,5 +44,8 @@ Always + + Always + \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiServiceTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiServiceTests.cs new file mode 100644 index 000000000..1b94a3557 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiServiceTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.IO; +using Microsoft.OpenApi.Hidi; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Services +{ + public class OpenApiServiceTests + { + [Fact] + public void ReturnConvertedCSDLFile() + { + // Arrange + var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UtilityFiles\\Todo.xml"); + var fileInput = new FileInfo(filePath); + var csdlStream = fileInput.OpenRead(); + + // Act + var openApiDoc = OpenApiService.ConvertCsdlToOpenApi(csdlStream); + var expectedPathCount = 5; + + // Assert + Assert.NotNull(openApiDoc); + Assert.NotEmpty(openApiDoc.Paths); + Assert.Equal(openApiDoc.Paths.Count, expectedPathCount); + } + + [Theory] + [InlineData("Todos.Todo.UpdateTodo",null, 1)] + [InlineData("Todos.Todo.ListTodo",null, 1)] + [InlineData(null, "Todos.Todo", 4)] + public void ReturnFilteredOpenApiDocBasedOnOperationIdsAndInputCsdlDocument(string operationIds, string tags, int expectedPathCount) + { + // Arrange + var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UtilityFiles\\Todo.xml"); + var fileInput = new FileInfo(filePath); + var csdlStream = fileInput.OpenRead(); + + // Act + var openApiDoc = OpenApiService.ConvertCsdlToOpenApi(csdlStream); + var predicate = OpenApiFilterService.CreatePredicate(operationIds, tags); + var subsetOpenApiDocument = OpenApiFilterService.CreateFilteredDocument(openApiDoc, predicate); + + // Assert + Assert.NotNull(subsetOpenApiDocument); + Assert.NotEmpty(subsetOpenApiDocument.Paths); + Assert.Equal(expectedPathCount, subsetOpenApiDocument.Paths.Count); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/UtilityFiles/Todo.xml b/test/Microsoft.OpenApi.Tests/UtilityFiles/Todo.xml new file mode 100644 index 000000000..b3b07debf --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/UtilityFiles/Todo.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + +