diff --git a/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs
index f6019a91e..a7f888484 100644
--- a/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs
+++ b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs
@@ -26,25 +26,27 @@ public class OpenApiYamlReader : IOpenApiReader
///
public async Task 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);
}
}
///
public ReadResult Read(MemoryStream input,
+ Uri location,
OpenApiReaderSettings settings)
{
if (input is null) throw new ArgumentNullException(nameof(input));
@@ -74,13 +76,13 @@ public ReadResult Read(MemoryStream input,
};
}
- return Read(jsonNode, settings);
+ return Read(jsonNode, location, settings);
}
///
- 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);
}
///
diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs
index 687599caa..d17371a68 100644
--- a/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs
+++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs
@@ -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;
@@ -18,18 +19,20 @@ public interface IOpenApiReader
/// Async method to reads the stream and parse it into an Open API document.
///
/// The stream input.
+ /// Location of where the document that is getting loaded is saved
/// The OpenApi reader settings.
/// Propagates notification that an operation should be cancelled.
///
- Task ReadAsync(Stream input, OpenApiReaderSettings settings, CancellationToken cancellationToken = default);
+ Task ReadAsync(Stream input, Uri location, OpenApiReaderSettings settings, CancellationToken cancellationToken = default);
///
/// Provides a synchronous method to read the input memory stream and parse it into an Open API document.
///
///
+ /// Location of where the document that is getting loaded is saved
///
///
- ReadResult Read(MemoryStream input, OpenApiReaderSettings settings);
+ ReadResult Read(MemoryStream input, Uri location, OpenApiReaderSettings settings);
///
/// Reads the MemoryStream and parses the fragment of an OpenAPI description into an Open API Element.
diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs
index 64049483e..56df6f9b0 100644
--- a/src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs
+++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
+using System;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Reader.ParseNodes;
@@ -24,8 +25,9 @@ internal interface IOpenApiVersionService
/// Converts a generic RootNode instance into a strongly typed OpenApiDocument
///
/// RootNode containing the information to be converted into an OpenAPI Document
+ /// Location of where the document that is getting loaded is saved
/// Instance of OpenApiDocument populated with data from rootNode
- OpenApiDocument LoadDocument(RootNode rootNode);
+ OpenApiDocument LoadDocument(RootNode rootNode, Uri location);
///
/// Gets the description and summary scalar values in a reference object for V3.1 support
diff --git a/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs b/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs
index e6438bac1..d56f6075e 100644
--- a/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs
+++ b/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs
@@ -17,9 +17,11 @@ public interface IStreamLoader
///
/// Use Uri to locate data and convert into an input object.
///
+ /// Base URL of parent to which a relative reference could be loaded.
+ /// If the is an absolute parameter the value of this parameter will be ignored
/// Identifier of some source of an OpenAPI Description
/// The cancellation token.
/// A data object that can be processed by a reader to generate an
- Task LoadAsync(Uri uri, CancellationToken cancellationToken = default);
+ Task LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
index e5f82e9cb..1b8718931 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
@@ -110,9 +110,9 @@ public ISet? Tags
public IDictionary? Metadata { get; set; }
///
- /// Implements IBaseDocument
+ /// Absolute location of the document or a generated placeholder if location is not given
///
- public Uri BaseUri { get; }
+ public Uri BaseUri { get; internal set; }
///
/// Parameter-less constructor
@@ -572,14 +572,15 @@ private static string ConvertByteArrayToString(byte[] hash)
}
else
{
- string relativePath = OpenApiConstants.ComponentsSegment + reference.Type.GetDisplayName() + "/" + id;
+ string relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{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(uriLocation);
+ return Workspace?.ResolveReference(new Uri(uriLocation).AbsoluteUri);
}
///
diff --git a/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs b/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs
index 87a56d90d..3432875a1 100644
--- a/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs
+++ b/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs
@@ -25,9 +25,11 @@ public class OpenApiJsonReader : IOpenApiReader
/// Reads the memory stream input and parses it into an Open API document.
///
/// Memory stream containing OpenAPI description to parse.
+ /// Location of where the document that is getting loaded is saved
/// The Reader settings to be used during parsing.
///
public ReadResult Read(MemoryStream input,
+ Uri location,
OpenApiReaderSettings settings)
{
if (input is null) throw new ArgumentNullException(nameof(input));
@@ -52,16 +54,18 @@ public ReadResult Read(MemoryStream input,
};
}
- return Read(jsonNode, settings);
+ return Read(jsonNode, location, settings);
}
///
/// Parses the JsonNode input into an Open API document.
///
/// The JsonNode input.
+ /// Location of where the document that is getting loaded is saved
/// The Reader settings to be used during parsing.
///
public ReadResult Read(JsonNode jsonNode,
+ Uri location,
OpenApiReaderSettings settings)
{
if (jsonNode is null) throw new ArgumentNullException(nameof(jsonNode));
@@ -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)
@@ -115,10 +119,12 @@ public ReadResult Read(JsonNode jsonNode,
/// Reads the stream input asynchronously and parses it into an Open API document.
///
/// Memory stream containing OpenAPI description to parse.
+ /// Location of where the document that is getting loaded is saved
/// The Reader settings to be used during parsing.
/// Propagates notifications that operations should be cancelled.
///
public async Task ReadAsync(Stream input,
+ Uri location,
OpenApiReaderSettings settings,
CancellationToken cancellationToken = default)
{
@@ -144,7 +150,7 @@ public async Task ReadAsync(Stream input,
};
}
- return Read(jsonNode, settings);
+ return Read(jsonNode, location, settings);
}
///
diff --git a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs
index e370720e3..dc206c47e 100644
--- a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs
+++ b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs
@@ -240,7 +240,13 @@ private static async Task 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)
{
@@ -258,13 +264,10 @@ private static async Task InternalLoadAsync(Stream input, string for
private static async Task 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(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 workspace = document?.Workspace ?? new OpenApiWorkspace();
+ var workspaceLoader = new OpenApiWorkspaceLoader(workspace, settings.CustomExternalLoader ?? streamLoader, settings);
return await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, format ?? OpenApiConstants.Json, null, token).ConfigureAwait(false);
}
@@ -280,8 +283,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;
}
diff --git a/src/Microsoft.OpenApi/Reader/ParsingContext.cs b/src/Microsoft.OpenApi/Reader/ParsingContext.cs
index 93d9517b9..68d06933a 100644
--- a/src/Microsoft.OpenApi/Reader/ParsingContext.cs
+++ b/src/Microsoft.OpenApi/Reader/ParsingContext.cs
@@ -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
///
/// Set of Json nodes to parse.
+ /// Location of where the document that is getting loaded is saved
/// An OpenApiDocument populated based on the passed yamlDocument
- public OpenApiDocument Parse(JsonNode jsonNode)
+ public OpenApiDocument Parse(JsonNode jsonNode, Uri location)
{
RootNode = new RootNode(this, jsonNode);
@@ -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;
diff --git a/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs
index ad36e5554..374e00f49 100644
--- a/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs
+++ b/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs
@@ -17,30 +17,24 @@ namespace Microsoft.OpenApi.Reader.Services
///
public class DefaultStreamLoader : IStreamLoader
{
- private readonly Uri baseUrl;
private readonly HttpClient _httpClient;
///
/// The default stream loader
///
- ///
/// The HttpClient to use to retrieve documents when needed
- public DefaultStreamLoader(Uri baseUrl, HttpClient httpClient)
+ public DefaultStreamLoader(HttpClient httpClient)
{
- this.baseUrl = baseUrl;
_httpClient = Utils.CheckArgumentNull(httpClient);
}
///
- public async Task LoadAsync(Uri uri, CancellationToken cancellationToken = default)
+ public async Task LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default)
{
- var absoluteUri = (baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri), baseUrl.IsAbsoluteUri, uri.IsAbsoluteUri) switch
+ var absoluteUri = baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri) switch
{
- (true, _, _) => new Uri(Path.Combine(Directory.GetCurrentDirectory(), uri.ToString())),
- // this overcomes a URI concatenation issue for local paths on linux OSes
- (_, true, false) when baseUrl.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) =>
- new Uri(Path.Combine(baseUrl.AbsoluteUri, uri.ToString())),
- (_, _, _) => new Uri(baseUrl, uri),
+ true => new Uri(Path.Combine(Directory.GetCurrentDirectory(), uri.ToString())),
+ _ => new Uri(baseUrl, uri),
};
return absoluteUri.Scheme switch
diff --git a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs
index 75dd43512..a9cba7989 100644
--- a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs
+++ b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs
@@ -48,7 +48,8 @@ internal async Task LoadAsync(OpenApiReference reference,
// If not already in workspace, load it and process references
if (item.ExternalResource is not null && !_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)
diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs
index f7cbe711a..ca5abcf74 100644
--- a/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs
@@ -227,9 +227,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();
diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2VersionService.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2VersionService.cs
index d92e9ce78..ec46036e0 100644
--- a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2VersionService.cs
+++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2VersionService.cs
@@ -50,9 +50,9 @@ public OpenApiV2VersionService(OpenApiDiagnostic diagnostic)
[typeof(OpenApiXml)] = OpenApiV2Deserializer.LoadXml
};
- public OpenApiDocument LoadDocument(RootNode rootNode)
+ public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
{
- return OpenApiV2Deserializer.LoadOpenApi(rootNode);
+ return OpenApiV2Deserializer.LoadOpenApi(rootNode, location);
}
public T? LoadElement(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement
diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs
index 6e5fb952b..eee2b6c3d 100644
--- a/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs
@@ -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);
diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiV3VersionService.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiV3VersionService.cs
index 364eb1d54..34ad86fe3 100644
--- a/src/Microsoft.OpenApi/Reader/V3/OpenApiV3VersionService.cs
+++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiV3VersionService.cs
@@ -64,9 +64,9 @@ public OpenApiV3VersionService(OpenApiDiagnostic diagnostic)
[typeof(OpenApiSchemaReference)] = OpenApiV3Deserializer.LoadMapping
};
- public OpenApiDocument LoadDocument(RootNode rootNode)
+ public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
{
- return OpenApiV3Deserializer.LoadOpenApi(rootNode);
+ return OpenApiV3Deserializer.LoadOpenApi(rootNode, location);
}
public T LoadElement(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement
diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs
index 0abe92234..ffc2bc175 100644
--- a/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs
@@ -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);
diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs
index 3e010be9b..1d05728f4 100644
--- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs
+++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31VersionService.cs
@@ -63,9 +63,9 @@ public OpenApiV31VersionService(OpenApiDiagnostic diagnostic)
[typeof(OpenApiSchemaReference)] = OpenApiV31Deserializer.LoadMapping
};
- public OpenApiDocument LoadDocument(RootNode rootNode)
+ public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
{
- return OpenApiV31Deserializer.LoadOpenApi(rootNode);
+ return OpenApiV31Deserializer.LoadOpenApi(rootNode, location);
}
public T LoadElement(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement
diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
index a8ffde23d..5519696d9 100644
--- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
+++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
@@ -18,7 +18,30 @@ public class OpenApiWorkspace
{
private readonly Dictionary _documentsIdRegistry = new();
private readonly Dictionary _artifactsRegistry = new();
- private readonly Dictionary _IOpenApiReferenceableRegistry = new();
+ private readonly Dictionary _IOpenApiReferenceableRegistry = new(new UriWithFragmentEquailityComparer());
+
+ private class UriWithFragmentEquailityComparer : IEqualityComparer
+ {
+ public bool Equals(Uri? x, Uri? y)
+ {
+ if (ReferenceEquals(x, y))
+ {
+ return true;
+ }
+
+ if (x is null || y is null)
+ {
+ return false;
+ }
+
+ return x.AbsoluteUri == y.AbsoluteUri;
+ }
+
+ public int GetHashCode(Uri obj)
+ {
+ return obj.AbsoluteUri.GetHashCode();
+ }
+ }
///
/// The base location from where all relative references are resolved
@@ -171,7 +194,7 @@ public void RegisterComponents(OpenApiDocument document)
private static string getBaseUri(OpenApiDocument openApiDocument)
{
- return openApiDocument.BaseUri + OpenApiConstants.ComponentsSegment;
+ return openApiDocument.BaseUri + "#" + OpenApiConstants.ComponentsSegment;
}
///
diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs
index 1171e8c20..f275ebb2a 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs
@@ -62,7 +62,7 @@ public Stream Load(Uri uri)
return null;
}
- public Task LoadAsync(Uri uri, CancellationToken cancellationToken = default)
+ public Task LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default)
{
var path = new Uri(new("http://example.org/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/"), uri).AbsolutePath;
path = path[1..]; // remove leading slash
diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs
index 720eade40..671a4a9eb 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs
@@ -1,9 +1,13 @@
-using System;
+using System;
+using System.Collections.Generic;
using System.IO;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.Models.Interfaces;
+using Microsoft.OpenApi.Models.References;
using Microsoft.OpenApi.Reader;
using Xunit;
@@ -59,14 +63,70 @@ public async Task LoadDocumentWithExternalReferenceShouldLoadBothDocumentsIntoWo
result = await OpenApiDocument.LoadAsync("V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml", settings);
var externalDocBaseUri = result.Document.Workspace.GetDocumentId("./TodoComponents.yaml");
- var schemasPath = "/components/schemas/";
- var parametersPath = "/components/parameters/";
+ var schemasPath = "#/components/schemas/";
+ var parametersPath = "#/components/parameters/";
Assert.NotNull(externalDocBaseUri);
Assert.True(result.Document.Workspace.Contains(externalDocBaseUri + schemasPath + "todo"));
Assert.True(result.Document.Workspace.Contains(externalDocBaseUri + schemasPath + "entity"));
Assert.True(result.Document.Workspace.Contains(externalDocBaseUri + parametersPath + "filter"));
}
+
+ [Fact]
+ public async Task LoadDocumentWithExternalReferencesInSubDirectories()
+ {
+ var sampleFolderPath = $"V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories";
+ var referenceBaseUri = "file://" + Path.GetFullPath(sampleFolderPath);
+
+ // Create a reader that will resolve all references also of documentes located in the non-root directory
+ var settings = new OpenApiReaderSettings()
+ {
+ LoadExternalRefs = true,
+ BaseUrl = new Uri("file://")
+ };
+ settings.AddYamlReader();
+
+ // Act
+ var result = await OpenApiDocument.LoadAsync($"{sampleFolderPath}/Root.yaml", settings);
+ var document = result.Document;
+ var workspace = result.Document.Workspace;
+
+ // Assert
+ Assert.True(workspace.Contains($"{Path.Combine(referenceBaseUri, "Directory", "PetsPage.yaml")}#/components/schemas/PetsPage"));
+ Assert.True(workspace.Contains($"{Path.Combine(referenceBaseUri, "Directory", "Pets.yaml")}#/components/schemas/Pets"));
+ Assert.True(workspace.Contains($"{Path.Combine(referenceBaseUri, "Directory", "Pets.yaml")}#/components/schemas/Pet"));
+
+ var operationResponseSchema = document.Paths["/pets"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema;
+ Assert.IsType(operationResponseSchema);
+
+ var petsSchema = operationResponseSchema.Properties["pets"];
+ Assert.IsType(petsSchema);
+ Assert.Equal(JsonSchemaType.Array, petsSchema.Type);
+
+ var petSchema = petsSchema.Items;
+ Assert.IsType(petSchema);
+
+ Assert.Equivalent(new OpenApiSchema
+ {
+ Required = new HashSet { "id", "name" },
+ Properties = new Dictionary
+ {
+ ["id"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Integer,
+ Format = "int64"
+ },
+ ["name"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.String
+ },
+ ["tag"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.String
+ }
+ }
+ }, petSchema);
+ }
}
public class MockLoader : IStreamLoader
@@ -76,7 +136,7 @@ public Stream Load(Uri uri)
return null;
}
- public Task LoadAsync(Uri uri, CancellationToken cancellationToken = default)
+ public Task LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default)
{
return Task.FromResult(null);
}
@@ -89,7 +149,7 @@ public Stream Load(Uri uri)
return null;
}
- public Task LoadAsync(Uri uri, CancellationToken cancellationToken = default)
+ public Task LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default)
{
var path = new Uri(new("http://example.org/V3Tests/Samples/OpenApiWorkspace/"), uri).AbsolutePath;
path = path[1..]; // remove leading slash
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs
index 167d59cc7..5b6d24d64 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs
@@ -528,7 +528,12 @@ public async Task ExternalDocumentDereferenceToOpenApiDocumentUsingJsonPointerWo
var responseSchema = result.Document.Paths["/resource"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema;
// Assert
- result.Document.Workspace.Contains("./externalResource.yaml");
+ var externalResourceUri = new Uri(
+ "file://" +
+ Path.Combine(Path.GetFullPath(SampleFolderPath),
+ "externalResource.yaml#/components/schemas/todo")).AbsoluteUri;
+
+ Assert.True(result.Document.Workspace.Contains(externalResourceUri));
Assert.Equal(2, responseSchema.Properties.Count); // reference has been resolved
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Directory/Pets.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Directory/Pets.yaml
new file mode 100644
index 000000000..ddf4c6cc3
--- /dev/null
+++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Directory/Pets.yaml
@@ -0,0 +1,23 @@
+openapi: 3.0.0
+info:
+ version: 1.0.0
+ title: Pet(s) Schema
+paths: {}
+components:
+ schemas:
+ Pets:
+ type: array
+ items:
+ "$ref": "#/components/schemas/Pet"
+ Pet:
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ tag:
+ type: string
\ No newline at end of file
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Directory/PetsPage.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Directory/PetsPage.yaml
new file mode 100644
index 000000000..139c6d4e1
--- /dev/null
+++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Directory/PetsPage.yaml
@@ -0,0 +1,16 @@
+openapi: 3.0.0
+info:
+ version: 1.0.0
+ title: AllPets Schema
+paths: {}
+components:
+ schemas:
+ PetsPage:
+ type: object
+ properties:
+ pageNumber:
+ type: integer
+ minimum: 0
+ maximum: 100
+ pets:
+ "$ref": "./Pets.yaml#/components/schemas/Pets"
\ No newline at end of file
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Root.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Root.yaml
new file mode 100644
index 000000000..97d386192
--- /dev/null
+++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/ExternalReferencesInSubDirectories/Root.yaml
@@ -0,0 +1,16 @@
+openapi: 3.0.0
+info:
+ version: 1.0.0
+ title: Example using relative references into sub directories
+paths:
+ "/pets":
+ get:
+ summary: List all pets
+ operationId: listPets
+ responses:
+ '200':
+ description: An array of pets
+ content:
+ application/json:
+ schema:
+ "$ref": "./Directory/PetsPage.yaml#/components/schemas/PetsPage"
\ No newline at end of file
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index 755a9e17e..b8e1b328f 100644
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -217,8 +217,8 @@ namespace Microsoft.OpenApi.Interfaces
}
public interface IOpenApiReader
{
- Microsoft.OpenApi.Reader.ReadResult Read(System.IO.MemoryStream input, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings);
- System.Threading.Tasks.Task ReadAsync(System.IO.Stream input, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings, System.Threading.CancellationToken cancellationToken = default);
+ Microsoft.OpenApi.Reader.ReadResult Read(System.IO.MemoryStream input, System.Uri location, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings);
+ System.Threading.Tasks.Task ReadAsync(System.IO.Stream input, System.Uri location, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings, System.Threading.CancellationToken cancellationToken = default);
T? ReadFragment(System.IO.MemoryStream input, Microsoft.OpenApi.OpenApiSpecVersion version, Microsoft.OpenApi.Models.OpenApiDocument openApiDocument, out Microsoft.OpenApi.Reader.OpenApiDiagnostic diagnostic, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null)
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement;
}
@@ -247,7 +247,7 @@ namespace Microsoft.OpenApi.Interfaces
}
public interface IStreamLoader
{
- System.Threading.Tasks.Task LoadAsync(System.Uri uri, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task LoadAsync(System.Uri baseUrl, System.Uri uri, System.Threading.CancellationToken cancellationToken = default);
}
}
namespace Microsoft.OpenApi
@@ -1441,9 +1441,9 @@ namespace Microsoft.OpenApi.Reader
public class OpenApiJsonReader : Microsoft.OpenApi.Interfaces.IOpenApiReader
{
public OpenApiJsonReader() { }
- public Microsoft.OpenApi.Reader.ReadResult Read(System.IO.MemoryStream input, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings) { }
- public Microsoft.OpenApi.Reader.ReadResult Read(System.Text.Json.Nodes.JsonNode jsonNode, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings) { }
- public System.Threading.Tasks.Task ReadAsync(System.IO.Stream input, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings, System.Threading.CancellationToken cancellationToken = default) { }
+ public Microsoft.OpenApi.Reader.ReadResult Read(System.IO.MemoryStream input, System.Uri location, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings) { }
+ public Microsoft.OpenApi.Reader.ReadResult Read(System.Text.Json.Nodes.JsonNode jsonNode, System.Uri location, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings) { }
+ public System.Threading.Tasks.Task ReadAsync(System.IO.Stream input, System.Uri location, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings, System.Threading.CancellationToken cancellationToken = default) { }
public T? ReadFragment(System.IO.MemoryStream input, Microsoft.OpenApi.OpenApiSpecVersion version, Microsoft.OpenApi.Models.OpenApiDocument openApiDocument, out Microsoft.OpenApi.Reader.OpenApiDiagnostic diagnostic, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null)
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
public T? ReadFragment(System.Text.Json.Nodes.JsonNode input, Microsoft.OpenApi.OpenApiSpecVersion version, Microsoft.OpenApi.Models.OpenApiDocument openApiDocument, out Microsoft.OpenApi.Reader.OpenApiDiagnostic diagnostic, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null)
@@ -1496,7 +1496,7 @@ namespace Microsoft.OpenApi.Reader
public void EndObject() { }
public T? GetFromTempStorage(string key, object? scope = null) { }
public string GetLocation() { }
- public Microsoft.OpenApi.Models.OpenApiDocument Parse(System.Text.Json.Nodes.JsonNode jsonNode) { }
+ public Microsoft.OpenApi.Models.OpenApiDocument Parse(System.Text.Json.Nodes.JsonNode jsonNode, System.Uri location) { }
public T? ParseFragment(System.Text.Json.Nodes.JsonNode jsonNode, Microsoft.OpenApi.OpenApiSpecVersion version, Microsoft.OpenApi.Models.OpenApiDocument openApiDocument)
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
public void PopLoop(string loopid) { }
@@ -1523,8 +1523,8 @@ namespace Microsoft.OpenApi.Reader.Services
{
public class DefaultStreamLoader : Microsoft.OpenApi.Interfaces.IStreamLoader
{
- public DefaultStreamLoader(System.Uri baseUrl, System.Net.Http.HttpClient httpClient) { }
- public System.Threading.Tasks.Task LoadAsync(System.Uri uri, System.Threading.CancellationToken cancellationToken = default) { }
+ public DefaultStreamLoader(System.Net.Http.HttpClient httpClient) { }
+ public System.Threading.Tasks.Task LoadAsync(System.Uri baseUrl, System.Uri uri, System.Threading.CancellationToken cancellationToken = default) { }
}
}
namespace Microsoft.OpenApi.Services