From a648591c6dc75f111218d8ec46d8e52812bc0017 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 25 Nov 2024 23:42:06 -0500 Subject: [PATCH 1/2] Refactor readers to reduce surface area --- .../OpenApiYamlReader.cs | 33 +++-- .../Interfaces/IOpenApiReader.cs | 19 ++- .../Models/OpenApiDocument.cs | 27 +--- .../Reader/OpenApiJsonReader.cs | 77 +++++++---- .../Reader/OpenApiModelFactory.cs | 124 +++++++----------- .../V31Tests/OpenApiSchemaTests.cs | 9 ++ .../V3Tests/OpenApiDiscriminatorTests.cs | 8 +- 7 files changed, 154 insertions(+), 143 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs index cff6dd1da..1a85e6a27 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs @@ -19,17 +19,34 @@ namespace Microsoft.OpenApi.Readers /// public class OpenApiYamlReader : IOpenApiReader { + private const int copyBufferSize = 4096; + /// - public async Task ReadAsync(TextReader input, + public async Task ReadAsync(Stream input, OpenApiReaderSettings settings = null, CancellationToken cancellationToken = default) + { + if (input is MemoryStream memoryStream) + { + return Read(memoryStream, settings); + } else { + using var preparedStream = new MemoryStream(); + await input.CopyToAsync(preparedStream, copyBufferSize, cancellationToken); + preparedStream.Position = 0; + return Read(preparedStream, settings); + } + } + + /// + public ReadResult Read(MemoryStream input, + OpenApiReaderSettings settings = null) { JsonNode jsonNode; // Parse the YAML text in the TextReader into a sequence of JsonNodes try { - jsonNode = LoadJsonNodesFromYamlDocument(input); + jsonNode = LoadJsonNodesFromYamlDocument(new StreamReader(input)); // Should we leave the stream open? } catch (JsonException ex) { @@ -42,11 +59,11 @@ public async Task ReadAsync(TextReader input, }; } - return await ReadAsync(jsonNode, settings, cancellationToken: cancellationToken); + return Read(jsonNode, settings); } /// - public T ReadFragment(TextReader input, + public T ReadFragment(MemoryStream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement @@ -56,7 +73,7 @@ public T ReadFragment(TextReader input, // Parse the YAML try { - jsonNode = LoadJsonNodesFromYamlDocument(input); + jsonNode = LoadJsonNodesFromYamlDocument(new StreamReader(input)); } catch (JsonException ex) { @@ -81,10 +98,10 @@ static JsonNode LoadJsonNodesFromYamlDocument(TextReader input) return yamlDocument.ToJsonNode(); } - /// - public async Task ReadAsync(JsonNode jsonNode, OpenApiReaderSettings settings, string format = null, CancellationToken cancellationToken = default) + /// + public ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings, string format = null) { - return await OpenApiReaderRegistry.DefaultReader.ReadAsync(jsonNode, settings, OpenApiConstants.Yaml, cancellationToken); + return OpenApiReaderRegistry.DefaultReader.Read(jsonNode, settings, OpenApiConstants.Yaml); } /// diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs index 5f8b1cb22..b746b857b 100644 --- a/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs +++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs @@ -15,33 +15,40 @@ namespace Microsoft.OpenApi.Interfaces public interface IOpenApiReader { /// - /// Reads the TextReader input and parses it into an Open API document. + /// Async method to reads the stream and parse it into an Open API document. /// /// The TextReader input. /// The OpenApi reader settings. /// Propagates notification that an operation should be cancelled. /// - Task ReadAsync(TextReader input, OpenApiReaderSettings settings = null, CancellationToken cancellationToken = default); + Task ReadAsync(Stream input, OpenApiReaderSettings settings = null, CancellationToken cancellationToken = default); + + /// + /// Provides a synchronous method to read the input memory stream and parse it into an Open API document. + /// + /// + /// + /// + ReadResult Read(MemoryStream input, OpenApiReaderSettings settings = null); /// /// Parses the JsonNode input into an Open API document. /// /// The JsonNode input. /// The Reader settings to be used during parsing. - /// Propagates notifications that operations should be cancelled. /// The OpenAPI format. /// - Task ReadAsync(JsonNode jsonNode, OpenApiReaderSettings settings, string format = null, CancellationToken cancellationToken = default); + ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings, string format = null); /// - /// Reads the TextReader input and parses the fragment of an OpenAPI description into an Open API Element. + /// Reads the MemoryStream and parses the fragment of an OpenAPI description into an Open API Element. /// /// TextReader containing OpenAPI description to parse. /// Version of the OpenAPI specification that the fragment conforms to. /// Returns diagnostic object containing errors detected during parsing. /// The OpenApiReader settings. /// Instance of newly created IOpenApiElement. - T ReadFragment(TextReader input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement; + T ReadFragment(MemoryStream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement; /// /// Reads the JsonNode input and parses the fragment of an OpenAPI description into an Open API Element. diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index bf8458f2c..a41e7ca6b 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -553,27 +553,13 @@ public static ReadResult Load(string url, OpenApiReaderSettings? settings = null /// The OpenAPI format to use during parsing. /// The OpenApi reader settings. /// - public static ReadResult Load(Stream stream, + public static ReadResult Load(MemoryStream stream, string format, OpenApiReaderSettings? settings = null) { return OpenApiModelFactory.Load(stream, format, settings); } - /// - /// Reads the text reader content and parses it into an Open API document. - /// - /// TextReader containing OpenAPI description to parse. - /// The OpenAPI format to use during parsing. - /// The OpenApi reader settings. - /// - public static ReadResult Load(TextReader input, - string format, - OpenApiReaderSettings? settings = null) - { - return OpenApiModelFactory.Load(input, format, settings); - } - /// /// Parses a local file path or Url into an Open API document. /// @@ -598,17 +584,6 @@ public static async Task LoadAsync(Stream stream, string format, Ope return await OpenApiModelFactory.LoadAsync(stream, format, settings, cancellationToken); } - /// - /// Reads the text reader content and parses it into an Open API document. - /// - /// TextReader containing OpenAPI description to parse. - /// The OpenAPI format to use during parsing. - /// The OpenApi reader settings. - /// - public static async Task LoadAsync(TextReader input, string format, OpenApiReaderSettings? settings = null) - { - return await OpenApiModelFactory.LoadAsync(input, format, settings); - } /// /// Parses a string into a object. diff --git a/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs b/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs index 27aad722e..568c94832 100644 --- a/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs +++ b/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs @@ -24,14 +24,47 @@ namespace Microsoft.OpenApi.Reader /// public class OpenApiJsonReader : IOpenApiReader { + /// - /// Reads the stream input and parses it into an Open API document. + /// Reads the memory stream input and parses it into an Open API document. + /// + /// TextReader containing OpenAPI description to parse. + /// The Reader settings to be used during parsing. + /// + public ReadResult Read(MemoryStream input, + OpenApiReaderSettings settings = null) + { + JsonNode jsonNode; + var diagnostic = new OpenApiDiagnostic(); + settings ??= new OpenApiReaderSettings(); + + // Parse the JSON text in the TextReader into JsonNodes + try + { + jsonNode = JsonNode.Parse(input); + } + catch (JsonException ex) + { + diagnostic.Errors.Add(new OpenApiError($"#line={ex.LineNumber}", $"Please provide the correct format, {ex.Message}")); + return new ReadResult + { + OpenApiDocument = null, + OpenApiDiagnostic = diagnostic + }; + } + + return Read(jsonNode, settings); + } + + + /// + /// Reads the stream input asynchronously and parses it into an Open API document. /// /// TextReader containing OpenAPI description to parse. /// The Reader settings to be used during parsing. /// Propagates notifications that operations should be cancelled. /// - public async Task ReadAsync(TextReader input, + public async Task ReadAsync(Stream input, OpenApiReaderSettings settings = null, CancellationToken cancellationToken = default) { @@ -42,7 +75,7 @@ public async Task ReadAsync(TextReader input, // Parse the JSON text in the TextReader into JsonNodes try { - jsonNode = LoadJsonNodes(input); + jsonNode = await JsonNode.ParseAsync(input);; } catch (JsonException ex) { @@ -54,7 +87,7 @@ public async Task ReadAsync(TextReader input, }; } - return await ReadAsync(jsonNode, settings, cancellationToken: cancellationToken); + return Read(jsonNode, settings); } /// @@ -63,12 +96,10 @@ public async Task ReadAsync(TextReader input, /// The JsonNode input. /// The Reader settings to be used during parsing. /// The OpenAPI format. - /// Propagates notifications that operations should be cancelled. /// - public async Task ReadAsync(JsonNode jsonNode, + public ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings, - string format = null, - CancellationToken cancellationToken = default) + string format = null) { var diagnostic = new OpenApiDiagnostic(); var context = new ParsingContext(diagnostic) @@ -84,16 +115,16 @@ public async Task ReadAsync(JsonNode jsonNode, // Parse the OpenAPI Document document = context.Parse(jsonNode); - if (settings.LoadExternalRefs) - { - var diagnosticExternalRefs = await LoadExternalRefsAsync(document, cancellationToken, settings, format); - // Merge diagnostics of external reference - if (diagnosticExternalRefs != null) - { - diagnostic.Errors.AddRange(diagnosticExternalRefs.Errors); - diagnostic.Warnings.AddRange(diagnosticExternalRefs.Warnings); - } - } + // if (settings.LoadExternalRefs) + // { + // var diagnosticExternalRefs = await LoadExternalRefsAsync(document, cancellationToken, settings, format); + // // Merge diagnostics of external reference + // if (diagnosticExternalRefs != null) + // { + // diagnostic.Errors.AddRange(diagnosticExternalRefs.Errors); + // diagnostic.Warnings.AddRange(diagnosticExternalRefs.Warnings); + // } + // } document.SetReferenceHostDocument(); } @@ -124,7 +155,7 @@ public async Task ReadAsync(JsonNode jsonNode, } /// - public T ReadFragment(TextReader input, + public T ReadFragment(MemoryStream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement @@ -134,7 +165,7 @@ public T ReadFragment(TextReader input, // Parse the JSON try { - jsonNode = LoadJsonNodes(input); + jsonNode = JsonNode.Parse(input); } catch (JsonException ex) { @@ -183,12 +214,6 @@ public T ReadFragment(JsonNode input, return (T)element; } - private JsonNode LoadJsonNodes(TextReader input) - { - var nodes = JsonNode.Parse(input.ReadToEnd()); - return nodes; - } - private async Task LoadExternalRefsAsync(OpenApiDocument document, CancellationToken cancellationToken, OpenApiReaderSettings settings, string format = null) { // Create workspace for all documents to live in. diff --git a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs index ddabdc6be..ad5d09968 100644 --- a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs +++ b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs @@ -2,10 +2,12 @@ // Licensed under the MIT license. using System; +using System.Buffers.Text; using System.IO; using System.Linq; using System.Net.Http; using System.Security; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.OpenApi.Interfaces; @@ -45,15 +47,13 @@ public static ReadResult Load(string url, OpenApiReaderSettings settings = null) /// The OpenApi reader settings. /// The OpenAPI format. /// An OpenAPI document instance. - public static ReadResult Load(Stream stream, + public static ReadResult Load(MemoryStream stream, string format, OpenApiReaderSettings settings = null) { settings ??= new OpenApiReaderSettings(); -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - var result = LoadAsync(stream, format, settings).GetAwaiter().GetResult(); -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + var result = InternalLoad(stream, format, settings); if (!settings.LeaveStreamOpen) { @@ -63,22 +63,6 @@ public static ReadResult Load(Stream stream, return result; } - /// - /// Loads the TextReader input and parses it into an Open API document. - /// - /// The TextReader input. - /// The OpenApi reader settings. - /// The Open API format - /// An OpenAPI document instance. - public static ReadResult Load(TextReader input, - string format, - OpenApiReaderSettings settings = null) - { -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - var result = LoadAsync(input, format, settings).GetAwaiter().GetResult(); -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits - return result; - } /// /// Loads the input URL and parses it into an Open API document. @@ -89,12 +73,12 @@ public static ReadResult Load(TextReader input, public static async Task LoadAsync(string url, OpenApiReaderSettings settings = null) { var format = GetFormat(url); - var stream = await GetStreamAsync(url); + var stream = await GetStreamAsync(url); // Get response back and then get Content return await LoadAsync(stream, format, settings); } /// - /// Loads the input stream and parses it into an Open API document. + /// Loads the input stream and parses it into an Open API document. If the stream is not buffered and it contains yaml, it will be buffered before parsing. /// /// The input stream. /// The OpenApi reader settings. @@ -122,24 +106,7 @@ public static async Task LoadAsync(Stream input, string format, Open } // Use StreamReader to process the prepared stream (buffered for YAML, direct for JSON) - using var reader = new StreamReader(preparedStream, default, true, -1, settings.LeaveStreamOpen); - return await LoadAsync(reader, format, settings, cancellationToken); - } - - - /// - /// Loads the TextReader input and parses it into an Open API document. - /// - /// The TextReader input. - /// The Open API format - /// The OpenApi reader settings. - /// Propagates notification that operations should be cancelled. - /// - public static async Task LoadAsync(TextReader input, string format, OpenApiReaderSettings settings = null, CancellationToken cancellationToken = default) - { - Utils.CheckArgumentNull(format, nameof(format)); - var reader = OpenApiReaderRegistry.GetReader(format); - return await reader.ReadAsync(input, settings, cancellationToken); + return await InternalLoadAsync(preparedStream, format, settings, cancellationToken); } /// @@ -155,29 +122,30 @@ public static ReadResult Parse(string input, { format ??= OpenApiConstants.Json; settings ??= new OpenApiReaderSettings(); - using var reader = new StringReader(input); -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - return ParseAsync(input, reader, format, settings).GetAwaiter().GetResult(); -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + // Copy string into MemoryStream + var stream = new MemoryStream(Encoding.UTF8.GetBytes(input)); + + return InternalLoad(stream, format, settings); } - /// - /// An Async method to prevent synchornously blocking the calling thread. - /// - /// - /// - /// - /// - /// - public static async Task ParseAsync(string input, - StringReader reader, - string format = null, - OpenApiReaderSettings settings = null) + private static async Task InternalLoadAsync(Stream input, string format, OpenApiReaderSettings settings = null, CancellationToken cancellationToken = default) { - return await LoadAsync(reader, format, settings); + Utils.CheckArgumentNull(format, nameof(format)); + var reader = OpenApiReaderRegistry.GetReader(format); + var readResult = await reader.ReadAsync(input, settings, cancellationToken); + return readResult; } + private static ReadResult InternalLoad(MemoryStream input, string format, OpenApiReaderSettings settings = null) + { + Utils.CheckArgumentNull(format, nameof(format)); + var reader = OpenApiReaderRegistry.GetReader(format); + var readResult = reader.Read(input, settings); + return readResult; + } + + /// /// Reads the input string and parses it into an Open API document. /// @@ -195,8 +163,8 @@ public static T Parse(string input, { format ??= OpenApiConstants.Json; settings ??= new OpenApiReaderSettings(); - using var reader = new StringReader(input); - return Load(reader, version, out diagnostic, format, settings); + var stream = new MemoryStream(Encoding.UTF8.GetBytes(input)); + return Load(stream, version, format, out diagnostic, settings); } /// @@ -218,45 +186,51 @@ public static T Load(string url, OpenApiSpecVersion version, out OpenApiDiagn var stream = GetStreamAsync(url).GetAwaiter().GetResult(); #pragma warning restore VSTHRD002 // Avoid problematic synchronous waits - return Load(stream, version, format, out diagnostic, settings); + return Load(stream as MemoryStream, version, format, out diagnostic, settings); } + /// - /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. + /// Reads the stream input and ensures it is buffered before passing it to the Load method. /// /// - /// Stream containing OpenAPI description to parse. - /// Version of the OpenAPI specification that the fragment conforms to. + /// + /// /// - /// Returns diagnostic object containing errors detected during parsing. - /// The OpenApiReader settings. - /// Instance of newly created IOpenApiElement. - /// The OpenAPI element. + /// + /// + /// public static T Load(Stream input, OpenApiSpecVersion version, string format, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement { - format ??= OpenApiConstants.Json; - using var reader = new StreamReader(input); - return Load(reader, version, out diagnostic, format, settings); + if (input is MemoryStream memoryStream) + { + return Load(memoryStream, version, format, out diagnostic, settings); + } else { + memoryStream = new MemoryStream(); + input.CopyTo(memoryStream); + memoryStream.Position = 0; + return Load(memoryStream, version, format, out diagnostic, settings); + } } + /// - /// Reads the TextReader input and parses the fragment of an OpenAPI description into an Open API Element. + /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. /// /// - /// TextReader containing OpenAPI description to parse. + /// Stream containing OpenAPI description to parse. /// Version of the OpenAPI specification that the fragment conforms to. - /// The OpenAPI format. + /// /// Returns diagnostic object containing errors detected during parsing. /// The OpenApiReader settings. /// Instance of newly created IOpenApiElement. /// The OpenAPI element. - public static T Load(TextReader input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic, string format, OpenApiReaderSettings settings = null) where T : IOpenApiElement + public static T Load(MemoryStream input, OpenApiSpecVersion version, string format, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement { format ??= OpenApiConstants.Json; return OpenApiReaderRegistry.GetReader(format).ReadFragment(input, version, out diagnostic, settings); } - private static string GetContentType(string url) { if (!string.IsNullOrEmpty(url)) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index 967bb0f3e..e591ad6e4 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; using System.IO; using System.Text.Json.Nodes; @@ -18,6 +19,14 @@ public class OpenApiSchemaTests { private const string SampleFolderPath = "V31Tests/Samples/OpenApiSchema/"; + + public MemoryStream GetMemoryStream(string fileName) + { + var filePath = Path.Combine(SampleFolderPath, fileName); + var fileBytes = File.ReadAllBytes(filePath); + return new MemoryStream(fileBytes); + } + public OpenApiSchemaTests() { OpenApiReaderRegistry.RegisterReader("yaml", new OpenApiYamlReader()); diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs index 6556ade48..bcbc6a02a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.IO; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Reader; @@ -20,13 +21,16 @@ public OpenApiDiscriminatorTests() } [Fact] - public void ParseBasicDiscriminatorShouldSucceed() + public async Task ParseBasicDiscriminatorShouldSucceed() { // Arrange using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "basicDiscriminator.yaml")); + // Copy stream to MemoryStream + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); // Act - var discriminator = OpenApiModelFactory.Load(stream, OpenApiSpecVersion.OpenApi3_0, OpenApiConstants.Yaml, out var diagnostic); + var discriminator = OpenApiModelFactory.Load(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiConstants.Yaml, out var diagnostic); // Assert discriminator.Should().BeEquivalentTo( From c9b01cbec2ecb367c4f64dc7c49cc782ec463d09 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Tue, 26 Nov 2024 09:39:31 -0500 Subject: [PATCH 2/2] Moved load external references --- .../Reader/OpenApiJsonReader.cs | 12 +---- .../Reader/OpenApiModelFactory.cs | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs b/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs index 568c94832..d24d31b9d 100644 --- a/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs +++ b/src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs @@ -214,16 +214,6 @@ public T ReadFragment(JsonNode input, return (T)element; } - private async Task LoadExternalRefsAsync(OpenApiDocument document, CancellationToken cancellationToken, OpenApiReaderSettings settings, string format = null) - { - // 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); - var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, settings.CustomExternalLoader ?? streamLoader, settings); - return await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, format ?? OpenApiConstants.Json, null, cancellationToken); - } + } } diff --git a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs index ad5d09968..2554c48a5 100644 --- a/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs +++ b/src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs @@ -12,6 +12,8 @@ using System.Threading.Tasks; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader.Services; +using Microsoft.OpenApi.Services; namespace Microsoft.OpenApi.Reader { @@ -71,7 +73,16 @@ public static ReadResult Load(MemoryStream stream, /// The OpenApi reader settings. /// public static async Task LoadAsync(string url, OpenApiReaderSettings settings = null) - { + { + // If url is HTTP + // Get the response object. + // Select format based on MediaType + // Get the stream from the response object. + // Load the stream. + // Else + // Determine the format from the file extension. + // Load the file from the local file system. + var format = GetFormat(url); var stream = await GetStreamAsync(url); // Get response back and then get Content return await LoadAsync(stream, format, settings); @@ -134,14 +145,45 @@ private static async Task InternalLoadAsync(Stream input, string for Utils.CheckArgumentNull(format, nameof(format)); var reader = OpenApiReaderRegistry.GetReader(format); var readResult = await reader.ReadAsync(input, settings, cancellationToken); + + if (settings.LoadExternalRefs) + { + var diagnosticExternalRefs = await LoadExternalRefsAsync(readResult.OpenApiDocument, cancellationToken, settings, format); + // Merge diagnostics of external reference + if (diagnosticExternalRefs != null) + { + readResult.OpenApiDiagnostic.Errors.AddRange(diagnosticExternalRefs.Errors); + readResult.OpenApiDiagnostic.Warnings.AddRange(diagnosticExternalRefs.Warnings); + } + } + + return readResult; } + private static async Task LoadExternalRefsAsync(OpenApiDocument document, CancellationToken cancellationToken, OpenApiReaderSettings settings, string format = null) + { + // 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); + var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, settings.CustomExternalLoader ?? streamLoader, settings); + return await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, format ?? OpenApiConstants.Json, null, cancellationToken); + } + private static ReadResult InternalLoad(MemoryStream input, string format, OpenApiReaderSettings settings = null) { Utils.CheckArgumentNull(format, nameof(format)); + if (settings.LoadExternalRefs) + { + throw new InvalidOperationException("Loading external references are not supported when using synchronous methods."); + } + var reader = OpenApiReaderRegistry.GetReader(format); var readResult = reader.Read(input, settings); + return readResult; }