Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal static class AttributeTypeGenerator
public static RecordDeclarationSyntax GenerateAttributeRecord(EntityDomainMetadata domain)
{
var propertyDeclarations = domain.Attributes
.Select(a => Property($"{a.ClrType.GetFriendlyName()}?", a.CSharpName)
.Select(a => AutoPropertyGetInit($"{a.ClrType.GetFriendlyName()}?", a.CSharpName)
.ToPublic()
.WithJsonPropertyName(a.JsonName));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,24 @@ private static TypeDeclarationSyntax GenerateRootEntitiesInterface(IEnumerable<s
var typeName = GetEntitiesForDomainClassName(domain);
var propertyName = domain.ToPascalCase();

return (MemberDeclarationSyntax)Property(typeName, propertyName, init: false);
}).ToArray();
return (MemberDeclarationSyntax)AutoPropertyGet(typeName, propertyName);
});

return Interface("IEntities").AddMembers(autoProperties).ToPublic();
return InterfaceDeclaration("IEntities").WithMembers(List(autoProperties)).ToPublic();
}

// The Entities class that provides properties to all Domains
private static TypeDeclarationSyntax GenerateRootEntitiesClass(IEnumerable<EntityDomainMetadata> entitySet)
private static TypeDeclarationSyntax GenerateRootEntitiesClass(IEnumerable<EntityDomainMetadata> domains)
{
var haContextNames = GetNames<IHaContext>();

var properties = entitySet.DistinctBy(s=>s.Domain).Select(set =>
var properties = domains.DistinctBy(s => s.Domain).Select(set =>
{
var entitiesTypeName = GetEntitiesForDomainClassName(set.Domain);
var entitiesPropertyName = set.Domain.ToPascalCase();

return (MemberDeclarationSyntax)ParseProperty($"{entitiesTypeName} {entitiesPropertyName} => new(_{haContextNames.VariableName});")
.ToPublic();
return PropertyWithExpressionBodyNew(entitiesTypeName, entitiesPropertyName, "_haContext");
}).ToArray();

return ClassWithInjected<IHaContext>(EntitiesClassName)
.ToPublic()
.AddModifiers(Token(SyntaxKind.PartialKeyword))
return ClassWithInjectedHaContext(EntitiesClassName)
.WithBase((string)"IEntities")
.AddMembers(properties);
}
Expand All @@ -66,9 +61,7 @@ private static TypeDeclarationSyntax GenerateRootEntitiesClass(IEnumerable<Entit
/// </summary>
private static TypeDeclarationSyntax GenerateEntiesForDomainClass(string className, IEnumerable<EntityDomainMetadata> entitySets)
{
var entityClass = ClassWithInjected<IHaContext>(className)
.ToPublic()
.AddModifiers(Token(SyntaxKind.PartialKeyword));
var entityClass = ClassWithInjectedHaContext(className);

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

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

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

var name = entity.friendlyName;
return ParseProperty(propertyCode).ToPublic().WithSummaryComment(name);
return PropertyWithExpressionBodyNew(className, normalizedPascalCase, "_haContext", $"\"{entity.id}\"").WithSummaryComment(name);
}

/// <summary>
/// Generates a record derived from Entity like ClimateEntity or SensorEntity for a specific set of entities
/// </summary>
private static TypeDeclarationSyntax GenerateEntityType(EntityDomainMetadata domainMetaData)
private static MemberDeclarationSyntax GenerateEntityType(EntityDomainMetadata domainMetaData)
{
string attributesGeneric = domainMetaData.AttributesClassName;

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

var (className, variableName) = GetNames<IHaContext>();
var classDeclaration = $@"record {domainMetaData.EntityClassName} : {baseClass}
{{
public {domainMetaData.EntityClassName}({className} {variableName}, string entityId) : base({variableName}, entityId)
{{}}

public {domainMetaData.EntityClassName}({SimplifyTypeName(typeof(Entity))} entity) : base(entity)
{{}}
}}";

return ParseRecord(classDeclaration)
var classDeclaration = $$"""
record {{domainMetaData.EntityClassName}} : {{baseClass}}
{
public {{domainMetaData.EntityClassName}}({{className}} {{variableName}}, string entityId) : base({{variableName}}, entityId)
{}

public {{domainMetaData.EntityClassName}}({{SimplifyTypeName(typeof(Entity))}} entity) : base(entity)
{}
}
""";

return ParseMemberDeclaration(classDeclaration)!
.ToPublic()
.AddModifiers(Token(SyntaxKind.PartialKeyword));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis.CSharp;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace NetDaemon.HassModel.CodeGenerator;

Expand Down Expand Up @@ -36,22 +36,27 @@ public static IEnumerable<MemberDeclarationSyntax> Generate(IEnumerable<HassServ
{
var serviceMethodDeclarations = serviceDomain.Services
.OrderBy(x => x.Service)
.SelectMany(service => GenerateExtensionMethods(serviceDomain.Domain, service, entityClassNameByDomain))
.SelectMany(service => GenerateExtensionMethodsForService(serviceDomain.Domain, service, entityClassNameByDomain))
.ToArray();

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

return SyntaxFactory.ClassDeclaration(GetEntityDomainExtensionMethodClassName(serviceDomain.Domain))
return ClassDeclaration(GetEntityDomainExtensionMethodClassName(serviceDomain.Domain))
.AddMembers(serviceMethodDeclarations)
.ToPublic()
.ToStatic();
}

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

return targetEntityDomains.SelectMany(targetEntityDomain => GenerateExtensionMethodsForService(domain, service, targetEntityDomain, entityClassNameByDomain));
}

private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethodsForService(string domain, HassService service, string targetEntityDomain, ILookup<string, string> entityClassNameByDomain)
{
var entityTypeName = entityClassNameByDomain[targetEntityDomain].FirstOrDefault();
if (entityTypeName == null) yield break;

Expand All @@ -74,41 +79,38 @@ private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethods(str
}
}

private static GlobalStatementSyntax ExtensionMethodWithoutArguments(HassService service, string serviceName, string entityTypeName)
private static MemberDeclarationSyntax ExtensionMethodWithoutArguments(HassService service, string serviceName, string entityTypeName)
{
return ParseMethod(
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} target)
{{
target.CallService(""{serviceName}"");
}}")
.ToPublic()
.ToStatic()
return ParseMemberDeclaration($$"""
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target)
{
target.CallService("{{serviceName}}");
}
""")!
.WithSummaryComment(service.Description);
}

private static GlobalStatementSyntax ExtensionMethodWithClassArgument(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
private static MemberDeclarationSyntax ExtensionMethodWithClassArgument(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
{
return ParseMethod(
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} target, {serviceArguments.TypeName} data)
{{
target.CallService(""{serviceName}"", data);
}}")
.ToPublic()
.ToStatic()
return ParseMemberDeclaration($$"""
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target, {{serviceArguments.TypeName}} data)
{
target.CallService("{{serviceName}}", data);
}
""")!
.WithSummaryComment(service.Description);
}

private static MemberDeclarationSyntax ExtensionMethodWithSeparateArguments(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
{
return ParseMethod(
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} target, {serviceArguments.GetParametersList()})
{{
target.CallService(""{serviceName}"", {serviceArguments.GetNewServiceArgumentsTypeExpression()});
}}")
.ToPublic()
.ToStatic()
return ParseMemberDeclaration($$"""
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target, {{serviceArguments.GetParametersList()}})
{
target.CallService("{{serviceName}}", {{serviceArguments.GetNewServiceArgumentsTypeExpression()}});
}
""")!
.WithSummaryComment(service.Description)
.WithParameterComment("target", $"The {entityTypeName} to call this service for")
.WithParameterComments(serviceArguments);
.AppendParameterComment("target", $"The {entityTypeName} to call this service for")
.AppendParameterComments(serviceArguments);
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,68 @@
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;
using Microsoft.CodeAnalysis.CSharp;
using NetDaemon.HassModel.CodeGenerator.CodeGeneration;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace NetDaemon.HassModel.CodeGenerator;

internal static class Generator
{
public static IEnumerable<MemberDeclarationSyntax> GenerateTypes(
IReadOnlyCollection<EntityDomainMetadata> domains,
/// <summary>
/// Generates all Types from Entity and Services metadata
/// </summary>
public static MemberDeclarationSyntax[] GenerateTypes(
IReadOnlyCollection<EntityDomainMetadata> entityDomains,
IReadOnlyCollection<HassServiceDomain> services)
{
var orderedServiceDomains = services.OrderBy(x => x.Domain).ToArray();

var helpers = HelpersGenerator.Generate(domains, orderedServiceDomains);
var entityClasses = EntitiesGenerator.Generate(domains);
var helpers = HelpersGenerator.Generate(entityDomains, orderedServiceDomains);
var entityClasses = EntitiesGenerator.Generate(entityDomains);
var serviceClasses = ServicesGenerator.Generate(orderedServiceDomains);
var extensionMethodClasses = ExtensionMethodsGenerator.Generate(orderedServiceDomains, domains);
var extensionMethodClasses = ExtensionMethodsGenerator.Generate(orderedServiceDomains, entityDomains);

return new[] {helpers, entityClasses, serviceClasses, extensionMethodClasses }.SelectMany(x => x).ToArray();
return new[] { helpers, entityClasses, serviceClasses, extensionMethodClasses }.SelectMany(x => x).ToArray();
}

public static CompilationUnitSyntax BuildCompilationUnit(string namespaceName, params MemberDeclarationSyntax[] generatedTypes)
{
return CompilationUnit()
.AddUsings(UsingNamespaces.Select(u => UsingDirective(ParseName(u))).ToArray())
.WithLeadingTrivia(TriviaHelper.GetFileHeader()
.WithLeadingTrivia(GetFileHeader()
.Append(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))))
.AddMembers(FileScopedNamespaceDeclaration(ParseName(namespaceName)))
.AddMembers(generatedTypes)
.NormalizeWhitespace();
}

private static readonly string GeneratorVersion = Assembly.GetAssembly(typeof(Generator))!.GetName().Version!.ToString();

private static SyntaxTrivia[] GetFileHeader()
{
string headerText = @$"
//------------------------------------------------------------------------------
// <auto-generated>
// Generated using NetDaemon CodeGenerator nd-codegen v{GeneratorVersion}
// At: {DateTime.Now:O}
//
// *** Make sure the version of the codegen tool and your nugets Joysoftware.NetDaemon.* have the same version.***
// You can use following command to keep it up to date with the latest version:
// dotnet tool update JoySoftware.NetDaemon.HassModel.CodeGen
//
// To update this file with latest entities run this command in your project directory:
// dotnet tool run nd-codegen
//
// In the template projects we provided a convenience powershell script that will update
// the codegen and nugets to latest versions update_all_dependencies.ps1.
//
// For more information: https://netdaemon.xyz/docs/v3/hass_model/hass_model_codegen
// For more information about NetDaemon: https://netdaemon.xyz/
// </auto-generated>
//------------------------------------------------------------------------------";

var lines = headerText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

SyntaxTrivia[] header = lines.SelectMany(l => new[] { Comment(l), LineFeed }).ToArray();
return header;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@

internal record ServiceArgument
{
public Type? Type { get; init; }

public required string HaName { get; init; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could consider HomeAssistantName or NameInHomeAssistant. It is a bit longer but more clear. Not a biggie!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the existing name, they are just re-ordered. Actually this whole class is a bit weird because it is just a slightly different view of the HassServiceField that is deserialized from the metadata. Maybe I will remove it in a future refactor

public required Type ClrType { get; init; }
public bool Required { get; init; }

public string? HaName { get; init; }

public string? TypeName => Type?.GetFriendlyName();
public string? Comment { get; init; }

public string? ParameterTypeName => Required ? TypeName : $"{TypeName}?";
public string TypeName => ClrType.GetFriendlyName();

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

public string? VariableName => HaName?.ToNormalizedCamelCase();
public string PropertyName => HaName.ToNormalizedPascalCase();

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

public string? Comment { get; init; }
public string ParameterVariableName => Required ? VariableName : $"{VariableName} = null";
}

internal class ServiceArguments
Expand All @@ -33,19 +31,19 @@ internal class ServiceArguments
return null;
}

return new ServiceArguments(domain, service.Service!, service.Fields);
return new ServiceArguments(domain, service.Service, service.Fields);
}
public ServiceArguments(string domain, string serviceName, IReadOnlyCollection<HassServiceField> serviceFields)

private ServiceArguments(string domain, string serviceName, IReadOnlyCollection<HassServiceField> serviceFields)
{
_domain = domain;
_serviceName = serviceName!;
Arguments = serviceFields.Select(HassServiceArgumentMapper.Map);
Arguments = serviceFields.Select(HassServiceArgumentMapper.Map).ToArray();
}

public IEnumerable<ServiceArgument> Arguments { get; }

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

public string GetParametersList()
{
Expand Down
Loading