diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index e1300935f..d09df2101 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -455,7 +455,10 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum { var nodeLocation = context.GetLocation(); var anchorName = JsonNodeHelper.ExtractDynamicAnchorName(dynamicPointer); - var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument); + string? externalResource = JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicPointer) + ? null + : JsonNodeHelper.ExtractDocumentUri(dynamicPointer); + var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument, externalResource); var referenceMetadata = new OpenApiSchema(); jsonObject.ParseMap(referenceMetadata, _openApiSchemaFixedFields, _openApiSchemaPatternFields, hostDocument, context, static (schema, name, value) => diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index ff26d92c0..723562a74 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -671,7 +671,8 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp /// /// Resolves a $dynamicRef value against the workspace's anchor registries. /// For fragment-only refs (#node), resolves against the host document. - /// For URI refs (absolute), finds the external document and resolves there. + /// For URI refs (absolute or relative), finds the external document and resolves there. + /// Relative URIs are resolved against the host document's BaseUri. /// internal IOpenApiSchema? ResolveDynamicRef(OpenApiDocument hostDocument, string dynamicRef) { @@ -682,7 +683,18 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp if (!JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicRef)) { var docUri = JsonNodeHelper.ExtractDocumentUri(dynamicRef); - targetDoc = docUri is not null ? FindDocumentByBaseUri(docUri) : null; + if (docUri is not null) + { + if (!Uri.IsWellFormedUriString(docUri, UriKind.Absolute) + && hostDocument.BaseUri is not null + && Uri.TryCreate(hostDocument.BaseUri, docUri, out var resolved)) + docUri = resolved.AbsoluteUri; + targetDoc = FindDocumentByBaseUri(docUri); + } + else + { + targetDoc = null; + } } if (targetDoc is null) return null; diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs index 4b8f7580a..49921034c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs @@ -38,6 +38,7 @@ public void BareDynamicRefDeserializesAsSchemaReference() var reference = Assert.IsType(result); Assert.Equal("#category", reference.Reference.DynamicRef); Assert.True(reference.Reference.IsDynamicRefOnly); + Assert.Null(reference.Reference.ExternalResource); } [Fact] @@ -634,7 +635,7 @@ public async Task AbsoluteDynamicRefResolvesAcrossDocuments() docA.BaseUri = new("https://example.com/main"); docB.BaseUri = new("https://example.com/external"); - var workspace = docA.Workspace ?? new OpenApiWorkspace(); + var workspace = docA.Workspace!; workspace.RegisterComponents(docA); workspace.RegisterComponents(docB); @@ -1493,7 +1494,7 @@ public async Task ExternalDynamicRefResolvesToPlainAnchorInExternalDocument() docA.BaseUri = new("https://example.com/main"); docB.BaseUri = new("https://example.com/external"); - var workspace = docA.Workspace ?? new OpenApiWorkspace(); + var workspace = docA.Workspace!; workspace.RegisterComponents(docA); workspace.RegisterComponents(docB); @@ -1501,6 +1502,67 @@ public async Task ExternalDynamicRefResolvesToPlainAnchorInExternalDocument() var next = tree.Properties["next"]; var reference = Assert.IsType(next); Assert.True(reference.Reference.IsDynamicRefOnly); + Assert.Equal("https://example.com/external", reference.Reference.ExternalResource); + Assert.NotNull(reference.Target); + Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target); + } + + [Fact] + public async Task ExternalDynamicRefWithRelativeUriResolvesAcrossDocuments() + { + var yamlA = + """ + openapi: 3.1.0 + info: + title: Doc A + version: 1.0.0 + paths: {} + components: + schemas: + Tree: + type: object + properties: + next: + $dynamicRef: 'external.yaml#node' + """; + + var yamlB = + """ + openapi: 3.1.0 + info: + title: Doc B + version: 1.0.0 + paths: {} + components: + schemas: + ExternalNode: + $dynamicAnchor: node + type: object + properties: + value: + type: string + """; + + using var streamA = new MemoryStream(Encoding.UTF8.GetBytes(yamlA)); + using var streamB = new MemoryStream(Encoding.UTF8.GetBytes(yamlB)); + var resultA = await OpenApiDocument.LoadAsync(streamA, "yaml", SettingsFixture.ReaderSettings); + var resultB = await OpenApiDocument.LoadAsync(streamB, "yaml", SettingsFixture.ReaderSettings); + + var docA = resultA.Document; + var docB = resultB.Document; + + docA.BaseUri = new("https://example.com/main.yaml"); + docB.BaseUri = new("https://example.com/external.yaml"); + + var workspace = docA.Workspace!; + workspace.RegisterComponents(docA); + workspace.RegisterComponents(docB); + + var tree = docA.Components.Schemas["Tree"]; + var next = tree.Properties["next"]; + var reference = Assert.IsType(next); + Assert.True(reference.Reference.IsDynamicRefOnly); + Assert.Equal("external.yaml", reference.Reference.ExternalResource); Assert.NotNull(reference.Target); Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target); } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConstNull.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConstNull.json deleted file mode 100644 index 586d2f726..000000000 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConstNull.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "status": { - "type": "string", - "const": null - }, - "user": { - "type": "object", - "properties": { - "role": { - "type": "string", - "const": null - } - }, - "required": [ "role" ] - } - }, - "required": [ "status" ] -} - diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml deleted file mode 100644 index 72b4e9ae8..000000000 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml +++ /dev/null @@ -1,2 +0,0 @@ -type: mutualTLS -description: Sample Description