diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs index b087c875..63bea6fc 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs @@ -214,6 +214,14 @@ public override AsyncApiAny CreateAny() return new AsyncApiAny(this.node); } + public void ParseFields(ref T parentInstance, IDictionary> fixedFields, IDictionary, Action> patternFields) + { + foreach (var propertyNode in this) + { + propertyNode.ParseField(parentInstance, fixedFields, patternFields); + } + } + private string ToScalarValue(JsonNode node) { var scalarNode = node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs index 30752870..9e7b828b 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs @@ -2,9 +2,7 @@ namespace LEGO.AsyncAPI.Readers { - using System.Collections.Generic; - using System.Globalization; - using LEGO.AsyncAPI.Extensions; + using System; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers.Exceptions; using LEGO.AsyncAPI.Readers.ParseNodes; @@ -26,10 +24,7 @@ public static AvroSchema LoadSchema(ParseNode node) var mapNode = node.CheckMapNode("schema"); var schema = new AvroSchema(); - foreach (var propertyNode in mapNode) - { - propertyNode.ParseField(schema, schemaFixedFields, null); - } + mapNode.ParseFields(ref schema, schemaFixedFields, null); return schema; } @@ -39,10 +34,7 @@ private static AvroField LoadField(ParseNode node) var mapNode = node.CheckMapNode("field"); var field = new AvroField(); - foreach (var propertyNode in mapNode) - { - propertyNode.ParseField(field, fieldFixedFields, null); - } + mapNode.ParseFields(ref field, fieldFixedFields, null); return field; } @@ -56,6 +48,44 @@ private static AvroField LoadField(ParseNode node) { "order", (a, n) => a.Order = n.GetScalarValue() }, }; + private static readonly FixedFieldMap recordFixedFields = new() + { + { "type", (a, n) => { } }, + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "fields", (a, n) => a.Fields = n.CreateList(LoadField) }, + }; + + private static readonly FixedFieldMap enumFixedFields = new() + { + { "type", (a, n) => { } }, + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "symbols", (a, n) => a.Symbols = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, + }; + + private static readonly FixedFieldMap fixedFixedFields = new() + { + { "type", (a, n) => { } }, + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "size", (a, n) => a.Size = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo) }, + }; + + private static readonly FixedFieldMap arrayFixedFields = new() + { + { "type", (a, n) => { } }, + { "items", (a, n) => a.Items = LoadFieldType(n) }, + }; + + private static readonly FixedFieldMap mapFixedFields = new() + { + { "type", (a, n) => { } }, + { "values", (a, n) => a.Values = LoadFieldType(n) }, + }; + + private static readonly FixedFieldMap unionFixedFields = new() + { + { "types", (a, n) => a.Types = n.CreateList(LoadFieldType) }, + }; + private static AvroFieldType LoadFieldType(ParseNode node) { if (node is ValueNode valueNode) @@ -63,49 +93,50 @@ private static AvroFieldType LoadFieldType(ParseNode node) return new AvroPrimitive(valueNode.GetScalarValue().GetEnumFromDisplayName()); } + if (node is ListNode) + { + var union = new AvroUnion(); + foreach (var item in node as ListNode) + { + union.Types.Add(LoadFieldType(item)); + } + + return union; + } + if (node is MapNode mapNode) { - //var typeNode = mapNode.GetValue("type"); - //var type = typeNode?.GetScalarValue(); - - //switch (type) - //{ - // case "record": - // return new AvroRecord - // { - // Name = mapNode.GetValue("name")?.GetScalarValue(), - // Fields = mapNode.GetValue("fields").CreateList(LoadField) - // }; - // case "enum": - // return new AvroEnum - // { - // Name = mapNode.GetValue("name")?.GetScalarValue(), - // Symbols = mapNode.GetValue("symbols").CreateSimpleList(n => n.GetScalarValue()) - // }; - // case "fixed": - // return new AvroFixed - // { - // Name = mapNode.GetValue("name")?.GetScalarValue(), - // Size = int.Parse(mapNode.GetValue("size").GetScalarValue()) - // }; - // case "array": - // return new AvroArray - // { - // Items = LoadFieldType(mapNode.GetValue("items")) - // }; - // case "map": - // return new AvroMap - // { - // Values = LoadFieldType(mapNode.GetValue("values")) - // }; - // case "union": - // return new AvroUnion - // { - // Types = mapNode.GetValue("types").CreateList(LoadFieldType) - // }; - // default: - // throw new InvalidOperationException($"Unsupported type: {type}"); - //} + var type = mapNode["type"].Value?.GetScalarValue(); + + switch (type) + { + case "record": + var record = new AvroRecord(); + mapNode.ParseFields(ref record, recordFixedFields, null); + return record; + case "enum": + var @enum = new AvroEnum(); + mapNode.ParseFields(ref @enum, enumFixedFields, null); + return @enum; + case "fixed": + var @fixed = new AvroFixed(); + mapNode.ParseFields(ref @fixed, fixedFixedFields, null); + return @fixed; + case "array": + var array = new AvroArray(); + mapNode.ParseFields(ref array, arrayFixedFields, null); + return array; + case "map": + var map = new AvroMap(); + mapNode.ParseFields(ref map, mapFixedFields, null); + return map; + case "union": + var union = new AvroUnion(); + mapNode.ParseFields(ref union, unionFixedFields, null); + return union; + default: + throw new InvalidOperationException($"Unsupported type: {type}"); + } } throw new AsyncApiReaderException("Invalid node type"); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index 0f9d3b06..033eeedd 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -36,6 +36,7 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation, [typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter, [typeof(AsyncApiSchema)] = AsyncApiSchemaDeserializer.LoadSchema, + [typeof(AvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, [typeof(AsyncApiJsonSchemaPayload)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, // #ToFix how do we get the schemaFormat?! [typeof(AsyncApiAvroSchemaPayload)] = AsyncApiV2Deserializer.LoadAvroPayload, [typeof(AsyncApiSecurityRequirement)] = AsyncApiV2Deserializer.LoadSecurityRequirement, diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs index dab81d5c..bf0d87dc 100644 --- a/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs @@ -7,7 +7,7 @@ namespace LEGO.AsyncAPI.Models public class AvroRecord : AvroFieldType { - public string Type { get; set; } = "record"; + public string Type { get; } = "record"; public string Name { get; set; } diff --git a/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs index dad759cb..4efc0e38 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs @@ -5,6 +5,7 @@ namespace LEGO.AsyncAPI.Tests.Models using System.Collections.Generic; using FluentAssertions; using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; using NUnit.Framework; public class AvroSchema_Should @@ -12,6 +13,7 @@ public class AvroSchema_Should [Test] public void SerializeV2_SerializesCorrectly() { + // Arrange var expected = """ type: record name: User @@ -164,11 +166,176 @@ public void SerializeV2_SerializesCorrectly() }, }; + // Act var actual = schema.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert actual.Should() .BePlatformAgnosticEquivalentTo(expected); } + + [Test] + public void ReadFragment_DeserializesCorrectly() + { + // Arrange + var input = """ + type: record + name: User + namespace: com.example + fields: + - name: username + type: string + doc: The username of the user. + default: guest + order: ascending + - name: status + type: + type: enum + name: Status + symbols: + - ACTIVE + - INACTIVE + - BANNED + doc: The status of the user. + - name: emails + type: + type: array + items: string + doc: A list of email addresses. + - name: metadata + type: + type: map + values: string + doc: Metadata associated with the user. + - name: address + type: + type: record + name: Address + fields: + - name: street + type: string + - name: city + type: string + - name: zipcode + type: string + doc: The address of the user. + - name: profilePicture + type: + type: fixed + name: ProfilePicture + size: 256 + doc: A fixed-size profile picture. + - name: contact + type: + - 'null' + - type: record + name: PhoneNumber + fields: + - name: countryCode + type: int + - name: number + type: string + doc: 'The contact information of the user, which can be either null or a phone number.' + """; + + var expected = new AvroSchema + { + Type = AvroSchemaType.Record, + Name = "User", + Namespace = "com.example", + Fields = new List + { + new AvroField + { + Name = "username", + Type = AvroPrimitiveType.String, + Doc = "The username of the user.", + Default = new AsyncApiAny("guest"), + Order = "ascending", + }, + new AvroField + { + Name = "status", + Type = new AvroEnum + { + Name = "Status", + Symbols = new List { "ACTIVE", "INACTIVE", "BANNED" }, + }, + Doc = "The status of the user.", + }, + new AvroField + { + Name = "emails", + Type = new AvroArray + { + Items = AvroPrimitiveType.String, + }, + Doc = "A list of email addresses.", + }, + new AvroField + { + Name = "metadata", + Type = new AvroMap + { + Values = AvroPrimitiveType.String, + }, + Doc = "Metadata associated with the user.", + }, + new AvroField + { + Name = "address", + Type = new AvroRecord + { + Name = "Address", + Fields = new List + { + new AvroField { Name = "street", Type = AvroPrimitiveType.String }, + new AvroField { Name = "city", Type = AvroPrimitiveType.String }, + new AvroField { Name = "zipcode", Type = AvroPrimitiveType.String }, + }, + }, + Doc = "The address of the user.", + }, + new AvroField + { + Name = "profilePicture", + Type = new AvroFixed + { + Name = "ProfilePicture", + Size = 256, + }, + Doc = "A fixed-size profile picture.", + }, + new AvroField + { + Name = "contact", + Type = new AvroUnion + { + Types = new List + { + AvroPrimitiveType.Null, + new AvroRecord + { + Name = "PhoneNumber", + Fields = new List + { + new AvroField { Name = "countryCode", Type = AvroPrimitiveType.Int }, + new AvroField { Name = "number", Type = AvroPrimitiveType.String }, + }, + }, + }, + }, + Doc = "The contact information of the user, which can be either null or a phone number.", + }, + }, + }; + + // Act + var actual = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + actual.Should() + .BeEquivalentTo(expected); + } } }