Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: relative references in subdirectory documents are not loading #1674 #2243

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,27 @@ public class OpenApiYamlReader : IOpenApiReader

/// <inheritdoc/>
public async Task<ReadResult> ReadAsync(Stream input,
Uri location,
OpenApiReaderSettings settings,
CancellationToken cancellationToken = default)
{
if (input is null) throw new ArgumentNullException(nameof(input));
if (input is MemoryStream memoryStream)
{
return Read(memoryStream, settings);
return Read(memoryStream, location, settings);
}
else
{
using var preparedStream = new MemoryStream();
await input.CopyToAsync(preparedStream, copyBufferSize, cancellationToken).ConfigureAwait(false);
preparedStream.Position = 0;
return Read(preparedStream, settings);
return Read(preparedStream, location, settings);
}
}

/// <inheritdoc/>
public ReadResult Read(MemoryStream input,
Uri location,
OpenApiReaderSettings settings)
{
if (input is null) throw new ArgumentNullException(nameof(input));
Expand Down Expand Up @@ -74,13 +76,13 @@ public ReadResult Read(MemoryStream input,
};
}

return Read(jsonNode, settings);
return Read(jsonNode, location, settings);
}

/// <inheritdoc/>
public static ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings)
public static ReadResult Read(JsonNode jsonNode, Uri location, OpenApiReaderSettings settings)
{
return _jsonReader.Read(jsonNode, settings);
return _jsonReader.Read(jsonNode, location, settings);
}

/// <inheritdoc/>
Expand Down
7 changes: 5 additions & 2 deletions src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

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

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

/// <summary>
/// Reads the MemoryStream and parses the fragment of an OpenAPI description into an Open API Element.
Expand Down
6 changes: 4 additions & 2 deletions src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Reader.ParseNodes;

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

/// <summary>
/// Gets the description and summary scalar values in a reference object for V3.1 support
Expand Down
4 changes: 3 additions & 1 deletion src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ public interface IStreamLoader
/// <summary>
/// Use Uri to locate data and convert into an input object.
/// </summary>
/// <param name="baseUrl">Base URL of parent to which a relative reference could be loaded.
/// If the <paramref name="uri"/> is an absolute parameter the value of this parameter will be ignored</param>
/// <param name="uri">Identifier of some source of an OpenAPI Description</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A data object that can be processed by a reader to generate an <see cref="OpenApiDocument"/></returns>
Task<Stream> LoadAsync(Uri uri, CancellationToken cancellationToken = default);
Task<Stream> LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default);
}
}
13 changes: 7 additions & 6 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ public ISet<OpenApiTag>? Tags
public IDictionary<string, object>? Metadata { get; set; }

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

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

uriLocation = useExternal
? Workspace?.GetDocumentId(reference.ExternalResource)?.OriginalString + relativePath
uriLocation = useExternal && externalResourceUri is not null
? externalResourceUri.AbsoluteUri + relativePath
: BaseUri + relativePath;
}

return Workspace?.ResolveReference<IOpenApiReferenceable>(uriLocation);
return Workspace?.ResolveReference<IOpenApiReferenceable>(new Uri(uriLocation).AbsoluteUri);
}

/// <summary>
Expand Down
12 changes: 9 additions & 3 deletions src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ public class OpenApiJsonReader : IOpenApiReader
/// Reads the memory stream input and parses it into an Open API document.
/// </summary>
/// <param name="input">Memory stream containing OpenAPI description to parse.</param>
/// <param name="location">Location of where the document that is getting loaded is saved</param>
/// <param name="settings">The Reader settings to be used during parsing.</param>
/// <returns></returns>
public ReadResult Read(MemoryStream input,
Uri location,
OpenApiReaderSettings settings)
{
if (input is null) throw new ArgumentNullException(nameof(input));
Expand All @@ -52,16 +54,18 @@ public ReadResult Read(MemoryStream input,
};
}

return Read(jsonNode, settings);
return Read(jsonNode, location, settings);
}

/// <summary>
/// Parses the JsonNode input into an Open API document.
/// </summary>
/// <param name="jsonNode">The JsonNode input.</param>
/// <param name="location">Location of where the document that is getting loaded is saved</param>
/// <param name="settings">The Reader settings to be used during parsing.</param>
/// <returns></returns>
public ReadResult Read(JsonNode jsonNode,
Uri location,
OpenApiReaderSettings settings)
{
if (jsonNode is null) throw new ArgumentNullException(nameof(jsonNode));
Expand All @@ -79,7 +83,7 @@ public ReadResult Read(JsonNode jsonNode,
try
{
// Parse the OpenAPI Document
document = context.Parse(jsonNode);
document = context.Parse(jsonNode, location);
document.SetReferenceHostDocument();
}
catch (OpenApiException ex)
Expand Down Expand Up @@ -112,10 +116,12 @@ public ReadResult Read(JsonNode jsonNode,
/// Reads the stream input asynchronously and parses it into an Open API document.
/// </summary>
/// <param name="input">Memory stream containing OpenAPI description to parse.</param>
/// <param name="location">Location of where the document that is getting loaded is saved</param>
/// <param name="settings">The Reader settings to be used during parsing.</param>
/// <param name="cancellationToken">Propagates notifications that operations should be cancelled.</param>
/// <returns></returns>
public async Task<ReadResult> ReadAsync(Stream input,
Uri location,
OpenApiReaderSettings settings,
CancellationToken cancellationToken = default)
{
Expand All @@ -140,7 +146,7 @@ public async Task<ReadResult> ReadAsync(Stream input,
};
}

return Read(jsonNode, settings);
return Read(jsonNode, location, settings);
}

/// <inheritdoc/>
Expand Down
21 changes: 12 additions & 9 deletions src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,13 @@ private static async Task<ReadResult> InternalLoadAsync(Stream input, string for
{
settings ??= DefaultReaderSettings.Value;
var reader = settings.GetReader(format);
var readResult = await reader.ReadAsync(input, settings, cancellationToken).ConfigureAwait(false);
var location = new Uri(OpenApiConstants.BaseRegistryUri);
if (input is FileStream fileStream)
{
location = new Uri(fileStream.Name);
}

var readResult = await reader.ReadAsync(input, location, settings, cancellationToken).ConfigureAwait(false);

if (settings.LoadExternalRefs)
{
Expand All @@ -258,13 +264,9 @@ private static async Task<ReadResult> InternalLoadAsync(Stream input, string for

private static async Task<OpenApiDiagnostic> LoadExternalRefsAsync(OpenApiDocument document, OpenApiReaderSettings settings, string format = null, CancellationToken token = default)
{
// Create workspace for all documents to live in.
var baseUrl = settings.BaseUrl ?? new Uri(OpenApiConstants.BaseRegistryUri);
var openApiWorkSpace = new OpenApiWorkspace(baseUrl);

// Load this root document into the workspace
var streamLoader = new DefaultStreamLoader(settings.BaseUrl, settings.HttpClient);
var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, settings.CustomExternalLoader ?? streamLoader, settings);
// Load this document into the workspace
var streamLoader = new DefaultStreamLoader(settings.HttpClient);
var workspaceLoader = new OpenApiWorkspaceLoader(document.Workspace, settings.CustomExternalLoader ?? streamLoader, settings);
return await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, format ?? OpenApiConstants.Json, null, token).ConfigureAwait(false);
}

Expand All @@ -280,8 +282,9 @@ private static ReadResult InternalLoad(MemoryStream input, string format, OpenAp
throw new ArgumentException($"Cannot parse the stream: {nameof(input)} is empty or contains no elements.");
}

var location = new Uri(OpenApiConstants.BaseRegistryUri);
var reader = settings.GetReader(format);
var readResult = reader.Read(input, settings);
var readResult = reader.Read(input, location, settings);
return readResult;
}

Expand Down
9 changes: 5 additions & 4 deletions src/Microsoft.OpenApi/Reader/ParsingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ public ParsingContext(OpenApiDiagnostic diagnostic)
/// Initiates the parsing process. Not thread safe and should only be called once on a parsing context
/// </summary>
/// <param name="jsonNode">Set of Json nodes to parse.</param>
/// <param name="location">Location of where the document that is getting loaded is saved</param>
/// <returns>An OpenApiDocument populated based on the passed yamlDocument </returns>
public OpenApiDocument Parse(JsonNode jsonNode)
public OpenApiDocument Parse(JsonNode jsonNode, Uri location)
{
RootNode = new RootNode(this, jsonNode);

Expand All @@ -75,20 +76,20 @@ public OpenApiDocument Parse(JsonNode jsonNode)
{
case string version when version.is2_0():
VersionService = new OpenApiV2VersionService(Diagnostic);
doc = VersionService.LoadDocument(RootNode);
doc = VersionService.LoadDocument(RootNode, location);
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0;
ValidateRequiredFields(doc, version);
break;

case string version when version.is3_0():
VersionService = new OpenApiV3VersionService(Diagnostic);
doc = VersionService.LoadDocument(RootNode);
doc = VersionService.LoadDocument(RootNode, location);
this.Diagnostic.SpecificationVersion = version.is3_1() ? OpenApiSpecVersion.OpenApi3_1 : OpenApiSpecVersion.OpenApi3_0;
ValidateRequiredFields(doc, version);
break;
case string version when version.is3_1():
VersionService = new OpenApiV31VersionService(Diagnostic);
doc = VersionService.LoadDocument(RootNode);
doc = VersionService.LoadDocument(RootNode, location);
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_1;
ValidateRequiredFields(doc, version);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,19 @@ namespace Microsoft.OpenApi.Reader.Services
/// </summary>
public class DefaultStreamLoader : IStreamLoader
{
private readonly Uri baseUrl;
private readonly HttpClient _httpClient;

/// <summary>
/// The default stream loader
/// </summary>
/// <param name="baseUrl"></param>
/// <param name="httpClient">The HttpClient to use to retrieve documents when needed</param>
public DefaultStreamLoader(Uri baseUrl, HttpClient httpClient)
public DefaultStreamLoader(HttpClient httpClient)
{
this.baseUrl = baseUrl;
_httpClient = Utils.CheckArgumentNull(httpClient);
}

/// <inheritdoc/>
public async Task<Stream> LoadAsync(Uri uri, CancellationToken cancellationToken = default)
public async Task<Stream> LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default)
{
var absoluteUri = (baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri), baseUrl.IsAbsoluteUri, uri.IsAbsoluteUri) switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ internal async Task<OpenApiDiagnostic> LoadAsync(OpenApiReference reference,
// If not already in workspace, load it and process references
if (!_workspace.Contains(item.ExternalResource))
{
var input = await _loader.LoadAsync(new(item.ExternalResource, UriKind.RelativeOrAbsolute), cancellationToken).ConfigureAwait(false);
var uri = new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute);
var input = await _loader.LoadAsync(item.HostDocument.BaseUri, uri, cancellationToken).ConfigureAwait(false);
var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false);
// Merge diagnostics
if (result.Diagnostic != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,12 @@ private static string BuildUrl(string scheme, string host, string basePath)
return uriBuilder.ToString();
}

public static OpenApiDocument LoadOpenApi(RootNode rootNode)
public static OpenApiDocument LoadOpenApi(RootNode rootNode, Uri location)
{
var openApiDoc = new OpenApiDocument();
var openApiDoc = new OpenApiDocument
{
BaseUri = location
};

var openApiNode = rootNode.GetMap();

Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Reader/V2/OpenApiV2VersionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ public OpenApiReference ConvertToOpenApiReference(string reference, ReferenceTyp
throw new OpenApiException(string.Format(SRResource.ReferenceHasInvalidFormat, reference));
}

public OpenApiDocument LoadDocument(RootNode rootNode)
public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
{
return OpenApiV2Deserializer.LoadOpenApi(rootNode);
return OpenApiV2Deserializer.LoadOpenApi(rootNode, location);
}

public T LoadElement<T>(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ internal static partial class OpenApiV3Deserializer
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p, n))}
};

public static OpenApiDocument LoadOpenApi(RootNode rootNode)
public static OpenApiDocument LoadOpenApi(RootNode rootNode, Uri location)
{
var openApiDoc = new OpenApiDocument();
var openApiDoc = new OpenApiDocument
{
BaseUri = location
};
var openApiNode = rootNode.GetMap();

ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc);
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Reader/V3/OpenApiV3VersionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ public OpenApiReference ConvertToOpenApiReference(
throw new OpenApiException(string.Format(SRResource.ReferenceHasInvalidFormat, reference));
}

public OpenApiDocument LoadDocument(RootNode rootNode)
public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
{
return OpenApiV3Deserializer.LoadOpenApi(rootNode);
return OpenApiV3Deserializer.LoadOpenApi(rootNode, location);
}

public T LoadElement<T>(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ internal static partial class OpenApiV31Deserializer
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p, n))}
};

public static OpenApiDocument LoadOpenApi(RootNode rootNode)
public static OpenApiDocument LoadOpenApi(RootNode rootNode, Uri location)
{
var openApiDoc = new OpenApiDocument();
var openApiDoc = new OpenApiDocument
{
BaseUri = location
};
var openApiNode = rootNode.GetMap();

ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc);
Expand Down
Loading