diff --git a/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs b/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs index c3178ccfb..0ffac77e7 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs @@ -26,5 +26,48 @@ public class OpenApiDiagnostic : IDiagnostic /// Open API specification version of the document parsed. /// public OpenApiSpecVersion SpecificationVersion { get; set; } + + /// + /// Append another set of diagnostic Errors and Warnings to this one, this may be appended from another external + /// document's parsing and we want to indicate which file it originated from. + /// + /// The diagnostic instance of which the errors and warnings are to be appended to this diagnostic's + /// The originating file of the diagnostic to be appended, this is prefixed to each error and warning to indicate the originating file + public void AppendDiagnostic(OpenApiDiagnostic diagnosticToAdd, string fileNameToAdd = null) + { + var fileNameIsSupplied = !string.IsNullOrEmpty(fileNameToAdd); + foreach (var err in diagnosticToAdd.Errors) + { + var errMsgWithFileName = fileNameIsSupplied ? $"[File: {fileNameToAdd}] {err.Message}" : err.Message; + Errors.Add(new OpenApiError(err.Pointer, errMsgWithFileName)); + } + foreach (var warn in diagnosticToAdd.Warnings) + { + var warnMsgWithFileName = fileNameIsSupplied ? $"[File: {fileNameToAdd}] {warn.Message}" : warn.Message; + Warnings.Add(new OpenApiError(warn.Pointer, warnMsgWithFileName)); + } + } + } +} + +/// +/// Extension class for IList to add the Method "AddRange" used above +/// +internal static class IDiagnosticExtensions +{ + /// + /// Extension method for IList so that another list can be added to the current list. + /// + /// + /// + /// + internal static void AddRange(this ICollection collection, IEnumerable enumerable) + { + if (collection is null || enumerable is null) return; + + foreach (var cur in enumerable) + { + collection.Add(cur); + } } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs index f6fd2325e..5b87ab7ae 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -102,7 +102,13 @@ public async Task ReadAsync(YamlDocument input, CancellationToken ca if (_settings.LoadExternalRefs) { - await LoadExternalRefs(document, cancellationToken); + var diagnosticExternalRefs = await LoadExternalRefs(document, cancellationToken); + // Merge diagnostics of external reference + if (diagnosticExternalRefs != null) + { + diagnostic.Errors.AddRange(diagnosticExternalRefs.Errors); + diagnostic.Warnings.AddRange(diagnosticExternalRefs.Warnings); + } } ResolveReferences(diagnostic, document); @@ -133,7 +139,7 @@ public async Task ReadAsync(YamlDocument input, CancellationToken ca }; } - private async Task LoadExternalRefs(OpenApiDocument document, CancellationToken cancellationToken) + private async Task LoadExternalRefs(OpenApiDocument document, CancellationToken cancellationToken) { // Create workspace for all documents to live in. var openApiWorkSpace = new OpenApiWorkspace(); @@ -141,7 +147,7 @@ private async Task LoadExternalRefs(OpenApiDocument document, CancellationToken // Load this root document into the workspace var streamLoader = new DefaultStreamLoader(_settings.BaseUrl); var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader, _settings); - await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, cancellationToken); + return await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, cancellationToken); } private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument document) diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs index 32e2db128..79f6206d0 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -24,7 +24,7 @@ public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, IStreamLoader loader, _readerSettings = readerSettings; } - internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument document, CancellationToken cancellationToken) + internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument document, CancellationToken cancellationToken, OpenApiDiagnostic diagnostic = null) { _workspace.AddDocument(reference.ExternalResource, document); document.Workspace = _workspace; @@ -36,6 +36,11 @@ internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument docume var reader = new OpenApiStreamReader(_readerSettings); + if (diagnostic is null) + { + diagnostic = new OpenApiDiagnostic(); + } + // Walk references foreach (var item in referenceCollector.References) { @@ -43,10 +48,21 @@ internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument docume if (!_workspace.Contains(item.ExternalResource)) { var input = await _loader.LoadAsync(new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute)); - var result = await reader.ReadAsync(input, cancellationToken); // TODO merge diagnostics - await LoadAsync(item, result.OpenApiDocument, cancellationToken); + var result = await reader.ReadAsync(input, cancellationToken); + // Merge diagnostics + if (result.OpenApiDiagnostic != null) + { + diagnostic.AppendDiagnostic(result.OpenApiDiagnostic, item.ExternalResource); + } + if (result.OpenApiDocument != null) + { + var loadDiagnostic = await LoadAsync(item, result.OpenApiDocument, cancellationToken, diagnostic); + diagnostic = loadDiagnostic; + } } } + + return diagnostic; } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index bde45294c..894883bd8 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -13,6 +13,8 @@ ..\..\src\Microsoft.OpenApi.snk + + @@ -20,6 +22,8 @@ + + Never diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs index 1bcd7b9d9..23c23b4d6 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs @@ -1,8 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; +using System.Threading.Tasks; +using System; using FluentAssertions; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Tests.OpenApiWorkspaceTests; using Xunit; +using Microsoft.OpenApi.Readers.Interface; +using System.IO; namespace Microsoft.OpenApi.Readers.Tests.OpenApiReaderTests { @@ -32,5 +40,45 @@ public void DetectedSpecificationVersionShouldBeV3_0() diagnostic.SpecificationVersion.Should().Be(OpenApiSpecVersion.OpenApi3_0); } } + + [Fact] + public async Task DiagnosticReportMergedForExternalReference() + { + // Create a reader that will resolve all references + var reader = new OpenApiStreamReader(new OpenApiReaderSettings() + { + LoadExternalRefs = true, + CustomExternalLoader = new ResourceLoader(), + BaseUrl = new Uri("fie://c:\\") + }); + + ReadResult result; + using (var stream = Resources.GetStream("OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/TodoMain.yaml")) + { + result = await reader.ReadAsync(stream); + } + + Assert.NotNull(result); + Assert.NotNull(result.OpenApiDocument.Workspace); + Assert.True(result.OpenApiDocument.Workspace.Contains("TodoReference.yaml")); + result.OpenApiDiagnostic.Errors.Should().BeEquivalentTo(new List { + new OpenApiError( new OpenApiException("[File: ./TodoReference.yaml] Invalid Reference identifier 'object-not-existing'.")) }); + + } + } + + public class ResourceLoader : IStreamLoader + { + public Stream Load(Uri uri) + { + return null; + } + + public Task LoadAsync(Uri uri) + { + var path = new Uri(new Uri("http://example.org/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/"), uri).AbsolutePath; + path = path.Substring(1); // remove leading slash + return Task.FromResult(Resources.GetStream(path)); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/TodoMain.yaml b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/TodoMain.yaml new file mode 100644 index 000000000..beaa7995c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/TodoMain.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.1 +info: + title: Example using a remote reference + version: 1.0.0 +paths: + "/todos": + get: + parameters: + - $ref: ./TodoReference.yaml#/components/parameters/filter + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: ./TodoReference.yaml#/components/schemas/todo \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/TodoReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/TodoReference.yaml new file mode 100644 index 000000000..db3958149 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/TodoReference.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.1 +info: + title: Components for the todo app + version: 1.0.0 +paths: {} +components: + parameters: + filter: + name: filter + in: query + schema: + type: string + schemas: + todo: + type: object + allOf: + - $ref: "#/components/schemas/entity" + - $ref: "#/components/schemas/object-not-existing" + properties: + subject: + type: string + entity: + type: object + properties: + id: + type:string \ No newline at end of file