-
-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Codegen uses short typenames with using in stead of fully quatified n…
…ames (#571) * refactor codegen * Clean usings * Simplify Generated type names * Clean up
- Loading branch information
1 parent
84cb828
commit a157210
Showing
23 changed files
with
812 additions
and
924 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 5 additions & 6 deletions
11
src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGenerationSettings.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
193
src/HassModel/NetDaemon.HassModel.CodeGenerator/EntitiesGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
19
src/HassModel/NetDaemon.HassModel.CodeGenerator/EntitySet.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
69 changes: 69 additions & 0 deletions
69
src/HassModel/NetDaemon.HassModel.CodeGenerator/ExtensionMethodsGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
38 changes: 0 additions & 38 deletions
38
src/HassModel/NetDaemon.HassModel.CodeGenerator/Extensions/CollectionExtensions.cs
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.