Skip to content

Commit

Permalink
Add C# record support to code generator
Browse files Browse the repository at this point in the history
  • Loading branch information
dstelljes committed Jan 8, 2025
1 parent a995994 commit 862c839
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 8 deletions.
3 changes: 3 additions & 0 deletions docs/cli/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ namespace Example.Models
`--nullable-references`
: Whether reference types selected for nullable record fields should be annotated as nullable.

`--record-type`
: Which kind of C# type to generate for records. Options are `class` (the default behavior) and `record`.

#### Resolve schema by ID

`-i`, `--id`
Expand Down
6 changes: 5 additions & 1 deletion src/Chr.Avro.Cli/Cli/GenerateCodeVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,18 @@ public class GenerateCodeVerb : Verb, ISchemaResolutionOptions
[Option("nullable-references", HelpText = "Whether reference types selected for nullable record fields should be annotated as nullable.")]
public bool NullableReferences { get; set; }

[Option("record-type", HelpText = "Which kind of C# type to generate for records.")]
public RecordType RecordType { get; set; }

[Option('v', "version", SetName = BySubjectSet, HelpText = "The version of the schema.")]
public int? SchemaVersion { get; set; }

protected override async Task Run()
{
var generator = new CSharpCodeGenerator(
enableDescriptionAttributeForDocumentation: ComponentModelAnnotations,
enableNullableReferenceTypes: NullableReferences);
enableNullableReferenceTypes: NullableReferences,
recordType: RecordType);
var reader = new JsonSchemaReader();
var schema = reader.Read(await ((ISchemaResolutionOptions)this).ResolveSchema());

Expand Down
73 changes: 69 additions & 4 deletions src/Chr.Avro.Codegen/Codegen/CSharpCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class CSharpCodeGenerator : ICodeGenerator
{
private readonly bool enableNullableReferenceTypes;
private readonly bool enableDescriptionAttributeForDocumentation;
private readonly RecordType recordType;

/// <summary>
/// Initializes a new instance of the <see cref="CSharpCodeGenerator" /> class.
Expand All @@ -30,10 +31,17 @@ public class CSharpCodeGenerator : ICodeGenerator
/// Whether enum and record schema documentation should be reflected in
/// <see cref="System.ComponentModel.DescriptionAttribute" />s on types and members.
/// </param>
public CSharpCodeGenerator(bool enableNullableReferenceTypes = true, bool enableDescriptionAttributeForDocumentation = false)
/// <param name="recordType">
/// Which kind of C# type to generate for records.
/// </param>
public CSharpCodeGenerator(
bool enableNullableReferenceTypes = true,
bool enableDescriptionAttributeForDocumentation = false,
RecordType recordType = RecordType.Class)
{
this.enableNullableReferenceTypes = enableNullableReferenceTypes;
this.enableDescriptionAttributeForDocumentation = enableDescriptionAttributeForDocumentation;
this.recordType = recordType;
}

/// <summary>
Expand Down Expand Up @@ -112,6 +120,58 @@ public virtual EnumDeclarationSyntax GenerateEnum(EnumSchema schema)
return declaration;
}

/// <summary>
/// Generates a record declaration for a record schema.
/// </summary>
/// <param name="schema">
/// The schema to generate a record for.
/// </param>
/// <returns>
/// A record declaration with a property for each field of the record schema.
/// </returns>
/// <throws cref="UnsupportedSchemaException">
/// Thrown when a field schema is not recognized.
/// </throws>
public virtual RecordDeclarationSyntax GenerateRecord(RecordSchema schema)
{
var declaration = SyntaxFactory.RecordDeclaration(SyntaxFactory.Token(SyntaxKind.RecordKeyword), schema.Name)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddMembers(schema.Fields
.Select(field =>
{
var child = SyntaxFactory
.PropertyDeclaration(
GetPropertyType(field.Type),
field.Name)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)))
.AddAttributeLists(GetDescriptionAttribute(field.Documentation));

if (!string.IsNullOrEmpty(field.Documentation))
{
child = AddSummaryComment(child, field.Documentation!);
}

return child;
})
.Where(field => field != null)
.ToArray())
.AddAttributeLists(GetDescriptionAttribute(schema.Documentation))
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken));

if (!string.IsNullOrEmpty(schema.Documentation))
{
declaration = AddSummaryComment(declaration, schema.Documentation!);
}

return declaration;
}

/// <summary>
/// Generates a compilation unit (essentially a single .cs file) that contains types that
/// match the schema.
Expand Down Expand Up @@ -146,9 +206,14 @@ public virtual CompilationUnitSyntax GenerateCompilationUnit(Schema schema)
var members = group
.Select(candidate => candidate switch
{
EnumSchema enumSchema => GenerateEnum(enumSchema) as MemberDeclarationSyntax,
RecordSchema recordSchema => GenerateClass(recordSchema) as MemberDeclarationSyntax,
_ => default,
EnumSchema enumSchema => GenerateEnum(enumSchema),
RecordSchema recordSchema => recordType switch
{
RecordType.Class => GenerateClass(recordSchema),
RecordType.Record => GenerateRecord(recordSchema),
_ => throw new ArgumentOutOfRangeException(nameof(recordType)),
},
_ => (MemberDeclarationSyntax?)default,
})
.OfType<MemberDeclarationSyntax>()
.ToArray();
Expand Down
21 changes: 18 additions & 3 deletions src/Chr.Avro.Codegen/Codegen/NamespaceRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node)
{
var descendants = node.DescendantNodesAndSelf();

var parameters = descendants.OfType<ParameterSyntax>().Select(p => p.Type);
var properties = descendants.OfType<PropertyDeclarationSyntax>().Select(p => p.Type);

internals = new HashSet<string>(descendants
.OfType<NamespaceDeclarationSyntax>()
.Select(n => n.Name.ToString()));

externals = new HashSet<string>(descendants
.OfType<PropertyDeclarationSyntax>()
.Select(p => p.Type)
externals = new HashSet<string>(Enumerable
.Concat(parameters, properties)
.OfType<QualifiedNameSyntax>()
.Select(n => StripGlobalAlias(n.Left).ToString())
.Where(n => !internals.Contains(n)));
Expand All @@ -59,6 +61,19 @@ public override SyntaxNode VisitNamespaceDeclaration(NamespaceDeclarationSyntax
return result;
}

/// <inheritdoc />
public override SyntaxNode? VisitParameter(ParameterSyntax node)
{
var result = (ParameterSyntax)base.VisitParameter(node)!;

if (result.Type is NameSyntax name)
{
result = result.WithType(Reduce(name)).WithTriviaFrom(result);
}

return result;
}

/// <inheritdoc />
public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
Expand Down
18 changes: 18 additions & 0 deletions src/Chr.Avro.Codegen/Codegen/RecordType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Chr.Avro.Codegen
{
/// <summary>
/// Options for representing Avro records in C#.
/// </summary>
public enum RecordType
{
/// <summary>
/// Represent Avro records as C# classes.
/// </summary>
Class,

/// <summary>
/// Represent Avro records as C# records.
/// </summary>
Record,
}
}

0 comments on commit 862c839

Please sign in to comment.