diff --git a/src/Microsoft.OpenApi/Services/CopyReferences.cs b/src/Microsoft.OpenApi/Services/CopyReferences.cs index 757471466..73bb667b6 100644 --- a/src/Microsoft.OpenApi/Services/CopyReferences.cs +++ b/src/Microsoft.OpenApi/Services/CopyReferences.cs @@ -26,8 +26,8 @@ public override void Visit(IOpenApiReferenceable referenceable) switch (referenceable) { case OpenApiSchema schema: - EnsureComponentsExists(); - EnsureSchemasExists(); + EnsureComponentsExist(); + EnsureSchemasExist(); if (!Components.Schemas.ContainsKey(schema.Reference.Id)) { Components.Schemas.Add(schema.Reference.Id, schema); @@ -35,8 +35,8 @@ public override void Visit(IOpenApiReferenceable referenceable) break; case OpenApiParameter parameter: - EnsureComponentsExists(); - EnsureParametersExists(); + EnsureComponentsExist(); + EnsureParametersExist(); if (!Components.Parameters.ContainsKey(parameter.Reference.Id)) { Components.Parameters.Add(parameter.Reference.Id, parameter); @@ -44,8 +44,8 @@ public override void Visit(IOpenApiReferenceable referenceable) break; case OpenApiResponse response: - EnsureComponentsExists(); - EnsureResponsesExists(); + EnsureComponentsExist(); + EnsureResponsesExist(); if (!Components.Responses.ContainsKey(response.Reference.Id)) { Components.Responses.Add(response.Reference.Id, response); @@ -53,18 +53,64 @@ public override void Visit(IOpenApiReferenceable referenceable) break; case OpenApiRequestBody requestBody: - EnsureComponentsExists(); - EnsureResponsesExists(); - EnsurRequestBodiesExists(); + EnsureComponentsExist(); + EnsureResponsesExist(); + EnsureRequestBodiesExist(); if (!Components.RequestBodies.ContainsKey(requestBody.Reference.Id)) { Components.RequestBodies.Add(requestBody.Reference.Id, requestBody); } break; + case OpenApiExample example: + EnsureComponentsExist(); + EnsureExamplesExist(); + if (!Components.Examples.ContainsKey(example.Reference.Id)) + { + Components.Examples.Add(example.Reference.Id, example); + } + break; + + case OpenApiHeader header: + EnsureComponentsExist(); + EnsureHeadersExist(); + if (!Components.Headers.ContainsKey(header.Reference.Id)) + { + Components.Headers.Add(header.Reference.Id, header); + } + break; + + case OpenApiCallback callback: + EnsureComponentsExist(); + EnsureCallbacksExist(); + if (!Components.Callbacks.ContainsKey(callback.Reference.Id)) + { + Components.Callbacks.Add(callback.Reference.Id, callback); + } + break; + + case OpenApiLink link: + EnsureComponentsExist(); + EnsureLinksExist(); + if (!Components.Links.ContainsKey(link.Reference.Id)) + { + Components.Links.Add(link.Reference.Id, link); + } + break; + + case OpenApiSecurityScheme securityScheme: + EnsureComponentsExist(); + EnsureSecuritySchemesExist(); + if (!Components.SecuritySchemes.ContainsKey(securityScheme.Reference.Id)) + { + Components.SecuritySchemes.Add(securityScheme.Reference.Id, securityScheme); + } + break; + default: break; } + base.Visit(referenceable); } @@ -77,8 +123,8 @@ public override void Visit(OpenApiSchema schema) // This is needed to handle schemas used in Responses in components if (schema.Reference != null) { - EnsureComponentsExists(); - EnsureSchemasExists(); + EnsureComponentsExist(); + EnsureSchemasExist(); if (!Components.Schemas.ContainsKey(schema.Reference.Id)) { Components.Schemas.Add(schema.Reference.Id, schema); @@ -87,41 +133,54 @@ public override void Visit(OpenApiSchema schema) base.Visit(schema); } - private void EnsureComponentsExists() + private void EnsureComponentsExist() { - if (_target.Components == null) - { - _target.Components = new(); - } + _target.Components ??= new(); } - private void EnsureSchemasExists() + private void EnsureSchemasExist() { - if (_target.Components.Schemas == null) - { - _target.Components.Schemas = new Dictionary(); - } + _target.Components.Schemas ??= new Dictionary(); } - private void EnsureParametersExists() + private void EnsureParametersExist() { - if (_target.Components.Parameters == null) - { - _target.Components.Parameters = new Dictionary(); - } + _target.Components.Parameters ??= new Dictionary(); } - private void EnsureResponsesExists() + private void EnsureResponsesExist() { - if (_target.Components.Responses == null) - { - _target.Components.Responses = new Dictionary(); - } + _target.Components.Responses ??= new Dictionary(); } - private void EnsurRequestBodiesExists() + private void EnsureRequestBodiesExist() { _target.Components.RequestBodies ??= new Dictionary(); } + + private void EnsureExamplesExist() + { + _target.Components.Examples ??= new Dictionary(); + } + + private void EnsureHeadersExist() + { + _target.Components.Headers ??= new Dictionary(); + } + + private void EnsureCallbacksExist() + { + _target.Components.Callbacks ??= new Dictionary(); + } + + private void EnsureLinksExist() + { + _target.Components.Links ??= new Dictionary(); + } + + private void EnsureSecuritySchemesExist() + { + _target.Components.SecuritySchemes ??= new Dictionary(); + } } } diff --git a/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs b/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs index 92051949b..fcc2af0b2 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiFilterService.cs @@ -278,6 +278,42 @@ private static bool AddReferences(OpenApiComponents newComponents, OpenApiCompon moreStuff = true; target.RequestBodies.Add(item); } + + foreach (var item in newComponents.Headers + .Where(item => !target.Headers.ContainsKey(item.Key))) + { + moreStuff = true; + target.Headers.Add(item); + } + + foreach (var item in newComponents.Links + .Where(item => !target.Links.ContainsKey(item.Key))) + { + moreStuff = true; + target.Links.Add(item); + } + + foreach (var item in newComponents.Callbacks + .Where(item => !target.Callbacks.ContainsKey(item.Key))) + { + moreStuff = true; + target.Callbacks.Add(item); + } + + foreach (var item in newComponents.Examples + .Where(item => !target.Examples.ContainsKey(item.Key))) + { + moreStuff = true; + target.Examples.Add(item); + } + + foreach (var item in newComponents.SecuritySchemes + .Where(item => !target.SecuritySchemes.ContainsKey(item.Key))) + { + moreStuff = true; + target.SecuritySchemes.Add(item); + } + return moreStuff; } diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index f5958e5b6..75c176305 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -34,4 +34,10 @@ + + + PreserveNewest + + + diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiFilterServiceTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiFilterServiceTests.cs index 5fb1b15f9..ac566bf0d 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiFilterServiceTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Services/OpenApiFilterServiceTests.cs @@ -3,9 +3,11 @@ using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Tests.UtilityFiles; using Moq; +using SharpYaml.Tokens; using Xunit; namespace Microsoft.OpenApi.Hidi.Tests @@ -170,6 +172,33 @@ public void ThrowsInvalidOperationExceptionInCreatePredicateWhenInvalidArguments Assert.Equal("Cannot specify both operationIds and tags at the same time.", message2); } + [Fact] + public void CopiesOverAllReferencedComponentsToTheSubsetDocumentCorrectly() + { + // Arrange + var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UtilityFiles", "docWithReusableHeadersAndExamples.yaml"); + var operationIds = "getItems"; + + // Act + using var stream = File.OpenRead(filePath); + var doc = new OpenApiStreamReader().Read(stream, out var diagnostic); + + var predicate = OpenApiFilterService.CreatePredicate(operationIds: operationIds); + var subsetOpenApiDocument = OpenApiFilterService.CreateFilteredDocument(doc, predicate); + + var response = subsetOpenApiDocument.Paths["/items"].Operations[OperationType.Get].Responses["200"]; + var responseHeader = response.Headers["x-custom-header"]; + var mediaTypeExample = response.Content["application/json"].Examples.First().Value; + var targetHeaders = subsetOpenApiDocument.Components.Headers; + var targetExamples = subsetOpenApiDocument.Components.Examples; + + // Assert + Assert.False(responseHeader.UnresolvedReference); + Assert.False(mediaTypeExample.UnresolvedReference); + Assert.Single(targetHeaders); + Assert.Single(targetExamples); + } + [Theory] [InlineData("reports.getTeamsUserActivityUserDetail-a3f1", null)] [InlineData(null, "reports.Functions")] diff --git a/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/docWithReusableHeadersAndExamples.yaml b/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/docWithReusableHeadersAndExamples.yaml new file mode 100644 index 000000000..2f86d7661 --- /dev/null +++ b/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/docWithReusableHeadersAndExamples.yaml @@ -0,0 +1,79 @@ +openapi: 3.0.1 +info: + title: Example with Multiple Operations and Local $refs + version: 1.0.0 +paths: + /items: + get: + operationId: getItems + summary: Get a list of items + responses: + '200': + description: A list of items + headers: + x-custom-header: + $ref: '#/components/headers/CustomHeader' + content: + application/json: + schema: + type: array + items: + type: string + examples: + ItemExample: + $ref: '#/components/examples/ItemExample' + post: + operationId: createItem + summary: Create a new item + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: + $ref: '#/components/examples/ItemExample' + responses: + '201': + description: Item created + content: + application/json: + schema: + type: object + properties: + id: + type: string + name: + type: string + example: + $ref: '#/components/examples/ItemExample' +components: + schemas: + pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + headers: + CustomHeader: + description: Custom header for authentication + required: true + schema: + type: string + examples: + ItemExample: + summary: Example of a new item to be created + value: + name: "New Item" +