Skip to content

Commit 7bd82c8

Browse files
author
Christoph Bühler
committed
feat(generator): generate entity initializer (static and partial)
1 parent 8351021 commit 7bd82c8

File tree

4 files changed

+494
-1
lines changed

4 files changed

+494
-1
lines changed

examples/Operator/Entities/V1TestEntity.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace Operator.Entities;
66

77
[KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")]
8-
public class V1TestEntity : CustomKubernetesEntity<V1TestEntity.EntitySpec, V1TestEntity.EntityStatus>
8+
public partial class V1TestEntity : CustomKubernetesEntity<V1TestEntity.EntitySpec, V1TestEntity.EntityStatus>
99
{
1010
public override string ToString() => $"Test Entity ({Metadata.Name}): {Spec.Username} ({Spec.Email})";
1111

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System.Text;
2+
3+
using KubeOps.Generator.SyntaxReceiver;
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
11+
12+
namespace KubeOps.Generator.Generators;
13+
14+
[Generator]
15+
internal class EntityInitializerGenerator : ISourceGenerator
16+
{
17+
public void Initialize(GeneratorInitializationContext context)
18+
{
19+
context.RegisterForSyntaxNotifications(() => new KubernetesEntitySyntaxReceiver());
20+
}
21+
22+
public void Execute(GeneratorExecutionContext context)
23+
{
24+
if (context.SyntaxContextReceiver is not KubernetesEntitySyntaxReceiver receiver)
25+
{
26+
return;
27+
}
28+
29+
// for each partial defined entity, create a partial class that
30+
// introduces a default constructor that initializes the ApiVersion and Kind.
31+
// But only, if there is no default constructor defined.
32+
foreach (var entity in receiver.Entities
33+
.Where(e => e.Class.Modifiers.Any(SyntaxKind.PartialKeyword))
34+
.Where(e => !e.Class.Members.Any(m => m is ConstructorDeclarationSyntax
35+
{
36+
ParameterList.Parameters.Count: 0,
37+
})))
38+
{
39+
var symbol = context.Compilation
40+
.GetSemanticModel(entity.Class.SyntaxTree)
41+
.GetDeclaredSymbol(entity.Class)!;
42+
43+
var ns = new List<MemberDeclarationSyntax>();
44+
if (!symbol.ContainingNamespace.IsGlobalNamespace)
45+
{
46+
ns.Add(FileScopedNamespaceDeclaration(IdentifierName(symbol.ContainingNamespace.ToDisplayString())));
47+
}
48+
49+
var partialEntityInitializer = CompilationUnit()
50+
.AddMembers(ns.ToArray())
51+
.AddMembers(ClassDeclaration(entity.Class.Identifier)
52+
.WithModifiers(entity.Class.Modifiers)
53+
.AddMembers(ConstructorDeclaration(entity.Class.Identifier)
54+
.WithModifiers(
55+
TokenList(
56+
Token(SyntaxKind.PublicKeyword)))
57+
.WithBody(
58+
Block(
59+
ExpressionStatement(
60+
AssignmentExpression(
61+
SyntaxKind.SimpleAssignmentExpression,
62+
IdentifierName("ApiVersion"),
63+
LiteralExpression(
64+
SyntaxKind.StringLiteralExpression,
65+
Literal($"{entity.Group}/{entity.Version}".TrimStart('/'))))),
66+
ExpressionStatement(
67+
AssignmentExpression(
68+
SyntaxKind.SimpleAssignmentExpression,
69+
IdentifierName("Kind"),
70+
LiteralExpression(
71+
SyntaxKind.StringLiteralExpression,
72+
Literal(entity.Kind))))))))
73+
.NormalizeWhitespace();
74+
75+
context.AddSource(
76+
$"{entity.Class.Identifier}.init.g.cs",
77+
SourceText.From(partialEntityInitializer.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256));
78+
}
79+
80+
// for each NON partial entity, generate a method extension that initializes the ApiVersion and Kind.
81+
var staticInitializers = CompilationUnit()
82+
.WithMembers(SingletonList<MemberDeclarationSyntax>(ClassDeclaration("EntityInitializer")
83+
.WithModifiers(TokenList(
84+
Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
85+
.WithMembers(List<MemberDeclarationSyntax>(receiver.Entities
86+
.Where(e => !e.Class.Modifiers.Any(SyntaxKind.PartialKeyword) || e.Class.Members.Any(m =>
87+
m is ConstructorDeclarationSyntax
88+
{
89+
ParameterList.Parameters.Count: 0,
90+
}))
91+
.Select(e => (Entity: e,
92+
ClassIdentifier: context.Compilation.GetSemanticModel(e.Class.SyntaxTree)
93+
.GetDeclaredSymbol(e.Class)!.ToDisplayString(SymbolDisplayFormat
94+
.FullyQualifiedFormat)))
95+
.Select(e =>
96+
MethodDeclaration(
97+
IdentifierName(e.ClassIdentifier),
98+
"Initialize")
99+
.WithModifiers(
100+
TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
101+
.WithParameterList(ParameterList(
102+
SingletonSeparatedList(
103+
Parameter(
104+
Identifier("entity"))
105+
.WithModifiers(
106+
TokenList(
107+
Token(SyntaxKind.ThisKeyword)))
108+
.WithType(IdentifierName(e.ClassIdentifier)))))
109+
.WithBody(Block(
110+
ExpressionStatement(
111+
AssignmentExpression(
112+
SyntaxKind.SimpleAssignmentExpression,
113+
MemberAccessExpression(
114+
SyntaxKind.SimpleMemberAccessExpression,
115+
IdentifierName("entity"),
116+
IdentifierName("ApiVersion")),
117+
LiteralExpression(
118+
SyntaxKind.StringLiteralExpression,
119+
Literal($"{e.Entity.Group}/{e.Entity.Version}".TrimStart('/'))))),
120+
ExpressionStatement(
121+
AssignmentExpression(
122+
SyntaxKind.SimpleAssignmentExpression,
123+
MemberAccessExpression(
124+
SyntaxKind.SimpleMemberAccessExpression,
125+
IdentifierName("entity"),
126+
IdentifierName("Kind")),
127+
LiteralExpression(
128+
SyntaxKind.StringLiteralExpression,
129+
Literal(e.Entity.Kind)))),
130+
ReturnStatement(IdentifierName("entity")))))))))
131+
.NormalizeWhitespace();
132+
133+
context.AddSource(
134+
"EntityInitializer.g.cs",
135+
SourceText.From(staticInitializers.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256));
136+
}
137+
}

src/KubeOps.Generator/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,75 @@ public static class EntityDefinitions
4848
}
4949
```
5050

51+
### Entity Initializer
52+
53+
All entities must have their `Kind` and `ApiVersion` fields set.
54+
To achieve this, the generator creates an initializer file for each entity
55+
that is annotated with the `KubernetesEntityAttribute`.
56+
57+
For each **partial** class that does not contain a default constructor,
58+
the generator will create a default constructor that sets the `Kind` and `ApiVersion` fields.
59+
60+
For each **non partial** class, a method extension is created that sets
61+
the `Kind` and `ApiVersion` fields.
62+
63+
> [!NOTE]
64+
> Setting your class partial is crucial for the generator to create the constructor.
65+
> Also, if some default constructor is already present, the generator uses the
66+
> method extension fallback.
67+
68+
#### Example
69+
70+
```csharp
71+
namespace Operator.Entities;
72+
73+
[KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")]
74+
public partial class V1TestEntity : CustomKubernetesEntity
75+
{
76+
}
77+
```
78+
79+
The **partial** defined entity above will generate the following `V1TestEntity.init.g.cs` file:
80+
81+
```csharp
82+
namespace Operator.Entities;
83+
public partial class V1TestEntity
84+
{
85+
public V1TestEntity()
86+
{
87+
ApiVersion = "testing.dev/v1";
88+
Kind = "TestEntity";
89+
}
90+
}
91+
```
92+
93+
The **non partial** defined entity below:
94+
95+
```csharp
96+
namespace Operator.Entities;
97+
98+
[KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")]
99+
public class V1TestEntity : CustomKubernetesEntity
100+
{
101+
}
102+
```
103+
104+
will generate a static method extension in `EntityInitializer.g.cs`
105+
for the entity to initialize the fields:
106+
107+
```csharp
108+
public static class EntityInitializer
109+
{
110+
public static global::Operator.Entities.V1ClusterTestEntity Initialize(this global::Operator.Entities.V1ClusterTestEntity entity)
111+
{
112+
entity.ApiVersion = "testing.dev/v1";
113+
entity.Kind = "ClusterTestEntity";
114+
return entity;
115+
}
116+
}
117+
```
118+
119+
51120
### Controller Registrations
52121

53122
The generator creates a file in the root namespace called `ControllerRegistrations.g.cs`.

0 commit comments

Comments
 (0)