Skip to content

Commit

Permalink
I AM AN AVRO GOD
Browse files Browse the repository at this point in the history
  • Loading branch information
VisualBean committed May 28, 2024
1 parent 484fc27 commit 8634d7a
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 53 deletions.
8 changes: 8 additions & 0 deletions src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ public override AsyncApiAny CreateAny()
return new AsyncApiAny(this.node);
}

public void ParseFields<T>(ref T parentInstance, IDictionary<string, Action<T, ParseNode>> fixedFields, IDictionary<Func<string, bool>, Action<T, string, ParseNode>> 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");
Expand Down
135 changes: 83 additions & 52 deletions src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -56,56 +48,95 @@ private static AvroField LoadField(ParseNode node)
{ "order", (a, n) => a.Order = n.GetScalarValue() },
};

private static readonly FixedFieldMap<AvroRecord> recordFixedFields = new()
{
{ "type", (a, n) => { } },
{ "name", (a, n) => a.Name = n.GetScalarValue() },
{ "fields", (a, n) => a.Fields = n.CreateList(LoadField) },
};

private static readonly FixedFieldMap<AvroEnum> 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<AvroFixed> 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<AvroArray> arrayFixedFields = new()
{
{ "type", (a, n) => { } },
{ "items", (a, n) => a.Items = LoadFieldType(n) },
};

private static readonly FixedFieldMap<AvroMap> mapFixedFields = new()
{
{ "type", (a, n) => { } },
{ "values", (a, n) => a.Values = LoadFieldType(n) },
};

private static readonly FixedFieldMap<AvroUnion> unionFixedFields = new()
{
{ "types", (a, n) => a.Types = n.CreateList(LoadFieldType) },
};

private static AvroFieldType LoadFieldType(ParseNode node)
{
if (node is ValueNode valueNode)
{
return new AvroPrimitive(valueNode.GetScalarValue().GetEnumFromDisplayName<AvroPrimitiveType>());
}

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");
Expand Down
1 change: 1 addition & 0 deletions src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
167 changes: 167 additions & 0 deletions test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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
{
[Test]
public void SerializeV2_SerializesCorrectly()
{
// Arrange
var expected = """
type: record
name: User
Expand Down Expand Up @@ -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<AvroField>
{
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<string> { "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<AvroField>
{
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<AvroFieldType>
{
AvroPrimitiveType.Null,
new AvroRecord
{
Name = "PhoneNumber",
Fields = new List<AvroField>
{
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<AvroSchema>(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic);

// Assert
actual.Should()
.BeEquivalentTo(expected);
}
}
}

0 comments on commit 8634d7a

Please sign in to comment.