diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 5fee30ac2..f04f47680 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -24,7 +24,7 @@ namespace Microsoft.OpenApi.Models public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible { /// - /// Related workspace containing OpenApiDocuments that are referenced in this document + /// Related workspace containing components that are referenced in a document /// public OpenApiWorkspace Workspace { get; set; } diff --git a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs index 6915d60bd..a3462da70 100644 --- a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs @@ -28,7 +28,7 @@ internal async Task LoadAsync(OpenApiReference reference, { _workspace.AddDocumentId(reference.ExternalResource, document.BaseUri); var version = diagnostic?.SpecificationVersion ?? OpenApiSpecVersion.OpenApi3_0; - _workspace.RegisterComponents(document, version); + _workspace.RegisterComponents(document); document.Workspace = _workspace; // Collect remote references by walking document diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs index b0e2a29ae..f33d98465 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs @@ -252,7 +252,7 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode) FixRequestBodyReferences(openApiDoc); // Register components - openApiDoc.Workspace.RegisterComponents(openApiDoc, OpenApiSpecVersion.OpenApi2_0); + openApiDoc.Workspace.RegisterComponents(openApiDoc); return openApiDoc; } diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs index 7a17de018..3fcdb9af7 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs @@ -54,7 +54,7 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode) ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc); // Register components - openApiDoc.Workspace.RegisterComponents(openApiDoc, OpenApiSpecVersion.OpenApi3_0); + openApiDoc.Workspace.RegisterComponents(openApiDoc); return openApiDoc; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs index b6e0fe5fc..8137fb460 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs @@ -53,7 +53,7 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode) ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc); // Register components - openApiDoc.Workspace.RegisterComponents(openApiDoc, OpenApiSpecVersion.OpenApi3_1); + openApiDoc.Workspace.RegisterComponents(openApiDoc); return openApiDoc; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs index a56590bf1..d6c9d0fcf 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs @@ -157,9 +157,11 @@ private static (string, string) GetReferenceIdAndExternalResource(string pointer string refId = !pointer.Contains('#') ? pointer : refSegments.Last(); var isExternalResource = !refSegments.First().StartsWith("#"); - string externalResource = isExternalResource - ? $"{refSegments.First()}/{refSegments[1].TrimEnd('#')}" - : null; + string externalResource = null; + if (isExternalResource && pointer.Contains('#')) + { + externalResource = $"{refSegments.First()}/{refSegments[1].TrimEnd('#')}"; + } return (refId, externalResource); } diff --git a/src/Microsoft.OpenApi/Services/OpenApiComponentsRegistryExtensions.cs b/src/Microsoft.OpenApi/Services/OpenApiComponentsRegistryExtensions.cs deleted file mode 100644 index 226853a13..000000000 --- a/src/Microsoft.OpenApi/Services/OpenApiComponentsRegistryExtensions.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.OpenApi.Extensions; -using Microsoft.OpenApi.Models; - -namespace Microsoft.OpenApi.Services -{ - internal static class OpenApiComponentsRegistryExtensions - { - public static void RegisterComponents(this OpenApiWorkspace workspace, OpenApiDocument document, OpenApiSpecVersion version = OpenApiSpecVersion.OpenApi3_0) - { - if (document?.Components == null) return; - - string baseUri = document.BaseUri + OpenApiConstants.ComponentsSegment; - string location; - - // Register Schema - foreach (var item in document.Components.Schemas) - { - if (item.Value.Id != null) - { - location = item.Value.Id; - } - else - { - location = baseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key; - } - - workspace.RegisterComponent(location, item.Value); - } - - // Register Parameters - foreach (var item in document.Components.Parameters) - { - location = baseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register Responses - foreach (var item in document.Components.Responses) - { - location = baseUri + ReferenceType.Response.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register RequestBodies - foreach (var item in document.Components.RequestBodies) - { - location = baseUri + ReferenceType.RequestBody.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register Links - foreach (var item in document.Components.Links) - { - location = baseUri + ReferenceType.Link.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register Callbacks - foreach (var item in document.Components.Callbacks) - { - location = baseUri + ReferenceType.Callback.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register PathItems - foreach (var item in document.Components.PathItems) - { - location = baseUri + ReferenceType.PathItem.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register Examples - foreach (var item in document.Components.Examples) - { - location = baseUri + ReferenceType.Example.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register Headers - foreach (var item in document.Components.Headers) - { - location = baseUri + ReferenceType.Header.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - - // Register SecuritySchemes - foreach (var item in document.Components.SecuritySchemes) - { - location = baseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key; - workspace.RegisterComponent(location, item.Value); - } - } - } -} diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 319a5d63f..7652ed242 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -54,6 +55,90 @@ public int ComponentsCount() return _IOpenApiReferenceableRegistry.Count + _artifactsRegistry.Count; } + /// + /// Registers a document's components into the workspace + /// + /// + public void RegisterComponents(OpenApiDocument document) + { + if (document?.Components == null) return; + + string baseUri = document.BaseUri + OpenApiConstants.ComponentsSegment; + string location; + + // Register Schema + foreach (var item in document.Components.Schemas) + { + location = item.Value.Id ?? baseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key; + + RegisterComponent(location, item.Value); + } + + // Register Parameters + foreach (var item in document.Components.Parameters) + { + location = baseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register Responses + foreach (var item in document.Components.Responses) + { + location = baseUri + ReferenceType.Response.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register RequestBodies + foreach (var item in document.Components.RequestBodies) + { + location = baseUri + ReferenceType.RequestBody.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register Links + foreach (var item in document.Components.Links) + { + location = baseUri + ReferenceType.Link.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register Callbacks + foreach (var item in document.Components.Callbacks) + { + location = baseUri + ReferenceType.Callback.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register PathItems + foreach (var item in document.Components.PathItems) + { + location = baseUri + ReferenceType.PathItem.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register Examples + foreach (var item in document.Components.Examples) + { + location = baseUri + ReferenceType.Example.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register Headers + foreach (var item in document.Components.Headers) + { + location = baseUri + ReferenceType.Header.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + + // Register SecuritySchemes + foreach (var item in document.Components.SecuritySchemes) + { + location = baseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key; + RegisterComponent(location, item.Value); + } + } + + /// /// Registers a component in the component registry. /// diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs index 2ada8e4bd..c954387a6 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs @@ -1,15 +1,17 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.OpenApi.Extensions; -using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models.References; using Microsoft.OpenApi.Reader; using Microsoft.OpenApi.Tests; using Microsoft.OpenApi.Writers; +using Microsoft.OpenApi.Services; using Xunit; +using System.Linq; namespace Microsoft.OpenApi.Readers.Tests.V31Tests { @@ -392,7 +394,7 @@ public void ParseDocumentsWithReusablePathItemInWebhooksSucceeds() new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_1 }); var outputWriter = new StringWriter(CultureInfo.InvariantCulture); - var writer = new OpenApiJsonWriter(outputWriter, new() { InlineLocalReferences = true } ); + var writer = new OpenApiJsonWriter(outputWriter, new() { InlineLocalReferences = true }); actual.OpenApiDocument.SerializeAsV31(writer); var serialized = outputWriter.ToString(); } @@ -445,7 +447,7 @@ public void ParseDocumentWithPatternPropertiesInSchemaWorks() } } }; - + // Serialization var mediaType = result.OpenApiDocument.Paths["/example"].Operations[OperationType.Get].Responses["200"].Content["application/json"]; @@ -461,7 +463,7 @@ public void ParseDocumentWithPatternPropertiesInSchemaWorks() type: string prop3: type: string"; - + var actualMediaType = mediaType.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_1); // Assert @@ -484,5 +486,49 @@ public void ParseDocumentWithReferenceByIdGetsResolved() Assert.Equal("object", requestBodySchema.Type); Assert.Equal("string", parameterSchema.Type); } + + [Fact] + public async Task ExternalDocumentDereferenceToOpenApiDocumentUsingJsonPointerWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + + // Act + var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalRefByJsonPointer.yaml"), settings); + var responseSchema = result.OpenApiDocument.Paths["/resource"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + result.OpenApiDocument.Workspace.Contains("./externalResource.yaml"); + responseSchema.Properties.Count.Should().Be(2); // reference has been resolved + } + + [Fact] + public async Task ParseExternalDocumentDereferenceToOpenApiDocumentByIdWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + + // Act + var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalRefById.yaml"), settings); + var doc2 = OpenApiDocument.Load(Path.Combine(SampleFolderPath, "externalResource.yaml")).OpenApiDocument; + + var requestBodySchema = result.OpenApiDocument.Paths["/resource"].Operations[OperationType.Get].Parameters.First().Schema; + result.OpenApiDocument.Workspace.RegisterComponents(doc2); + + // Assert + requestBodySchema.Properties.Count.Should().Be(2); // reference has been resolved + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalRefById.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalRefById.yaml new file mode 100644 index 000000000..bb3755180 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalRefById.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +info: + title: ReferenceById + version: 1.0.0 +paths: + /resource: + get: + parameters: + - name: id + in: query + required: true + schema: + $ref: 'https://example.com/schemas/user.json' +components: {} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalRefByJsonPointer.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalRefByJsonPointer.yaml new file mode 100644 index 000000000..913b20e7c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalRefByJsonPointer.yaml @@ -0,0 +1,15 @@ +openapi: 3.1.0 +info: + title: ReferenceById + version: 1.0.0 +paths: + /resource: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './externalResource.yaml#/components/schemas/todo' +components: {} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalResource.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalResource.yaml new file mode 100644 index 000000000..78d6c0851 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/externalResource.yaml @@ -0,0 +1,22 @@ +openapi: 3.1.0 +info: + title: ReferencedById + version: 1.0.0 +paths: {} +components: + schemas: + todo: + type: object + properties: + id: + type: string + name: + type: string + user: + $id: 'https://example.com/schemas/user.json' + type: object + properties: + id: + type: string + name: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs index ec532bed7..2d7354f78 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiPathItemReferenceTests.cs @@ -83,8 +83,8 @@ public OpenApiPathItemReferenceTests() _openApiDoc = OpenApiDocument.Parse(OpenApi, OpenApiConstants.Yaml).OpenApiDocument; _openApiDoc_2 = OpenApiDocument.Parse(OpenApi_2, OpenApiConstants.Yaml).OpenApiDocument; _openApiDoc.Workspace.AddDocumentId("https://myserver.com/beta", _openApiDoc_2.BaseUri); - _openApiDoc.Workspace.RegisterComponents(_openApiDoc_2, OpenApiSpecVersion.OpenApi3_1); - _openApiDoc_2.Workspace.RegisterComponents(_openApiDoc_2, OpenApiSpecVersion.OpenApi3_1); + _openApiDoc.Workspace.RegisterComponents(_openApiDoc_2); + _openApiDoc_2.Workspace.RegisterComponents(_openApiDoc_2); _localPathItemReference = new OpenApiPathItemReference("userPathItem", _openApiDoc_2) { diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 7eb01a70c..00b16a254 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1512,6 +1512,7 @@ namespace Microsoft.OpenApi.Services public bool Contains(string location) { } public System.Uri GetDocumentId(string key) { } public bool RegisterComponent(string location, T component) { } + public void RegisterComponents(Microsoft.OpenApi.Models.OpenApiDocument document) { } public T ResolveReference(string location) { } } public class OperationSearch : Microsoft.OpenApi.Services.OpenApiVisitorBase