Skip to content

Commit a13c612

Browse files
bieluclaude
authored andcommitted
fix: report invalid MQTT and ROS2 binding scalars as errors
Several MQTT and ROS2 binding fields were parsed with Get*ValueOrDefault(), which returns null for a value that cannot be parsed. A document with e.g. `cleanSession: yes-please` or `keepAlive: abc` was read with no diagnostics and the value was silently dropped, so writing it back lost data. These fields now use GetIntegerValue()/GetBooleanValue(), which report "Value could not parse to ..." in diagnostic.Errors, matching the other bindings (IBM MQ, JMS, Kafka, ...): - MQTT server: cleanSession, keepAlive, lastWill.qos - MQTT message: payloadFormatIndicator - ROS2 server: domainId - ROS2 QoS policies: lifespan, deadline, leaseDuration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 150d6d7 commit a13c612

5 files changed

Lines changed: 77 additions & 8 deletions

File tree

‎src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version
9797
protected override FixedFieldMap<MQTTMessageBinding> FixedFieldMap => new()
9898
{
9999
{ "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } },
100-
{ "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValueOrDefault(); } },
100+
{ "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValue(); } },
101101
{ "correlationData", (a, n) => { a.CorrelationData = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } },
102102
{ "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } },
103103
{ "responseTopic", (a, n) => { a.ResponseTopicValue = MQTTStringOrSchema.Parse(n); } },

‎src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,17 @@ public int? MaximumPacketSize
6565
{
6666
{ "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } },
6767
{ "clientId", (a, n) => { a.ClientId = n.GetScalarValue(); } },
68-
{ "cleanSession", (a, n) => { a.CleanSession = n.GetBooleanValueOrDefault(); } },
68+
{ "cleanSession", (a, n) => { a.CleanSession = n.GetBooleanValue(); } },
6969
{ "lastWill", (a, n) => { a.LastWill = n.ParseMap(LastWillFixedFields); } },
70-
{ "keepAlive", (a, n) => { a.KeepAlive = n.GetIntegerValueOrDefault(); } },
70+
{ "keepAlive", (a, n) => { a.KeepAlive = n.GetIntegerValue(); } },
7171
{ "sessionExpiryInterval", (a, n) => { a.SessionExpiryIntervalValue = MQTTIntegerOrSchema.Parse(n); } },
7272
{ "maximumPacketSize", (a, n) => { a.MaximumPacketSizeValue = MQTTIntegerOrSchema.Parse(n); } },
7373
};
7474

7575
private static FixedFieldMap<LastWill> LastWillFixedFields = new()
7676
{
7777
{ "topic", (a, n) => { a.Topic = n.GetScalarValue(); } },
78-
{ "qos", (a, n) => { a.QoS = (uint?)n.GetIntegerValueOrDefault(); } },
78+
{ "qos", (a, n) => { a.QoS = (uint?)n.GetIntegerValue(); } },
7979
{ "message", (a, n) => { a.Message = n.GetScalarValue(); } },
8080
{ "retain", (a, n) => { a.Retain = n.GetBooleanValue(); } },
8181
};

‎src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2QosPolicies.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ public class ROS2QosPolicies : IAsyncApiElement
2121
{ "reliability", (a, n) => { a.Reliability = n.GetScalarValue(); } },
2222
{ "history", (a, n) => { a.History = n.GetScalarValue(); } },
2323
{ "durability", (a, n) => { a.Durability = n.GetScalarValue(); } },
24-
{ "lifespan", (a, n) => { a.Lifespan = n.GetIntegerValueOrDefault(); } },
25-
{ "deadline", (a, n) => { a.Deadline = n.GetIntegerValueOrDefault(); } },
24+
{ "lifespan", (a, n) => { a.Lifespan = n.GetIntegerValue(); } },
25+
{ "deadline", (a, n) => { a.Deadline = n.GetIntegerValue(); } },
2626
{ "liveliness", (a, n) => { a.Liveliness = n.GetScalarValue(); } },
27-
{ "leaseDuration", (a, n) => { a.LeaseDuration = n.GetIntegerValueOrDefault(); } },
27+
{ "leaseDuration", (a, n) => { a.LeaseDuration = n.GetIntegerValue(); } },
2828
};
2929

3030
/// <summary>

‎src/ByteBard.AsyncAPI.Bindings/ROS2/ROS2ServerBinding.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class ROS2ServerBinding : ServerBinding<ROS2ServerBinding>
1818
protected override FixedFieldMap<ROS2ServerBinding> FixedFieldMap => new()
1919
{
2020
{ "rmwImplementation", (a, n) => { a.RmwImplementation = n.GetScalarValue(); } },
21-
{ "domainId", (a, n) => { a.DomainId = n.GetIntegerValueOrDefault(); } },
21+
{ "domainId", (a, n) => { a.DomainId = n.GetIntegerValue(); } },
2222
};
2323

2424
/// <summary>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace ByteBard.AsyncAPI.Tests.Bindings
2+
{
3+
using FluentAssertions;
4+
using ByteBard.AsyncAPI.Bindings;
5+
using ByteBard.AsyncAPI.Models;
6+
using ByteBard.AsyncAPI.Readers;
7+
using NUnit.Framework;
8+
9+
internal class BindingInvalidScalars_Should : TestBase
10+
{
11+
[TestCase("mqtt", "cleanSession: yes-please", "bool")]
12+
[TestCase("mqtt", "keepAlive: abc", "integer")]
13+
[TestCase("mqtt", "lastWill:\n topic: /wills\n qos: high", "integer")]
14+
[TestCase("ros2", "domainId: abc", "integer")]
15+
public void ServerBinding_WithInvalidScalar_ReportsError(string bindingKey, string field, string expectedType)
16+
{
17+
var yaml =
18+
"host: localhost\n" +
19+
"protocol: " + bindingKey + "\n" +
20+
"bindings:\n" +
21+
" " + bindingKey + ":\n" +
22+
Indent(field, 4);
23+
24+
new AsyncApiStringReader(Settings()).ReadFragment<AsyncApiServer>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
25+
26+
diagnostics.Errors.Should().ContainSingle().Which.Message.Should().Contain($"could not parse to {expectedType}");
27+
}
28+
29+
[Test]
30+
public void MessageBinding_WithInvalidPayloadFormatIndicator_ReportsError()
31+
{
32+
var yaml =
33+
"bindings:\n" +
34+
" mqtt:\n" +
35+
" payloadFormatIndicator: abc\n";
36+
37+
new AsyncApiStringReader(Settings()).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
38+
39+
diagnostics.Errors.Should().ContainSingle().Which.Message.Should().Contain("could not parse to integer");
40+
}
41+
42+
[TestCase("lifespan")]
43+
[TestCase("deadline")]
44+
[TestCase("leaseDuration")]
45+
public void ROS2OperationBinding_WithInvalidQosDuration_ReportsError(string field)
46+
{
47+
var yaml =
48+
"action: send\n" +
49+
"channel:\n" +
50+
" $ref: '#/channels/camera'\n" +
51+
"bindings:\n" +
52+
" ros2:\n" +
53+
" qosPolicies:\n" +
54+
" " + field + ": forever\n";
55+
56+
new AsyncApiStringReader(Settings()).ReadFragment<AsyncApiOperation>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);
57+
58+
diagnostics.Errors.Should().ContainSingle().Which.Message.Should().Contain("could not parse to integer");
59+
}
60+
61+
private static AsyncApiReaderSettings Settings() => new() { Bindings = BindingsCollection.All };
62+
63+
private static string Indent(string value, int spaces)
64+
{
65+
var prefix = new string(' ', spaces);
66+
return prefix + value.Replace("\n", "\n" + prefix) + "\n";
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)