Skip to content

Commit 86262b6

Browse files
committed
Added basic version of filter by manifest
1 parent 8bda200 commit 86262b6

File tree

10 files changed

+147
-27
lines changed

10 files changed

+147
-27
lines changed

.vscode/launch.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,36 @@
55
// Use IntelliSense to find out which attributes exist for C# debugging
66
// Use hover for the description of the existing attributes
77
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
8-
"name": ".NET Core Launch (console)",
8+
"name": "Launch Hidi",
99
"type": "coreclr",
1010
"request": "launch",
1111
"preLaunchTask": "build",
1212
// If you have changed target frameworks, make sure to update the program path.
1313
"program": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi/bin/Debug/net7.0/Microsoft.OpenApi.Hidi.dll",
14-
"args": [],
14+
"args": ["transform",
15+
"-m","C:\\Users\\darrmi\\src\\github\\microsoft\\openapi.net\\test\\Microsoft.OpenApi.Hidi.Tests\\UtilityFiles\\exampleapimanifest.json",
16+
"-o","minimoose.yaml"],
1517
"cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi",
1618
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
1719
"console": "internalConsole",
1820
"stopAtEntry": false
1921
},
22+
{
23+
// Use IntelliSense to find out which attributes exist for C# debugging
24+
// Use hover for the description of the existing attributes
25+
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
26+
"name": "Launch Workbench",
27+
"type": "coreclr",
28+
"request": "launch",
29+
"preLaunchTask": "build",
30+
// If you have changed target frameworks, make sure to update the program path.
31+
"program": "${workspaceFolder}/src/Microsoft.OpenApi.WorkBench/bin/Debug/net7.0-windows/Microsoft.OpenApi.Workbench.exe",
32+
"args": [],
33+
"cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Workbench",
34+
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
35+
"console": "internalConsole",
36+
"stopAtEntry": false
37+
},
2038
{
2139
"name": ".NET Core Attach",
2240
"type": "coreclr",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
<ItemGroup>
5252
<ProjectReference Include="..\Microsoft.OpenApi.Readers\Microsoft.OpenApi.Readers.csproj" />
5353
<ProjectReference Include="..\Microsoft.OpenApi\Microsoft.OpenApi.csproj" />
54+
<ProjectReference Include="..\..\..\OpenApi.ApiManifest\src\lib\apimanifest.csproj" />
5455
</ItemGroup>
5556

5657
<!-- Make internals available for Unit Testing -->

src/Microsoft.OpenApi.Hidi/OpenApiService.cs

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
using Microsoft.Extensions.Configuration;
3030
using Microsoft.OpenApi.Hidi.Utilities;
3131
using Microsoft.OpenApi.Hidi.Formatters;
32+
using Microsoft.OpenApi.ApiManifest;
33+
using System.Linq;
3234

3335
namespace Microsoft.OpenApi.Hidi
3436
{
@@ -39,7 +41,7 @@ internal class OpenApiService
3941
/// </summary>
4042
public static async Task TransformOpenApiDocument(HidiOptions options, ILogger logger, CancellationToken cancellationToken)
4143
{
42-
if (string.IsNullOrEmpty(options.OpenApi) && string.IsNullOrEmpty(options.Csdl))
44+
if (string.IsNullOrEmpty(options.OpenApi) && string.IsNullOrEmpty(options.Csdl) && string.IsNullOrEmpty(options.FilterOptions?.FilterByApiManifest))
4345
{
4446
throw new ArgumentException("Please input a file path or URL");
4547
}
@@ -65,11 +67,34 @@ public static async Task TransformOpenApiDocument(HidiOptions options, ILogger l
6567
OpenApiFormat openApiFormat = options.OpenApiFormat ?? (!string.IsNullOrEmpty(options.OpenApi) ? GetOpenApiFormat(options.OpenApi, logger) : OpenApiFormat.Yaml);
6668
OpenApiSpecVersion openApiVersion = options.Version != null ? TryParseOpenApiSpecVersion(options.Version) : OpenApiSpecVersion.OpenApi3_0;
6769

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;
78+
}
79+
80+
// If Postman Collection is provided, load it
81+
JsonDocument postmanCollection = null;
82+
if (!String.IsNullOrEmpty(options.FilterOptions?.FilterByCollection))
83+
{
84+
using (var collectionStream = await GetStream(options.FilterOptions.FilterByCollection, logger, cancellationToken)) {
85+
postmanCollection = JsonDocument.Parse(collectionStream);
86+
}
87+
}
88+
89+
// Load OpenAPI document
6890
OpenApiDocument document = await GetOpenApi(options.OpenApi, options.Csdl, options.CsdlFilter, options.SettingsConfig, options.InlineExternal, logger, cancellationToken, options.MetadataVersion);
91+
6992
if (options.FilterOptions != null)
70-
document = await FilterOpenApiDocument(options.FilterOptions.FilterByOperationIds, options.FilterOptions.FilterByTags, options.FilterOptions.FilterByCollection, document, logger, cancellationToken);
93+
{
94+
document = ApplyFilters(options, logger, apiManifest, postmanCollection, document, cancellationToken);
95+
}
7196

72-
var languageFormat = options.SettingsConfig.GetSection("LanguageFormat").Value;
97+
var languageFormat = options.SettingsConfig?.GetSection("LanguageFormat")?.Value;
7398
if (Extensions.StringExtensions.IsEquals(languageFormat, "PowerShell"))
7499
{
75100
// PowerShell Walker.
@@ -93,6 +118,38 @@ public static async Task TransformOpenApiDocument(HidiOptions options, ILogger l
93118
}
94119
}
95120

121+
private static OpenApiDocument ApplyFilters(HidiOptions options, ILogger logger, ApiManifestDocument apiManifest, JsonDocument postmanCollection, OpenApiDocument document, CancellationToken cancellationToken)
122+
{
123+
Dictionary<string, List<string>> requestUrls = null;
124+
if (apiManifest != null)
125+
{
126+
requestUrls = GetRequestUrlsFromManifest(apiManifest, document);
127+
}
128+
else if (postmanCollection != null)
129+
{
130+
requestUrls = EnumerateJsonDocument(postmanCollection.RootElement, requestUrls);
131+
logger.LogTrace("Finished fetching the list of paths and Http methods defined in the Postman collection.");
132+
}
133+
134+
135+
logger.LogTrace("Creating predicate from filter options.");
136+
var predicate = FilterOpenApiDocument(options.FilterOptions.FilterByOperationIds,
137+
options.FilterOptions.FilterByTags,
138+
requestUrls,
139+
document,
140+
logger, cancellationToken);
141+
if (predicate != null)
142+
{
143+
var stopwatch = new Stopwatch();
144+
stopwatch.Start();
145+
document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
146+
stopwatch.Stop();
147+
logger.LogTrace("{timestamp}ms: Creating filtered OpenApi document with {paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count);
148+
}
149+
150+
return document;
151+
}
152+
96153
private static void WriteOpenApi(FileInfo output, bool terseOutput, bool inlineLocal, bool inlineExternal, OpenApiFormat openApiFormat, OpenApiSpecVersion openApiVersion, OpenApiDocument document, ILogger logger)
97154
{
98155
using (logger.BeginScope("Output"))
@@ -163,12 +220,12 @@ private static async Task<OpenApiDocument> GetOpenApi(string openapi, string csd
163220
return document;
164221
}
165222

166-
private static async Task<OpenApiDocument> FilterOpenApiDocument(string filterbyoperationids, string filterbytags, string filterbycollection, OpenApiDocument document, ILogger logger, CancellationToken cancellationToken)
223+
private static Func<string, OperationType?, OpenApiOperation, bool> FilterOpenApiDocument(string filterbyoperationids, string filterbytags, Dictionary<string, List<string>> requestUrls, OpenApiDocument document, ILogger logger, CancellationToken cancellationToken)
167224
{
168-
using (logger.BeginScope("Filter"))
169-
{
170-
Func<string, OperationType?, OpenApiOperation, bool> predicate = null;
225+
Func<string, OperationType?, OpenApiOperation, bool> predicate = null;
171226

227+
using (logger.BeginScope("Create Filter"))
228+
{
172229
// Check if filter options are provided, then slice the OpenAPI document
173230
if (!string.IsNullOrEmpty(filterbyoperationids) && !string.IsNullOrEmpty(filterbytags))
174231
{
@@ -186,25 +243,30 @@ private static async Task<OpenApiDocument> FilterOpenApiDocument(string filterby
186243
predicate = OpenApiFilterService.CreatePredicate(tags: filterbytags);
187244

188245
}
189-
if (!string.IsNullOrEmpty(filterbycollection))
246+
if (requestUrls != null)
190247
{
191-
var fileStream = await GetStream(filterbycollection, logger, cancellationToken);
192-
var requestUrls = ParseJsonCollectionFile(fileStream, logger);
193-
194248
logger.LogTrace("Creating predicate based on the paths and Http methods defined in the Postman collection.");
195249
predicate = OpenApiFilterService.CreatePredicate(requestUrls: requestUrls, source: document);
196250
}
197-
if (predicate != null)
198-
{
199-
var stopwatch = new Stopwatch();
200-
stopwatch.Start();
201-
document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
202-
stopwatch.Stop();
203-
logger.LogTrace("{timestamp}ms: Creating filtered OpenApi document with {paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count);
204-
}
205251
}
206252

207-
return document;
253+
return predicate;
254+
}
255+
256+
private static Dictionary<string, List<string>> GetRequestUrlsFromManifest(ApiManifestDocument apiManifestDocument, OpenApiDocument document)
257+
{
258+
// 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
259+
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.
265+
.ToDictionary(g => g.Key, g => g.Select(r => r.Method).ToList());
266+
// This makes the assumption that the UriTemplate in the ApiManifest matches exactly the UriTemplate in the OpenAPI document
267+
// This does not need to be the case. The URI template in the API manifest could map to a set of OpenAPI paths.
268+
// Additional logic will be required to handle this scenario. I sugggest we build this into the OpenAPI.Net library at some point.
269+
return requests;
208270
}
209271

210272
private static XslCompiledTransform GetFilterTransform()

src/Microsoft.OpenApi.Hidi/Options/CommandOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal class CommandOptions
2222
public Option<string> FilterByOperationIdsOption = new("--filter-by-operationids", "Filters OpenApiDocument by comma delimited list of OperationId(s) provided");
2323
public Option<string> FilterByTagsOption = new("--filter-by-tags", "Filters OpenApiDocument by comma delimited list of Tag(s) provided. Also accepts a single regex.");
2424
public Option<string> FilterByCollectionOption = new("--filter-by-collection", "Filters OpenApiDocument by Postman collection provided. Provide path to collection file.");
25+
public Option<string> FilterByApiManifestOption = new("--filter-by-manifest", "Filters OpenApiDocument by API Manifest provided. Provide path to API Manifest file.");
2526
public Option<bool> InlineLocalOption = new("--inline-local", "Inline local $ref instances");
2627
public Option<bool> InlineExternalOption = new("--inline-external", "Inline external $ref instances");
2728

@@ -41,6 +42,7 @@ public CommandOptions()
4142
FilterByOperationIdsOption.AddAlias("--op");
4243
FilterByTagsOption.AddAlias("--t");
4344
FilterByCollectionOption.AddAlias("-c");
45+
FilterByApiManifestOption.AddAlias("-m");
4446
InlineLocalOption.AddAlias("--il");
4547
InlineExternalOption.AddAlias("--ie");
4648
}
@@ -63,6 +65,7 @@ public IReadOnlyList<Option> GetAllCommandOptions()
6365
FilterByOperationIdsOption,
6466
FilterByTagsOption,
6567
FilterByCollectionOption,
68+
FilterByApiManifestOption,
6669
InlineLocalOption,
6770
InlineExternalOption
6871
};

src/Microsoft.OpenApi.Hidi/Options/FilterOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ internal class FilterOptions
88
public string FilterByOperationIds { get; internal set; }
99
public string FilterByTags { get; internal set; }
1010
public string FilterByCollection { get; internal set; }
11+
public string FilterByApiManifest { get; internal set; }
1112
}
1213
}

src/Microsoft.OpenApi.Hidi/Options/HidiOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ private void ParseHidiOptions(ParseResult parseResult, CommandOptions options)
5555
{
5656
FilterByOperationIds = parseResult.GetValueForOption(options.FilterByOperationIdsOption),
5757
FilterByTags = parseResult.GetValueForOption(options.FilterByTagsOption),
58-
FilterByCollection = parseResult.GetValueForOption(options.FilterByCollectionOption)
58+
FilterByCollection = parseResult.GetValueForOption(options.FilterByCollectionOption),
59+
FilterByApiManifest = parseResult.GetValueForOption(options.FilterByApiManifestOption)
5960
};
6061
}
6162
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
using System.Runtime.CompilerServices;
3+
4+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

src/Microsoft.OpenApi/Services/OpenApiFilterService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ public static class OpenApiFilterService
2222
/// <param name="requestUrls">A dictionary of requests from a postman collection.</param>
2323
/// <param name="source">The input OpenAPI document.</param>
2424
/// <returns>A predicate.</returns>
25-
public static Func<string, OperationType?, OpenApiOperation, bool> CreatePredicate(string operationIds = null,
26-
string tags = null, Dictionary<string, List<string>> requestUrls = null, OpenApiDocument source = null)
25+
public static Func<string, OperationType?, OpenApiOperation, bool> CreatePredicate(
26+
string operationIds = null,
27+
string tags = null,
28+
Dictionary<string, List<string>> requestUrls = null,
29+
OpenApiDocument source = null)
2730
{
2831
Func<string, OperationType?, OpenApiOperation, bool> predicate;
2932

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ public void InvokeTransformCommand()
282282
var handler = rootCommand.Subcommands.Where(c => c.Name == "transform").First().Handler;
283283
var context = new InvocationContext(parseResult);
284284

285-
handler.Invoke(context);
285+
handler!.Invoke(context);
286286

287287
var output = File.ReadAllText("sample.json");
288288
Assert.NotEmpty(output);
@@ -298,7 +298,7 @@ public void InvokeShowCommand()
298298
var handler = rootCommand.Subcommands.Where(c => c.Name == "show").First().Handler;
299299
var context = new InvocationContext(parseResult);
300300

301-
handler.Invoke(context);
301+
handler!.Invoke(context);
302302

303303
var output = File.ReadAllText("sample.md");
304304
Assert.Contains("graph LR", output);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"publisher": {
3+
"name": "Alice",
4+
"contactEmail": "[email protected]"
5+
},
6+
"apiDependencies": [
7+
{
8+
"apiDescripionUrl": "https://raw.githubusercontent.com/APIPatterns/Moostodon/main/spec/tsp-output/%40typespec/openapi3/openapi.yaml",
9+
"baseUrl": "https://mastodon.example/",
10+
"auth": {
11+
"clientIdentifier": "some-uuid-here",
12+
"access": [ "resourceA.ReadWrite",
13+
"resourceB.ReadWrite","resourceB.Read"]
14+
},
15+
"requests": [
16+
{
17+
"method": "GET",
18+
"uriTemplate": "/api/v1/accounts/search"
19+
},
20+
{
21+
"method": "GET",
22+
"uriTemplate": "/api/v1/accounts/{id}"
23+
}
24+
]
25+
}
26+
]
27+
}

0 commit comments

Comments
 (0)