Skip to content

Commit 4890e0e

Browse files
Merge branch 'vnext' into mk/fix-parsing-errors
2 parents a7b1e1e + 6adac99 commit 4890e0e

File tree

8 files changed

+192
-35
lines changed

8 files changed

+192
-35
lines changed

src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<TargetFramework>netcoreapp3.1</TargetFramework>
66
<LangVersion>9.0</LangVersion>
77
<PackAsTool>true</PackAsTool>
8+
<PackageIconUrl>http://go.microsoft.com/fwlink/?LinkID=288890</PackageIconUrl>
89
<PackageProjectUrl>https://github.com/Microsoft/OpenAPI.NET</PackageProjectUrl>
910
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1011
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
@@ -15,6 +16,7 @@
1516
<ToolCommandName>hidi</ToolCommandName>
1617
<PackageOutputPath>./../../artifacts</PackageOutputPath>
1718
<Version>0.5.0-preview4</Version>
19+
<Description>OpenAPI.NET CLI tool for slicing OpenAPI documents</Description>
1820
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
1921
<PackageTags>OpenAPI .NET</PackageTags>
2022
<RepositoryUrl>https://github.com/Microsoft/OpenAPI.NET</RepositoryUrl>
@@ -34,6 +36,8 @@
3436
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
3537
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
3638
<PackageReference Include="System.CommandLine" Version="2.0.0-beta2.21617.1" />
39+
<PackageReference Include="Microsoft.OData.Edm" Version="7.9.4" />
40+
<PackageReference Include="Microsoft.OpenApi.OData" Version="1.0.9" />
3741
</ItemGroup>
3842

3943
<ItemGroup>

src/Microsoft.OpenApi.Hidi/OpenApiService.cs

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
using System.Text.Json;
1414
using System.Threading.Tasks;
1515
using Microsoft.Extensions.Logging;
16+
using System.Xml.Linq;
17+
using Microsoft.OData.Edm.Csdl;
1618
using Microsoft.OpenApi.Extensions;
1719
using Microsoft.OpenApi.Models;
20+
using Microsoft.OpenApi.OData;
1821
using Microsoft.OpenApi.Readers;
1922
using Microsoft.OpenApi.Services;
2023
using Microsoft.OpenApi.Validations;
@@ -26,6 +29,7 @@ public class OpenApiService
2629
{
2730
public static async void ProcessOpenApiDocument(
2831
string openapi,
32+
string csdl,
2933
FileInfo output,
3034
OpenApiSpecVersion? version,
3135
OpenApiFormat? format,
@@ -41,9 +45,9 @@ string filterbycollection
4145

4246
try
4347
{
44-
if (string.IsNullOrEmpty(openapi))
48+
if (string.IsNullOrEmpty(openapi) && string.IsNullOrEmpty(csdl))
4549
{
46-
throw new ArgumentNullException(nameof(openapi));
50+
throw new ArgumentNullException("Please input a file path");
4751
}
4852
}
4953
catch (ArgumentNullException ex)
@@ -75,36 +79,56 @@ string filterbycollection
7579
logger.LogError(ex.Message);
7680
return;
7781
}
78-
79-
var stream = await GetStream(openapi, logger);
8082

81-
// Parsing OpenAPI file
83+
Stream stream;
84+
OpenApiDocument document;
85+
OpenApiFormat openApiFormat;
8286
var stopwatch = new Stopwatch();
83-
stopwatch.Start();
84-
logger.LogTrace("Parsing OpenApi file");
85-
var result = new OpenApiStreamReader(new OpenApiReaderSettings
86-
{
87-
ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
88-
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
89-
}
90-
).ReadAsync(stream).GetAwaiter().GetResult();
91-
var document = result.OpenApiDocument;
92-
stopwatch.Stop();
9387

94-
var context = result.OpenApiDiagnostic;
95-
if (context.Errors.Count > 0)
88+
if (!string.IsNullOrEmpty(csdl))
9689
{
97-
var errorReport = new StringBuilder();
90+
// Default to yaml and OpenApiVersion 3 during csdl to OpenApi conversion
91+
openApiFormat = format ?? GetOpenApiFormat(csdl, logger);
92+
version ??= OpenApiSpecVersion.OpenApi3_0;
9893

99-
foreach (var error in context.Errors)
100-
{
101-
errorReport.AppendLine(error.ToString());
102-
}
103-
logger.LogError($"{stopwatch.ElapsedMilliseconds}ms: OpenApi Parsing errors {string.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray())}");
94+
stream = await GetStream(csdl, logger);
95+
document = ConvertCsdlToOpenApi(stream);
10496
}
10597
else
10698
{
107-
logger.LogTrace("{timestamp}ms: Parsed OpenApi successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, document.Paths.Count);
99+
stream = await GetStream(openapi, logger);
100+
101+
// Parsing OpenAPI file
102+
stopwatch.Start();
103+
logger.LogTrace("Parsing OpenApi file");
104+
var result = new OpenApiStreamReader(new OpenApiReaderSettings
105+
{
106+
ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
107+
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
108+
}
109+
).ReadAsync(stream).GetAwaiter().GetResult();
110+
111+
document = result.OpenApiDocument;
112+
stopwatch.Stop();
113+
114+
var context = result.OpenApiDiagnostic;
115+
if (context.Errors.Count > 0)
116+
{
117+
var errorReport = new StringBuilder();
118+
119+
foreach (var error in context.Errors)
120+
{
121+
errorReport.AppendLine(error.ToString());
122+
}
123+
logger.LogError($"{stopwatch.ElapsedMilliseconds}ms: OpenApi Parsing errors {string.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray())}");
124+
}
125+
else
126+
{
127+
logger.LogTrace("{timestamp}ms: Parsed OpenApi successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, document.Paths.Count);
128+
}
129+
130+
openApiFormat = format ?? GetOpenApiFormat(openapi, logger);
131+
version ??= result.OpenApiDiagnostic.SpecificationVersion;
108132
}
109133

110134
Func<string, OperationType?, OpenApiOperation, bool> predicate;
@@ -151,8 +175,6 @@ string filterbycollection
151175
ReferenceInline = inline ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences
152176
};
153177

154-
var openApiFormat = format ?? GetOpenApiFormat(openapi, logger);
155-
var openApiVersion = version ?? result.OpenApiDiagnostic.SpecificationVersion;
156178
IOpenApiWriter writer = openApiFormat switch
157179
{
158180
OpenApiFormat.Json => new OpenApiJsonWriter(textWriter, settings),
@@ -163,14 +185,62 @@ string filterbycollection
163185
logger.LogTrace("Serializing to OpenApi document using the provided spec version and writer");
164186

165187
stopwatch.Start();
166-
document.Serialize(writer, openApiVersion);
188+
document.Serialize(writer, (OpenApiSpecVersion)version);
167189
stopwatch.Stop();
168190

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

171193
textWriter.Flush();
172194
}
173195

196+
/// <summary>
197+
/// Converts CSDL to OpenAPI
198+
/// </summary>
199+
/// <param name="csdl">The CSDL stream.</param>
200+
/// <returns>An OpenAPI document.</returns>
201+
public static OpenApiDocument ConvertCsdlToOpenApi(Stream csdl)
202+
{
203+
using var reader = new StreamReader(csdl);
204+
var csdlText = reader.ReadToEndAsync().GetAwaiter().GetResult();
205+
var edmModel = CsdlReader.Parse(XElement.Parse(csdlText).CreateReader());
206+
207+
var settings = new OpenApiConvertSettings()
208+
{
209+
EnableKeyAsSegment = true,
210+
EnableOperationId = true,
211+
PrefixEntityTypeNameBeforeKey = true,
212+
TagDepth = 2,
213+
EnablePagination = true,
214+
EnableDiscriminatorValue = false,
215+
EnableDerivedTypesReferencesForRequestBody = false,
216+
EnableDerivedTypesReferencesForResponses = false,
217+
ShowRootPath = true,
218+
ShowLinks = true
219+
};
220+
OpenApiDocument document = edmModel.ConvertToOpenApi(settings);
221+
222+
document = FixReferences(document);
223+
224+
return document;
225+
}
226+
227+
/// <summary>
228+
/// Fixes the references in the resulting OpenApiDocument.
229+
/// </summary>
230+
/// <param name="document"> The converted OpenApiDocument.</param>
231+
/// <returns> A valid OpenApiDocument instance.</returns>
232+
public static OpenApiDocument FixReferences(OpenApiDocument document)
233+
{
234+
// This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance.
235+
// So we write it out, and read it back in again to fix it up.
236+
237+
var sb = new StringBuilder();
238+
document.SerializeAsV3(new OpenApiYamlWriter(new StringWriter(sb)));
239+
var doc = new OpenApiStringReader().Read(sb.ToString(), out _);
240+
241+
return doc;
242+
}
243+
174244
private static async Task<Stream> GetStream(string input, ILogger logger)
175245
{
176246
var stopwatch = new Stopwatch();
@@ -286,10 +356,10 @@ internal static async void ValidateOpenApiDocument(string openapi, LogLevel logl
286356
Console.WriteLine(statsVisitor.GetStatisticsReport());
287357
}
288358

289-
private static OpenApiFormat GetOpenApiFormat(string openapi, ILogger logger)
359+
private static OpenApiFormat GetOpenApiFormat(string input, ILogger logger)
290360
{
291361
logger.LogTrace("Getting the OpenApi format");
292-
return !openapi.StartsWith("http") && Path.GetExtension(openapi) == ".json" ? OpenApiFormat.Json : OpenApiFormat.Yaml;
362+
return !input.StartsWith("http") && Path.GetExtension(input) == ".json" ? OpenApiFormat.Json : OpenApiFormat.Yaml;
293363
}
294364

295365
private static ILogger ConfigureLoggerInstance(LogLevel loglevel)

src/Microsoft.OpenApi.Hidi/Program.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ static async Task<int> Main(string[] args)
1919
var descriptionOption = new Option<string>("--openapi", "Input OpenAPI description file path or URL");
2020
descriptionOption.AddAlias("-d");
2121

22+
var csdlOption = new Option<string>("--csdl", "Input CSDL file path or URL");
23+
csdlOption.AddAlias("-cs");
24+
2225
var outputOption = new Option<FileInfo>("--output", () => new FileInfo("./output"), "The output directory path for the generated file.") { Arity = ArgumentArity.ZeroOrOne };
2326
outputOption.AddAlias("-o");
2427

@@ -57,6 +60,7 @@ static async Task<int> Main(string[] args)
5760
var transformCommand = new Command("transform")
5861
{
5962
descriptionOption,
63+
csdlOption,
6064
outputOption,
6165
versionOption,
6266
formatOption,
@@ -68,8 +72,8 @@ static async Task<int> Main(string[] args)
6872
resolveExternalOption,
6973
};
7074

71-
transformCommand.SetHandler<string, FileInfo, OpenApiSpecVersion?, OpenApiFormat?, LogLevel, bool, bool, string, string, string> (
72-
OpenApiService.ProcessOpenApiDocument, descriptionOption, outputOption, versionOption, formatOption, logLevelOption, inlineOption, resolveExternalOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption);
75+
transformCommand.SetHandler<string, string, FileInfo, OpenApiSpecVersion?, OpenApiFormat?, LogLevel, bool, bool, string, string, string> (
76+
OpenApiService.ProcessOpenApiDocument, descriptionOption, csdlOption, outputOption, versionOption, formatOption, logLevelOption, inlineOption, resolveExternalOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption);
7377

7478
rootCommand.Add(transformCommand);
7579
rootCommand.Add(validateCommand);

src/Microsoft.OpenApi/Microsoft.OpenApi.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
<ItemGroup>
4040
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
41-
<PackageReference Include="System.Text.Json" Version="6.0.1" />
41+
<PackageReference Include="System.Text.Json" Version="6.0.2" />
4242
</ItemGroup>
4343

4444
<ItemGroup>

src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ public static class OpenApiParameterRules
103103
new ValidationRule<OpenApiParameter>(
104104
(context, parameter) =>
105105
{
106-
if (parameter.In == ParameterLocation.Path && !context.PathString.Contains("{" + parameter.Name + "}"))
106+
if (parameter.In == ParameterLocation.Path &&
107+
!(context.PathString.Contains("{" + parameter.Name + "}") || context.PathString.Contains("#/components")))
107108
{
108109
context.Enter("in");
109110
context.CreateError(

test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
<PackageReference Include="Moq" Version="4.16.1" />
2121
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
2222
<PackageReference Include="SharpYaml" Version="1.8.0" />
23-
<PackageReference Include="Verify" Version="15.2.1" />
24-
<PackageReference Include="Verify.Xunit" Version="14.14.1" />
23+
<PackageReference Include="Verify" Version="16.1.1" />
24+
<PackageReference Include="Verify.Xunit" Version="16.1.1" />
2525
<PackageReference Include="xunit" Version="2.4.1" />
2626
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
2727
<PrivateAssets>all</PrivateAssets>
@@ -44,5 +44,8 @@
4444
<None Update="UtilityFiles\postmanCollection_ver2.json">
4545
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
4646
</None>
47+
<None Update="UtilityFiles\Todo.xml">
48+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
49+
</None>
4750
</ItemGroup>
4851
</Project>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using Microsoft.OpenApi.Hidi;
7+
using Microsoft.OpenApi.Services;
8+
using Xunit;
9+
10+
namespace Microsoft.OpenApi.Tests.Services
11+
{
12+
public class OpenApiServiceTests
13+
{
14+
[Fact]
15+
public void ReturnConvertedCSDLFile()
16+
{
17+
// Arrange
18+
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UtilityFiles\\Todo.xml");
19+
var fileInput = new FileInfo(filePath);
20+
var csdlStream = fileInput.OpenRead();
21+
22+
// Act
23+
var openApiDoc = OpenApiService.ConvertCsdlToOpenApi(csdlStream);
24+
var expectedPathCount = 5;
25+
26+
// Assert
27+
Assert.NotNull(openApiDoc);
28+
Assert.NotEmpty(openApiDoc.Paths);
29+
Assert.Equal(openApiDoc.Paths.Count, expectedPathCount);
30+
}
31+
32+
[Theory]
33+
[InlineData("Todos.Todo.UpdateTodo",null, 1)]
34+
[InlineData("Todos.Todo.ListTodo",null, 1)]
35+
[InlineData(null, "Todos.Todo", 4)]
36+
public void ReturnFilteredOpenApiDocBasedOnOperationIdsAndInputCsdlDocument(string operationIds, string tags, int expectedPathCount)
37+
{
38+
// Arrange
39+
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UtilityFiles\\Todo.xml");
40+
var fileInput = new FileInfo(filePath);
41+
var csdlStream = fileInput.OpenRead();
42+
43+
// Act
44+
var openApiDoc = OpenApiService.ConvertCsdlToOpenApi(csdlStream);
45+
var predicate = OpenApiFilterService.CreatePredicate(operationIds, tags);
46+
var subsetOpenApiDocument = OpenApiFilterService.CreateFilteredDocument(openApiDoc, predicate);
47+
48+
// Assert
49+
Assert.NotNull(subsetOpenApiDocument);
50+
Assert.NotEmpty(subsetOpenApiDocument.Paths);
51+
Assert.Equal(expectedPathCount, subsetOpenApiDocument.Paths.Count);
52+
}
53+
}
54+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
2+
<edmx:DataServices>
3+
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="microsoft.graph" >
4+
5+
<EntityContainer Name="TodoService">
6+
<EntitySet Name="Todos" EntityType="microsoft.graph.Todo">
7+
</EntitySet>
8+
</EntityContainer>
9+
10+
<EntityType Name="Todo" HasStream="true">
11+
<Key>
12+
<PropertyRef Name="Id"/>
13+
</Key>
14+
<Property Name="Id" Type="Edm.String"/>
15+
<Property Name="Logo" Type="Edm.Stream"/>
16+
<Property Name="Description" Type="Edm.String"/>
17+
</EntityType>
18+
19+
</Schema>
20+
</edmx:DataServices>
21+
</edmx:Edmx>

0 commit comments

Comments
 (0)