Skip to content

Commit c0c5ab2

Browse files
bieluclaude
andauthored
refactor!: model Solace values and StringOrStringList as type hierarchies (#55)
* refactor: model Solace integer-or-schema values as a type hierarchy SolaceIntegerOrSchema is now an abstract base with SolaceInteger and SolaceIntegerSchema subtypes, matching the MQTT union types. Callers use type checks instead of null-checking Integer and Schema, and an empty value can no longer be constructed, so Validate() is gone. Implicit conversions from long and AsyncApiJsonSchema keep assignments such as Priority = 7 compiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor!: model StringOrStringList as a type hierarchy StringOrStringList is now an abstract base with SingleString and StringList subtypes instead of wrapping an AsyncApiAny that callers had to inspect. Implicit conversions from string, string[] and List<string> replace new StringOrStringList(new AsyncApiAny(...)), and invalid values (non-strings, null items) can no longer be constructed. The Value property is replaced by Write(IAsyncApiWriter) for serialization. BREAKING CHANGE: the public constructor and the Value property are removed. Construct values through the implicit conversions or the SingleString/StringList constructors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f86120b commit c0c5ab2

13 files changed

Lines changed: 215 additions & 204 deletions

File tree

‎src/ByteBard.AsyncAPI.Bindings/Sns/Condition.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void Serialize(IAsyncApiWriter writer)
2727
writer.WriteStartObject();
2828
foreach (var conditionValue in this.Value)
2929
{
30-
writer.WriteRequiredMap(conditionValue.Key, conditionValue.Value, (w, t) => t.Value.Write(w));
30+
writer.WriteRequiredMap(conditionValue.Key, conditionValue.Value, (w, t) => t.Write(w));
3131
}
3232

3333
writer.WriteEndObject();

‎src/ByteBard.AsyncAPI.Bindings/Sns/PrincipalObject.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public override void Serialize(IAsyncApiWriter writer)
2121
}
2222

2323
writer.WriteStartObject();
24-
writer.WriteRequiredObject(this.Value.Key, this.Value.Value, (w, t) => t.Value.Write(w));
24+
writer.WriteRequiredObject(this.Value.Key, this.Value.Value, (w, t) => t.Write(w));
2525
writer.WriteEndObject();
2626
}
2727
}

‎src/ByteBard.AsyncAPI.Bindings/Sns/Statement.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public void Serialize(IAsyncApiWriter writer)
4545
writer.WriteStartObject();
4646
writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName());
4747
writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Serialize(w));
48-
writer.WriteRequiredObject("action", this.Action, (w, t) => t.Value.Write(w));
49-
writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Value.Write(w));
48+
writer.WriteRequiredObject("action", this.Action, (w, t) => t.Write(w));
49+
writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Write(w));
5050
writer.WriteOptionalObject("condition", this.Condition, (w, t) => t.Serialize(w));
5151
writer.WriteExtensions(this.Extensions);
5252
writer.WriteEndObject();

‎src/ByteBard.AsyncAPI.Bindings/Solace/SolaceIntegerOrSchema.cs‎

Lines changed: 59 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,81 +3,85 @@ namespace ByteBard.AsyncAPI.Bindings.Solace
33
using System;
44
using ByteBard.AsyncAPI.Models;
55
using ByteBard.AsyncAPI.Models.Interfaces;
6+
using ByteBard.AsyncAPI.Readers;
7+
using ByteBard.AsyncAPI.Readers.ParseNodes;
68
using ByteBard.AsyncAPI.Writers;
79

8-
/// <summary>Represents a Solace integer, JSON Schema object, or JSON Schema reference.</summary>
9-
public class SolaceIntegerOrSchema : IAsyncApiSerializable
10+
/// <summary>
11+
/// A Solace value represented by either an integer (<see cref="SolaceInteger"/>) or a JSON Schema or schema reference (<see cref="SolaceIntegerSchema"/>).
12+
/// </summary>
13+
/// <remarks>Integers and schemas convert implicitly, so <c>binding.Priority = 7;</c> works.</remarks>
14+
public abstract class SolaceIntegerOrSchema : IAsyncApiSerializable
1015
{
11-
private long? integer;
12-
private AsyncApiJsonSchema schema;
13-
14-
public long? Integer
16+
private protected SolaceIntegerOrSchema()
1517
{
16-
get => this.integer;
17-
set
18-
{
19-
this.integer = value;
20-
if (value.HasValue) this.schema = null;
21-
}
2218
}
2319

24-
public AsyncApiJsonSchema Schema
25-
{
26-
get => this.schema;
27-
set
28-
{
29-
this.schema = value;
30-
if (value != null) this.integer = null;
31-
}
32-
}
20+
public static implicit operator SolaceIntegerOrSchema(long value) => new SolaceInteger(value);
21+
22+
public static implicit operator SolaceIntegerOrSchema(AsyncApiJsonSchema schema) => schema == null ? null : new SolaceIntegerSchema(schema);
3323

3424
/// <summary>
35-
/// Writes the integer or schema value directly for AsyncAPI 2.0, after validating that a value is present.
25+
/// Parses a scalar as a signed 64-bit integer, or an object as a schema or schema reference.
3626
/// </summary>
37-
/// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
38-
/// <exception cref="InvalidOperationException">Neither an integer nor a schema is set.</exception>
39-
public void SerializeV2(IAsyncApiWriter writer) => this.Serialize(writer, false);
27+
/// <returns>A <see cref="SolaceInteger"/> or <see cref="SolaceIntegerSchema"/>.</returns>
28+
/// <remarks>Schema field errors handled by the reader are recorded as diagnostics. Scalar parsing errors propagate to the caller.</remarks>
29+
/// <exception cref="ByteBard.AsyncAPI.Readers.Exceptions.AsyncApiReaderException">The scalar cannot be parsed as an Int64, or a non-scalar node is not a schema object.</exception>
30+
/// <exception cref="ByteBard.AsyncAPI.Exceptions.AsyncApiException">A schema reference has a non-scalar $ref value.</exception>
31+
public static SolaceIntegerOrSchema Parse(ParseNode node)
32+
{
33+
return node is ValueNode
34+
? new SolaceInteger(node.GetLongValue())
35+
: new SolaceIntegerSchema(AsyncApiJsonSchemaDeserializer.LoadSchema(node));
36+
}
37+
4038
/// <summary>
41-
/// Writes the integer or schema value directly for AsyncAPI 3.0, after validating that a value is present.
39+
/// Writes the integer or schema value directly for AsyncAPI 2.0, without a property name or wrapper object.
4240
/// </summary>
43-
/// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
44-
/// <exception cref="InvalidOperationException">Neither an integer nor a schema is set.</exception>
45-
public void SerializeV3(IAsyncApiWriter writer) => this.Serialize(writer, true);
41+
public abstract void SerializeV2(IAsyncApiWriter writer);
4642

4743
/// <summary>
48-
/// Checks that an integer or schema is present, without validating its range or schema contents.
44+
/// Writes the integer or schema value directly for AsyncAPI 3.0, without a property name or wrapper object.
4945
/// </summary>
50-
/// <exception cref="InvalidOperationException">Neither an integer nor a schema is set.</exception>
51-
public void Validate()
46+
public abstract void SerializeV3(IAsyncApiWriter writer);
47+
}
48+
49+
/// <summary>The integer alternative of <see cref="SolaceIntegerOrSchema"/>.</summary>
50+
public sealed class SolaceInteger : SolaceIntegerOrSchema
51+
{
52+
public SolaceInteger(long value)
5253
{
53-
if (!this.Integer.HasValue && this.Schema == null)
54-
{
55-
throw new InvalidOperationException("A Solace integer-or-schema value must contain an integer or a schema.");
56-
}
54+
this.Value = value;
5755
}
5856

57+
public long Value { get; }
58+
59+
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
60+
public static implicit operator long(SolaceInteger value) => (value ?? throw new ArgumentNullException(nameof(value))).Value;
61+
62+
public override void SerializeV2(IAsyncApiWriter writer) => writer.WriteValue(this.Value);
63+
64+
public override void SerializeV3(IAsyncApiWriter writer) => writer.WriteValue(this.Value);
65+
}
66+
67+
/// <summary>The schema alternative of <see cref="SolaceIntegerOrSchema"/>.</summary>
68+
public sealed class SolaceIntegerSchema : SolaceIntegerOrSchema
69+
{
5970
/// <summary>
60-
/// Validates and writes the integer or schema value without a property name or wrapper object.
71+
/// Retains the supplied schema or reference without copying it.
6172
/// </summary>
62-
/// <param name="writer">The writer receiving the value.</param>
63-
/// <param name="v3">Uses AsyncAPI 3.0 schema serialization when true and AsyncAPI 2.0 when false.</param>
64-
/// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
65-
/// <exception cref="InvalidOperationException">Neither an integer nor a schema is set.</exception>
66-
private void Serialize(IAsyncApiWriter writer, bool v3)
73+
/// <exception cref="ArgumentNullException"><paramref name="schema"/> is null.</exception>
74+
public SolaceIntegerSchema(AsyncApiJsonSchema schema)
6775
{
68-
if (writer is null) throw new ArgumentNullException(nameof(writer));
69-
this.Validate();
70-
if (this.Integer.HasValue)
71-
{
72-
writer.WriteValue(this.Integer.Value);
73-
return;
74-
}
75-
76-
if (this.Schema != null)
77-
{
78-
if (v3) this.Schema.SerializeV3(writer);
79-
else this.Schema.SerializeV2(writer);
80-
}
76+
this.Schema = schema ?? throw new ArgumentNullException(nameof(schema));
8177
}
78+
79+
public AsyncApiJsonSchema Schema { get; }
80+
81+
public static implicit operator AsyncApiJsonSchema(SolaceIntegerSchema value) => value?.Schema;
82+
83+
public override void SerializeV2(IAsyncApiWriter writer) => this.Schema.SerializeV2(writer);
84+
85+
public override void SerializeV3(IAsyncApiWriter writer) => this.Schema.SerializeV3(writer);
8286
}
8387
}

‎src/ByteBard.AsyncAPI.Bindings/Solace/SolaceOperationBinding.cs‎

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,61 +20,38 @@ public class SolaceOperationBinding : OperationBinding<SolaceOperationBinding>
2020
{
2121
{ "bindingVersion", (a, n) => a.BindingVersion = n.GetScalarValue() },
2222
{ "destinations", (a, n) => a.Destinations = n.CreateList(s => s.ParseMap(SolaceDestination.Fields)) },
23-
{ "timeToLive", (a, n) => a.TimeToLive = ReadIntegerOrSchema(n) },
24-
{ "priority", (a, n) => a.Priority = ReadIntegerOrSchema(n) },
23+
{ "timeToLive", (a, n) => a.TimeToLive = SolaceIntegerOrSchema.Parse(n) },
24+
{ "priority", (a, n) => a.Priority = SolaceIntegerOrSchema.Parse(n) },
2525
{ "dmqEligible", (a, n) => a.DmqEligible = n.GetBooleanValue() },
2626
};
2727

28-
/// <summary>
29-
/// Parses a scalar as a signed 64-bit integer, or an object as a schema or schema reference.
30-
/// </summary>
31-
/// <returns>A value containing the parsed integer or schema.</returns>
32-
/// <remarks>Schema field errors handled by the reader are recorded as diagnostics. Scalar parsing errors propagate to the caller.</remarks>
33-
/// <exception cref="ByteBard.AsyncAPI.Readers.Exceptions.AsyncApiReaderException">The scalar cannot be parsed as an Int64, or a non-scalar node is not a schema object.</exception>
34-
/// <exception cref="ByteBard.AsyncAPI.Exceptions.AsyncApiException">A schema reference has a non-scalar $ref value.</exception>
35-
internal static SolaceIntegerOrSchema ReadIntegerOrSchema(ParseNode node)
36-
{
37-
if (node is ValueNode)
38-
{
39-
return new SolaceIntegerOrSchema { Integer = node.GetLongValue() };
40-
}
41-
42-
return new SolaceIntegerOrSchema { Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(node) };
43-
}
44-
4528
/// <summary>
4629
/// Writes the Solace operation binding object and its extensions for AsyncAPI 2.0.
4730
/// </summary>
4831
/// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
49-
/// <exception cref="InvalidOperationException">TimeToLive or Priority is present but contains neither an integer nor a schema.</exception>
5032
public override void SerializeV2(IAsyncApiWriter writer) => Serialize(writer, false);
5133
/// <summary>
5234
/// Writes the Solace operation binding object and its extensions for AsyncAPI 3.0.
5335
/// </summary>
5436
/// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
55-
/// <exception cref="InvalidOperationException">TimeToLive or Priority is present but contains neither an integer nor a schema.</exception>
5637
public override void SerializeV3(IAsyncApiWriter writer) => Serialize(writer, true);
5738

5839
/// <summary>
5940
/// Writes the Solace operation binding object and its extensions for AsyncAPI 2.0.
6041
/// </summary>
61-
/// <remarks>Validates the optional integer-or-schema values before writing the binding. Explicit false DmqEligible values are preserved.</remarks>
42+
/// <remarks>Explicit false DmqEligible values are preserved.</remarks>
6243
/// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
63-
/// <exception cref="InvalidOperationException">TimeToLive or Priority is present but contains neither an integer nor a schema.</exception>
6444
public override void SerializeProperties(IAsyncApiWriter writer) => Serialize(writer, false);
6545

6646
/// <summary>
67-
/// Validates the optional integer-or-schema values, then writes the binding object and its extensions.
47+
/// Writes the binding object and its extensions.
6848
/// </summary>
6949
/// <param name="writer">The writer receiving the value.</param>
7050
/// <param name="v3">Uses AsyncAPI 3.0 schema serialization when true and AsyncAPI 2.0 when false.</param>
7151
/// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
72-
/// <exception cref="InvalidOperationException">TimeToLive or Priority is present but contains neither an integer nor a schema.</exception>
7352
private void Serialize(IAsyncApiWriter writer, bool v3)
7453
{
7554
if (writer is null) throw new ArgumentNullException(nameof(writer));
76-
this.TimeToLive?.Validate();
77-
this.Priority?.Validate();
7855
writer.WriteStartObject();
7956
writer.WriteOptionalCollection("destinations", this.Destinations, (w, d) => d.Serialize(w));
8057
writer.WriteOptionalObject("timeToLive", this.TimeToLive, (w, value) => { if (v3) value.SerializeV3(w); else value.SerializeV2(w); });

‎src/ByteBard.AsyncAPI.Bindings/Sqs/Condition.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void Serialize(IAsyncApiWriter writer)
2727
writer.WriteStartObject();
2828
foreach (var conditionValue in this.Value)
2929
{
30-
writer.WriteRequiredMap(conditionValue.Key, conditionValue.Value, (w, t) => t.Value.Write(w));
30+
writer.WriteRequiredMap(conditionValue.Key, conditionValue.Value, (w, t) => t.Write(w));
3131
}
3232

3333
writer.WriteEndObject();

‎src/ByteBard.AsyncAPI.Bindings/Sqs/PrincipalObject.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public override void Serialize(IAsyncApiWriter writer)
2121
}
2222

2323
writer.WriteStartObject();
24-
writer.WriteRequiredObject(this.Value.Key, this.Value.Value, (w, t) => t.Value.Write(w));
24+
writer.WriteRequiredObject(this.Value.Key, this.Value.Value, (w, t) => t.Write(w));
2525
writer.WriteEndObject();
2626
}
2727
}

‎src/ByteBard.AsyncAPI.Bindings/Sqs/Statement.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public void Serialize(IAsyncApiWriter writer)
4545
writer.WriteStartObject();
4646
writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName());
4747
writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Serialize(w));
48-
writer.WriteRequiredObject("action", this.Action, (w, t) => t.Value.Write(w));
49-
writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Value.Write(w));
48+
writer.WriteRequiredObject("action", this.Action, (w, t) => t.Write(w));
49+
writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Write(w));
5050
writer.WriteOptionalObject("condition", this.Condition, (w, t) => t.Serialize(w));
5151
writer.WriteExtensions(this.Extensions);
5252
writer.WriteEndObject();

0 commit comments

Comments
 (0)