diff --git a/src/Microsoft.OpenApi.Tool/OpenApiService.cs b/src/Microsoft.OpenApi.Tool/OpenApiService.cs index c52c08941..87f02dcce 100644 --- a/src/Microsoft.OpenApi.Tool/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Tool/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; @@ -21,29 +23,44 @@ public static void ProcessOpenApiDocument( FileInfo output, OpenApiSpecVersion version, OpenApiFormat format, + string filterByOperationIds, 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)) + { + var predicate = OpenApiFilterService.CreatePredicate(filterByOperationIds); + 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 +69,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; - - if (outputStream != null) - { - textWriter = new StreamWriter(outputStream); - } - else - { - textWriter = Console.Out; - } + using var outputStream = output?.Create(); - 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"); - } + var textWriter = outputStream != null ? new StreamWriter(outputStream) : Console.Out; - 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 +127,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.Tool/Program.cs index 446e2829a..21be3406b 100644 --- a/src/Microsoft.OpenApi.Tool/Program.cs +++ b/src/Microsoft.OpenApi.Tool/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 { - 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,10 @@ 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 by OperationId provided", typeof(string)) }; - transformCommand.Handler = CommandHandler.Create( + transformCommand.Handler = CommandHandler.Create( OpenApiService.ProcessOpenApiDocument); rootCommand.Add(transformCommand); 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..5b5e6f59f --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs @@ -0,0 +1,162 @@ +// 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 +{ + /// + /// 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. + /// A predicate. + public static Func CreatePredicate(string operationIds) + { + Func predicate; + if (operationIds != null) + { + if (operationIds == "*") + { + predicate = (o) => true; // All operations + } + else + { + var operationIdsArray = operationIds.Split(','); + predicate = (o) => operationIdsArray.Contains(o.OperationId); + } + } + + else + { + throw new InvalidOperationException("OperationId needs 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/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.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..180a7fd81 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) { } + } 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..308f00952 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiFilterServiceTests.cs @@ -0,0 +1,45 @@ +// 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")] + [InlineData("users.user.GetUser")] + [InlineData("administrativeUnits.restore")] + [InlineData("graphService.GetGraphService")] + public void ReturnFilteredOpenApiDocumentBasedOnOperationIds(string operationId) + { + // Act + var predicate = OpenApiFilterService.CreatePredicate(operationId); + var subsetOpenApiDocument = OpenApiFilterService.CreateFilteredDocument(_openApiDocumentMock, predicate); + + // Assert + Assert.NotNull(subsetOpenApiDocument); + Assert.Single(subsetOpenApiDocument.Paths); + } + + [Fact] + public void ThrowsInvalidOperationExceptionInCreatePredicateWhenInvalidOperationIdIsSpecified() + { + // Act and Assert + var message = Assert.Throws(() =>OpenApiFilterService.CreatePredicate(null)).Message; + Assert.Equal("OperationId needs to be specified.", message); + } + } +} 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,