Skip to content

Commit 44741bd

Browse files
bieluclaude
andcommitted
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>
1 parent cc88390 commit 44741bd

10 files changed

Lines changed: 136 additions & 98 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/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();
Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,41 @@
11
namespace ByteBard.AsyncAPI.Bindings
22
{
33
using System;
4+
using System.Collections.Generic;
45
using System.Linq;
5-
using System.Text.Json;
6-
using System.Text.Json.Nodes;
7-
using ByteBard.AsyncAPI.Models;
86
using ByteBard.AsyncAPI.Models.Interfaces;
97
using ByteBard.AsyncAPI.Readers.Exceptions;
108
using ByteBard.AsyncAPI.Readers.ParseNodes;
9+
using ByteBard.AsyncAPI.Writers;
1110

12-
public class StringOrStringList : IAsyncApiElement
11+
/// <summary>
12+
/// A value represented by either a single string (<see cref="SingleString"/>) or a list of strings (<see cref="StringList"/>).
13+
/// </summary>
14+
/// <remarks>Strings and string lists convert implicitly, so <c>statement.Action = "sns:Publish";</c> works.</remarks>
15+
public abstract class StringOrStringList : IAsyncApiElement
1316
{
14-
public StringOrStringList(AsyncApiAny value)
17+
private protected StringOrStringList()
1518
{
16-
this.Value = value.GetNode() switch
17-
{
18-
JsonArray array => IsValidStringList(array) ? new AsyncApiAny(array) : throw new ArgumentException($"{nameof(StringOrStringList)} value should only contain string items."),
19-
JsonValue jValue => IsString(jValue) ? new AsyncApiAny(jValue) : throw new ArgumentException($"{nameof(StringOrStringList)} should be a string value or a string list."),
20-
_ => throw new ArgumentException($"{nameof(StringOrStringList)} should be a string value or a string list."),
21-
};
2219
}
2320

24-
public AsyncApiAny Value { get; }
21+
public static implicit operator StringOrStringList(string value) => value == null ? null : new SingleString(value);
22+
23+
public static implicit operator StringOrStringList(string[] values) => values == null ? null : new StringList(values);
24+
25+
public static implicit operator StringOrStringList(List<string> values) => values == null ? null : new StringList(values);
2526

27+
/// <summary>
28+
/// Parses a scalar as a single string, or a list as a list of strings.
29+
/// </summary>
30+
/// <exception cref="AsyncApiReaderException">The node is neither a scalar nor a list of scalars.</exception>
2631
public static StringOrStringList Parse(ParseNode node)
2732
{
2833
switch (node)
2934
{
3035
case ValueNode:
31-
return new StringOrStringList(new AsyncApiAny(node.GetScalarValue()));
36+
return new SingleString(node.GetScalarValue());
3237
case ListNode listNode:
33-
{
34-
var jsonArray = new JsonArray();
35-
foreach (var item in listNode)
36-
{
37-
jsonArray.Add(item.GetScalarValue());
38-
}
39-
40-
return new StringOrStringList(new AsyncApiAny(jsonArray));
41-
}
42-
38+
return new StringList(listNode.Select(item => item.GetScalarValue()));
4339
default:
4440
throw new AsyncApiReaderException(
4541
$"An error occured while parsing a {nameof(StringOrStringList)} node. " +
@@ -48,15 +44,60 @@ public static StringOrStringList Parse(ParseNode node)
4844
}
4945
}
5046

51-
private static bool IsString(JsonNode value)
47+
/// <summary>
48+
/// Writes the string or the list of strings directly, without a property name.
49+
/// </summary>
50+
public abstract void Write(IAsyncApiWriter writer);
51+
}
52+
53+
/// <summary>The single string alternative of <see cref="StringOrStringList"/>.</summary>
54+
public sealed class SingleString : StringOrStringList
55+
{
56+
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
57+
public SingleString(string value)
58+
{
59+
this.Value = value ?? throw new ArgumentNullException(nameof(value));
60+
}
61+
62+
public string Value { get; }
63+
64+
public static implicit operator string(SingleString value) => value?.Value;
65+
66+
public override void Write(IAsyncApiWriter writer) => writer.WriteValue(this.Value);
67+
}
68+
69+
/// <summary>The list alternative of <see cref="StringOrStringList"/>.</summary>
70+
public sealed class StringList : StringOrStringList
71+
{
72+
/// <summary>
73+
/// Copies the supplied strings.
74+
/// </summary>
75+
/// <exception cref="ArgumentNullException"><paramref name="values"/> is null.</exception>
76+
/// <exception cref="ArgumentException"><paramref name="values"/> contains a null item.</exception>
77+
public StringList(IEnumerable<string> values)
5278
{
53-
var element = JsonDocument.Parse(value.ToJsonString()).RootElement;
54-
return element.ValueKind == JsonValueKind.String;
79+
var copy = (values ?? throw new ArgumentNullException(nameof(values))).ToArray();
80+
if (copy.Any(value => value == null))
81+
{
82+
throw new ArgumentException($"{nameof(StringList)} items cannot be null.", nameof(values));
83+
}
84+
85+
this.Values = copy;
5586
}
5687

57-
private static bool IsValidStringList(JsonArray array)
88+
public IReadOnlyList<string> Values { get; }
89+
90+
public static implicit operator string[](StringList value) => value?.Values.ToArray();
91+
92+
public override void Write(IAsyncApiWriter writer)
5893
{
59-
return array.All(x => IsString(x));
94+
writer.WriteStartArray();
95+
foreach (var value in this.Values)
96+
{
97+
writer.WriteValue(value);
98+
}
99+
100+
writer.WriteEndArray();
60101
}
61102
}
62-
}
103+
}

‎test/ByteBard.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,18 @@ public void V2_SnsChannelBinding_WithFilledObject_SerializesAndDeserializes()
8585
{
8686
Effect = Effect.Deny,
8787
Principal = new PrincipalStar(),
88-
Action = new StringOrStringList(new AsyncApiAny(new List<string>()
88+
Action = new List<string>()
8989
{
9090
"sns:Publish",
9191
"sns:Delete",
92-
})),
92+
},
9393
Condition = new Condition(new Dictionary<string, Dictionary<string, StringOrStringList>>
9494
{
9595
{
9696
"StringEquals", new Dictionary<string, StringOrStringList>
9797
{
9898
{
99-
"aws:username", new StringOrStringList(new AsyncApiAny(new List<string>() { "johndoe", "mrsmith" }))
99+
"aws:username", new List<string>() { "johndoe", "mrsmith" }
100100
},
101101
}
102102
},
@@ -106,16 +106,16 @@ public void V2_SnsChannelBinding_WithFilledObject_SerializesAndDeserializes()
106106
{
107107
Effect = Effect.Allow,
108108
Principal = new PrincipalObject(new KeyValuePair<string, StringOrStringList>(
109-
"AWS", new StringOrStringList(new AsyncApiAny(new List<string>
110-
{ "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })))),
111-
Action = new StringOrStringList(new AsyncApiAny("sns:Create")),
109+
"AWS", new List<string>
110+
{ "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })),
111+
Action = "sns:Create",
112112
Condition = new Condition(new Dictionary<string, Dictionary<string, StringOrStringList>>
113113
{
114114
{
115115
"NumericLessThanEquals", new Dictionary<string, StringOrStringList>
116116
{
117117
{
118-
"aws:MultiFactorAuthAge", new StringOrStringList(new AsyncApiAny("3600"))
118+
"aws:MultiFactorAuthAge", "3600"
119119
},
120120
}
121121
},

‎test/ByteBard.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,19 @@ public void V2_SqsChannelBinding_WithFilledObject_SerializesAndDeserializes()
136136
{
137137
Effect = Effect.Deny,
138138
Principal = new PrincipalObject(new KeyValuePair<string, StringOrStringList>(
139-
"AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))),
140-
Action = new StringOrStringList(new AsyncApiAny(new List<string>
139+
"AWS", "arn:aws:iam::123456789012:user/alex.wichmann")),
140+
Action = new List<string>
141141
{
142142
"sqs:SendMessage",
143143
"sqs:ReceiveMessage",
144-
})),
144+
},
145145
Condition = new Condition(new Dictionary<string, Dictionary<string, StringOrStringList>>
146146
{
147147
{
148148
"StringEquals", new Dictionary<string, StringOrStringList>
149149
{
150150
{
151-
"aws:username", new StringOrStringList(new AsyncApiAny(new List<string> { "johndoe", "mrsmith" }))
151+
"aws:username", new List<string> { "johndoe", "mrsmith" }
152152
},
153153
}
154154
},
@@ -168,16 +168,16 @@ public void V2_SqsChannelBinding_WithFilledObject_SerializesAndDeserializes()
168168
{
169169
Effect = Effect.Allow,
170170
Principal = new PrincipalObject(new KeyValuePair<string, StringOrStringList>(
171-
"AWS", new StringOrStringList(new AsyncApiAny(new List<string>
172-
{ "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })))),
173-
Action = new StringOrStringList(new AsyncApiAny("sqs:CreateQueue")),
171+
"AWS", new List<string>
172+
{ "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })),
173+
Action = "sqs:CreateQueue",
174174
Condition = new Condition(new Dictionary<string, Dictionary<string, StringOrStringList>>
175175
{
176176
{
177177
"NumericLessThanEquals", new Dictionary<string, StringOrStringList>
178178
{
179179
{
180-
"aws:MultiFactorAuthAge", new StringOrStringList(new AsyncApiAny("3600"))
180+
"aws:MultiFactorAuthAge", "3600"
181181
},
182182
}
183183
},
@@ -227,11 +227,11 @@ public void V2_SqsChannelBinding_WithFilledObject_SerializesAndDeserializes()
227227
{
228228
Effect = Effect.Allow,
229229
Principal = new PrincipalObject(new KeyValuePair<string, StringOrStringList>(
230-
"Service", new StringOrStringList(new AsyncApiAny("s3.amazonaws.com")))),
231-
Action = new StringOrStringList(new AsyncApiAny(new List<string>
230+
"Service", "s3.amazonaws.com")),
231+
Action = new List<string>
232232
{
233233
"sqs:*",
234-
})),
234+
},
235235
},
236236
},
237237
},
@@ -383,12 +383,12 @@ public void V2_SqsOperationBinding_WithFilledObject_SerializesAndDeserializes()
383383
{
384384
Effect = Effect.Deny,
385385
Principal = new PrincipalObject(new KeyValuePair<string, StringOrStringList>(
386-
"AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))),
387-
Action = new StringOrStringList(new AsyncApiAny(new List<string>()
386+
"AWS", "arn:aws:iam::123456789012:user/alex.wichmann")),
387+
Action = new List<string>()
388388
{
389389
"sqs:SendMessage",
390390
"sqs:ReceiveMessage",
391-
})),
391+
},
392392
Extensions = new Dictionary<string, IAsyncApiExtension>()
393393
{
394394
{
@@ -404,9 +404,9 @@ public void V2_SqsOperationBinding_WithFilledObject_SerializesAndDeserializes()
404404
{
405405
Effect = Effect.Allow,
406406
Principal = new PrincipalObject(new KeyValuePair<string, StringOrStringList>(
407-
"AWS", new StringOrStringList(new AsyncApiAny(new List<string>
408-
{ "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })))),
409-
Action = new StringOrStringList(new AsyncApiAny("sqs:CreateQueue")),
407+
"AWS", new List<string>
408+
{ "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })),
409+
Action = "sqs:CreateQueue",
410410
},
411411
},
412412
Extensions = new Dictionary<string, IAsyncApiExtension>()
@@ -452,11 +452,11 @@ public void V2_SqsOperationBinding_WithFilledObject_SerializesAndDeserializes()
452452
{
453453
Effect = Effect.Allow,
454454
Principal = new PrincipalObject(new KeyValuePair<string, StringOrStringList>(
455-
"AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))),
456-
Action = new StringOrStringList(new AsyncApiAny(new List<string>
455+
"AWS", "arn:aws:iam::123456789012:user/alex.wichmann")),
456+
Action = new List<string>
457457
{
458458
"sqs:*",
459-
})),
459+
},
460460
},
461461
},
462462
},

0 commit comments

Comments
 (0)