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