Skip to content

Commit

Permalink
Codegen uses short typenames with using in stead of fully quatified n…
Browse files Browse the repository at this point in the history
…ames (#571)

* refactor codegen

* Clean usings

* Simplify Generated type names

* Clean up
  • Loading branch information
FrankBakkerNl committed Jan 5, 2022
1 parent 84cb828 commit a157210
Show file tree
Hide file tree
Showing 23 changed files with 812 additions and 924 deletions.
2 changes: 1 addition & 1 deletion src/DaemonRunner/DaemonRunner/DaemonRunner.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0-1.final" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
namespace NetDaemon.HassModel.CodeGenerator
namespace NetDaemon.HassModel.CodeGenerator;

public class CodeGenerationSettings
{
public class CodeGenerationSettings
{
public string OutputFile { get; init; } = "HomeAssistantGenerated.cs";
public string Namespace { get; init; } = "HomeAssistantGenerated";
}
public string OutputFile { get; init; } = "HomeAssistantGenerated.cs";
public string Namespace { get; init; } = "HomeAssistantGenerated";
}
193 changes: 193 additions & 0 deletions src/HassModel/NetDaemon.HassModel.CodeGenerator/EntitiesGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
namespace NetDaemon.HassModel.CodeGenerator;

internal static class EntitiesGenerator
{
public static IEnumerable<MemberDeclarationSyntax> Generate(IReadOnlyList<HassState> entities)
{
var entitySets = entities.GroupBy(e => (EntityIdHelper.GetDomain(e.EntityId), IsNumeric(e)))
.Select(g => new EntitySet(g.Key.Item1, g.Key.Item2, g))
.OrderBy(s => s.Domain)
.ToList();

var entityIds = entities.Select(x => x.EntityId).ToList();

var entityDomains = GetDomainsFromEntities(entityIds).OrderBy(s => s).ToList();

yield return GenerateRootEntitiesInterface(entityDomains);

yield return GenerateRootEntitiesClass(entitySets);

foreach (var entityClass in entitySets.GroupBy(s => s.EntitiesForDomainClassName).Select(g => GenerateEntiesForDomainClass(g.Key, g)))
{
yield return entityClass;
}

foreach (var entitytype in entitySets.Select(GenerateEntityType))
{
yield return entitytype;
}

foreach (var attributeRecord in entitySets.Select(GenerateAtributeRecord))
{
yield return attributeRecord;
}
}
private static TypeDeclarationSyntax GenerateRootEntitiesInterface(IEnumerable<string> domains)
{
var autoProperties = domains.Select(domain =>
{
var typeName = GetEntitiesForDomainClassName(domain);
var propertyName = domain.ToPascalCase();
return (MemberDeclarationSyntax)Property(typeName, propertyName, init: false);
}).ToArray();

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

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

var properties = entitySet.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();
}).ToArray();

return ClassWithInjected<IHaContext>("Entities").WithBase((string)"IEntities").AddMembers(properties).ToPublic();
}

/// <summary>
/// Generates a record with all the attributes found in a set of entities providing unique names for each.
/// </summary>
private static RecordDeclarationSyntax GenerateAtributeRecord(EntitySet entitySet)
{
// Get all attributes of all entities in this set
var jsonProperties = entitySet.EntityStates.SelectMany(s => s.AttributesJson?.EnumerateObject() ?? Enumerable.Empty<JsonProperty>());

// Group the attributes by JsonPropertyName and find the best ClrType that fits all
var attributesByJsonName = jsonProperties
.GroupBy(p => p.Name)
.Select(group => (CSharpName: group.Key.ToNormalizedPascalCase(),
JsonName: group.Key,
ClrType: GetBestClrType(group)));

// We might have different json names that after CamelCasing result in the same CSharpName
var uniqueProperties = attributesByJsonName
.GroupBy(t => t.CSharpName)
.SelectMany(DeduplictateCSharpName)
.OrderBy(p => p.CSharpName);

var propertyDeclarations = uniqueProperties.Select(a => Property($"{a.ClrType.GetFriendlyName()}?", a.CSharpName)
.ToPublic()
.WithJsonPropertyName(a.JsonName));

return Record(entitySet.AttributesClassName, propertyDeclarations).ToPublic();
}

private static IEnumerable<(string CSharpName, string JsonName, Type ClrType)> DeduplictateCSharpName(IEnumerable<(string CSharpName, string JsonName, Type ClrType)> items)
{
var list = items.ToList();
if (list.Count == 1) return new[] { list.First() };

return list.OrderBy(i => i.JsonName).Select((p, i) => ($"{p.CSharpName}_{i}", jsonName: p.JsonName, type: p.ClrType));
}

private static Type GetBestClrType(IEnumerable<JsonProperty> valueKinds)
{
var distinctCrlTypes = valueKinds
.Select(p => p.Value.ValueKind)
.Distinct()
.Where(k => k!= JsonValueKind.Null) // null fits in any type so we can ignore it for now
.Select(MapJsonType)
.ToHashSet();

// If all have the same clr type use that, if not it will be 'object'
return distinctCrlTypes.Count == 1
? distinctCrlTypes.First()
: typeof(object);
}

private static Type MapJsonType(JsonValueKind kind) =>
kind switch
{
JsonValueKind.False => typeof(bool),
JsonValueKind.Undefined => typeof(object),
JsonValueKind.Object => typeof(object),
JsonValueKind.Array => typeof(object),
JsonValueKind.String => typeof(string),
JsonValueKind.Number => typeof(double),
JsonValueKind.True => typeof(bool),
JsonValueKind.Null => typeof(object),
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
};

/// <summary>
/// Generates the class with all the properties for the Entities of one domain
/// </summary>
private static TypeDeclarationSyntax GenerateEntiesForDomainClass(string className, IEnumerable<EntitySet> entitySets)
{
var entityClass = ClassWithInjected<IHaContext>(className).ToPublic();

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

return entityClass.AddMembers(entityProperty);
}

private static MemberDeclarationSyntax GenerateEntityProperty(HassState entity, string className)
{
var entityName = EntityIdHelper.GetEntity(entity.EntityId);

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

var name = entity.AttributesAs<attributes>()?.friendly_name;
return ParseProperty(propertyCode).ToPublic().WithSummaryComment(name);
}

record attributes(string friendly_name);

private static bool IsNumeric(HassState entity)
{
var domain = EntityIdHelper.GetDomain(entity.EntityId);
if (EntityIdHelper.NumericDomains.Contains(domain)) return true;

// Mixed domains have both numeric and non-numeric entities, if it has a 'unit_of_measurement' we threat it as numeric
return EntityIdHelper.MixedDomains.Contains(domain) && entity.Attributes?.ContainsKey("unit_of_measurement") == true;
}

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

var baseType = entitySet.IsNumeric ? typeof(NumericEntity) : typeof(Entity);
var entityStateType = entitySet.IsNumeric ? typeof(NumericEntityState) : typeof(EntityState);

var baseClass = $"{SimplifyTypeName(baseType)}<{entitySet.EntityClassName}, {SimplifyTypeName(entityStateType)}<{attributesGeneric}>, {attributesGeneric}>";

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

return ParseRecord(classDeclaration).ToPublic();
}

/// <summary>
/// Returns a list of domains from all entities
/// </summary>
/// <param name="entities">A list of entities</param>
private static IEnumerable<string> GetDomainsFromEntities(IEnumerable<string> entities) => entities.Select(EntityIdHelper.GetDomain).Distinct();
}
19 changes: 6 additions & 13 deletions src/HassModel/NetDaemon.HassModel.CodeGenerator/EntitySet.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using JoySoftware.HomeAssistant.Model;
using NetDaemon.HassModel.CodeGenerator.Extensions;
using NetDaemon.HassModel.CodeGenerator.Helpers;
namespace NetDaemon.HassModel.CodeGenerator;

namespace NetDaemon.HassModel.CodeGenerator
internal record EntitySet(string Domain, bool IsNumeric, IEnumerable<HassState> EntityStates)
{
internal record EntitySet(string Domain, bool IsNumeric, IEnumerable<HassState> EntityStates)
{
private readonly string prefixedDomain = (IsNumeric && EntityIdHelper.MixedDomains.Contains(Domain) ? "numeric_" : "") + Domain;
private readonly string prefixedDomain = (IsNumeric && EntityIdHelper.MixedDomains.Contains(Domain) ? "numeric_" : "") + Domain;

public string EntityClassName => NamingHelper.GetDomainEntityTypeName(prefixedDomain);
public string EntityClassName => NamingHelper.GetDomainEntityTypeName(prefixedDomain);

public string AttributesClassName => $"{prefixedDomain}Attributes".ToNormalizedPascalCase();
public string AttributesClassName => $"{prefixedDomain}Attributes".ToNormalizedPascalCase();

public string EntitiesForDomainClassName => $"{Domain}Entities".ToNormalizedPascalCase();
}
public string EntitiesForDomainClassName => $"{Domain}Entities".ToNormalizedPascalCase();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace NetDaemon.HassModel.CodeGenerator;

public static class ExtensionMethodsGenerator
{
public static IEnumerable<MemberDeclarationSyntax> Generate(IEnumerable<HassServiceDomain> serviceDomains, IReadOnlyCollection<HassState> entities)
{
var entityDomains = entities.GroupBy(e => EntityIdHelper.GetDomain(e.EntityId)).Select(x => x.Key);

return serviceDomains
.Where(sd =>
sd.Services?.Any() == true
&& sd.Services.Any(s => entityDomains.Contains(s.Target?.Entity?.Domain)))
.GroupBy(x => x.Domain, x => x.Services)
.Select(GenarteClass);
}

private static ClassDeclarationSyntax GenarteClass(IGrouping<string?, IReadOnlyCollection<HassService>?> domainServicesGroup)
{
var domain = domainServicesGroup.Key!;

var domainServices = domainServicesGroup
.SelectMany(services => services!)
.Where(s => s.Target?.Entity?.Domain != null)
.Select(group => @group)
.OrderBy(x => x.Service)
.ToList();

return GenerateDomainExtensionClass(domain, domainServices);
}

private static ClassDeclarationSyntax GenerateDomainExtensionClass(string domain, IEnumerable<HassService> services)
{
var serviceTypeDeclaration = Class(GetEntityDomainExtensionMethodClassName(domain)).ToPublic().ToStatic();

var serviceMethodDeclarations = services
.SelectMany(service => GenerateExtensionMethod(domain, service))
.ToArray();

return serviceTypeDeclaration.AddMembers(serviceMethodDeclarations);
}

private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethod(string domain, HassService service)
{
var serviceName = service.Service!;

var serviceArguments = ServiceArguments.Create(domain, service);

var entityTypeName = GetDomainEntityTypeName(service.Target?.Entity?.Domain!);

yield return ParseMethod(
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} entity {(serviceArguments is not null ? $", {serviceArguments.GetParametersString()}" : string.Empty)})
{{
entity.CallService(""{serviceName}""{(serviceArguments is not null ? $", {serviceArguments.GetParametersVariable()}" : string.Empty)});
}}").ToPublic().ToStatic()
.WithSummaryComment(service.Description);

if (serviceArguments is not null)
{
yield return ParseMethod(
$@"void {GetServiceMethodName(serviceName)}(this {entityTypeName} entity, {serviceArguments.GetParametersDecomposedString()})
{{
entity.CallService(""{serviceName}"", {serviceArguments.GetParametersDecomposedVariable()});
}}").ToPublic().ToStatic()
.WithSummaryComment(service.Description)
.WithParameterComment("entity", $"The {entityTypeName} to call this service for")
.WithParameterComments(serviceArguments);
}
}
}

This file was deleted.

Loading

0 comments on commit a157210

Please sign in to comment.