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