diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml
index 5649e50c5..b6944af2f 100644
--- a/.azure-pipelines/ci-build.yml
+++ b/.azure-pipelines/ci-build.yml
@@ -30,7 +30,7 @@ steps:
inputs:
testAssemblyVer2: |
**\*.Tests.dll
-
+
vsTestVersion: 16.0
codeCoverageEnabled: true
@@ -94,6 +94,13 @@ steps:
configuration: Release
msbuildArguments: '/t:pack /p:PackageOutputPath=$(Build.ArtifactStagingDirectory) /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg'
+- task: MSBuild@1
+ displayName: 'Pack OpenApi Hidi'
+ inputs:
+ solution: src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
+ configuration: Release
+ msbuildArguments: '/t:pack /p:PackageOutputPath=$(Build.ArtifactStagingDirectory) /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg'
+
- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1
displayName: 'ESRP CodeSigning Nuget Packages'
inputs:
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 4ab9ed7c3..7afeeebed 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -62,7 +62,7 @@ jobs:
$projectsArray = @(
'.\src\Microsoft.OpenApi\Microsoft.OpenApi.csproj',
'.\src\Microsoft.OpenApi.Readers\Microsoft.OpenApi.Readers.csproj',
- '.\src\Microsoft.OpenApi.Tool\Microsoft.OpenApi.Tool.csproj'
+ '.\src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj'
)
$gitNewVersion = if ("${{ steps.tag_generator.outputs.new_version }}") {"${{ steps.tag_generator.outputs.new_version }}"} else {$null}
$projectCurrentVersion = ([xml](Get-Content .\src\Microsoft.OpenApi\Microsoft.OpenApi.csproj)).Project.PropertyGroup.Version
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 95c813772..999e48f53 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -33,7 +33,7 @@ jobs:
$projectsArray = @(
'.\src\Microsoft.OpenApi\Microsoft.OpenApi.csproj',
'.\src\Microsoft.OpenApi.Readers\Microsoft.OpenApi.Readers.csproj',
- '.\src\Microsoft.OpenApi.Tool\Microsoft.OpenApi.Tool.csproj'
+ '.\src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj'
)
$projectsArray | ForEach-Object {
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 0d20a9b46..c26bf0c9f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,9 +10,9 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
- "program": "${workspaceFolder}/src/Microsoft.OpenApi.Tool/bin/Debug/netcoreapp3.1/Microsoft.OpenApi.Tool.dll",
+ "program": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi/bin/Debug/netcoreapp3.1/Microsoft.OpenApi.Hidi.dll",
"args": [],
- "cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Tool",
+ "cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
diff --git a/Microsoft.OpenApi.sln b/Microsoft.OpenApi.sln
index e64ff3a24..dc489bff8 100644
--- a/Microsoft.OpenApi.sln
+++ b/Microsoft.OpenApi.sln
@@ -26,7 +26,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6357D7FD-2
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.SmokeTests", "test\Microsoft.OpenApi.SmokeTests\Microsoft.OpenApi.SmokeTests.csproj", "{AD79B61D-88CF-497C-9ED5-41AE3867C5AC}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.Tool", "src\Microsoft.OpenApi.Tool\Microsoft.OpenApi.Tool.csproj", "{254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.Hidi", "src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj", "{254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/build.cmd b/build.cmd
index b3612c9ed..43cc95956 100644
--- a/build.cmd
+++ b/build.cmd
@@ -1,21 +1,21 @@
@echo off
-Echo Building Microsoft.OpenApi
+Echo Building Microsoft.OpenApi
-SET PROJ=%~dp0src\Microsoft.OpenApi\Microsoft.OpenApi.csproj
+SET PROJ=%~dp0src\Microsoft.OpenApi\Microsoft.OpenApi.csproj
dotnet msbuild %PROJ% /t:restore /p:Configuration=Release
dotnet msbuild %PROJ% /t:build /p:Configuration=Release
dotnet msbuild %PROJ% /t:pack /p:Configuration=Release;PackageOutputPath=%~dp0artifacts
Echo Building Microsoft.OpenApi.Readers
-SET PROJ=%~dp0src\Microsoft.OpenApi.Readers\Microsoft.OpenApi.Readers.csproj
+SET PROJ=%~dp0src\Microsoft.OpenApi.Readers\Microsoft.OpenApi.Readers.csproj
dotnet msbuild %PROJ% /t:restore /p:Configuration=Release
dotnet msbuild %PROJ% /t:build /p:Configuration=Release
dotnet msbuild %PROJ% /t:pack /p:Configuration=Release;PackageOutputPath=%~dp0artifacts
-Echo Building Microsoft.OpenApi.Tool
+Echo Building Microsoft.OpenApi.Hidi
-SET PROJ=%~dp0src\Microsoft.OpenApi.Tool\Microsoft.OpenApi.Tool.csproj
+SET PROJ=%~dp0src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj
dotnet msbuild %PROJ% /t:restore /p:Configuration=Release
dotnet msbuild %PROJ% /t:build /p:Configuration=Release
dotnet msbuild %PROJ% /t:pack /p:Configuration=Release;PackageOutputPath=%~dp0artifacts
diff --git a/install-tool.ps1 b/install-tool.ps1
index 0e6521110..0b4615c67 100644
--- a/install-tool.ps1
+++ b/install-tool.ps1
@@ -1,7 +1,7 @@
-$latest = Get-ChildItem .\artifacts\ Microsoft.OpenApi.Tool* | select-object -Last 1
+$latest = Get-ChildItem .\artifacts\Microsoft.OpenApi.Hidi* | select-object -Last 1
$version = $latest.Name.Split(".")[3..5] | join-string -Separator "."
-if (Test-Path -Path ./artifacts/openapi-parser.exe) {
- dotnet tool uninstall --tool-path artifacts Microsoft.OpenApi.Tool
+if (Test-Path -Path ./artifacts/hidi.exe) {
+ dotnet tool uninstall --tool-path artifacts Microsoft.OpenApi.Hidi
}
-dotnet tool install --tool-path artifacts --add-source .\artifacts\ --version $version Microsoft.OpenApi.Tool
\ No newline at end of file
+dotnet tool install --tool-path artifacts --add-source .\artifacts\ --version $version Microsoft.OpenApi.Hidi
\ No newline at end of file
diff --git a/src/Microsoft.OpenApi.Tool/Microsoft.OpenApi.Tool.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
similarity index 88%
rename from src/Microsoft.OpenApi.Tool/Microsoft.OpenApi.Tool.csproj
rename to src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
index 40e46f1a4..f0d7943e7 100644
--- a/src/Microsoft.OpenApi.Tool/Microsoft.OpenApi.Tool.csproj
+++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
@@ -4,9 +4,9 @@
Exe
netcoreapp3.1
true
- openapi-parser
+ hidi
./../../artifacts
- 1.3.0-preview
+ 0.5.0-preview
diff --git a/src/Microsoft.OpenApi.Tool/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs
similarity index 54%
rename from src/Microsoft.OpenApi.Tool/OpenApiService.cs
rename to src/Microsoft.OpenApi.Hidi/OpenApiService.cs
index c52c08941..486666568 100644
--- a/src/Microsoft.OpenApi.Tool/OpenApiService.cs
+++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs
@@ -1,5 +1,7 @@
-using System;
-using System.Collections.Generic;
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System;
using System.IO;
using System.Linq;
using System.Net;
@@ -12,7 +14,7 @@
using Microsoft.OpenApi.Validations;
using Microsoft.OpenApi.Writers;
-namespace Microsoft.OpenApi.Tool
+namespace Microsoft.OpenApi.Hidi
{
static class OpenApiService
{
@@ -21,29 +23,55 @@ public static void ProcessOpenApiDocument(
FileInfo output,
OpenApiSpecVersion version,
OpenApiFormat format,
+ string filterByOperationIds,
+ string filterByTags,
bool inline,
bool resolveExternal)
{
- if (input == null)
+ if (string.IsNullOrEmpty(input))
{
- throw new ArgumentNullException("input");
+ throw new ArgumentNullException(nameof(input));
+ }
+ if(output == null)
+ {
+ throw new ArgumentException(nameof(output));
+ }
+ if (output.Exists)
+ {
+ throw new IOException("The file you're writing to already exists. Please input a new output path.");
}
var stream = GetStream(input);
-
- OpenApiDocument document;
-
var result = new OpenApiStreamReader(new OpenApiReaderSettings
{
- ReferenceResolution = resolveExternal == true ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
+ ReferenceResolution = resolveExternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
}
).ReadAsync(stream).GetAwaiter().GetResult();
+ OpenApiDocument document;
document = result.OpenApiDocument;
+
+ // Check if filter options are provided, then execute
+ if (!string.IsNullOrEmpty(filterByOperationIds) && !string.IsNullOrEmpty(filterByTags))
+ {
+ throw new InvalidOperationException("Cannot filter by operationIds and tags at the same time.");
+ }
+
+ if (!string.IsNullOrEmpty(filterByOperationIds))
+ {
+ var predicate = OpenApiFilterService.CreatePredicate(operationIds: filterByOperationIds);
+ document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
+ }
+ if (!string.IsNullOrEmpty(filterByTags))
+ {
+ var predicate = OpenApiFilterService.CreatePredicate(tags: filterByTags);
+ document = OpenApiFilterService.CreateFilteredDocument(document, predicate);
+ }
+
var context = result.OpenApiDiagnostic;
- if (context.Errors.Count != 0)
+ if (context.Errors.Count > 0)
{
var errorReport = new StringBuilder();
@@ -52,43 +80,26 @@ public static void ProcessOpenApiDocument(
errorReport.AppendLine(error.ToString());
}
- throw new ArgumentException(String.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray()));
+ throw new ArgumentException(string.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray()));
}
- using (var outputStream = output?.Create())
- {
- TextWriter textWriter;
+ using var outputStream = output?.Create();
- if (outputStream != null)
- {
- textWriter = new StreamWriter(outputStream);
- }
- else
- {
- textWriter = Console.Out;
- }
+ var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out;
- var settings = new OpenApiWriterSettings()
- {
- ReferenceInline = inline == true ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences
- };
- IOpenApiWriter writer;
- switch (format)
- {
- case OpenApiFormat.Json:
- writer = new OpenApiJsonWriter(textWriter, settings);
- break;
- case OpenApiFormat.Yaml:
- writer = new OpenApiYamlWriter(textWriter, settings);
- break;
- default:
- throw new ArgumentException("Unknown format");
- }
-
- document.Serialize(writer, version);
+ var settings = new OpenApiWriterSettings()
+ {
+ ReferenceInline = inline ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences
+ };
+ IOpenApiWriter writer = format switch
+ {
+ OpenApiFormat.Json => new OpenApiJsonWriter(textWriter, settings),
+ OpenApiFormat.Yaml => new OpenApiYamlWriter(textWriter, settings),
+ _ => throw new ArgumentException("Unknown format"),
+ };
+ document.Serialize(writer, version);
- textWriter.Flush();
- }
+ textWriter.Flush();
}
private static Stream GetStream(string input)
@@ -127,7 +138,6 @@ internal static void ValidateOpenApiDocument(string input)
document = new OpenApiStreamReader(new OpenApiReaderSettings
{
- //ReferenceResolution = resolveExternal == true ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
}
).Read(stream, out var context);
diff --git a/src/Microsoft.OpenApi.Tool/Program.cs b/src/Microsoft.OpenApi.Hidi/Program.cs
similarity index 56%
rename from src/Microsoft.OpenApi.Tool/Program.cs
rename to src/Microsoft.OpenApi.Hidi/Program.cs
index 446e2829a..533878a0d 100644
--- a/src/Microsoft.OpenApi.Tool/Program.cs
+++ b/src/Microsoft.OpenApi.Hidi/Program.cs
@@ -1,34 +1,15 @@
-using System;
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading.Tasks;
-using Microsoft.OpenApi;
-namespace Microsoft.OpenApi.Tool
+namespace Microsoft.OpenApi.Hidi
{
- class Program
+ static class Program
{
- static async Task OldMain(string[] args)
- {
-
- var command = new RootCommand
- {
- new Option("--input", "Input OpenAPI description file path or URL", typeof(string) ),
- new Option("--output","Output OpenAPI description file", typeof(FileInfo), arity: ArgumentArity.ZeroOrOne),
- new Option("--version", "OpenAPI specification version", typeof(OpenApiSpecVersion)),
- new Option("--format", "File format",typeof(OpenApiFormat) ),
- new Option("--inline", "Inline $ref instances", typeof(bool) ),
- new Option("--resolveExternal","Resolve external $refs", typeof(bool))
- };
-
- command.Handler = CommandHandler.Create(
- OpenApiService.ProcessOpenApiDocument);
-
- // Parse the incoming args and invoke the handler
- return await command.InvokeAsync(args);
- }
-
static async Task Main(string[] args)
{
var rootCommand = new RootCommand() {
@@ -47,9 +28,11 @@ static async Task Main(string[] args)
new Option("--version", "OpenAPI specification version", typeof(OpenApiSpecVersion)),
new Option("--format", "File format",typeof(OpenApiFormat) ),
new Option("--inline", "Inline $ref instances", typeof(bool) ),
- new Option("--resolveExternal","Resolve external $refs", typeof(bool))
+ new Option("--resolveExternal","Resolve external $refs", typeof(bool)),
+ new Option("--filterByOperationIds", "Filters OpenApiDocument by OperationId(s) provided", typeof(string)),
+ new Option("--filterByTags", "Filters OpenApiDocument by Tag(s) provided", typeof(string))
};
- transformCommand.Handler = CommandHandler.Create(
+ transformCommand.Handler = CommandHandler.Create(
OpenApiService.ProcessOpenApiDocument);
rootCommand.Add(transformCommand);
diff --git a/src/Microsoft.OpenApi.Tool/StatsVisitor.cs b/src/Microsoft.OpenApi.Hidi/StatsVisitor.cs
similarity index 95%
rename from src/Microsoft.OpenApi.Tool/StatsVisitor.cs
rename to src/Microsoft.OpenApi.Hidi/StatsVisitor.cs
index 3c633d860..b05b0de7c 100644
--- a/src/Microsoft.OpenApi.Tool/StatsVisitor.cs
+++ b/src/Microsoft.OpenApi.Hidi/StatsVisitor.cs
@@ -3,13 +3,10 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Services;
-namespace Microsoft.OpenApi.Tool
+namespace Microsoft.OpenApi.Hidi
{
internal class StatsVisitor : OpenApiVisitorBase
{
diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs
index da178ae86..732708459 100644
--- a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs
+++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs
@@ -1,5 +1,5 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT license.
+// Licensed under the MIT license.
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
@@ -61,11 +61,17 @@ public class OpenApiReaderSettings
public Uri BaseUrl { get; set; }
///
- /// Function used to provide an alternative loader for accessing external references.
+ /// Function used to provide an alternative loader for accessing external references.
///
///
/// Default loader will attempt to dereference http(s) urls and file urls.
///
public IStreamLoader CustomExternalLoader { get; set; }
+
+ ///
+ /// Whether to leave the object open after reading
+ /// from an object.
+ ///
+ public bool LeaveStreamOpen { get; set; }
}
}
diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs
index cab5d1a83..cccf06a68 100644
--- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs
+++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs
@@ -1,5 +1,5 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT license.
+// Licensed under the MIT license.
using System.IO;
using System.Threading.Tasks;
@@ -29,14 +29,18 @@ public OpenApiStreamReader(OpenApiReaderSettings settings = null)
/// Reads the stream input and parses it into an Open API document.
///
/// Stream containing OpenAPI description to parse.
- /// Returns diagnostic object containing errors detected during parsing
- /// Instance of newly created OpenApiDocument
+ /// Returns diagnostic object containing errors detected during parsing.
+ /// Instance of newly created OpenApiDocument.
public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic)
{
- using (var reader = new StreamReader(input))
+ var reader = new StreamReader(input);
+ var result = new OpenApiTextReaderReader(_settings).Read(reader, out diagnostic);
+ if (!_settings.LeaveStreamOpen)
{
- return new OpenApiTextReaderReader(_settings).Read(reader, out diagnostic);
+ reader.Dispose();
}
+
+ return result;
}
///
@@ -50,8 +54,8 @@ public async Task ReadAsync(Stream input)
if (input is MemoryStream)
{
bufferedStream = (MemoryStream)input;
- }
- else
+ }
+ else
{
// Buffer stream so that OpenApiTextReaderReader can process it synchronously
// YamlDocument doesn't support async reading.
diff --git a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj
index d0ff2fbcd..9c36ab07c 100644
--- a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj
+++ b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj
@@ -1,6 +1,7 @@
netstandard2.0
+ 9.0
true
http://go.microsoft.com/fwlink/?LinkID=288890
https://github.com/Microsoft/OpenAPI.NET
@@ -36,7 +37,7 @@
-
+
diff --git a/src/Microsoft.OpenApi/Services/CopyReferences.cs b/src/Microsoft.OpenApi/Services/CopyReferences.cs
new file mode 100644
index 000000000..24dcfee25
--- /dev/null
+++ b/src/Microsoft.OpenApi/Services/CopyReferences.cs
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Collections.Generic;
+using Microsoft.OpenApi.Interfaces;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.OpenApi.Services
+{
+ internal class CopyReferences : OpenApiVisitorBase
+ {
+ private readonly OpenApiDocument _target;
+ public OpenApiComponents Components = new();
+
+ public CopyReferences(OpenApiDocument target)
+ {
+ _target = target;
+ }
+
+ ///
+ /// Visits IOpenApiReferenceable instances that are references and not in components.
+ ///
+ /// An IOpenApiReferenceable object.
+ public override void Visit(IOpenApiReferenceable referenceable)
+ {
+ switch (referenceable)
+ {
+ case OpenApiSchema schema:
+ EnsureComponentsExists();
+ EnsureSchemasExists();
+ if (!Components.Schemas.ContainsKey(schema.Reference.Id))
+ {
+ Components.Schemas.Add(schema.Reference.Id, schema);
+ }
+ break;
+
+ case OpenApiParameter parameter:
+ EnsureComponentsExists();
+ EnsureParametersExists();
+ if (!Components.Parameters.ContainsKey(parameter.Reference.Id))
+ {
+ Components.Parameters.Add(parameter.Reference.Id, parameter);
+ }
+ break;
+
+ case OpenApiResponse response:
+ EnsureComponentsExists();
+ EnsureResponsesExists();
+ if (!Components.Responses.ContainsKey(response.Reference.Id))
+ {
+ Components.Responses.Add(response.Reference.Id, response);
+ }
+ break;
+
+ default:
+ break;
+ }
+ base.Visit(referenceable);
+ }
+
+ ///
+ /// Visits
+ ///
+ /// The OpenApiSchema to be visited.
+ public override void Visit(OpenApiSchema schema)
+ {
+ // This is needed to handle schemas used in Responses in components
+ if (schema.Reference != null)
+ {
+ EnsureComponentsExists();
+ EnsureSchemasExists();
+ if (!Components.Schemas.ContainsKey(schema.Reference.Id))
+ {
+ Components.Schemas.Add(schema.Reference.Id, schema);
+ }
+ }
+ base.Visit(schema);
+ }
+
+ private void EnsureComponentsExists()
+ {
+ if (_target.Components == null)
+ {
+ _target.Components = new OpenApiComponents();
+ }
+ }
+
+ private void EnsureSchemasExists()
+ {
+ if (_target.Components.Schemas == null)
+ {
+ _target.Components.Schemas = new Dictionary();
+ }
+ }
+
+ private void EnsureParametersExists()
+ {
+ if (_target.Components.Parameters == null)
+ {
+ _target.Components.Parameters = new Dictionary();
+ }
+ }
+
+ private void EnsureResponsesExists()
+ {
+ if (_target.Components.Responses == null)
+ {
+ _target.Components.Responses = new Dictionary();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs b/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs
new file mode 100644
index 000000000..08774995e
--- /dev/null
+++ b/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.OpenApi.Services
+{
+ ///
+ /// A service that slices an OpenApiDocument into a subset document
+ ///
+ public static class OpenApiFilterService
+ {
+ ///
+ /// Create predicate function based on passed query parameters
+ ///
+ /// Comma delimited list of operationIds or * for all operations.
+ /// Comma delimited list of tags or a single regex.
+ /// A predicate.
+ public static Func CreatePredicate(string operationIds = null, string tags = null)
+ {
+ Func predicate;
+ if (!string.IsNullOrEmpty(operationIds) && !string.IsNullOrEmpty(tags))
+ {
+ throw new InvalidOperationException("Cannot specify both operationIds and tags at the same time.");
+ }
+ if (operationIds != null)
+ {
+ if (operationIds == "*")
+ {
+ predicate = (o) => true; // All operations
+ }
+ else
+ {
+ var operationIdsArray = operationIds.Split(',');
+ predicate = (o) => operationIdsArray.Contains(o.OperationId);
+ }
+ }
+ else if (tags != null)
+ {
+ var tagsArray = tags.Split(',');
+ if (tagsArray.Length == 1)
+ {
+ var regex = new Regex(tagsArray[0]);
+
+ predicate = (o) => o.Tags.Any(t => regex.IsMatch(t.Name));
+ }
+ else
+ {
+ predicate = (o) => o.Tags.Any(t => tagsArray.Contains(t.Name));
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException("Either operationId(s) or tag(s) need to be specified.");
+ }
+
+ return predicate;
+ }
+
+ ///
+ /// Create partial OpenAPI document based on the provided predicate.
+ ///
+ /// The target .
+ /// A predicate function.
+ /// A partial OpenAPI document.
+ public static OpenApiDocument CreateFilteredDocument(OpenApiDocument source, Func predicate)
+ {
+ // Fetch and copy title, graphVersion and server info from OpenApiDoc
+ var subset = new OpenApiDocument
+ {
+ Info = new OpenApiInfo()
+ {
+ Title = source.Info.Title + " - Subset",
+ Description = source.Info.Description,
+ TermsOfService = source.Info.TermsOfService,
+ Contact = source.Info.Contact,
+ License = source.Info.License,
+ Version = source.Info.Version,
+ Extensions = source.Info.Extensions
+ },
+
+ Components = new OpenApiComponents()
+ };
+
+ subset.Components.SecuritySchemes = source.Components.SecuritySchemes;
+ subset.SecurityRequirements = source.SecurityRequirements;
+ subset.Servers = source.Servers;
+
+ var results = FindOperations(source, predicate);
+ foreach (var result in results)
+ {
+ OpenApiPathItem pathItem;
+ var pathKey = result.CurrentKeys.Path;
+
+ if (subset.Paths == null)
+ {
+ subset.Paths = new OpenApiPaths();
+ pathItem = new OpenApiPathItem();
+ subset.Paths.Add(pathKey, pathItem);
+ }
+ else
+ {
+ if (!subset.Paths.TryGetValue(pathKey, out pathItem))
+ {
+ pathItem = new OpenApiPathItem();
+ subset.Paths.Add(pathKey, pathItem);
+ }
+ }
+
+ pathItem.Operations.Add((OperationType)result.CurrentKeys.Operation, result.Operation);
+ }
+
+ if (subset.Paths == null)
+ {
+ throw new ArgumentException("No paths found for the supplied parameters.");
+ }
+
+ CopyReferences(subset);
+
+ return subset;
+ }
+
+ private static IList FindOperations(OpenApiDocument graphOpenApi, Func predicate)
+ {
+ var search = new OperationSearch(predicate);
+ var walker = new OpenApiWalker(search);
+ walker.Walk(graphOpenApi);
+ return search.SearchResults;
+ }
+
+ private static void CopyReferences(OpenApiDocument target)
+ {
+ bool morestuff;
+ do
+ {
+ var copy = new CopyReferences(target);
+ var walker = new OpenApiWalker(copy);
+ walker.Walk(target);
+
+ morestuff = AddReferences(copy.Components, target.Components);
+
+ } while (morestuff);
+ }
+
+ private static bool AddReferences(OpenApiComponents newComponents, OpenApiComponents target)
+ {
+ var moreStuff = false;
+ foreach (var item in newComponents.Schemas)
+ {
+ if (!target.Schemas.ContainsKey(item.Key))
+ {
+ moreStuff = true;
+ target.Schemas.Add(item);
+ }
+ }
+
+ foreach (var item in newComponents.Parameters)
+ {
+ if (!target.Parameters.ContainsKey(item.Key))
+ {
+ moreStuff = true;
+ target.Parameters.Add(item);
+ }
+ }
+
+ foreach (var item in newComponents.Responses)
+ {
+ if (!target.Responses.ContainsKey(item.Key))
+ {
+ moreStuff = true;
+ target.Responses.Add(item);
+ }
+ }
+ return moreStuff;
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs
index 296068914..30a47bdd7 100644
--- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs
+++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs
@@ -47,11 +47,16 @@ public class OpenApiUrlTreeNode
public string Segment { get; private set; }
///
- /// Flag indicating whether the node's PathItems has operations.
+ /// Flag indicating whether the node's PathItems dictionary has operations
+ /// under a given label.
///
+ /// The name of the key for the target operations
+ /// in the node's PathItems dictionary.
/// true or false.
public bool HasOperations(string label)
{
+ Utils.CheckArgumentNullOrEmpty(label, nameof(label));
+
if (!(PathItems?.ContainsKey(label) ?? false))
{
return false;
@@ -139,6 +144,8 @@ public OpenApiUrlTreeNode Attach(string path,
string label)
{
Utils.CheckArgumentNullOrEmpty(label, nameof(label));
+ Utils.CheckArgumentNullOrEmpty(path, nameof(path));
+ Utils.CheckArgumentNull(pathItem, nameof(pathItem));
if (path.StartsWith(RootPathSegment))
{
diff --git a/src/Microsoft.OpenApi/Services/OperationSearch.cs b/src/Microsoft.OpenApi/Services/OperationSearch.cs
new file mode 100644
index 000000000..35d36b38f
--- /dev/null
+++ b/src/Microsoft.OpenApi/Services/OperationSearch.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.OpenApi.Services
+{
+ ///
+ /// Visits OpenApi operations and parameters.
+ ///
+ public class OperationSearch : OpenApiVisitorBase
+ {
+ private readonly Func _predicate;
+ private readonly List _searchResults = new();
+
+ ///
+ /// A list of operations from the operation search.
+ ///
+ public IList SearchResults => _searchResults;
+
+ ///
+ /// The OperationSearch constructor.
+ ///
+ /// A predicate function.
+ public OperationSearch(Func predicate)
+ {
+ _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
+ }
+
+ ///
+ /// Visits .
+ ///
+ /// The target .
+ public override void Visit(OpenApiOperation operation)
+ {
+ if (_predicate(operation))
+ {
+ _searchResults.Add(new SearchResult()
+ {
+ Operation = operation,
+ CurrentKeys = CopyCurrentKeys(CurrentKeys)
+ });
+ }
+ }
+
+ ///
+ /// Visits list of .
+ ///
+ /// The target list of .
+ public override void Visit(IList parameters)
+ {
+ /* The Parameter.Explode property should be true
+ * if Parameter.Style == Form; but OData query params
+ * as used in Microsoft Graph implement explode: false
+ * ex: $select=id,displayName,givenName
+ */
+ foreach (var parameter in parameters.Where(x => x.Style == ParameterStyle.Form))
+ {
+ parameter.Explode = false;
+ }
+
+ base.Visit(parameters);
+ }
+
+ private static CurrentKeys CopyCurrentKeys(CurrentKeys currentKeys)
+ {
+ return new CurrentKeys
+ {
+ Path = currentKeys.Path,
+ Operation = currentKeys.Operation
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi/Services/SearchResult.cs b/src/Microsoft.OpenApi/Services/SearchResult.cs
new file mode 100644
index 000000000..381a11f95
--- /dev/null
+++ b/src/Microsoft.OpenApi/Services/SearchResult.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.OpenApi.Services
+{
+ ///
+ /// Defines a search result model for visited operations.
+ ///
+ public class SearchResult
+ {
+ ///
+ /// An object containing contextual information based on where the walker is currently referencing in an OpenApiDocument.
+ ///
+ public CurrentKeys CurrentKeys { get; set; }
+
+ ///
+ /// An Operation object.
+ ///
+ public OpenApiOperation Operation { get; set; }
+ }
+}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs
new file mode 100644
index 000000000..7567e0b7d
--- /dev/null
+++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.IO;
+using Xunit;
+
+namespace Microsoft.OpenApi.Readers.Tests.OpenApiReaderTests
+{
+ public class OpenApiStreamReaderTests
+ {
+ private const string SampleFolderPath = "V3Tests/Samples/OpenApiDocument/";
+
+ [Fact]
+ public void StreamShouldCloseIfLeaveStreamOpenSettingEqualsFalse()
+ {
+ using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStore.yaml")))
+ {
+ var reader = new OpenApiStreamReader(new OpenApiReaderSettings { LeaveStreamOpen = false });
+ reader.Read(stream, out _);
+ Assert.False(stream.CanRead);
+ }
+ }
+
+ [Fact]
+ public void StreamShouldNotCloseIfLeaveStreamOpenSettingEqualsTrue()
+ {
+ using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStore.yaml")))
+ {
+ var reader = new OpenApiStreamReader(new OpenApiReaderSettings { LeaveStreamOpen = true});
+ reader.Read(stream, out _);
+ Assert.True(stream.CanRead);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.OpenApi.Tests/Documents/OpenApiDocumentMock.cs b/test/Microsoft.OpenApi.Tests/Documents/OpenApiDocumentMock.cs
new file mode 100644
index 000000000..676bf8e65
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Documents/OpenApiDocumentMock.cs
@@ -0,0 +1,730 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Interfaces;
+using Microsoft.OpenApi.Models;
+using System.Collections.Generic;
+
+namespace OpenAPIService.Test
+{
+ ///
+ /// Mock class that creates a sample OpenAPI document.
+ ///
+ public static class OpenApiDocumentMock
+ {
+ ///
+ /// Creates an OpenAPI document.
+ ///
+ /// Instance of an OpenApi document
+ public static OpenApiDocument CreateOpenApiDocument()
+ {
+ var applicationJsonMediaType = "application/json";
+
+ var document = new OpenApiDocument()
+ {
+ Info = new OpenApiInfo()
+ {
+ Title = "People",
+ Version = "v1.0"
+ },
+ Paths = new OpenApiPaths()
+ {
+ ["/"] = new OpenApiPathItem() // root path
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ OperationId = "graphService.GetGraphService",
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200",new OpenApiResponse()
+ {
+ Description = "OK"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/reports/microsoft.graph.getTeamsUserActivityCounts(period={period})"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "reports.Functions"
+ }
+ }
+ },
+ OperationId = "reports.getTeamsUserActivityCounts",
+ Summary = "Invoke function getTeamsUserActivityUserCounts",
+ Parameters = new List
+ {
+ {
+ new OpenApiParameter()
+ {
+ Name = "period",
+ In = ParameterLocation.Path,
+ Required = true,
+ Schema = new OpenApiSchema()
+ {
+ Type = "string"
+ }
+ }
+ }
+ },
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Success",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Type = "array"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/reports/microsoft.graph.getTeamsUserActivityUserDetail(date={date})"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "reports.Functions"
+ }
+ }
+ },
+ OperationId = "reports.getTeamsUserActivityUserDetail-a3f1",
+ Summary = "Invoke function getTeamsUserActivityUserDetail",
+ Parameters = new List
+ {
+ {
+ new OpenApiParameter()
+ {
+ Name = "period",
+ In = ParameterLocation.Path,
+ Required = true,
+ Schema = new OpenApiSchema()
+ {
+ Type = "string"
+ }
+ }
+ }
+ },
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Success",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Type = "array"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/users"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "users.user"
+ }
+ }
+ },
+ OperationId = "users.user.ListUser",
+ Summary = "Get entities from users",
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Retrieved entities",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Title = "Collection of user",
+ Type = "object",
+ Properties = new Dictionary
+ {
+ {
+ "value",
+ new OpenApiSchema
+ {
+ Type = "array",
+ Items = new OpenApiSchema
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Schema,
+ Id = "microsoft.graph.user"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/users/{user-id}"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "users.user"
+ }
+ }
+ },
+ OperationId = "users.user.GetUser",
+ Summary = "Get entity from users by key",
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Retrieved entity",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Schema,
+ Id = "microsoft.graph.user"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ OperationType.Patch, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "users.user"
+ }
+ }
+ },
+ OperationId = "users.user.UpdateUser",
+ Summary = "Update entity in users",
+ Responses = new OpenApiResponses()
+ {
+ {
+ "204", new OpenApiResponse()
+ {
+ Description = "Success"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/users/{user-id}/messages/{message-id}"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "users.message"
+ }
+ }
+ },
+ OperationId = "users.GetMessages",
+ Summary = "Get messages from users",
+ Description = "The messages in a mailbox or folder. Read-only. Nullable.",
+ Parameters = new List
+ {
+ new OpenApiParameter()
+ {
+ Name = "$select",
+ In = ParameterLocation.Query,
+ Required = true,
+ Description = "Select properties to be returned",
+ Schema = new OpenApiSchema()
+ {
+ Type = "array"
+ }
+ // missing explode parameter
+ }
+ },
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Retrieved navigation property",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Schema,
+ Id = "microsoft.graph.message"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/administrativeUnits/{administrativeUnit-id}/microsoft.graph.restore"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Post, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "administrativeUnits.Actions"
+ }
+ }
+ },
+ OperationId = "administrativeUnits.restore",
+ Summary = "Invoke action restore",
+ Parameters = new List
+ {
+ {
+ new OpenApiParameter()
+ {
+ Name = "administrativeUnit-id",
+ In = ParameterLocation.Path,
+ Required = true,
+ Description = "key: id of administrativeUnit",
+ Schema = new OpenApiSchema()
+ {
+ Type = "string"
+ }
+ }
+ }
+ },
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Success",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ AnyOf = new List
+ {
+ new OpenApiSchema
+ {
+ Type = "string"
+ }
+ },
+ Nullable = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/applications/{application-id}/logo"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Put, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "applications.application"
+ }
+ }
+ },
+ OperationId = "applications.application.UpdateLogo",
+ Summary = "Update media content for application in applications",
+ Responses = new OpenApiResponses()
+ {
+ {
+ "204", new OpenApiResponse()
+ {
+ Description = "Success"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/security/hostSecurityProfiles"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "security.hostSecurityProfile"
+ }
+ }
+ },
+ OperationId = "security.ListHostSecurityProfiles",
+ Summary = "Get hostSecurityProfiles from security",
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Retrieved navigation property",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Title = "Collection of hostSecurityProfile",
+ Type = "object",
+ Properties = new Dictionary
+ {
+ {
+ "value",
+ new OpenApiSchema
+ {
+ Type = "array",
+ Items = new OpenApiSchema
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Schema,
+ Id = "microsoft.graph.networkInterface"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/communications/calls/{call-id}/microsoft.graph.keepAlive"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Post, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ {
+ new OpenApiTag()
+ {
+ Name = "communications.Actions"
+ }
+ }
+ },
+ OperationId = "communications.calls.call.keepAlive",
+ Summary = "Invoke action keepAlive",
+ Parameters = new List
+ {
+ new OpenApiParameter()
+ {
+ Name = "call-id",
+ In = ParameterLocation.Path,
+ Description = "key: id of call",
+ Required = true,
+ Schema = new OpenApiSchema()
+ {
+ Type = "string"
+ },
+ Extensions = new Dictionary
+ {
+ {
+ "x-ms-docs-key-type", new OpenApiString("call")
+ }
+ }
+ }
+ },
+ Responses = new OpenApiResponses()
+ {
+ {
+ "204", new OpenApiResponse()
+ {
+ Description = "Success"
+ }
+ }
+ },
+ Extensions = new Dictionary
+ {
+ {
+ "x-ms-docs-operation-type", new OpenApiString("action")
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/groups/{group-id}/events/{event-id}/calendar/events/microsoft.graph.delta"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ new OpenApiTag()
+ {
+ Name = "groups.Functions"
+ }
+ },
+ OperationId = "groups.group.events.event.calendar.events.delta",
+ Summary = "Invoke function delta",
+ Parameters = new List
+ {
+ new OpenApiParameter()
+ {
+ Name = "group-id",
+ In = ParameterLocation.Path,
+ Description = "key: id of group",
+ Required = true,
+ Schema = new OpenApiSchema()
+ {
+ Type = "string"
+ },
+ Extensions = new Dictionary
+ {
+ {
+ "x-ms-docs-key-type", new OpenApiString("group")
+ }
+ }
+ },
+ new OpenApiParameter()
+ {
+ Name = "event-id",
+ In = ParameterLocation.Path,
+ Description = "key: id of event",
+ Required = true,
+ Schema = new OpenApiSchema()
+ {
+ Type = "string"
+ },
+ Extensions = new Dictionary
+ {
+ {
+ "x-ms-docs-key-type", new OpenApiString("event")
+ }
+ }
+ }
+ },
+ Responses = new OpenApiResponses()
+ {
+ {
+ "200", new OpenApiResponse()
+ {
+ Description = "Success",
+ Content = new Dictionary
+ {
+ {
+ applicationJsonMediaType,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Type = "array",
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Schema,
+ Id = "microsoft.graph.event"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ Extensions = new Dictionary
+ {
+ {
+ "x-ms-docs-operation-type", new OpenApiString("function")
+ }
+ }
+ }
+ }
+ }
+ },
+ ["/applications/{application-id}/createdOnBehalfOf/$ref"] = new OpenApiPathItem()
+ {
+ Operations = new Dictionary
+ {
+ {
+ OperationType.Get, new OpenApiOperation
+ {
+ Tags = new List
+ {
+ new OpenApiTag()
+ {
+ Name = "applications.directoryObject"
+ }
+ },
+ OperationId = "applications.GetRefCreatedOnBehalfOf",
+ Summary = "Get ref of createdOnBehalfOf from applications"
+ }
+ }
+ }
+ }
+ },
+ Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ {
+ "microsoft.graph.networkInterface", new OpenApiSchema
+ {
+ Title = "networkInterface",
+ Type = "object",
+ Properties = new Dictionary
+ {
+ {
+ "description", new OpenApiSchema
+ {
+ Type = "string",
+ Description = "Description of the NIC (e.g. Ethernet adapter, Wireless LAN adapter Local Area Connection <#>, etc.).",
+ Nullable = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ return document;
+ }
+ }
+}
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index d5a89e586..0b681a8ec 100755
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -954,6 +954,11 @@ namespace Microsoft.OpenApi.Services
public string Response { get; set; }
public string ServerVariable { get; }
}
+ public static class OpenApiFilterService
+ {
+ public static Microsoft.OpenApi.Models.OpenApiDocument CreateFilteredDocument(Microsoft.OpenApi.Models.OpenApiDocument source, System.Func predicate) { }
+ public static System.Func CreatePredicate(string operationIds = null, string tags = null) { }
+ }
public class OpenApiReferenceError : Microsoft.OpenApi.Models.OpenApiError
{
public OpenApiReferenceError(Microsoft.OpenApi.Exceptions.OpenApiException exception) { }
@@ -1044,6 +1049,19 @@ namespace Microsoft.OpenApi.Services
public System.IO.Stream GetArtifact(string location) { }
public Microsoft.OpenApi.Interfaces.IOpenApiReferenceable ResolveReference(Microsoft.OpenApi.Models.OpenApiReference reference) { }
}
+ public class OperationSearch : Microsoft.OpenApi.Services.OpenApiVisitorBase
+ {
+ public OperationSearch(System.Func predicate) { }
+ public System.Collections.Generic.IList SearchResults { get; }
+ public override void Visit(Microsoft.OpenApi.Models.OpenApiOperation operation) { }
+ public override void Visit(System.Collections.Generic.IList parameters) { }
+ }
+ public class SearchResult
+ {
+ public SearchResult() { }
+ public Microsoft.OpenApi.Services.CurrentKeys CurrentKeys { get; set; }
+ public Microsoft.OpenApi.Models.OpenApiOperation Operation { get; set; }
+ }
}
namespace Microsoft.OpenApi.Validations
{
diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiFilterServiceTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiFilterServiceTests.cs
new file mode 100644
index 000000000..ab65ed744
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiFilterServiceTests.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System;
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.Services;
+using OpenAPIService.Test;
+using Xunit;
+
+namespace Microsoft.OpenApi.Tests.Services
+{
+ public class OpenApiFilterServiceTests
+ {
+ private readonly OpenApiDocument _openApiDocumentMock;
+
+ public OpenApiFilterServiceTests()
+ {
+ _openApiDocumentMock = OpenApiDocumentMock.CreateOpenApiDocument();
+ }
+
+ [Theory]
+ [InlineData("users.user.ListUser", null, 1)]
+ [InlineData("users.user.GetUser", null, 1)]
+ [InlineData("users.user.ListUser,users.user.GetUser", null, 2)]
+ [InlineData("*", null, 12)]
+ [InlineData("administrativeUnits.restore", null, 1)]
+ [InlineData("graphService.GetGraphService", null, 1)]
+ [InlineData(null, "users.user,applications.application", 3)]
+ [InlineData(null, "^users\\.", 3)]
+ [InlineData(null, "users.user", 2)]
+ [InlineData(null, "applications.application", 1)]
+ [InlineData(null, "reports.Functions", 2)]
+ public void ReturnFilteredOpenApiDocumentBasedOnOperationIdsAndTags(string operationIds, string tags, int expectedPathCount)
+ {
+ // Act
+ var predicate = OpenApiFilterService.CreatePredicate(operationIds, tags);
+ var subsetOpenApiDocument = OpenApiFilterService.CreateFilteredDocument(_openApiDocumentMock, predicate);
+
+ // Assert
+ Assert.NotNull(subsetOpenApiDocument);
+ Assert.NotEmpty(subsetOpenApiDocument.Paths);
+ Assert.Equal(expectedPathCount, subsetOpenApiDocument.Paths.Count);
+ }
+
+ [Fact]
+ public void ThrowsInvalidOperationExceptionInCreatePredicateWhenInvalidArgumentsArePassed()
+ {
+ // Act and Assert
+ var message1 = Assert.Throws(() => OpenApiFilterService.CreatePredicate(null, null)).Message;
+ Assert.Equal("Either operationId(s) or tag(s) need to be specified.", message1);
+
+ var message2 = Assert.Throws(() => OpenApiFilterService.CreatePredicate("users.user.ListUser", "users.user")).Message;
+ Assert.Equal("Cannot specify both operationIds and tags at the same time.", message2);
+ }
+ }
+}
diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs
index a246c66ff..944e6c830 100644
--- a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs
@@ -11,6 +11,26 @@ namespace Microsoft.OpenApi.Tests.Services
{
public class OpenApiUrlTreeNodeTests
{
+ private OpenApiDocument OpenApiDocumentSample_1 => new OpenApiDocument()
+ {
+ Paths = new OpenApiPaths()
+ {
+ ["/"] = new OpenApiPathItem(),
+ ["/houses"] = new OpenApiPathItem(),
+ ["/cars"] = new OpenApiPathItem()
+ }
+ };
+
+ private OpenApiDocument OpenApiDocumentSample_2 => new OpenApiDocument()
+ {
+ Paths = new OpenApiPaths()
+ {
+ ["/"] = new OpenApiPathItem(),
+ ["/hotels"] = new OpenApiPathItem(),
+ ["/offices"] = new OpenApiPathItem()
+ }
+ };
+
[Fact]
public void CreateUrlSpaceWithoutOpenApiDocument()
{
@@ -64,15 +84,7 @@ public void CreatePathWithoutRootWorks()
[Fact]
public void CreateMultiplePathsWorks()
{
- var doc = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/houses"] = new OpenApiPathItem(),
- ["/cars"] = new OpenApiPathItem()
- }
- };
+ var doc = OpenApiDocumentSample_1;
string label = "assets";
var rootNode = OpenApiUrlTreeNode.Create(doc, label);
@@ -89,25 +101,9 @@ public void CreateMultiplePathsWorks()
[Fact]
public void AttachDocumentWorks()
{
- var doc1 = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/houses"] = new OpenApiPathItem(),
- ["/cars"] = new OpenApiPathItem()
- }
- };
+ var doc1 = OpenApiDocumentSample_1;
- var doc2 = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/hotels"] = new OpenApiPathItem(),
- ["/offices"] = new OpenApiPathItem()
- }
- };
+ var doc2 = OpenApiDocumentSample_2;
var label1 = "personal";
var label2 = "business";
@@ -123,15 +119,7 @@ public void AttachDocumentWorks()
[Fact]
public void AttachPathWorks()
{
- var doc = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/houses"] = new OpenApiPathItem(),
- ["/cars"] = new OpenApiPathItem()
- }
- };
+ var doc = OpenApiDocumentSample_1;
var label1 = "personal";
var rootNode = OpenApiUrlTreeNode.Create(doc, label1);
@@ -335,96 +323,10 @@ public void SegmentIsParameterWorks()
Assert.Equal("{apartment-id}", rootNode.Children["houses"].Children["apartments"].Children["{apartment-id}"].Segment);
}
- [Fact]
- public void ThrowsArgumentExceptionForDuplicateLabels()
- {
- var doc1 = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/houses"] = new OpenApiPathItem(),
- ["/cars"] = new OpenApiPathItem()
- }
- };
-
- var doc2 = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/hotels"] = new OpenApiPathItem(),
- ["/offices"] = new OpenApiPathItem()
- }
- };
-
- var label1 = "personal";
- var rootNode = OpenApiUrlTreeNode.Create(doc1, label1);
-
- Assert.Throws(() => rootNode.Attach(doc2, label1));
- }
-
- [Fact]
- public void ThrowsArgumentNullExceptionForNullArgumentsInCreateMethod()
- {
- var doc = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/houses"] = new OpenApiPathItem(),
- ["/cars"] = new OpenApiPathItem()
- }
- };
-
- Assert.Throws(() => OpenApiUrlTreeNode.Create(doc, ""));
- Assert.Throws(() => OpenApiUrlTreeNode.Create(doc, null));
- Assert.Throws(() => OpenApiUrlTreeNode.Create(null, "beta"));
- }
-
- [Fact]
- public void ThrowsArgumentNullExceptionForNullArgumentsInAttachMethod()
- {
- var doc1 = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/houses"] = new OpenApiPathItem(),
- ["/cars"] = new OpenApiPathItem()
- }
- };
-
- var doc2 = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/hotels"] = new OpenApiPathItem(),
- ["/offices"] = new OpenApiPathItem()
- }
- };
-
- var label1 = "personal";
- var rootNode = OpenApiUrlTreeNode.Create(doc1, label1);
-
- Assert.Throws(() => rootNode.Attach(doc2, ""));
- Assert.Throws(() => rootNode.Attach(doc2, null));
- Assert.Throws(() => rootNode.Attach(null, "beta"));
- }
-
[Fact]
public void AdditionalDataWorks()
{
- var doc = new OpenApiDocument()
- {
- Paths = new OpenApiPaths()
- {
- ["/"] = new OpenApiPathItem(),
- ["/houses"] = new OpenApiPathItem(),
- ["/cars"] = new OpenApiPathItem()
- }
- };
+ var doc = OpenApiDocumentSample_1;
var label = "personal";
var rootNode = OpenApiUrlTreeNode.Create(doc, label);
@@ -476,5 +378,70 @@ public void AdditionalDataWorks()
Assert.Equal("Convertible", item);
});
}
+
+ [Fact]
+ public void ThrowsArgumentExceptionForDuplicateLabels()
+ {
+ var doc1 = OpenApiDocumentSample_1;
+
+ var doc2 = OpenApiDocumentSample_2;
+
+ var label1 = "personal";
+ var rootNode = OpenApiUrlTreeNode.Create(doc1, label1);
+
+ Assert.Throws(() => rootNode.Attach(doc2, label1));
+ }
+
+ [Fact]
+ public void ThrowsArgumentNullExceptionForNullOrEmptyArgumentsInCreateMethod()
+ {
+ var doc = OpenApiDocumentSample_1;
+
+ Assert.Throws(() => OpenApiUrlTreeNode.Create(doc, ""));
+ Assert.Throws(() => OpenApiUrlTreeNode.Create(doc, null));
+ Assert.Throws(() => OpenApiUrlTreeNode.Create(null, "beta"));
+ Assert.Throws(() => OpenApiUrlTreeNode.Create(null, null));
+ Assert.Throws(() => OpenApiUrlTreeNode.Create(null, ""));
+ }
+
+ [Fact]
+ public void ThrowsArgumentNullExceptionForNullOrEmptyArgumentsInAttachMethod()
+ {
+ var doc1 = OpenApiDocumentSample_1;
+
+ var doc2 = OpenApiDocumentSample_2;
+
+ var label1 = "personal";
+ var rootNode = OpenApiUrlTreeNode.Create(doc1, label1);
+
+ Assert.Throws(() => rootNode.Attach(doc2, ""));
+ Assert.Throws(() => rootNode.Attach(doc2, null));
+ Assert.Throws(() => rootNode.Attach(null, "beta"));
+ Assert.Throws(() => rootNode.Attach(null, null));
+ Assert.Throws(() => rootNode.Attach(null, ""));
+ }
+
+ [Fact]
+ public void ThrowsArgumentNullExceptionForNullOrEmptyArgumentInHasOperationsMethod()
+ {
+ var doc = OpenApiDocumentSample_1;
+
+ var label = "personal";
+ var rootNode = OpenApiUrlTreeNode.Create(doc, label);
+
+ Assert.Throws(() => rootNode.HasOperations(null));
+ Assert.Throws(() => rootNode.HasOperations(""));
+ }
+
+ [Fact]
+ public void ThrowsArgumentNullExceptionForNullArgumentInAddAdditionalDataMethod()
+ {
+ var doc = OpenApiDocumentSample_1;
+
+ var label = "personal";
+ var rootNode = OpenApiUrlTreeNode.Create(doc, label);
+
+ Assert.Throws(() => rootNode.AddAdditionalData(null));
+ }
}
}
diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs
index 41a9a6ab0..a7abfd9d8 100644
--- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs
@@ -218,7 +218,7 @@ public void PathParameterNotInThePathShouldReturnAnError()
}
[Fact]
- public void PathParameterInThePastShouldBeOk()
+ public void PathParameterInThePathShouldBeOk()
{
// Arrange
IEnumerable errors;
diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs
index b82327a5d..dd6e2554b 100644
--- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs
@@ -1,5 +1,5 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT license.
+// Licensed under the MIT license.
using System;
using System.Collections.Generic;
@@ -12,7 +12,7 @@
namespace Microsoft.OpenApi.Tests
{
-
+
public class OpenApiWorkspaceTests
{
[Fact]
@@ -61,7 +61,7 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther()
}
}
}
- }
+ }
}
});
workspace.AddDocument("common", new OpenApiDocument() {
@@ -111,7 +111,7 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short()
re.CreateContent("application/json", co =>
co.Schema = new OpenApiSchema()
{
- Reference = new OpenApiReference() // Reference
+ Reference = new OpenApiReference() // Reference
{
Id = "test",
Type = ReferenceType.Schema,