Skip to content

Commit 6091f5a

Browse files
authored
Merge pull request #2961 from microsoft/feat/external-dynamic-to-v2
feat: support relative URI resolution in $dynamicRef (#2928) (#2945)
2 parents 26de615 + a8688dc commit 6091f5a

5 files changed

Lines changed: 82 additions & 29 deletions

File tree

src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,10 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
455455
{
456456
var nodeLocation = context.GetLocation();
457457
var anchorName = JsonNodeHelper.ExtractDynamicAnchorName(dynamicPointer);
458-
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument);
458+
string? externalResource = JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicPointer)
459+
? null
460+
: JsonNodeHelper.ExtractDocumentUri(dynamicPointer);
461+
var result = new OpenApiSchemaReference(!string.IsNullOrEmpty(anchorName) ? anchorName! : dynamicPointer, hostDocument, externalResource);
459462
var referenceMetadata = new OpenApiSchema();
460463
jsonObject.ParseMap(referenceMetadata, _openApiSchemaFixedFields, _openApiSchemaPatternFields, hostDocument, context,
461464
static (schema, name, value) =>

src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,8 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp
671671
/// <summary>
672672
/// Resolves a $dynamicRef value against the workspace's anchor registries.
673673
/// For fragment-only refs (#node), resolves against the host document.
674-
/// For URI refs (absolute), finds the external document and resolves there.
674+
/// For URI refs (absolute or relative), finds the external document and resolves there.
675+
/// Relative URIs are resolved against the host document's BaseUri.
675676
/// </summary>
676677
internal IOpenApiSchema? ResolveDynamicRef(OpenApiDocument hostDocument, string dynamicRef)
677678
{
@@ -682,7 +683,18 @@ private void RegisterAnchor(OpenApiDocument document, string anchorName, IOpenAp
682683
if (!JsonNodeHelper.IsFragmentOnlyDynamicRef(dynamicRef))
683684
{
684685
var docUri = JsonNodeHelper.ExtractDocumentUri(dynamicRef);
685-
targetDoc = docUri is not null ? FindDocumentByBaseUri(docUri) : null;
686+
if (docUri is not null)
687+
{
688+
if (!Uri.IsWellFormedUriString(docUri, UriKind.Absolute)
689+
&& hostDocument.BaseUri is not null
690+
&& Uri.TryCreate(hostDocument.BaseUri, docUri, out var resolved))
691+
docUri = resolved.AbsoluteUri;
692+
targetDoc = FindDocumentByBaseUri(docUri);
693+
}
694+
else
695+
{
696+
targetDoc = null;
697+
}
686698
}
687699

688700
if (targetDoc is null) return null;

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public void BareDynamicRefDeserializesAsSchemaReference()
3838
var reference = Assert.IsType<OpenApiSchemaReference>(result);
3939
Assert.Equal("#category", reference.Reference.DynamicRef);
4040
Assert.True(reference.Reference.IsDynamicRefOnly);
41+
Assert.Null(reference.Reference.ExternalResource);
4142
}
4243

4344
[Fact]
@@ -634,7 +635,7 @@ public async Task AbsoluteDynamicRefResolvesAcrossDocuments()
634635
docA.BaseUri = new("https://example.com/main");
635636
docB.BaseUri = new("https://example.com/external");
636637

637-
var workspace = docA.Workspace ?? new OpenApiWorkspace();
638+
var workspace = docA.Workspace!;
638639
workspace.RegisterComponents(docA);
639640
workspace.RegisterComponents(docB);
640641

@@ -1493,14 +1494,75 @@ public async Task ExternalDynamicRefResolvesToPlainAnchorInExternalDocument()
14931494
docA.BaseUri = new("https://example.com/main");
14941495
docB.BaseUri = new("https://example.com/external");
14951496

1496-
var workspace = docA.Workspace ?? new OpenApiWorkspace();
1497+
var workspace = docA.Workspace!;
14971498
workspace.RegisterComponents(docA);
14981499
workspace.RegisterComponents(docB);
14991500

15001501
var tree = docA.Components.Schemas["Tree"];
15011502
var next = tree.Properties["next"];
15021503
var reference = Assert.IsType<OpenApiSchemaReference>(next);
15031504
Assert.True(reference.Reference.IsDynamicRefOnly);
1505+
Assert.Equal("https://example.com/external", reference.Reference.ExternalResource);
1506+
Assert.NotNull(reference.Target);
1507+
Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target);
1508+
}
1509+
1510+
[Fact]
1511+
public async Task ExternalDynamicRefWithRelativeUriResolvesAcrossDocuments()
1512+
{
1513+
var yamlA =
1514+
"""
1515+
openapi: 3.1.0
1516+
info:
1517+
title: Doc A
1518+
version: 1.0.0
1519+
paths: {}
1520+
components:
1521+
schemas:
1522+
Tree:
1523+
type: object
1524+
properties:
1525+
next:
1526+
$dynamicRef: 'external.yaml#node'
1527+
""";
1528+
1529+
var yamlB =
1530+
"""
1531+
openapi: 3.1.0
1532+
info:
1533+
title: Doc B
1534+
version: 1.0.0
1535+
paths: {}
1536+
components:
1537+
schemas:
1538+
ExternalNode:
1539+
$dynamicAnchor: node
1540+
type: object
1541+
properties:
1542+
value:
1543+
type: string
1544+
""";
1545+
1546+
using var streamA = new MemoryStream(Encoding.UTF8.GetBytes(yamlA));
1547+
using var streamB = new MemoryStream(Encoding.UTF8.GetBytes(yamlB));
1548+
var resultA = await OpenApiDocument.LoadAsync(streamA, "yaml", SettingsFixture.ReaderSettings);
1549+
var resultB = await OpenApiDocument.LoadAsync(streamB, "yaml", SettingsFixture.ReaderSettings);
1550+
1551+
var docA = resultA.Document;
1552+
var docB = resultB.Document;
1553+
1554+
docA.BaseUri = new("https://example.com/main.yaml");
1555+
docB.BaseUri = new("https://example.com/external.yaml");
1556+
1557+
var workspace = docA.Workspace!;
1558+
workspace.RegisterComponents(docA);
1559+
workspace.RegisterComponents(docB);
1560+
1561+
var tree = docA.Components.Schemas["Tree"];
1562+
var next = tree.Properties["next"];
1563+
var reference = Assert.IsType<OpenApiSchemaReference>(next);
1564+
Assert.True(reference.Reference.IsDynamicRefOnly);
1565+
Assert.Equal("external.yaml", reference.Reference.ExternalResource);
15041566
Assert.NotNull(reference.Target);
15051567
Assert.Same(docB.Components.Schemas["ExternalNode"], reference.Target);
15061568
}

test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConstNull.json

Lines changed: 0 additions & 22 deletions
This file was deleted.

test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)