Skip to content

Commit 8634d7a

Browse files
committed
I AM AN AVRO GOD
1 parent 484fc27 commit 8634d7a

File tree

5 files changed

+260
-53
lines changed

5 files changed

+260
-53
lines changed

src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ public override AsyncApiAny CreateAny()
214214
return new AsyncApiAny(this.node);
215215
}
216216

217+
public void ParseFields<T>(ref T parentInstance, IDictionary<string, Action<T, ParseNode>> fixedFields, IDictionary<Func<string, bool>, Action<T, string, ParseNode>> patternFields)
218+
{
219+
foreach (var propertyNode in this)
220+
{
221+
propertyNode.ParseField(parentInstance, fixedFields, patternFields);
222+
}
223+
}
224+
217225
private string ToScalarValue(JsonNode node)
218226
{
219227
var scalarNode = node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value");

src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs

Lines changed: 83 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
namespace LEGO.AsyncAPI.Readers
44
{
5-
using System.Collections.Generic;
6-
using System.Globalization;
7-
using LEGO.AsyncAPI.Extensions;
5+
using System;
86
using LEGO.AsyncAPI.Models;
97
using LEGO.AsyncAPI.Readers.Exceptions;
108
using LEGO.AsyncAPI.Readers.ParseNodes;
@@ -26,10 +24,7 @@ public static AvroSchema LoadSchema(ParseNode node)
2624
var mapNode = node.CheckMapNode("schema");
2725
var schema = new AvroSchema();
2826

29-
foreach (var propertyNode in mapNode)
30-
{
31-
propertyNode.ParseField(schema, schemaFixedFields, null);
32-
}
27+
mapNode.ParseFields(ref schema, schemaFixedFields, null);
3328

3429
return schema;
3530
}
@@ -39,10 +34,7 @@ private static AvroField LoadField(ParseNode node)
3934
var mapNode = node.CheckMapNode("field");
4035
var field = new AvroField();
4136

42-
foreach (var propertyNode in mapNode)
43-
{
44-
propertyNode.ParseField(field, fieldFixedFields, null);
45-
}
37+
mapNode.ParseFields(ref field, fieldFixedFields, null);
4638

4739
return field;
4840
}
@@ -56,56 +48,95 @@ private static AvroField LoadField(ParseNode node)
5648
{ "order", (a, n) => a.Order = n.GetScalarValue() },
5749
};
5850

51+
private static readonly FixedFieldMap<AvroRecord> recordFixedFields = new()
52+
{
53+
{ "type", (a, n) => { } },
54+
{ "name", (a, n) => a.Name = n.GetScalarValue() },
55+
{ "fields", (a, n) => a.Fields = n.CreateList(LoadField) },
56+
};
57+
58+
private static readonly FixedFieldMap<AvroEnum> enumFixedFields = new()
59+
{
60+
{ "type", (a, n) => { } },
61+
{ "name", (a, n) => a.Name = n.GetScalarValue() },
62+
{ "symbols", (a, n) => a.Symbols = n.CreateSimpleList(n2 => n2.GetScalarValue()) },
63+
};
64+
65+
private static readonly FixedFieldMap<AvroFixed> fixedFixedFields = new()
66+
{
67+
{ "type", (a, n) => { } },
68+
{ "name", (a, n) => a.Name = n.GetScalarValue() },
69+
{ "size", (a, n) => a.Size = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo) },
70+
};
71+
72+
private static readonly FixedFieldMap<AvroArray> arrayFixedFields = new()
73+
{
74+
{ "type", (a, n) => { } },
75+
{ "items", (a, n) => a.Items = LoadFieldType(n) },
76+
};
77+
78+
private static readonly FixedFieldMap<AvroMap> mapFixedFields = new()
79+
{
80+
{ "type", (a, n) => { } },
81+
{ "values", (a, n) => a.Values = LoadFieldType(n) },
82+
};
83+
84+
private static readonly FixedFieldMap<AvroUnion> unionFixedFields = new()
85+
{
86+
{ "types", (a, n) => a.Types = n.CreateList(LoadFieldType) },
87+
};
88+
5989
private static AvroFieldType LoadFieldType(ParseNode node)
6090
{
6191
if (node is ValueNode valueNode)
6292
{
6393
return new AvroPrimitive(valueNode.GetScalarValue().GetEnumFromDisplayName<AvroPrimitiveType>());
6494
}
6595

96+
if (node is ListNode)
97+
{
98+
var union = new AvroUnion();
99+
foreach (var item in node as ListNode)
100+
{
101+
union.Types.Add(LoadFieldType(item));
102+
}
103+
104+
return union;
105+
}
106+
66107
if (node is MapNode mapNode)
67108
{
68-
//var typeNode = mapNode.GetValue("type");
69-
//var type = typeNode?.GetScalarValue();
70-
71-
//switch (type)
72-
//{
73-
// case "record":
74-
// return new AvroRecord
75-
// {
76-
// Name = mapNode.GetValue("name")?.GetScalarValue(),
77-
// Fields = mapNode.GetValue("fields").CreateList(LoadField)
78-
// };
79-
// case "enum":
80-
// return new AvroEnum
81-
// {
82-
// Name = mapNode.GetValue("name")?.GetScalarValue(),
83-
// Symbols = mapNode.GetValue("symbols").CreateSimpleList(n => n.GetScalarValue())
84-
// };
85-
// case "fixed":
86-
// return new AvroFixed
87-
// {
88-
// Name = mapNode.GetValue("name")?.GetScalarValue(),
89-
// Size = int.Parse(mapNode.GetValue("size").GetScalarValue())
90-
// };
91-
// case "array":
92-
// return new AvroArray
93-
// {
94-
// Items = LoadFieldType(mapNode.GetValue("items"))
95-
// };
96-
// case "map":
97-
// return new AvroMap
98-
// {
99-
// Values = LoadFieldType(mapNode.GetValue("values"))
100-
// };
101-
// case "union":
102-
// return new AvroUnion
103-
// {
104-
// Types = mapNode.GetValue("types").CreateList(LoadFieldType)
105-
// };
106-
// default:
107-
// throw new InvalidOperationException($"Unsupported type: {type}");
108-
//}
109+
var type = mapNode["type"].Value?.GetScalarValue();
110+
111+
switch (type)
112+
{
113+
case "record":
114+
var record = new AvroRecord();
115+
mapNode.ParseFields(ref record, recordFixedFields, null);
116+
return record;
117+
case "enum":
118+
var @enum = new AvroEnum();
119+
mapNode.ParseFields(ref @enum, enumFixedFields, null);
120+
return @enum;
121+
case "fixed":
122+
var @fixed = new AvroFixed();
123+
mapNode.ParseFields(ref @fixed, fixedFixedFields, null);
124+
return @fixed;
125+
case "array":
126+
var array = new AvroArray();
127+
mapNode.ParseFields(ref array, arrayFixedFields, null);
128+
return array;
129+
case "map":
130+
var map = new AvroMap();
131+
mapNode.ParseFields(ref map, mapFixedFields, null);
132+
return map;
133+
case "union":
134+
var union = new AvroUnion();
135+
mapNode.ParseFields(ref union, unionFixedFields, null);
136+
return union;
137+
default:
138+
throw new InvalidOperationException($"Unsupported type: {type}");
139+
}
109140
}
110141

111142
throw new AsyncApiReaderException("Invalid node type");

src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic)
3636
[typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation,
3737
[typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter,
3838
[typeof(AsyncApiSchema)] = AsyncApiSchemaDeserializer.LoadSchema,
39+
[typeof(AvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema,
3940
[typeof(AsyncApiJsonSchemaPayload)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, // #ToFix how do we get the schemaFormat?!
4041
[typeof(AsyncApiAvroSchemaPayload)] = AsyncApiV2Deserializer.LoadAvroPayload,
4142
[typeof(AsyncApiSecurityRequirement)] = AsyncApiV2Deserializer.LoadSecurityRequirement,

src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace LEGO.AsyncAPI.Models
77

88
public class AvroRecord : AvroFieldType
99
{
10-
public string Type { get; set; } = "record";
10+
public string Type { get; } = "record";
1111

1212
public string Name { get; set; }
1313

test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ namespace LEGO.AsyncAPI.Tests.Models
55
using System.Collections.Generic;
66
using FluentAssertions;
77
using LEGO.AsyncAPI.Models;
8+
using LEGO.AsyncAPI.Readers;
89
using NUnit.Framework;
910

1011
public class AvroSchema_Should
1112
{
1213
[Test]
1314
public void SerializeV2_SerializesCorrectly()
1415
{
16+
// Arrange
1517
var expected = """
1618
type: record
1719
name: User
@@ -164,11 +166,176 @@ public void SerializeV2_SerializesCorrectly()
164166
},
165167
};
166168

169+
// Act
167170
var actual = schema.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);
168171

169172
// Assert
170173
actual.Should()
171174
.BePlatformAgnosticEquivalentTo(expected);
172175
}
176+
177+
[Test]
178+
public void ReadFragment_DeserializesCorrectly()
179+
{
180+
// Arrange
181+
var input = """
182+
type: record
183+
name: User
184+
namespace: com.example
185+
fields:
186+
- name: username
187+
type: string
188+
doc: The username of the user.
189+
default: guest
190+
order: ascending
191+
- name: status
192+
type:
193+
type: enum
194+
name: Status
195+
symbols:
196+
- ACTIVE
197+
- INACTIVE
198+
- BANNED
199+
doc: The status of the user.
200+
- name: emails
201+
type:
202+
type: array
203+
items: string
204+
doc: A list of email addresses.
205+
- name: metadata
206+
type:
207+
type: map
208+
values: string
209+
doc: Metadata associated with the user.
210+
- name: address
211+
type:
212+
type: record
213+
name: Address
214+
fields:
215+
- name: street
216+
type: string
217+
- name: city
218+
type: string
219+
- name: zipcode
220+
type: string
221+
doc: The address of the user.
222+
- name: profilePicture
223+
type:
224+
type: fixed
225+
name: ProfilePicture
226+
size: 256
227+
doc: A fixed-size profile picture.
228+
- name: contact
229+
type:
230+
- 'null'
231+
- type: record
232+
name: PhoneNumber
233+
fields:
234+
- name: countryCode
235+
type: int
236+
- name: number
237+
type: string
238+
doc: 'The contact information of the user, which can be either null or a phone number.'
239+
""";
240+
241+
var expected = new AvroSchema
242+
{
243+
Type = AvroSchemaType.Record,
244+
Name = "User",
245+
Namespace = "com.example",
246+
Fields = new List<AvroField>
247+
{
248+
new AvroField
249+
{
250+
Name = "username",
251+
Type = AvroPrimitiveType.String,
252+
Doc = "The username of the user.",
253+
Default = new AsyncApiAny("guest"),
254+
Order = "ascending",
255+
},
256+
new AvroField
257+
{
258+
Name = "status",
259+
Type = new AvroEnum
260+
{
261+
Name = "Status",
262+
Symbols = new List<string> { "ACTIVE", "INACTIVE", "BANNED" },
263+
},
264+
Doc = "The status of the user.",
265+
},
266+
new AvroField
267+
{
268+
Name = "emails",
269+
Type = new AvroArray
270+
{
271+
Items = AvroPrimitiveType.String,
272+
},
273+
Doc = "A list of email addresses.",
274+
},
275+
new AvroField
276+
{
277+
Name = "metadata",
278+
Type = new AvroMap
279+
{
280+
Values = AvroPrimitiveType.String,
281+
},
282+
Doc = "Metadata associated with the user.",
283+
},
284+
new AvroField
285+
{
286+
Name = "address",
287+
Type = new AvroRecord
288+
{
289+
Name = "Address",
290+
Fields = new List<AvroField>
291+
{
292+
new AvroField { Name = "street", Type = AvroPrimitiveType.String },
293+
new AvroField { Name = "city", Type = AvroPrimitiveType.String },
294+
new AvroField { Name = "zipcode", Type = AvroPrimitiveType.String },
295+
},
296+
},
297+
Doc = "The address of the user.",
298+
},
299+
new AvroField
300+
{
301+
Name = "profilePicture",
302+
Type = new AvroFixed
303+
{
304+
Name = "ProfilePicture",
305+
Size = 256,
306+
},
307+
Doc = "A fixed-size profile picture.",
308+
},
309+
new AvroField
310+
{
311+
Name = "contact",
312+
Type = new AvroUnion
313+
{
314+
Types = new List<AvroFieldType>
315+
{
316+
AvroPrimitiveType.Null,
317+
new AvroRecord
318+
{
319+
Name = "PhoneNumber",
320+
Fields = new List<AvroField>
321+
{
322+
new AvroField { Name = "countryCode", Type = AvroPrimitiveType.Int },
323+
new AvroField { Name = "number", Type = AvroPrimitiveType.String },
324+
},
325+
},
326+
},
327+
},
328+
Doc = "The contact information of the user, which can be either null or a phone number.",
329+
},
330+
},
331+
};
332+
333+
// Act
334+
var actual = new AsyncApiStringReader().ReadFragment<AvroSchema>(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic);
335+
336+
// Assert
337+
actual.Should()
338+
.BeEquivalentTo(expected);
339+
}
173340
}
174341
}

0 commit comments

Comments
 (0)