Skip to content

Commit ca241c0

Browse files
committed
Updated with latest Apimanifest changes
1 parent 86262b6 commit ca241c0

File tree

6 files changed

+102
-41
lines changed

6 files changed

+102
-41
lines changed

src/Microsoft.OpenApi.Hidi/OpenApiService.cs

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,18 @@ public static async Task TransformOpenApiDocument(HidiOptions options, ILogger l
6767
OpenApiFormat openApiFormat = options.OpenApiFormat ?? (!string.IsNullOrEmpty(options.OpenApi) ? GetOpenApiFormat(options.OpenApi, logger) : OpenApiFormat.Yaml);
6868
OpenApiSpecVersion openApiVersion = options.Version != null ? TryParseOpenApiSpecVersion(options.Version) : OpenApiSpecVersion.OpenApi3_0;
6969

70-
// If API Manifest is provided, load it, use it get the OpenAPI path
71-
ApiManifestDocument apiManifest = null;
72-
if (!string.IsNullOrEmpty(options.FilterOptions?.FilterByApiManifest))
73-
{
74-
using(var fileStream = await GetStream(options.FilterOptions.FilterByApiManifest, logger, cancellationToken)) {
75-
apiManifest = ApiManifestDocument.Load(JsonDocument.Parse(fileStream).RootElement);
76-
}
77-
options.OpenApi = apiManifest.ApiDependencies[0].ApiDescripionUrl;
70+
// If ApiManifest is provided, set the referenced OpenAPI document
71+
var apiDependency = await FindApiDependency(options.FilterOptions?.FilterByApiManifest, logger, cancellationToken);
72+
if (apiDependency != null) {
73+
options.OpenApi = apiDependency.ApiDescripionUrl;
7874
}
7975

8076
// If Postman Collection is provided, load it
8177
JsonDocument postmanCollection = null;
8278
if (!String.IsNullOrEmpty(options.FilterOptions?.FilterByCollection))
8379
{
84-
using (var collectionStream = await GetStream(options.FilterOptions.FilterByCollection, logger, cancellationToken)) {
80+
using (var collectionStream = await GetStream(options.FilterOptions.FilterByCollection, logger, cancellationToken))
81+
{
8582
postmanCollection = JsonDocument.Parse(collectionStream);
8683
}
8784
}
@@ -91,7 +88,7 @@ public static async Task TransformOpenApiDocument(HidiOptions options, ILogger l
9188

9289
if (options.FilterOptions != null)
9390
{
94-
document = ApplyFilters(options, logger, apiManifest, postmanCollection, document, cancellationToken);
91+
document = ApplyFilters(options, logger, apiDependency, postmanCollection, document, cancellationToken);
9592
}
9693

9794
var languageFormat = options.SettingsConfig?.GetSection("LanguageFormat")?.Value;
@@ -118,20 +115,50 @@ public static async Task TransformOpenApiDocument(HidiOptions options, ILogger l
118115
}
119116
}
120117

121-
private static OpenApiDocument ApplyFilters(HidiOptions options, ILogger logger, ApiManifestDocument apiManifest, JsonDocument postmanCollection, OpenApiDocument document, CancellationToken cancellationToken)
118+
private static async Task<ApiDependency> FindApiDependency(string apiManifestPath, ILogger logger, CancellationToken cancellationToken)
119+
{
120+
ApiDependency apiDependency = null;
121+
// If API Manifest is provided, load it, use it get the OpenAPI path
122+
ApiManifestDocument apiManifest = null;
123+
if (!string.IsNullOrEmpty(apiManifestPath))
124+
{
125+
// Extract fragment identifier if passed as the name of the ApiDependency
126+
var apiManifestRef = apiManifestPath.Split('#');
127+
string apiDependencyName = null;
128+
if (apiManifestRef.Length > 1)
129+
{
130+
apiDependencyName = apiManifestRef[1];
131+
}
132+
using (var fileStream = await GetStream(apiManifestRef[0], logger, cancellationToken))
133+
{
134+
apiManifest = ApiManifestDocument.Load(JsonDocument.Parse(fileStream).RootElement);
135+
}
136+
if (apiDependencyName != null)
137+
{
138+
apiDependency = apiManifest.ApiDependencies[apiDependencyName];
139+
}
140+
else
141+
{
142+
apiDependency = apiManifest.ApiDependencies.First().Value;
143+
}
144+
}
145+
146+
return apiDependency;
147+
}
148+
149+
private static OpenApiDocument ApplyFilters(HidiOptions options, ILogger logger, ApiDependency apiDependency, JsonDocument postmanCollection, OpenApiDocument document, CancellationToken cancellationToken)
122150
{
123151
Dictionary<string, List<string>> requestUrls = null;
124-
if (apiManifest != null)
152+
if (apiDependency != null)
125153
{
126-
requestUrls = GetRequestUrlsFromManifest(apiManifest, document);
154+
requestUrls = GetRequestUrlsFromManifest(apiDependency, document);
127155
}
128156
else if (postmanCollection != null)
129157
{
130158
requestUrls = EnumerateJsonDocument(postmanCollection.RootElement, requestUrls);
131159
logger.LogTrace("Finished fetching the list of paths and Http methods defined in the Postman collection.");
132160
}
133161

134-
135162
logger.LogTrace("Creating predicate from filter options.");
136163
var predicate = FilterOpenApiDocument(options.FilterOptions.FilterByOperationIds,
137164
options.FilterOptions.FilterByTags,
@@ -253,15 +280,14 @@ private static async Task<OpenApiDocument> GetOpenApi(string openapi, string csd
253280
return predicate;
254281
}
255282

256-
private static Dictionary<string, List<string>> GetRequestUrlsFromManifest(ApiManifestDocument apiManifestDocument, OpenApiDocument document)
283+
private static Dictionary<string, List<string>> GetRequestUrlsFromManifest(ApiDependency apiDependency, OpenApiDocument document)
257284
{
258285
// Get the request URLs from the API Dependencies in the API manifest that have a baseURL that matches the server URL in the OpenAPI document
259286
var serversUrls = document.Servers.Select(s => s.Url);
260-
var requests = apiManifestDocument.ApiDependencies
261-
.Where(a => serversUrls.Any(s => s == a.BaseUrl))
262-
.SelectMany(ad => ad.Requests.Where(r => r.Exclude == false)
263-
.Select(r=> new { BaseUrl=ad.BaseUrl, UriTemplate= r.UriTemplate, Method=r.Method } ))
264-
.GroupBy(r => r.BaseUrl.TrimEnd('/') + r.UriTemplate) // The OpenApiFilterService expects non-relative URLs.
287+
var requests = apiDependency
288+
.Requests.Where(r => r.Exclude == false)
289+
.Select(r=> new { UriTemplate= r.UriTemplate, Method=r.Method } )
290+
.GroupBy(r => r.UriTemplate)
265291
.ToDictionary(g => g.Key, g => g.Select(r => r.Method).ToList());
266292
// This makes the assumption that the UriTemplate in the ApiManifest matches exactly the UriTemplate in the OpenAPI document
267293
// This does not need to be the case. The URI template in the API manifest could map to a set of OpenAPI paths.

src/Microsoft.OpenApi.Hidi/readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ This command accepts the following parameters:
8080
• --filterByOperationIds(-op) - Slice document based on OperationId(s) provided. Accepts a comma delimited list of operation ids.
8181
• --filterByTags(-t) - Slice document based on tag(s) provided. Accepts a comma delimited list of tags.
8282
• --filterByCollection(-c) - Slices the OpenAPI document based on the Postman Collection file generated by Resource Explorer
83+
• --filterByManifest (-m) - Slices the OpenAPI document based on the requests defined in the API Manifest file referenced by the provided URI. For API manifests with multiple API Dependenties, use a fragment identifier to select the desired one. e.g ./apimanifest.json#example
8384

8485
**Examples:**
8586

@@ -95,7 +96,7 @@ This command accepts the following parameters:
9596
4. CSDL Filtering by EntitySets and Singletons
9697
hidi transform -cs dataverse.csdl --csdlFilter "appointments,opportunities" -o appointmentsAndOpportunities.yaml -ll trace
9798
98-
Run transform -h to see all the available usage options.
99+
Run transform -h to see all the available usage options.
99100

100101
### Show
101102

src/Microsoft.OpenApi/Services/OpenApiFilterService.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Linq;
78
using System.Text.RegularExpressions;
89
using Microsoft.OpenApi.Models;
@@ -79,12 +80,13 @@ public static class OpenApiFilterService
7980
foreach (var url in requestUrls)
8081
{
8182
var serverList = source.Servers;
83+
8284
var path = ExtractPath(url.Key, serverList);
8385

8486
var openApiOperations = GetOpenApiOperations(rootNode, path, apiVersion);
8587
if (openApiOperations == null)
8688
{
87-
Console.WriteLine($"The url {url.Key} could not be found in the OpenApi description");
89+
Debug.WriteLine($"The url {url.Key} could not be found in the OpenApi description");
8890
continue;
8991
}
9092

@@ -345,20 +347,16 @@ private static bool AddReferences(OpenApiComponents newComponents, OpenApiCompon
345347

346348
private static string ExtractPath(string url, IList<OpenApiServer> serverList)
347349
{
348-
var queryPath = string.Empty;
349-
foreach (var server in serverList)
350-
{
351-
var serverUrl = server.Url.TrimEnd('/');
352-
if (!url.Contains(serverUrl))
353-
{
354-
continue;
355-
}
356-
357-
var urlComponents = url.Split(new[] { serverUrl }, StringSplitOptions.None);
358-
queryPath = urlComponents[1];
350+
// if OpenAPI has servers, then see if the url matches one of them
351+
var baseUrl = serverList.Select(s => s.Url.TrimEnd('/')).Where(c => url.Contains(c)).FirstOrDefault();
352+
353+
if (baseUrl == null) {
354+
// if no match, then extract path from either the absolute or relative url
355+
return new Uri(new Uri("http://localhost/"), url).GetComponents(UriComponents.Path | UriComponents.KeepDelimiter, UriFormat.Unescaped);
356+
} else {
357+
// if match, then extract path from the url relative to the matched server
358+
return url.Split(new[] { baseUrl }, StringSplitOptions.None)[1];
359359
}
360-
361-
return queryPath;
362360
}
363361
}
364362
}

test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiFilterServiceTests.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
using Moq;
1212
using Xunit;
1313

14-
namespace Microsoft.OpenApi.Tests.Services
14+
namespace Microsoft.OpenApi.Hidi.Tests
1515
{
1616
public class OpenApiFilterServiceTests
1717
{
@@ -69,6 +69,43 @@ public void ReturnFilteredOpenApiDocumentBasedOnPostmanCollection()
6969
Assert.Equal(3, subsetOpenApiDocument.Paths.Count);
7070
}
7171

72+
// Create predicate based RequestUrls
73+
[Fact]
74+
public void TestPredicateFiltersUsingRelativeRequestUrls()
75+
{
76+
var openApiDocument = new OpenApiDocument()
77+
{
78+
Info = new() { Title = "Test", Version = "1.0" },
79+
Servers = new List<OpenApiServer>() { new() { Url = "https://localhost/" } },
80+
Paths = new()
81+
{
82+
{"/foo", new() {
83+
Operations = new Dictionary<OperationType, OpenApiOperation>() {
84+
{ OperationType.Get, new() },
85+
{ OperationType.Patch, new() },
86+
{ OperationType.Post, new() }
87+
}
88+
}
89+
}
90+
}
91+
};
92+
93+
// Given a set of RequestUrls
94+
var requestUrls = new Dictionary<string, List<string>>
95+
{
96+
{"/foo", new List<string> {"GET","POST"}}
97+
};
98+
99+
// When
100+
var predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source: openApiDocument);
101+
102+
// Then
103+
Assert.True(predicate("/foo",OperationType.Get,null));
104+
Assert.True(predicate("/foo",OperationType.Post,null));
105+
Assert.False(predicate("/foo",OperationType.Patch,null));
106+
}
107+
108+
72109
[Fact]
73110
public void ShouldParseNestedPostmanCollection()
74111
{

test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiServiceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
using Microsoft.OpenApi.Services;
1414
using Xunit;
1515

16-
namespace Microsoft.OpenApi.Tests.Services
16+
namespace Microsoft.OpenApi.Hidi.Tests
1717
{
1818
public class OpenApiServiceTests
1919
{

test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/exampleapimanifest.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
"name": "Alice",
44
"contactEmail": "[email protected]"
55
},
6-
"apiDependencies": [
7-
{
6+
"apiDependencies": {
7+
"moostodon" : {
88
"apiDescripionUrl": "https://raw.githubusercontent.com/APIPatterns/Moostodon/main/spec/tsp-output/%40typespec/openapi3/openapi.yaml",
9-
"baseUrl": "https://mastodon.example/",
109
"auth": {
1110
"clientIdentifier": "some-uuid-here",
1211
"access": [ "resourceA.ReadWrite",
@@ -23,5 +22,5 @@
2322
}
2423
]
2524
}
26-
]
25+
}
2726
}

0 commit comments

Comments
 (0)