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,