diff --git a/docs/cli/generate.md b/docs/cli/generate.md index 6ec1c9e28..7b165d886 100644 --- a/docs/cli/generate.md +++ b/docs/cli/generate.md @@ -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` diff --git a/src/Chr.Avro.Cli/Cli/GenerateCodeVerb.cs b/src/Chr.Avro.Cli/Cli/GenerateCodeVerb.cs index d7821b491..45e6c0899 100644 --- a/src/Chr.Avro.Cli/Cli/GenerateCodeVerb.cs +++ b/src/Chr.Avro.Cli/Cli/GenerateCodeVerb.cs @@ -52,6 +52,9 @@ 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; } @@ -59,7 +62,8 @@ 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()); diff --git a/src/Chr.Avro.Codegen/Codegen/CSharpCodeGenerator.cs b/src/Chr.Avro.Codegen/Codegen/CSharpCodeGenerator.cs index 1f70aaf7a..dda1da41e 100644 --- a/src/Chr.Avro.Codegen/Codegen/CSharpCodeGenerator.cs +++ b/src/Chr.Avro.Codegen/Codegen/CSharpCodeGenerator.cs @@ -18,6 +18,7 @@ public class CSharpCodeGenerator : ICodeGenerator { private readonly bool enableNullableReferenceTypes; private readonly bool enableDescriptionAttributeForDocumentation; + private readonly RecordType recordType; /// /// Initializes a new instance of the class. @@ -30,10 +31,17 @@ public class CSharpCodeGenerator : ICodeGenerator /// Whether enum and record schema documentation should be reflected in /// s on types and members. /// - public CSharpCodeGenerator(bool enableNullableReferenceTypes = true, bool enableDescriptionAttributeForDocumentation = false) + /// + /// Which kind of C# type to generate for records. + /// + public CSharpCodeGenerator( + bool enableNullableReferenceTypes = true, + bool enableDescriptionAttributeForDocumentation = false, + RecordType recordType = RecordType.Class) { this.enableNullableReferenceTypes = enableNullableReferenceTypes; this.enableDescriptionAttributeForDocumentation = enableDescriptionAttributeForDocumentation; + this.recordType = recordType; } /// @@ -112,6 +120,58 @@ public virtual EnumDeclarationSyntax GenerateEnum(EnumSchema schema) return declaration; } + /// + /// Generates a record declaration for a record schema. + /// + /// + /// The schema to generate a record for. + /// + /// + /// A record declaration with a property for each field of the record schema. + /// + /// + /// Thrown when a field schema is not recognized. + /// + 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; + } + /// /// Generates a compilation unit (essentially a single .cs file) that contains types that /// match the schema. @@ -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() .ToArray(); diff --git a/src/Chr.Avro.Codegen/Codegen/NamespaceRewriter.cs b/src/Chr.Avro.Codegen/Codegen/NamespaceRewriter.cs index 81cd6e41e..1160501b6 100644 --- a/src/Chr.Avro.Codegen/Codegen/NamespaceRewriter.cs +++ b/src/Chr.Avro.Codegen/Codegen/NamespaceRewriter.cs @@ -28,13 +28,15 @@ public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node) { var descendants = node.DescendantNodesAndSelf(); + var parameters = descendants.OfType().Select(p => p.Type); + var properties = descendants.OfType().Select(p => p.Type); + internals = new HashSet(descendants .OfType() .Select(n => n.Name.ToString())); - externals = new HashSet(descendants - .OfType() - .Select(p => p.Type) + externals = new HashSet(Enumerable + .Concat(parameters, properties) .OfType() .Select(n => StripGlobalAlias(n.Left).ToString()) .Where(n => !internals.Contains(n))); @@ -59,6 +61,19 @@ public override SyntaxNode VisitNamespaceDeclaration(NamespaceDeclarationSyntax return result; } + /// + 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; + } + /// public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) { diff --git a/src/Chr.Avro.Codegen/Codegen/RecordType.cs b/src/Chr.Avro.Codegen/Codegen/RecordType.cs new file mode 100644 index 000000000..49d750384 --- /dev/null +++ b/src/Chr.Avro.Codegen/Codegen/RecordType.cs @@ -0,0 +1,18 @@ +namespace Chr.Avro.Codegen +{ + /// + /// Options for representing Avro records in C#. + /// + public enum RecordType + { + /// + /// Represent Avro records as C# classes. + /// + Class, + + /// + /// Represent Avro records as C# records. + /// + Record, + } +}