Skip to content

Commit 4d295d8

Browse files
committed
Fix: relative references in subdirectory documents are not loading microsoft#1674
Use OpenApiDocuments BaseUri as location of the document. This allows to have during loading further documents a base Url for retrieval, which can be combined with a relative Uri to get an absolute.
1 parent f7529ad commit 4d295d8

24 files changed

+242
-69
lines changed

src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,27 @@ public class OpenApiYamlReader : IOpenApiReader
2626

2727
/// <inheritdoc/>
2828
public async Task<ReadResult> ReadAsync(Stream input,
29+
Uri location,
2930
OpenApiReaderSettings settings,
3031
CancellationToken cancellationToken = default)
3132
{
3233
if (input is null) throw new ArgumentNullException(nameof(input));
3334
if (input is MemoryStream memoryStream)
3435
{
35-
return Read(memoryStream, settings);
36+
return Read(memoryStream, location, settings);
3637
}
3738
else
3839
{
3940
using var preparedStream = new MemoryStream();
4041
await input.CopyToAsync(preparedStream, copyBufferSize, cancellationToken).ConfigureAwait(false);
4142
preparedStream.Position = 0;
42-
return Read(preparedStream, settings);
43+
return Read(preparedStream, location, settings);
4344
}
4445
}
4546

4647
/// <inheritdoc/>
4748
public ReadResult Read(MemoryStream input,
49+
Uri location,
4850
OpenApiReaderSettings settings)
4951
{
5052
if (input is null) throw new ArgumentNullException(nameof(input));
@@ -74,13 +76,13 @@ public ReadResult Read(MemoryStream input,
7476
};
7577
}
7678

77-
return Read(jsonNode, settings);
79+
return Read(jsonNode, location, settings);
7880
}
7981

8082
/// <inheritdoc/>
81-
public static ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings)
83+
public static ReadResult Read(JsonNode jsonNode, Uri location, OpenApiReaderSettings settings)
8284
{
83-
return _jsonReader.Read(jsonNode, settings);
85+
return _jsonReader.Read(jsonNode, location, settings);
8486
}
8587

8688
/// <inheritdoc/>

src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System;
45
using System.IO;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -18,18 +19,20 @@ public interface IOpenApiReader
1819
/// Async method to reads the stream and parse it into an Open API document.
1920
/// </summary>
2021
/// <param name="input">The stream input.</param>
22+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
2123
/// <param name="settings"> The OpenApi reader settings.</param>
2224
/// <param name="cancellationToken">Propagates notification that an operation should be cancelled.</param>
2325
/// <returns></returns>
24-
Task<ReadResult> ReadAsync(Stream input, OpenApiReaderSettings settings, CancellationToken cancellationToken = default);
26+
Task<ReadResult> ReadAsync(Stream input, Uri location, OpenApiReaderSettings settings, CancellationToken cancellationToken = default);
2527

2628
/// <summary>
2729
/// Provides a synchronous method to read the input memory stream and parse it into an Open API document.
2830
/// </summary>
2931
/// <param name="input"></param>
32+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
3033
/// <param name="settings"></param>
3134
/// <returns></returns>
32-
ReadResult Read(MemoryStream input, OpenApiReaderSettings settings);
35+
ReadResult Read(MemoryStream input, Uri location, OpenApiReaderSettings settings);
3336

3437
/// <summary>
3538
/// Reads the MemoryStream and parses the fragment of an OpenAPI description into an Open API Element.

src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System;
45
using Microsoft.OpenApi.Models;
56
using Microsoft.OpenApi.Reader.ParseNodes;
67

@@ -34,8 +35,9 @@ internal interface IOpenApiVersionService
3435
/// Converts a generic RootNode instance into a strongly typed OpenApiDocument
3536
/// </summary>
3637
/// <param name="rootNode">RootNode containing the information to be converted into an OpenAPI Document</param>
38+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
3739
/// <returns>Instance of OpenApiDocument populated with data from rootNode</returns>
38-
OpenApiDocument LoadDocument(RootNode rootNode);
40+
OpenApiDocument LoadDocument(RootNode rootNode, Uri location);
3941

4042
/// <summary>
4143
/// Gets the description and summary scalar values in a reference object for V3.1 support

src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ public interface IStreamLoader
1717
/// <summary>
1818
/// Use Uri to locate data and convert into an input object.
1919
/// </summary>
20+
/// <param name="baseUrl">Base URL of parent to which a relative reference could be loaded.
21+
/// If the <paramref name="uri"/> is an absolute parameter the value of this parameter will be ignored</param>
2022
/// <param name="uri">Identifier of some source of an OpenAPI Description</param>
2123
/// <param name="cancellationToken">The cancellation token.</param>
2224
/// <returns>A data object that can be processed by a reader to generate an <see cref="OpenApiDocument"/></returns>
23-
Task<Stream> LoadAsync(Uri uri, CancellationToken cancellationToken = default);
25+
Task<Stream> LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default);
2426
}
2527
}

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ public ISet<OpenApiTag>? Tags
112112
public IDictionary<string, object>? Metadata { get; set; }
113113

114114
/// <summary>
115-
/// Implements IBaseDocument
115+
/// Absolute location of the document or a generated placeholder if location is not given
116116
/// </summary>
117-
public Uri BaseUri { get; }
117+
public Uri BaseUri { get; internal set; }
118118

119119
/// <summary>
120120
/// Parameter-less constructor
@@ -533,14 +533,15 @@ private static string ConvertByteArrayToString(byte[] hash)
533533
}
534534
else
535535
{
536-
string relativePath = OpenApiConstants.ComponentsSegment + reference.Type.GetDisplayName() + "/" + reference.Id;
536+
string relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{reference.Id}";
537+
Uri? externalResourceUri = useExternal ? Workspace?.GetDocumentId(reference.ExternalResource) : null;
537538

538-
uriLocation = useExternal
539-
? Workspace?.GetDocumentId(reference.ExternalResource)?.OriginalString + relativePath
539+
uriLocation = useExternal && externalResourceUri is not null
540+
? externalResourceUri.AbsoluteUri + relativePath
540541
: BaseUri + relativePath;
541542
}
542543

543-
return Workspace?.ResolveReference<IOpenApiReferenceable>(uriLocation);
544+
return Workspace?.ResolveReference<IOpenApiReferenceable>(new Uri(uriLocation).AbsoluteUri);
544545
}
545546

546547
/// <summary>

src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ public class OpenApiJsonReader : IOpenApiReader
2525
/// Reads the memory stream input and parses it into an Open API document.
2626
/// </summary>
2727
/// <param name="input">Memory stream containing OpenAPI description to parse.</param>
28+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
2829
/// <param name="settings">The Reader settings to be used during parsing.</param>
2930
/// <returns></returns>
3031
public ReadResult Read(MemoryStream input,
32+
Uri location,
3133
OpenApiReaderSettings settings)
3234
{
3335
if (input is null) throw new ArgumentNullException(nameof(input));
@@ -52,16 +54,18 @@ public ReadResult Read(MemoryStream input,
5254
};
5355
}
5456

55-
return Read(jsonNode, settings);
57+
return Read(jsonNode, location, settings);
5658
}
5759

5860
/// <summary>
5961
/// Parses the JsonNode input into an Open API document.
6062
/// </summary>
6163
/// <param name="jsonNode">The JsonNode input.</param>
64+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
6265
/// <param name="settings">The Reader settings to be used during parsing.</param>
6366
/// <returns></returns>
6467
public ReadResult Read(JsonNode jsonNode,
68+
Uri location,
6569
OpenApiReaderSettings settings)
6670
{
6771
if (jsonNode is null) throw new ArgumentNullException(nameof(jsonNode));
@@ -79,7 +83,7 @@ public ReadResult Read(JsonNode jsonNode,
7983
try
8084
{
8185
// Parse the OpenAPI Document
82-
document = context.Parse(jsonNode);
86+
document = context.Parse(jsonNode, location);
8387
document.SetReferenceHostDocument();
8488
}
8589
catch (OpenApiException ex)
@@ -112,10 +116,12 @@ public ReadResult Read(JsonNode jsonNode,
112116
/// Reads the stream input asynchronously and parses it into an Open API document.
113117
/// </summary>
114118
/// <param name="input">Memory stream containing OpenAPI description to parse.</param>
119+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
115120
/// <param name="settings">The Reader settings to be used during parsing.</param>
116121
/// <param name="cancellationToken">Propagates notifications that operations should be cancelled.</param>
117122
/// <returns></returns>
118123
public async Task<ReadResult> ReadAsync(Stream input,
124+
Uri location,
119125
OpenApiReaderSettings settings,
120126
CancellationToken cancellationToken = default)
121127
{
@@ -140,7 +146,7 @@ public async Task<ReadResult> ReadAsync(Stream input,
140146
};
141147
}
142148

143-
return Read(jsonNode, settings);
149+
return Read(jsonNode, location, settings);
144150
}
145151

146152
/// <inheritdoc/>

src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs

+12-5
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,13 @@ private static async Task<ReadResult> InternalLoadAsync(Stream input, string for
240240
{
241241
settings ??= DefaultReaderSettings.Value;
242242
var reader = settings.GetReader(format);
243-
var readResult = await reader.ReadAsync(input, settings, cancellationToken).ConfigureAwait(false);
243+
var location = new Uri(OpenApiConstants.BaseRegistryUri);
244+
if (input is FileStream fileStream)
245+
{
246+
location = new Uri(fileStream.Name);
247+
}
248+
249+
var readResult = await reader.ReadAsync(input, location, settings, cancellationToken).ConfigureAwait(false);
244250

245251
if (settings.LoadExternalRefs)
246252
{
@@ -259,11 +265,11 @@ private static async Task<ReadResult> InternalLoadAsync(Stream input, string for
259265
private static async Task<OpenApiDiagnostic> LoadExternalRefsAsync(OpenApiDocument document, OpenApiReaderSettings settings, string format = null, CancellationToken token = default)
260266
{
261267
// Create workspace for all documents to live in.
262-
var baseUrl = settings.BaseUrl ?? new Uri(OpenApiConstants.BaseRegistryUri);
263-
var openApiWorkSpace = new OpenApiWorkspace(baseUrl);
268+
var baseUrl = document.BaseUri;
269+
var openApiWorkSpace = document.Workspace;
264270

265271
// Load this root document into the workspace
266-
var streamLoader = new DefaultStreamLoader(settings.BaseUrl, settings.HttpClient);
272+
var streamLoader = new DefaultStreamLoader(settings.HttpClient);
267273
var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, settings.CustomExternalLoader ?? streamLoader, settings);
268274
return await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, format ?? OpenApiConstants.Json, null, token).ConfigureAwait(false);
269275
}
@@ -280,8 +286,9 @@ private static ReadResult InternalLoad(MemoryStream input, string format, OpenAp
280286
throw new ArgumentException($"Cannot parse the stream: {nameof(input)} is empty or contains no elements.");
281287
}
282288

289+
var location = new Uri(OpenApiConstants.BaseRegistryUri);
283290
var reader = settings.GetReader(format);
284-
var readResult = reader.Read(input, settings);
291+
var readResult = reader.Read(input, location, settings);
285292
return readResult;
286293
}
287294

src/Microsoft.OpenApi/Reader/ParsingContext.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ public ParsingContext(OpenApiDiagnostic diagnostic)
6262
/// Initiates the parsing process. Not thread safe and should only be called once on a parsing context
6363
/// </summary>
6464
/// <param name="jsonNode">Set of Json nodes to parse.</param>
65+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
6566
/// <returns>An OpenApiDocument populated based on the passed yamlDocument </returns>
66-
public OpenApiDocument Parse(JsonNode jsonNode)
67+
public OpenApiDocument Parse(JsonNode jsonNode, Uri location)
6768
{
6869
RootNode = new RootNode(this, jsonNode);
6970

@@ -75,20 +76,20 @@ public OpenApiDocument Parse(JsonNode jsonNode)
7576
{
7677
case string version when version.is2_0():
7778
VersionService = new OpenApiV2VersionService(Diagnostic);
78-
doc = VersionService.LoadDocument(RootNode);
79+
doc = VersionService.LoadDocument(RootNode, location);
7980
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0;
8081
ValidateRequiredFields(doc, version);
8182
break;
8283

8384
case string version when version.is3_0():
8485
VersionService = new OpenApiV3VersionService(Diagnostic);
85-
doc = VersionService.LoadDocument(RootNode);
86+
doc = VersionService.LoadDocument(RootNode, location);
8687
this.Diagnostic.SpecificationVersion = version.is3_1() ? OpenApiSpecVersion.OpenApi3_1 : OpenApiSpecVersion.OpenApi3_0;
8788
ValidateRequiredFields(doc, version);
8889
break;
8990
case string version when version.is3_1():
9091
VersionService = new OpenApiV31VersionService(Diagnostic);
91-
doc = VersionService.LoadDocument(RootNode);
92+
doc = VersionService.LoadDocument(RootNode, location);
9293
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_1;
9394
ValidateRequiredFields(doc, version);
9495
break;

src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs

+2-5
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,19 @@ namespace Microsoft.OpenApi.Reader.Services
1717
/// </summary>
1818
public class DefaultStreamLoader : IStreamLoader
1919
{
20-
private readonly Uri baseUrl;
2120
private readonly HttpClient _httpClient;
2221

2322
/// <summary>
2423
/// The default stream loader
2524
/// </summary>
26-
/// <param name="baseUrl"></param>
2725
/// <param name="httpClient">The HttpClient to use to retrieve documents when needed</param>
28-
public DefaultStreamLoader(Uri baseUrl, HttpClient httpClient)
26+
public DefaultStreamLoader(HttpClient httpClient)
2927
{
30-
this.baseUrl = baseUrl;
3128
_httpClient = Utils.CheckArgumentNull(httpClient);
3229
}
3330

3431
/// <inheritdoc/>
35-
public async Task<Stream> LoadAsync(Uri uri, CancellationToken cancellationToken = default)
32+
public async Task<Stream> LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default)
3633
{
3734
var absoluteUri = (baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri), baseUrl.IsAbsoluteUri, uri.IsAbsoluteUri) switch
3835
{

src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ internal async Task<OpenApiDiagnostic> LoadAsync(OpenApiReference reference,
3737
collectorWalker.Walk(document);
3838

3939
diagnostic ??= new() { SpecificationVersion = version };
40-
40+
4141
// Walk references
4242
foreach (var item in referenceCollector.References)
4343
{
4444

4545
// If not already in workspace, load it and process references
4646
if (!_workspace.Contains(item.ExternalResource))
4747
{
48-
var input = await _loader.LoadAsync(new(item.ExternalResource, UriKind.RelativeOrAbsolute), cancellationToken).ConfigureAwait(false);
48+
var uri = new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute);
49+
var input = await _loader.LoadAsync(item.HostDocument.BaseUri, uri, cancellationToken).ConfigureAwait(false);
4950
var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false);
5051
// Merge diagnostics
5152
if (result.Diagnostic != null)

src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,12 @@ private static string BuildUrl(string scheme, string host, string basePath)
221221
return uriBuilder.ToString();
222222
}
223223

224-
public static OpenApiDocument LoadOpenApi(RootNode rootNode)
224+
public static OpenApiDocument LoadOpenApi(RootNode rootNode, Uri location)
225225
{
226-
var openApiDoc = new OpenApiDocument();
226+
var openApiDoc = new OpenApiDocument
227+
{
228+
BaseUri = location
229+
};
227230

228231
var openApiNode = rootNode.GetMap();
229232

src/Microsoft.OpenApi/Reader/V2/OpenApiV2VersionService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ public OpenApiReference ConvertToOpenApiReference(string reference, ReferenceTyp
209209
throw new OpenApiException(string.Format(SRResource.ReferenceHasInvalidFormat, reference));
210210
}
211211

212-
public OpenApiDocument LoadDocument(RootNode rootNode)
212+
public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
213213
{
214-
return OpenApiV2Deserializer.LoadOpenApi(rootNode);
214+
return OpenApiV2Deserializer.LoadOpenApi(rootNode, location);
215215
}
216216

217217
public T LoadElement<T>(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement

src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ internal static partial class OpenApiV3Deserializer
3838
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p, n))}
3939
};
4040

41-
public static OpenApiDocument LoadOpenApi(RootNode rootNode)
41+
public static OpenApiDocument LoadOpenApi(RootNode rootNode, Uri location)
4242
{
43-
var openApiDoc = new OpenApiDocument();
43+
var openApiDoc = new OpenApiDocument
44+
{
45+
BaseUri = location
46+
};
4447
var openApiNode = rootNode.GetMap();
4548

4649
ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc);

src/Microsoft.OpenApi/Reader/V3/OpenApiV3VersionService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,9 @@ public OpenApiReference ConvertToOpenApiReference(
170170
throw new OpenApiException(string.Format(SRResource.ReferenceHasInvalidFormat, reference));
171171
}
172172

173-
public OpenApiDocument LoadDocument(RootNode rootNode)
173+
public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
174174
{
175-
return OpenApiV3Deserializer.LoadOpenApi(rootNode);
175+
return OpenApiV3Deserializer.LoadOpenApi(rootNode, location);
176176
}
177177

178178
public T LoadElement<T>(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement

0 commit comments

Comments
 (0)