Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
}
}