From a488b73a09f2eb2d327fd81dd99817fe965ca0c4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:41:48 +0100 Subject: [PATCH 1/2] fix: mark MQTT int accessors that hide large and schema values as obsolete MQTTServerBinding.SessionExpiryInterval, MaximumPacketSize and MQTTOperationBinding.MessageExpiryInterval are int? views over the MQTTIntegerOrSchema values. They return null when the value is a schema or above Int32.MaxValue, which MQTT allows (up to 4294967295; for the session expiry interval 0xFFFFFFFF means "never expires"). Callers cannot tell that apart from "not set". Keep the properties for compatibility, but document the behaviour and mark them [Obsolete] pointing to the *Value properties. Tests now use the *Value properties. Co-Authored-By: Claude Opus 5.5 --- .../MQTT/MQTTOperationBinding.cs | 5 +++++ .../MQTT/MQTTServerBinding.cs | 10 ++++++++++ .../Bindings/BindingExtensions_Should.cs | 2 +- .../MQTT/MQTTBindings_Should.cs | 6 +++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs index 193f4e6..7dfa79d 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -35,6 +35,11 @@ public bool Retain /// /// Interval in seconds or a Schema Object containing the definition of the lifetime of the message. /// + /// + /// Returns null when the value is a schema or does not fit in an Int32 (MQTT allows up to 4294967295). + /// Use to read every value. + /// + [Obsolete("Returns null for schema values and values above Int32.MaxValue. Use MessageExpiryIntervalValue instead.")] public int? MessageExpiryInterval { get => this.MessageExpiryIntervalValue?.Integer; diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs index e916fef..bb0cc86 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs @@ -37,6 +37,11 @@ public class MQTTServerBinding : ServerBinding /// Interval in seconds the broker maintains a session /// for a disconnected client until this interval expires. /// + /// + /// Returns null when the value is a schema or does not fit in an Int32 (MQTT allows up to 4294967295). + /// Use to read every value. + /// + [Obsolete("Returns null for schema values and values above Int32.MaxValue. Use SessionExpiryIntervalValue instead.")] public int? SessionExpiryInterval { get => this.SessionExpiryIntervalValue?.Integer; @@ -50,6 +55,11 @@ public int? SessionExpiryInterval /// Number of bytes representing the maximum packet size /// the client is willing to accept. /// + /// + /// Returns null when the value is a schema or does not fit in an Int32 (MQTT allows up to 4294967295). + /// Use to read every value. + /// + [Obsolete("Returns null for schema values and values above Int32.MaxValue. Use MaximumPacketSizeValue instead.")] public int? MaximumPacketSize { get => this.MaximumPacketSizeValue?.Integer; diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs index 3e658ff..c8d9e27 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs @@ -55,7 +55,7 @@ public void V2_TryGetValue_WithOperationBinding_ReturnsBinding() operation.Bindings.Add(new MQTTOperationBinding { QoS = 23, - MessageExpiryInterval = 1, + MessageExpiryIntervalValue = MQTTIntegerOrSchema.FromInteger(1), Retain = true, }); diff --git a/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs index 53cb570..ebd5460 100644 --- a/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs @@ -46,8 +46,8 @@ public void V2_MQTTServerBinding_FilledObject_SerializesAndDeserializes() Retain = false, }, KeepAlive = 60, - SessionExpiryInterval = 600, - MaximumPacketSize = 1200, + SessionExpiryIntervalValue = MQTTIntegerOrSchema.FromInteger(600), + MaximumPacketSizeValue = MQTTIntegerOrSchema.FromInteger(1200), }); // Act @@ -82,7 +82,7 @@ public void V2_MQTTOperationBinding_WithFilledObject_SerializesAndDeserializes() { QoS = 2, Retain = true, - MessageExpiryInterval = 60, + MessageExpiryIntervalValue = MQTTIntegerOrSchema.FromInteger(60), }); // Act From ccdb44e8962d490d57f2b715dd41c5be45721d78 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:57:30 +0100 Subject: [PATCH 2/2] refactor: model MQTT scalar-or-schema values as a type hierarchy MQTTIntegerOrSchema and MQTTStringOrSchema are now abstract bases with MQTTInteger/MQTTIntegerSchema and MQTTString/MQTTStringSchema subtypes, so callers use type checks instead of null checks. Implicit conversions from long/string/AsyncApiJsonSchema keep assignments such as SessionExpiryInterval = 600 compiling, and each subtype converts back to its underlying value. The lossy int?/string accessors and the *Value properties are removed; SessionExpiryInterval, MaximumPacketSize, MessageExpiryInterval and ResponseTopic now expose the union type directly. Co-Authored-By: Claude Opus 5.5 --- .../MQTT/MQTTMessageBinding.cs | 22 +-- .../MQTT/MQTTOperationBinding.cs | 27 +-- .../MQTT/MQTTServerBinding.cs | 49 ++--- .../MQTT/MQTTValueUnions.cs | 179 ++++++++++-------- .../Bindings/BindingExtensions_Should.cs | 2 +- .../MQTT/MQTTBindingVariants_Should.cs | 33 +++- .../Bindings/MQTT5/MQTT5Bindings_Should.cs | 16 +- .../MQTT/MQTTBindings_Should.cs | 6 +- 8 files changed, 168 insertions(+), 166 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs index 473bf23..460699e 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -30,15 +30,9 @@ public class MQTTMessageBinding : MessageBinding /// /// The topic (channel URI) for a response message. + /// Either an or an . /// - public string ResponseTopic - { - get => this.ResponseTopicValue?.Value; - set => this.ResponseTopicValue = value == null ? null : MQTTStringOrSchema.FromString(value); - } - - /// The string-or-schema response topic value. - public MQTTStringOrSchema ResponseTopicValue { get; set; } + public MQTTStringOrSchema ResponseTopic { get; set; } /// /// Writes the MQTT message binding object and its extensions for AsyncAPI 2.0. @@ -61,7 +55,7 @@ public override void SerializeV3(IAsyncApiWriter writer) /// /// Writes the MQTT message binding object and its extensions for AsyncAPI 3.0. /// - /// ResponseTopicValue is written as a string or schema; nested schemas use AsyncAPI 3.0 serialization. + /// ResponseTopic is written as a string or schema; nested schemas use AsyncAPI 3.0 serialization. /// is null. public override void SerializeProperties(IAsyncApiWriter writer) { @@ -85,8 +79,12 @@ private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version writer.WriteOptionalProperty("payloadFormatIndicator", this.PayloadFormatIndicator); writer.WriteOptionalObject("correlationData", this.CorrelationData, (w, h) => { if (version == AsyncApiVersion.AsyncApi2_0) h.SerializeV2(w); else h.SerializeV3(w); }); writer.WriteOptionalProperty("contentType", this.ContentType); - if (this.ResponseTopicValue?.Value != null) writer.WriteOptionalProperty("responseTopic", this.ResponseTopicValue.Value); - else if (this.ResponseTopicValue?.Schema != null) writer.WriteOptionalObject("responseTopic", this.ResponseTopicValue.Schema, (w, schema) => { if (version == AsyncApiVersion.AsyncApi2_0) schema.SerializeV2(w); else schema.SerializeV3(w); }); + if (this.ResponseTopic != null) + { + writer.WritePropertyName("responseTopic"); + this.ResponseTopic.Serialize(writer, version); + } + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); @@ -100,7 +98,7 @@ private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version { "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValue(); } }, { "correlationData", (a, n) => { a.CorrelationData = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, - { "responseTopic", (a, n) => { a.ResponseTopicValue = MQTTStringOrSchema.Parse(n); } }, + { "responseTopic", (a, n) => { a.ResponseTopic = MQTTStringOrSchema.Parse(n); } }, }; } } diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs index 7dfa79d..96c9dbb 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -34,20 +34,9 @@ public bool Retain /// /// Interval in seconds or a Schema Object containing the definition of the lifetime of the message. + /// Either an or an . /// - /// - /// Returns null when the value is a schema or does not fit in an Int32 (MQTT allows up to 4294967295). - /// Use to read every value. - /// - [Obsolete("Returns null for schema values and values above Int32.MaxValue. Use MessageExpiryIntervalValue instead.")] - public int? MessageExpiryInterval - { - get => this.MessageExpiryIntervalValue?.Integer; - set => this.MessageExpiryIntervalValue = value.HasValue ? MQTTIntegerOrSchema.FromInteger(value.Value) : null; - } - - /// The integer-or-schema message expiry interval value. - public MQTTIntegerOrSchema MessageExpiryIntervalValue { get; set; } + public MQTTIntegerOrSchema MessageExpiryInterval { get; set; } public override string BindingKey => "mqtt"; @@ -56,7 +45,7 @@ public int? MessageExpiryInterval { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "qos", (a, n) => { a.QoS = n.GetIntegerValue(); } }, { "retain", (a, n) => { a.Retain = n.GetBooleanValue(); } }, - { "messageExpiryInterval", (a, n) => { a.MessageExpiryIntervalValue = MQTTIntegerOrSchema.Parse(n); } }, + { "messageExpiryInterval", (a, n) => { a.MessageExpiryInterval = MQTTIntegerOrSchema.Parse(n); } }, }; /// @@ -103,12 +92,12 @@ private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version writer.WriteStartObject(); writer.WriteOptionalProperty("qos", this.qos); writer.WriteOptionalProperty("retain", this.retain); - if (this.MessageExpiryIntervalValue?.LongInteger is long integer) writer.WriteRequiredProperty("messageExpiryInterval", integer); - else if (this.MessageExpiryIntervalValue?.Schema != null) writer.WriteOptionalObject("messageExpiryInterval", this.MessageExpiryIntervalValue.Schema, (w, schema) => + if (this.MessageExpiryInterval != null) { - if (version == AsyncApiVersion.AsyncApi2_0) schema.SerializeV2(w); - else schema.SerializeV3(w); - }); + writer.WritePropertyName("messageExpiryInterval"); + this.MessageExpiryInterval.Serialize(writer, version); + } + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs index bb0cc86..164fef1 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs @@ -36,38 +36,16 @@ public class MQTTServerBinding : ServerBinding /// /// Interval in seconds the broker maintains a session /// for a disconnected client until this interval expires. + /// Either an or an . /// - /// - /// Returns null when the value is a schema or does not fit in an Int32 (MQTT allows up to 4294967295). - /// Use to read every value. - /// - [Obsolete("Returns null for schema values and values above Int32.MaxValue. Use SessionExpiryIntervalValue instead.")] - public int? SessionExpiryInterval - { - get => this.SessionExpiryIntervalValue?.Integer; - set => this.SessionExpiryIntervalValue = value.HasValue ? MQTTIntegerOrSchema.FromInteger(value.Value) : null; - } - - /// The integer-or-schema session expiry interval value. - public MQTTIntegerOrSchema SessionExpiryIntervalValue { get; set; } + public MQTTIntegerOrSchema SessionExpiryInterval { get; set; } /// /// Number of bytes representing the maximum packet size /// the client is willing to accept. + /// Either an or an . /// - /// - /// Returns null when the value is a schema or does not fit in an Int32 (MQTT allows up to 4294967295). - /// Use to read every value. - /// - [Obsolete("Returns null for schema values and values above Int32.MaxValue. Use MaximumPacketSizeValue instead.")] - public int? MaximumPacketSize - { - get => this.MaximumPacketSizeValue?.Integer; - set => this.MaximumPacketSizeValue = value.HasValue ? MQTTIntegerOrSchema.FromInteger(value.Value) : null; - } - - /// The integer-or-schema maximum packet size value. - public MQTTIntegerOrSchema MaximumPacketSizeValue { get; set; } + public MQTTIntegerOrSchema MaximumPacketSize { get; set; } public override string BindingKey => "mqtt"; @@ -78,8 +56,8 @@ public int? MaximumPacketSize { "cleanSession", (a, n) => { a.CleanSession = n.GetBooleanValue(); } }, { "lastWill", (a, n) => { a.LastWill = n.ParseMap(LastWillFixedFields); } }, { "keepAlive", (a, n) => { a.KeepAlive = n.GetIntegerValue(); } }, - { "sessionExpiryInterval", (a, n) => { a.SessionExpiryIntervalValue = MQTTIntegerOrSchema.Parse(n); } }, - { "maximumPacketSize", (a, n) => { a.MaximumPacketSizeValue = MQTTIntegerOrSchema.Parse(n); } }, + { "sessionExpiryInterval", (a, n) => { a.SessionExpiryInterval = MQTTIntegerOrSchema.Parse(n); } }, + { "maximumPacketSize", (a, n) => { a.MaximumPacketSize = MQTTIntegerOrSchema.Parse(n); } }, }; private static FixedFieldMap LastWillFixedFields = new() @@ -136,8 +114,8 @@ private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version writer.WriteOptionalProperty("cleanSession", this.CleanSession); writer.WriteOptionalObject("lastWill", this.LastWill, (w, l) => l.Serialize(w)); writer.WriteOptionalProperty("keepAlive", this.KeepAlive); - this.WriteIntegerOrSchema(writer, "sessionExpiryInterval", this.SessionExpiryIntervalValue, version); - this.WriteIntegerOrSchema(writer, "maximumPacketSize", this.MaximumPacketSizeValue, version); + this.WriteIntegerOrSchema(writer, "sessionExpiryInterval", this.SessionExpiryInterval, version); + this.WriteIntegerOrSchema(writer, "maximumPacketSize", this.MaximumPacketSize, version); writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); @@ -153,12 +131,13 @@ private void SerializeProperties(IAsyncApiWriter writer, AsyncApiVersion version /// A value is present and or is null. private void WriteIntegerOrSchema(IAsyncApiWriter writer, string name, MQTTIntegerOrSchema value, AsyncApiVersion version) { - if (value?.LongInteger is long integer) writer.WriteRequiredProperty(name, integer); - else if (value?.Schema != null) writer.WriteOptionalObject(name, value.Schema, (w, schema) => + if (value == null) { - if (version == AsyncApiVersion.AsyncApi2_0) schema.SerializeV2(w); - else schema.SerializeV3(w); - }); + return; + } + + writer.WritePropertyName(name); + value.Serialize(writer, version); } } } diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs index 71f809b..b5630f7 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTValueUnions.cs @@ -6,57 +6,32 @@ namespace ByteBard.AsyncAPI.Bindings.MQTT using ByteBard.AsyncAPI.Readers.ParseNodes; using ByteBard.AsyncAPI.Writers; - /// A MQTT binding value represented by either an integer or a JSON Schema. - public sealed class MQTTIntegerOrSchema + /// + /// A MQTT binding value represented by either an integer () or a JSON Schema (). + /// + /// Integers and schemas convert implicitly, so binding.SessionExpiryInterval = 600; works. + public abstract class MQTTIntegerOrSchema { - /// - /// Stores the supplied integer and schema without validating that exactly one is present. - /// - private MQTTIntegerOrSchema(long? integer, AsyncApiJsonSchema schema) + private protected MQTTIntegerOrSchema() { - this.LongInteger = integer; - this.Schema = schema; } - /// The integer value when it is within the range of the legacy int API. - public int? Integer => this.LongInteger.HasValue && this.LongInteger.Value >= int.MinValue && this.LongInteger.Value <= int.MaxValue - ? (int)this.LongInteger.Value - : null; + public static implicit operator MQTTIntegerOrSchema(long value) => new MQTTInteger(value); - /// The full-width integer value. - public long? LongInteger { get; } - - public AsyncApiJsonSchema Schema { get; } - - /// - /// Creates an integer alternative without enforcing MQTT field limits. - /// - public static MQTTIntegerOrSchema FromInteger(int value) => new(value, null); - - /// - /// Creates an integer alternative without enforcing MQTT field limits. - /// - /// Values outside the Int32 range remain available through LongInteger; Integer returns null for them. - public static MQTTIntegerOrSchema FromLong(long value) => new(value, null); - - /// - /// Creates a schema alternative, retaining the supplied schema or reference without copying it. - /// - /// is null. - public static MQTTIntegerOrSchema FromSchema(AsyncApiJsonSchema schema) => new(null, schema ?? throw new ArgumentNullException(nameof(schema))); + public static implicit operator MQTTIntegerOrSchema(AsyncApiJsonSchema schema) => schema == null ? null : new MQTTIntegerSchema(schema); /// /// Parses a scalar as a signed 64-bit integer, or an object as a schema or schema reference. /// - /// A value containing the parsed scalar or schema. + /// A or . /// Schema field errors handled by the reader are recorded as diagnostics. /// The scalar cannot be parsed as an Int64, or a non-scalar node is not a schema object. /// A schema reference has a non-scalar $ref value. public static MQTTIntegerOrSchema Parse(ParseNode node) { return node is ValueNode - ? FromLong(node.GetLongValue()) - : FromSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); + ? new MQTTInteger(node.GetLongValue()) + : new MQTTIntegerSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); } /// @@ -64,62 +39,75 @@ public static MQTTIntegerOrSchema Parse(ParseNode node) /// /// The writer receiving the value. /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. - /// The writer is null when writing a scalar or an ordinary schema. - /// The writer is null when writing a schema reference. - public void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) - { - if (this.LongInteger.HasValue) - { - writer.WriteValue(this.LongInteger.Value); - } - else if (this.Schema != null) - { - if (version == AsyncApiVersion.AsyncApi2_0) this.Schema.SerializeV2(writer); - else this.Schema.SerializeV3(writer); - } - } + public abstract void Serialize(IAsyncApiWriter writer, AsyncApiVersion version); } - /// A MQTT message response topic represented by either a string or a JSON Schema. - public sealed class MQTTStringOrSchema + /// The integer alternative of . + /// Holds the full 64-bit value, so MQTT values up to 4294967295 are kept. MQTT field limits are not enforced. + public sealed class MQTTInteger : MQTTIntegerOrSchema { - /// - /// Stores the supplied response topic and schema without validating that exactly one is present. - /// - private MQTTStringOrSchema(string value, AsyncApiJsonSchema schema) + public MQTTInteger(long value) { this.Value = value; - this.Schema = schema; } - public string Value { get; } - - public AsyncApiJsonSchema Schema { get; } + public long Value { get; } - /// - /// Creates a response topic alternative without validating topic syntax. - /// /// is null. - public static MQTTStringOrSchema FromString(string value) => new(value ?? throw new ArgumentNullException(nameof(value)), null); + public static implicit operator long(MQTTInteger value) => (value ?? throw new ArgumentNullException(nameof(value))).Value; + + public override void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) => writer.WriteValue(this.Value); + } + /// The schema alternative of . + public sealed class MQTTIntegerSchema : MQTTIntegerOrSchema + { /// - /// Creates a schema alternative, retaining the supplied schema or reference without copying it. + /// Retains the supplied schema or reference without copying it. /// /// is null. - public static MQTTStringOrSchema FromSchema(AsyncApiJsonSchema schema) => new(null, schema ?? throw new ArgumentNullException(nameof(schema))); + public MQTTIntegerSchema(AsyncApiJsonSchema schema) + { + this.Schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + + public AsyncApiJsonSchema Schema { get; } + + public static implicit operator AsyncApiJsonSchema(MQTTIntegerSchema value) => value?.Schema; + + public override void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) + { + if (version == AsyncApiVersion.AsyncApi2_0) this.Schema.SerializeV2(writer); + else this.Schema.SerializeV3(writer); + } + } + + /// + /// A MQTT message response topic represented by either a string () or a JSON Schema (). + /// + /// Strings and schemas convert implicitly, so binding.ResponseTopic = "replies"; works. + public abstract class MQTTStringOrSchema + { + private protected MQTTStringOrSchema() + { + } + + public static implicit operator MQTTStringOrSchema(string value) => value == null ? null : new MQTTString(value); + + public static implicit operator MQTTStringOrSchema(AsyncApiJsonSchema schema) => schema == null ? null : new MQTTStringSchema(schema); /// /// Converts a scalar to a response topic string, or parses an object as a schema or schema reference. /// - /// A value containing the parsed scalar or schema. + /// A or . /// Schema field errors handled by the reader are recorded as diagnostics. /// A non-scalar node is not a schema object. /// A schema reference has a non-scalar $ref value. public static MQTTStringOrSchema Parse(ParseNode node) { return node is ValueNode - ? FromString(node.GetScalarValue()) - : FromSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); + ? new MQTTString(node.GetScalarValue()) + : new MQTTStringSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node)); } /// @@ -127,19 +115,48 @@ public static MQTTStringOrSchema Parse(ParseNode node) /// /// The writer receiving the value. /// Uses AsyncAPI 2.0 schema serialization for ; otherwise uses AsyncAPI 3.0. - /// The writer is null when writing a scalar or an ordinary schema. - /// The writer is null when writing a schema reference. - public void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) + public abstract void Serialize(IAsyncApiWriter writer, AsyncApiVersion version); + } + + /// The string alternative of . + public sealed class MQTTString : MQTTStringOrSchema + { + /// + /// Creates a response topic without validating topic syntax. + /// + /// is null. + public MQTTString(string value) + { + this.Value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public string Value { get; } + + public static implicit operator string(MQTTString value) => value?.Value; + + public override void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) => writer.WriteValue(this.Value); + } + + /// The schema alternative of . + public sealed class MQTTStringSchema : MQTTStringOrSchema + { + /// + /// Retains the supplied schema or reference without copying it. + /// + /// is null. + public MQTTStringSchema(AsyncApiJsonSchema schema) + { + this.Schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + + public AsyncApiJsonSchema Schema { get; } + + public static implicit operator AsyncApiJsonSchema(MQTTStringSchema value) => value?.Schema; + + public override void Serialize(IAsyncApiWriter writer, AsyncApiVersion version) { - if (this.Schema != null) - { - if (version == AsyncApiVersion.AsyncApi2_0) this.Schema.SerializeV2(writer); - else this.Schema.SerializeV3(writer); - } - else if (this.Value != null) - { - writer.WriteValue(this.Value); - } + if (version == AsyncApiVersion.AsyncApi2_0) this.Schema.SerializeV2(writer); + else this.Schema.SerializeV3(writer); } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs index c8d9e27..3e658ff 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs @@ -55,7 +55,7 @@ public void V2_TryGetValue_WithOperationBinding_ReturnsBinding() operation.Bindings.Add(new MQTTOperationBinding { QoS = 23, - MessageExpiryIntervalValue = MQTTIntegerOrSchema.FromInteger(1), + MessageExpiryInterval = 1, Retain = true, }); diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs index 68d5063..8aa6627 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs @@ -30,8 +30,8 @@ public void V2_SchemaAndReferenceVariants_AndBindingVersions_AreParsedAndSeriali var serverBinding = server.Bindings["mqtt"] as MQTTServerBinding; serverBinding.BindingVersion.Should().Be("0.2.0"); - serverBinding.SessionExpiryIntervalValue.Schema.Type.Should().Be(SchemaType.Integer); - serverBinding.MaximumPacketSizeValue.Schema.Should().BeOfType(); + serverBinding.SessionExpiryInterval.Should().BeOfType().Which.Schema.Type.Should().Be(SchemaType.Integer); + serverBinding.MaximumPacketSize.Should().BeOfType().Which.Schema.Should().BeOfType(); server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should().Contain("minimum: 1").And.Contain("$ref: '#/components/schemas/PacketSize'"); var operationYaml = @@ -47,7 +47,7 @@ public void V2_SchemaAndReferenceVariants_AndBindingVersions_AreParsedAndSeriali var operation = new AsyncApiStringReader(settings).ReadFragment(operationYaml, AsyncApiVersion.AsyncApi2_0, out _); var operationBinding = operation.Bindings["mqtt"] as MQTTOperationBinding; operationBinding.BindingVersion.Should().Be("0.2.0"); - operationBinding.MessageExpiryIntervalValue.Schema.Should().BeOfType(); + operationBinding.MessageExpiryInterval.Should().BeOfType().Which.Schema.Should().BeOfType(); var messageYaml = """ @@ -61,7 +61,7 @@ public void V2_SchemaAndReferenceVariants_AndBindingVersions_AreParsedAndSeriali var message = new AsyncApiStringReader(settings).ReadFragment(messageYaml, AsyncApiVersion.AsyncApi2_0, out _); var messageBinding = message.Bindings["mqtt"] as MQTTMessageBinding; messageBinding.BindingVersion.Should().Be("0.2.0"); - messageBinding.ResponseTopicValue.Schema.Type.Should().Be(SchemaType.String); + messageBinding.ResponseTopic.Should().BeOfType().Which.Schema.Type.Should().Be(SchemaType.String); message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should().Contain("pattern: ^reply/"); } @@ -84,13 +84,13 @@ public void ServerSchemaOnlyBinding_RoundTripsWithoutDiagnostics(AsyncApiVersion var second = reader.ReadFragment(first.SerializeAsYaml(version), version, out var secondDiagnostics); secondDiagnostics.Errors.Should().BeEmpty(); - (second.Bindings["mqtt"] as MQTTServerBinding).SessionExpiryIntervalValue.Schema.Type.Should().Be(SchemaType.Integer); + (second.Bindings["mqtt"] as MQTTServerBinding).SessionExpiryInterval.Should().BeOfType().Which.Schema.Type.Should().Be(SchemaType.Integer); } [Test] public void IntegerUnion_PreservesUnsigned32BitMaximum() { - var binding = new MQTTServerBinding { SessionExpiryIntervalValue = MQTTIntegerOrSchema.FromLong(4294967295L) }; + var binding = new MQTTServerBinding { SessionExpiryInterval = 4294967295L }; var yaml = binding.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); yaml.Should().Contain("sessionExpiryInterval: 4294967295"); @@ -102,7 +102,26 @@ public void IntegerUnion_PreservesUnsigned32BitMaximum() var parsed = new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = BindingsCollection.MQTT }) .ReadFragment(parsedYaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics); diagnostics.Errors.Should().BeEmpty(); - (parsed.Bindings["mqtt"] as MQTTServerBinding).SessionExpiryIntervalValue.LongInteger.Should().Be(4294967295L); + (parsed.Bindings["mqtt"] as MQTTServerBinding).SessionExpiryInterval.Should().BeOfType().Which.Value.Should().Be(4294967295L); + } + + [Test] + public void Unions_ConvertImplicitlyToAndFromTheirAlternatives() + { + var schema = new AsyncApiJsonSchema { Type = SchemaType.Integer }; + var server = new MQTTServerBinding { SessionExpiryInterval = 600, MaximumPacketSize = schema }; + var message = new MQTTMessageBinding { ResponseTopic = "reply/topic" }; + + long interval = server.SessionExpiryInterval.Should().BeOfType().Subject; + AsyncApiJsonSchema packetSize = server.MaximumPacketSize.Should().BeOfType().Subject; + string topic = message.ResponseTopic.Should().BeOfType().Subject; + + interval.Should().Be(600); + packetSize.Should().BeSameAs(schema); + topic.Should().Be("reply/topic"); + + message.ResponseTopic = (string)null; + message.ResponseTopic.Should().BeNull(); } [Test] diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs index 92fff0a..9bb18bc 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT5/MQTT5Bindings_Should.cs @@ -16,17 +16,17 @@ public void ServerBinding_SerializesScalarZeroAndMinusOne(AsyncApiVersion versio { var binding = new MQTT5ServerBinding { - SessionExpiryInterval = MQTTIntegerOrSchema.FromInteger(0), + SessionExpiryInterval = 0, BindingVersion = "0.1.0", }; var actual = binding.SerializeAsYaml(version); actual.Should().Contain("sessionExpiryInterval: 0").And.Contain("bindingVersion: 0.1.0"); - binding.SessionExpiryInterval = MQTTIntegerOrSchema.FromInteger(-1); + binding.SessionExpiryInterval = -1; actual = binding.SerializeAsYaml(version); actual.Should().Contain("sessionExpiryInterval: -1"); - binding.SessionExpiryInterval = MQTTIntegerOrSchema.FromLong(4294967295L); + binding.SessionExpiryInterval = 4294967295L; binding.SerializeAsYaml(version).Should().Contain("sessionExpiryInterval: 4294967295"); } @@ -36,13 +36,13 @@ public void ServerBinding_SerializesSchemaAndReference(AsyncApiVersion version) { var schemaBinding = new MQTT5ServerBinding { - SessionExpiryInterval = MQTTIntegerOrSchema.FromSchema(new AsyncApiJsonSchema { Type = SchemaType.Integer }), + SessionExpiryInterval = new MQTTIntegerSchema(new AsyncApiJsonSchema { Type = SchemaType.Integer }), }; schemaBinding.SerializeAsYaml(version).Should().Contain("type: integer"); var referenceBinding = new MQTT5ServerBinding { - SessionExpiryInterval = MQTTIntegerOrSchema.FromSchema(new AsyncApiJsonSchemaReference("#/components/schemas/interval")), + SessionExpiryInterval = new MQTTIntegerSchema(new AsyncApiJsonSchemaReference("#/components/schemas/interval")), }; referenceBinding.SerializeAsYaml(version).Should().Contain("#/components/schemas/interval"); } @@ -64,13 +64,13 @@ public void ServerBinding_ReadsAndRoundTripsSchemaAndReference(AsyncApiVersion v var parsed = reader.ReadFragment(yaml, version, out var diagnostics); diagnostics.Errors.Should().BeEmpty(); var binding = parsed.Bindings["mqtt5"] as MQTT5ServerBinding; - binding.SessionExpiryInterval.Schema.Should().BeOfType(); + binding.SessionExpiryInterval.Should().BeOfType().Which.Schema.Should().BeOfType(); var serialized = parsed.SerializeAsYaml(version); serialized.Should().Contain("#/components/schemas/interval"); var reparsed = reader.ReadFragment(serialized, version, out var roundTripDiagnostics); roundTripDiagnostics.Errors.Should().BeEmpty(); - (reparsed.Bindings["mqtt5"] as MQTT5ServerBinding).SessionExpiryInterval.Schema.Should().BeOfType(); + (reparsed.Bindings["mqtt5"] as MQTT5ServerBinding).SessionExpiryInterval.Should().BeOfType().Which.Schema.Should().BeOfType(); var schemaYaml = (version == AsyncApiVersion.AsyncApi2_0 ? "url: localhost\nprotocol: mqtt\n" : "host: localhost\nprotocol: mqtt\n") + """ bindings: @@ -81,7 +81,7 @@ public void ServerBinding_ReadsAndRoundTripsSchemaAndReference(AsyncApiVersion v """; var schemaParsed = reader.ReadFragment(schemaYaml, version, out var schemaDiagnostics); schemaDiagnostics.Errors.Should().BeEmpty(); - (schemaParsed.Bindings["mqtt5"] as MQTT5ServerBinding).SessionExpiryInterval.Schema.Type.Should().Be(SchemaType.Integer); + (schemaParsed.Bindings["mqtt5"] as MQTT5ServerBinding).SessionExpiryInterval.Should().BeOfType().Which.Schema.Type.Should().Be(SchemaType.Integer); schemaParsed.SerializeAsYaml(version).Should().Contain("minimum: 1"); } } diff --git a/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs index ebd5460..53cb570 100644 --- a/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs @@ -46,8 +46,8 @@ public void V2_MQTTServerBinding_FilledObject_SerializesAndDeserializes() Retain = false, }, KeepAlive = 60, - SessionExpiryIntervalValue = MQTTIntegerOrSchema.FromInteger(600), - MaximumPacketSizeValue = MQTTIntegerOrSchema.FromInteger(1200), + SessionExpiryInterval = 600, + MaximumPacketSize = 1200, }); // Act @@ -82,7 +82,7 @@ public void V2_MQTTOperationBinding_WithFilledObject_SerializesAndDeserializes() { QoS = 2, Retain = true, - MessageExpiryIntervalValue = MQTTIntegerOrSchema.FromInteger(60), + MessageExpiryInterval = 60, }); // Act