diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs
index e51742d..0229808 100644
--- a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs
+++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs
@@ -197,6 +197,7 @@ private async Task ResolveReferencesAsync(AsyncApiDiagnostic diagnostic, IAsyncA
continue;
}
+ var errorCount = diagnostic.Errors.Count;
IAsyncApiSerializable component = null;
if (reference.Reference.IsExternal)
{
@@ -215,7 +216,12 @@ private async Task ResolveReferencesAsync(AsyncApiDiagnostic diagnostic, IAsyncA
if (component == null)
{
- diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference '{reference.Reference.Reference}'"));
+ // Only add a generic error when resolving did not already explain what went wrong.
+ if (diagnostic.Errors.Count == errorCount)
+ {
+ diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference '{reference.Reference.Reference}'"));
+ }
+
continue;
}
@@ -242,6 +248,7 @@ private void ResolveReferences(AsyncApiDiagnostic diagnostic, IAsyncApiSerializa
continue;
}
+ var errorCount = diagnostic.Errors.Count;
IAsyncApiSerializable component = null;
if (reference.Reference.IsExternal)
{
@@ -260,7 +267,12 @@ private void ResolveReferences(AsyncApiDiagnostic diagnostic, IAsyncApiSerializa
if (component == null)
{
- diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference '{reference.Reference.Reference}'"));
+ // Only add a generic error when resolving did not already explain what went wrong.
+ if (diagnostic.Errors.Count == errorCount)
+ {
+ diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference '{reference.Reference.Reference}'"));
+ }
+
continue;
}
@@ -349,9 +361,9 @@ private IAsyncApiSerializable ResolveStreamReference(Stream stream, IAsyncApiRef
{
json = this.ReadToJson(stream);
}
- catch
+ catch (Exception ex)
{
- diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference: '{reference.Reference.Reference}'"));
+ diagnostic.Errors.Add(new AsyncApiError(reference.Reference.Reference, $"Unable to deserialize reference '{reference.Reference.Reference}': {ex.Message}"));
return null;
}
diff --git a/src/ByteBard.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs b/src/ByteBard.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs
index 70fb101..74f5cd9 100644
--- a/src/ByteBard.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs
+++ b/src/ByteBard.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs
@@ -1,6 +1,5 @@
namespace ByteBard.AsyncAPI.Readers.ParseNodes
{
- using System;
using System.Text.Json.Nodes;
public static class JsonPointerExtensions
@@ -12,29 +11,26 @@ public static JsonNode Find(this JsonPointer currentPointer, JsonNode baseJsonNo
return baseJsonNode;
}
- try
+ var pointer = baseJsonNode;
+ foreach (var token in currentPointer.Tokens)
{
- var pointer = baseJsonNode;
- foreach (var token in currentPointer.Tokens)
+ if (pointer is JsonArray sequence)
{
- var sequence = pointer as JsonArray;
-
- if (sequence != null && int.TryParse(token, out var tokenValue))
- {
- pointer = sequence[tokenValue];
- }
- else if (pointer is JsonObject map && !map.TryGetPropertyValue(token, out pointer))
+ if (!int.TryParse(token, out var index) || index < 0 || index >= sequence.Count)
{
return null;
}
- }
- return pointer;
- }
- catch (Exception)
- {
- return null;
+ pointer = sequence[index];
+ }
+ else if (pointer is not JsonObject map || !map.TryGetPropertyValue(token, out pointer))
+ {
+ // The token walks into a scalar or a missing property.
+ return null;
+ }
}
+
+ return pointer;
}
}
}
\ No newline at end of file
diff --git a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs
index 78fa352..03a4d36 100644
--- a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs
+++ b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs
@@ -2,6 +2,7 @@ namespace ByteBard.AsyncAPI.Readers.ParseNodes
{
using System;
using System.Globalization;
+ using System.Linq;
using System.Text.Json.Nodes;
using ByteBard.AsyncAPI.Exceptions;
using ByteBard.AsyncAPI.Models;
@@ -53,7 +54,7 @@ public override int GetIntegerValue()
return value;
}
- throw new AsyncApiReaderException("Value could not parse to integer.");
+ throw new AsyncApiReaderException(this.DescribeInvalidInteger("integer", int.MinValue, int.MaxValue), this.Context);
}
public override int? GetIntegerValueOrDefault(int? defaultValue = null)
@@ -73,7 +74,7 @@ public override long GetLongValue()
return value;
}
- throw new AsyncApiReaderException("Value could not parse to long.");
+ throw new AsyncApiReaderException(this.DescribeInvalidInteger("long", long.MinValue, long.MaxValue), this.Context);
}
public override long? GetLongValueOrDefault(long? defaultValue = null)
@@ -93,7 +94,7 @@ public override bool GetBooleanValue()
return value;
}
- throw new AsyncApiReaderException("Value could not parse to bool.");
+ throw new AsyncApiReaderException($"Value '{this.GetScalarValue()}' could not parse to bool. Expected true or false.", this.Context);
}
public override bool? GetBooleanValueOrDefault(bool? defaultValue = null)
@@ -111,5 +112,19 @@ public override AsyncApiAny CreateAny()
var value = this.GetScalarValue();
return new AsyncApiAny(this.node);
}
+
+ ///
+ /// Describes why the scalar is not a valid integer of the given type, telling an out-of-range number apart from a value that is not a whole number.
+ ///
+ private string DescribeInvalidInteger(string typeName, long min, long max)
+ {
+ var value = this.GetScalarValue();
+ var digits = value?.Trim().TrimStart('+', '-');
+ var isWholeNumber = !string.IsNullOrEmpty(digits) && digits.All(c => c >= '0' && c <= '9');
+
+ return isWholeNumber
+ ? $"Value '{value}' could not parse to {typeName}. It must be between {min} and {max}."
+ : $"Value '{value}' could not parse to {typeName}. Expected a whole number.";
+ }
}
}
\ No newline at end of file
diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiJsonSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiJsonSchemaDeserializer.cs
index 4813619..2bf4c58 100644
--- a/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiJsonSchemaDeserializer.cs
+++ b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiJsonSchemaDeserializer.cs
@@ -80,30 +80,30 @@ public class AsyncApiJsonSchemaDeserializer
}
},
{
- "maxLength", (a, n) => { a.MaxLength = ParseInteger(n); }
+ "maxLength", (a, n) => { a.MaxLength = n.GetIntegerValue(); }
},
{
- "minLength", (a, n) => { a.MinLength = ParseInteger(n); }
+ "minLength", (a, n) => { a.MinLength = n.GetIntegerValue(); }
},
{
"pattern", (a, n) => { a.Pattern = n.GetScalarValue(); }
},
{
- "maxItems", (a, n) => { a.MaxItems = ParseInteger(n); }
+ "maxItems", (a, n) => { a.MaxItems = n.GetIntegerValue(); }
},
{
- "minItems", (a, n) => { a.MinItems = ParseInteger(n); }
+ "minItems", (a, n) => { a.MinItems = n.GetIntegerValue(); }
},
{
"uniqueItems", (a, n) => { a.UniqueItems = bool.Parse(n.GetScalarValue()); }
},
{
"maxProperties",
- (a, n) => { a.MaxProperties = ParseInteger(n); }
+ (a, n) => { a.MaxProperties = n.GetIntegerValue(); }
},
{
"minProperties",
- (a, n) => { a.MinProperties = ParseInteger(n); }
+ (a, n) => { a.MinProperties = n.GetIntegerValue(); }
},
{
"enum", (a, n) => { a.Enum = n.CreateListOfAny(); }
@@ -215,18 +215,6 @@ public class AsyncApiJsonSchemaDeserializer
},
};
- private static int ParseInteger(ParseNode node)
- {
- try
- {
- return int.Parse(node.GetScalarValue(), node.Context.Settings.CultureInfo);
- }
- catch (Exception e)
- {
- throw new AsyncApiReaderException("The value is out of range.", node.Context);
- }
- }
-
private static readonly PatternFieldMap schemaPatternFields =
new()
{
diff --git a/test/ByteBard.AsyncAPI.Tests/ReaderErrorDetails_Should.cs b/test/ByteBard.AsyncAPI.Tests/ReaderErrorDetails_Should.cs
new file mode 100644
index 0000000..ac9cd66
--- /dev/null
+++ b/test/ByteBard.AsyncAPI.Tests/ReaderErrorDetails_Should.cs
@@ -0,0 +1,68 @@
+namespace ByteBard.AsyncAPI.Tests
+{
+ using System.Text.Json.Nodes;
+ using ByteBard.AsyncAPI.Models;
+ using ByteBard.AsyncAPI.Readers;
+ using ByteBard.AsyncAPI.Readers.ParseNodes;
+ using FluentAssertions;
+ using NUnit.Framework;
+
+ internal class ReaderErrorDetails_Should : TestBase
+ {
+ [TestCase("abc", "Value 'abc' could not parse to integer. Expected a whole number.")]
+ [TestCase("1.5", "Value '1.5' could not parse to integer. Expected a whole number.")]
+ [TestCase("99999999999", "Value '99999999999' could not parse to integer. It must be between -2147483648 and 2147483647.")]
+ public void SchemaIntegerField_ReportsTheValueAndWhyItIsInvalid(string value, string expectedMessage)
+ {
+ var yaml = $"type: string\nmaxLength: '{value}'";
+
+ new AsyncApiStringReader().ReadFragment(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
+
+ var error = diagnostics.Errors.Should().ContainSingle().Subject;
+ error.Message.Should().Be(expectedMessage);
+ error.Pointer.Should().Be("#/maxLength");
+ }
+
+ [Test]
+ public void MissingLocalReference_IsReportedOnce()
+ {
+ var yaml =
+ """
+ asyncapi: 3.0.0
+ info:
+ title: test
+ version: 1.0.0
+ channels:
+ users:
+ messages:
+ created:
+ payload:
+ $ref: '#/components/schemas/Missing'
+ """;
+
+ new AsyncApiStringReader().Read(yaml, out var diagnostics);
+
+ diagnostics.Errors.Should().ContainSingle()
+ .Which.Message.Should().Be("Could not resolve reference fragment.");
+ }
+
+ [TestCase("/info/title/nested")]
+ [TestCase("/tags/5")]
+ [TestCase("/tags/name")]
+ [TestCase("/missing")]
+ public void Find_ReturnsNull_WhenThePointerDoesNotLeadToANode(string pointer)
+ {
+ var json = JsonNode.Parse("""{ "info": { "title": "test" }, "tags": [ { "name": "a" } ] }""");
+
+ new JsonPointer(pointer).Find(json).Should().BeNull();
+ }
+
+ [Test]
+ public void Find_FollowsObjectsAndArrays()
+ {
+ var json = JsonNode.Parse("""{ "tags": [ { "name": "a" } ] }""");
+
+ new JsonPointer("/tags/0/name").Find(json).GetValue().Should().Be("a");
+ }
+ }
+}