Skip to content

Commit 01c1f3e

Browse files
Refactor code generator and fix issue with services with multiple target domains (#819)
1 parent 42a0fe1 commit 01c1f3e

35 files changed

+568
-606
lines changed

src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/AttributeTypeGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal static class AttributeTypeGenerator
2424
public static RecordDeclarationSyntax GenerateAttributeRecord(EntityDomainMetadata domain)
2525
{
2626
var propertyDeclarations = domain.Attributes
27-
.Select(a => Property($"{a.ClrType.GetFriendlyName()}?", a.CSharpName)
27+
.Select(a => AutoPropertyGetInit($"{a.ClrType.GetFriendlyName()}?", a.CSharpName)
2828
.ToPublic()
2929
.WithJsonPropertyName(a.JsonName));
3030

src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/EntitiesGenerator.cs

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,24 @@ private static TypeDeclarationSyntax GenerateRootEntitiesInterface(IEnumerable<s
3434
var typeName = GetEntitiesForDomainClassName(domain);
3535
var propertyName = domain.ToPascalCase();
3636

37-
return (MemberDeclarationSyntax)Property(typeName, propertyName, init: false);
38-
}).ToArray();
37+
return (MemberDeclarationSyntax)AutoPropertyGet(typeName, propertyName);
38+
});
3939

40-
return Interface("IEntities").AddMembers(autoProperties).ToPublic();
40+
return InterfaceDeclaration("IEntities").WithMembers(List(autoProperties)).ToPublic();
4141
}
4242

4343
// The Entities class that provides properties to all Domains
44-
private static TypeDeclarationSyntax GenerateRootEntitiesClass(IEnumerable<EntityDomainMetadata> entitySet)
44+
private static TypeDeclarationSyntax GenerateRootEntitiesClass(IEnumerable<EntityDomainMetadata> domains)
4545
{
46-
var haContextNames = GetNames<IHaContext>();
47-
48-
var properties = entitySet.DistinctBy(s=>s.Domain).Select(set =>
46+
var properties = domains.DistinctBy(s => s.Domain).Select(set =>
4947
{
5048
var entitiesTypeName = GetEntitiesForDomainClassName(set.Domain);
5149
var entitiesPropertyName = set.Domain.ToPascalCase();
5250

53-
return (MemberDeclarationSyntax)ParseProperty($"{entitiesTypeName} {entitiesPropertyName} => new(_{haContextNames.VariableName});")
54-
.ToPublic();
51+
return PropertyWithExpressionBodyNew(entitiesTypeName, entitiesPropertyName, "_haContext");
5552
}).ToArray();
5653

57-
return ClassWithInjected<IHaContext>(EntitiesClassName)
58-
.ToPublic()
59-
.AddModifiers(Token(SyntaxKind.PartialKeyword))
54+
return ClassWithInjectedHaContext(EntitiesClassName)
6055
.WithBase((string)"IEntities")
6156
.AddMembers(properties);
6257
}
@@ -66,9 +61,7 @@ private static TypeDeclarationSyntax GenerateRootEntitiesClass(IEnumerable<Entit
6661
/// </summary>
6762
private static TypeDeclarationSyntax GenerateEntiesForDomainClass(string className, IEnumerable<EntityDomainMetadata> entitySets)
6863
{
69-
var entityClass = ClassWithInjected<IHaContext>(className)
70-
.ToPublic()
71-
.AddModifiers(Token(SyntaxKind.PartialKeyword));
64+
var entityClass = ClassWithInjectedHaContext(className);
7265

7366
var entityProperty = entitySets.SelectMany(s=>s.Entities.Select(e => GenerateEntityProperty(e, s.EntityClassName))).ToArray();
7467

@@ -79,16 +72,16 @@ private static MemberDeclarationSyntax GenerateEntityProperty(EntityMetaData ent
7972
{
8073
var entityName = EntityIdHelper.GetEntity(entity.id);
8174

82-
var propertyCode = $@"{className} {entityName.ToNormalizedPascalCase((string)"E_")} => new(_{GetNames<IHaContext>().VariableName}, ""{entity.id}"");";
75+
var normalizedPascalCase = entityName.ToNormalizedPascalCase((string)"E_");
8376

8477
var name = entity.friendlyName;
85-
return ParseProperty(propertyCode).ToPublic().WithSummaryComment(name);
78+
return PropertyWithExpressionBodyNew(className, normalizedPascalCase, "_haContext", $"\"{entity.id}\"").WithSummaryComment(name);
8679
}
8780

8881
/// <summary>
8982
/// Generates a record derived from Entity like ClimateEntity or SensorEntity for a specific set of entities
9083
/// </summary>
91-
private static TypeDeclarationSyntax GenerateEntityType(EntityDomainMetadata domainMetaData)
84+
private static MemberDeclarationSyntax GenerateEntityType(EntityDomainMetadata domainMetaData)
9285
{
9386
string attributesGeneric = domainMetaData.AttributesClassName;
9487

@@ -98,16 +91,18 @@ private static TypeDeclarationSyntax GenerateEntityType(EntityDomainMetadata dom
9891
var baseClass = $"{SimplifyTypeName(baseType)}<{domainMetaData.EntityClassName}, {SimplifyTypeName(entityStateType)}<{attributesGeneric}>, {attributesGeneric}>";
9992

10093
var (className, variableName) = GetNames<IHaContext>();
101-
var classDeclaration = $@"record {domainMetaData.EntityClassName} : {baseClass}
102-
{{
103-
public {domainMetaData.EntityClassName}({className} {variableName}, string entityId) : base({variableName}, entityId)
104-
{{}}
105-
106-
public {domainMetaData.EntityClassName}({SimplifyTypeName(typeof(Entity))} entity) : base(entity)
107-
{{}}
108-
}}";
109-
110-
return ParseRecord(classDeclaration)
94+
var classDeclaration = $$"""
95+
record {{domainMetaData.EntityClassName}} : {{baseClass}}
96+
{
97+
public {{domainMetaData.EntityClassName}}({{className}} {{variableName}}, string entityId) : base({{variableName}}, entityId)
98+
{}
99+
100+
public {{domainMetaData.EntityClassName}}({{SimplifyTypeName(typeof(Entity))}} entity) : base(entity)
101+
{}
102+
}
103+
""";
104+
105+
return ParseMemberDeclaration(classDeclaration)!
111106
.ToPublic()
112107
.AddModifiers(Token(SyntaxKind.PartialKeyword));
113108
}

src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.CodeAnalysis.CSharp;
1+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
22

33
namespace NetDaemon.HassModel.CodeGenerator;
44

@@ -36,22 +36,27 @@ public static IEnumerable<MemberDeclarationSyntax> Generate(IEnumerable<HassServ
3636
{
3737
var serviceMethodDeclarations = serviceDomain.Services
3838
.OrderBy(x => x.Service)
39-
.SelectMany(service => GenerateExtensionMethods(serviceDomain.Domain, service, entityClassNameByDomain))
39+
.SelectMany(service => GenerateExtensionMethodsForService(serviceDomain.Domain, service, entityClassNameByDomain))
4040
.ToArray();
4141

4242
if (!serviceMethodDeclarations.Any()) return null;
4343

44-
return SyntaxFactory.ClassDeclaration(GetEntityDomainExtensionMethodClassName(serviceDomain.Domain))
44+
return ClassDeclaration(GetEntityDomainExtensionMethodClassName(serviceDomain.Domain))
4545
.AddMembers(serviceMethodDeclarations)
4646
.ToPublic()
4747
.ToStatic();
4848
}
4949

50-
private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethods(string domain, HassService service, ILookup<string, string> entityClassNameByDomain)
50+
private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethodsForService(string domain, HassService service, ILookup<string, string> entityClassNameByDomain)
5151
{
52-
var targetEntityDomain = service.Target?.Entity?.Domain;
53-
if (targetEntityDomain == null) yield break;
52+
// There can be multiple Target Domains, so generate methods for each
53+
var targetEntityDomains = service.Target?.Entity?.Domain ?? Array.Empty<string>();
5454

55+
return targetEntityDomains.SelectMany(targetEntityDomain => GenerateExtensionMethodsForService(domain, service, targetEntityDomain, entityClassNameByDomain));
56+
}
57+
58+
private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethodsForService(string domain, HassService service, string targetEntityDomain, ILookup<string, string> entityClassNameByDomain)
59+
{
5560
var entityTypeName = entityClassNameByDomain[targetEntityDomain].FirstOrDefault();
5661
if (entityTypeName == null) yield break;
5762

@@ -74,41 +79,38 @@ private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethods(str
7479
}
7580
}
7681

77-
private static GlobalStatementSyntax ExtensionMethodWithoutArguments(HassService service, string serviceName, string entityTypeName)
82+
private static MemberDeclarationSyntax ExtensionMethodWithoutArguments(HassService service, string serviceName, string entityTypeName)
7883
{
79-
return ParseMethod(
80-
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} target)
81-
{{
82-
target.CallService(""{serviceName}"");
83-
}}")
84-
.ToPublic()
85-
.ToStatic()
84+
return ParseMemberDeclaration($$"""
85+
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target)
86+
{
87+
target.CallService("{{serviceName}}");
88+
}
89+
""")!
8690
.WithSummaryComment(service.Description);
8791
}
8892

89-
private static GlobalStatementSyntax ExtensionMethodWithClassArgument(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
93+
private static MemberDeclarationSyntax ExtensionMethodWithClassArgument(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
9094
{
91-
return ParseMethod(
92-
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} target, {serviceArguments.TypeName} data)
93-
{{
94-
target.CallService(""{serviceName}"", data);
95-
}}")
96-
.ToPublic()
97-
.ToStatic()
95+
return ParseMemberDeclaration($$"""
96+
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target, {{serviceArguments.TypeName}} data)
97+
{
98+
target.CallService("{{serviceName}}", data);
99+
}
100+
""")!
98101
.WithSummaryComment(service.Description);
99102
}
100103

101104
private static MemberDeclarationSyntax ExtensionMethodWithSeparateArguments(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
102105
{
103-
return ParseMethod(
104-
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} target, {serviceArguments.GetParametersList()})
105-
{{
106-
target.CallService(""{serviceName}"", {serviceArguments.GetNewServiceArgumentsTypeExpression()});
107-
}}")
108-
.ToPublic()
109-
.ToStatic()
106+
return ParseMemberDeclaration($$"""
107+
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target, {{serviceArguments.GetParametersList()}})
108+
{
109+
target.CallService("{{serviceName}}", {{serviceArguments.GetNewServiceArgumentsTypeExpression()}});
110+
}
111+
""")!
110112
.WithSummaryComment(service.Description)
111-
.WithParameterComment("target", $"The {entityTypeName} to call this service for")
112-
.WithParameterComments(serviceArguments);
113+
.AppendParameterComment("target", $"The {entityTypeName} to call this service for")
114+
.AppendParameterComments(serviceArguments);
113115
}
114116
}
Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,68 @@
1-
using Microsoft.CodeAnalysis.CSharp;
1+
using System.Reflection;
2+
using Microsoft.CodeAnalysis.CSharp;
23
using NetDaemon.HassModel.CodeGenerator.CodeGeneration;
34
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
45

56
namespace NetDaemon.HassModel.CodeGenerator;
67

78
internal static class Generator
89
{
9-
public static IEnumerable<MemberDeclarationSyntax> GenerateTypes(
10-
IReadOnlyCollection<EntityDomainMetadata> domains,
10+
/// <summary>
11+
/// Generates all Types from Entity and Services metadata
12+
/// </summary>
13+
public static MemberDeclarationSyntax[] GenerateTypes(
14+
IReadOnlyCollection<EntityDomainMetadata> entityDomains,
1115
IReadOnlyCollection<HassServiceDomain> services)
1216
{
1317
var orderedServiceDomains = services.OrderBy(x => x.Domain).ToArray();
1418

15-
var helpers = HelpersGenerator.Generate(domains, orderedServiceDomains);
16-
var entityClasses = EntitiesGenerator.Generate(domains);
19+
var helpers = HelpersGenerator.Generate(entityDomains, orderedServiceDomains);
20+
var entityClasses = EntitiesGenerator.Generate(entityDomains);
1721
var serviceClasses = ServicesGenerator.Generate(orderedServiceDomains);
18-
var extensionMethodClasses = ExtensionMethodsGenerator.Generate(orderedServiceDomains, domains);
22+
var extensionMethodClasses = ExtensionMethodsGenerator.Generate(orderedServiceDomains, entityDomains);
1923

20-
return new[] {helpers, entityClasses, serviceClasses, extensionMethodClasses }.SelectMany(x => x).ToArray();
24+
return new[] { helpers, entityClasses, serviceClasses, extensionMethodClasses }.SelectMany(x => x).ToArray();
2125
}
2226

2327
public static CompilationUnitSyntax BuildCompilationUnit(string namespaceName, params MemberDeclarationSyntax[] generatedTypes)
2428
{
2529
return CompilationUnit()
2630
.AddUsings(UsingNamespaces.Select(u => UsingDirective(ParseName(u))).ToArray())
27-
.WithLeadingTrivia(TriviaHelper.GetFileHeader()
31+
.WithLeadingTrivia(GetFileHeader()
2832
.Append(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))))
2933
.AddMembers(FileScopedNamespaceDeclaration(ParseName(namespaceName)))
3034
.AddMembers(generatedTypes)
3135
.NormalizeWhitespace();
3236
}
37+
38+
private static readonly string GeneratorVersion = Assembly.GetAssembly(typeof(Generator))!.GetName().Version!.ToString();
39+
40+
private static SyntaxTrivia[] GetFileHeader()
41+
{
42+
string headerText = @$"
43+
//------------------------------------------------------------------------------
44+
// <auto-generated>
45+
// Generated using NetDaemon CodeGenerator nd-codegen v{GeneratorVersion}
46+
// At: {DateTime.Now:O}
47+
//
48+
// *** Make sure the version of the codegen tool and your nugets Joysoftware.NetDaemon.* have the same version.***
49+
// You can use following command to keep it up to date with the latest version:
50+
// dotnet tool update JoySoftware.NetDaemon.HassModel.CodeGen
51+
//
52+
// To update this file with latest entities run this command in your project directory:
53+
// dotnet tool run nd-codegen
54+
//
55+
// In the template projects we provided a convenience powershell script that will update
56+
// the codegen and nugets to latest versions update_all_dependencies.ps1.
57+
//
58+
// For more information: https://netdaemon.xyz/docs/v3/hass_model/hass_model_codegen
59+
// For more information about NetDaemon: https://netdaemon.xyz/
60+
// </auto-generated>
61+
//------------------------------------------------------------------------------";
62+
63+
var lines = headerText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
64+
65+
SyntaxTrivia[] header = lines.SelectMany(l => new[] { Comment(l), LineFeed }).ToArray();
66+
return header;
67+
}
3368
}

src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ServiceArguments.cs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22

33
internal record ServiceArgument
44
{
5-
public Type? Type { get; init; }
6-
5+
public required string HaName { get; init; }
6+
public required Type ClrType { get; init; }
77
public bool Required { get; init; }
88

9-
public string? HaName { get; init; }
10-
11-
public string? TypeName => Type?.GetFriendlyName();
9+
public string? Comment { get; init; }
1210

13-
public string? ParameterTypeName => Required ? TypeName : $"{TypeName}?";
11+
public string TypeName => ClrType.GetFriendlyName();
1412

15-
public string? PropertyName => HaName?.ToNormalizedPascalCase();
13+
public string ParameterTypeName => Required ? TypeName : $"{TypeName}?";
1614

17-
public string? VariableName => HaName?.ToNormalizedCamelCase();
15+
public string PropertyName => HaName.ToNormalizedPascalCase();
1816

19-
public string? ParameterVariableName => Required ? VariableName : $"{VariableName} = null";
17+
public string VariableName => HaName.ToNormalizedCamelCase();
2018

21-
public string? Comment { get; init; }
19+
public string ParameterVariableName => Required ? VariableName : $"{VariableName} = null";
2220
}
2321

2422
internal class ServiceArguments
@@ -33,19 +31,19 @@ internal class ServiceArguments
3331
return null;
3432
}
3533

36-
return new ServiceArguments(domain, service.Service!, service.Fields);
34+
return new ServiceArguments(domain, service.Service, service.Fields);
3735
}
38-
39-
public ServiceArguments(string domain, string serviceName, IReadOnlyCollection<HassServiceField> serviceFields)
36+
37+
private ServiceArguments(string domain, string serviceName, IReadOnlyCollection<HassServiceField> serviceFields)
4038
{
4139
_domain = domain;
4240
_serviceName = serviceName!;
43-
Arguments = serviceFields.Select(HassServiceArgumentMapper.Map);
41+
Arguments = serviceFields.Select(HassServiceArgumentMapper.Map).ToArray();
4442
}
4543

4644
public IEnumerable<ServiceArgument> Arguments { get; }
4745

48-
public string TypeName => NamingHelper.GetServiceArgumentsTypeName(_domain, _serviceName);
46+
public string TypeName => $"{_domain.ToNormalizedPascalCase()}{GetServiceMethodName(_serviceName)}Parameters";
4947

5048
public string GetParametersList()
5149
{

0 commit comments

Comments
 (0)