Skip to content

Commit

Permalink
Support service calls with return values in codegen (#1111)
Browse files Browse the repository at this point in the history
* Support service calls with return values in codegen

* removed the enumerable extension that is not needed

* Added additional tests and fixed a bug

* Small refactor as requested on review

* Refactor and clean up big function

* Fix comment from review

* Add test case

* Added tests
  • Loading branch information
helto4real authored Jun 8, 2024
1 parent 7f6be19 commit 0869be2
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static IEnumerable<MemberDeclarationSyntax> Generate(IEnumerable<HassServ
private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethodsForService(string domain, HassService service, ILookup<string, string> entityClassNameByDomain)
{
// There can be multiple Target Domains, so generate methods for each
var targetEntityDomains = service.Target?.Entity.SelectMany(e => e.Domain) ?? Array.Empty<string>();
var targetEntityDomains = service.Target?.Entity.SelectMany(e => e.Domain) ?? [];

return targetEntityDomains.SelectMany(targetEntityDomain => GenerateExtensionMethodsForService(domain, service, targetEntityDomain, entityClassNameByDomain));
}
Expand All @@ -69,51 +69,105 @@ private static IEnumerable<MemberDeclarationSyntax> GenerateExtensionMethodsForS

if (serviceArguments is null)
{
yield return ExtensionMethodWithoutArguments(service, serviceName, entityTypeName);
yield return ExtensionMethodWithoutArguments(service, serviceName, enumerableTargetTypeName);
if (service.Response is not null)
{
yield return ExtensionMethodWithoutArguments(service, serviceName, entityTypeName, true);
// No support for IEnumerable<Entity> with async methods for now may be added later
}
yield return ExtensionMethodWithoutArguments(service, serviceName, entityTypeName, false);
yield return ExtensionMethodWithoutArguments(service, serviceName, enumerableTargetTypeName, false);
}
else
{
yield return ExtensionMethodWithClassArgument(service, serviceName, entityTypeName, serviceArguments);
yield return ExtensionMethodWithClassArgument(service, serviceName, enumerableTargetTypeName, serviceArguments);

yield return ExtensionMethodWithSeparateArguments(service, serviceName, entityTypeName, serviceArguments);
yield return ExtensionMethodWithSeparateArguments(service, serviceName, enumerableTargetTypeName, serviceArguments);
if (service.Response is not null)
{
yield return ExtensionMethodWithClassArgument(service, serviceName, entityTypeName, serviceArguments, true);
yield return ExtensionMethodWithSeparateArguments(service, serviceName, entityTypeName, serviceArguments, true);
// No support for IEnumerable<Entity> with async methods now may be added later
}
yield return ExtensionMethodWithClassArgument(service, serviceName, entityTypeName, serviceArguments, false);
yield return ExtensionMethodWithClassArgument(service, serviceName, enumerableTargetTypeName, serviceArguments, false);

yield return ExtensionMethodWithSeparateArguments(service, serviceName, entityTypeName, serviceArguments, false);
yield return ExtensionMethodWithSeparateArguments(service, serviceName, enumerableTargetTypeName, serviceArguments, false);
}
}

private static MemberDeclarationSyntax ExtensionMethodWithoutArguments(HassService service, string serviceName, string entityTypeName)
private static MemberDeclarationSyntax ExtensionMethodWithoutArguments(HassService service, string serviceName, string entityTypeName, bool generateAsync)
{
return ParseMemberDeclaration($$"""
if (generateAsync)
{
return ParseMemberDeclaration($$"""
public static Task<JsonElement?> {{GetServiceMethodName(serviceName)}}Async(this {{entityTypeName}} target, object? data = null)
{
return target.CallServiceWithResponseAsync("{{serviceName}}", data);
}
""")!
.WithSummaryComment(service.Description);
}
else
{
return ParseMemberDeclaration($$"""
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target, object? data = null)
{
target.CallService("{{serviceName}}", data);
}
""")!
.WithSummaryComment(service.Description);
.WithSummaryComment(service.Description);
}
}

private static MemberDeclarationSyntax ExtensionMethodWithClassArgument(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
private static MemberDeclarationSyntax ExtensionMethodWithClassArgument(HassService service, string serviceName, string entityTypeName,
ServiceArguments serviceArguments, bool isAsync)
{
return ParseMemberDeclaration($$"""
if (isAsync)
{
return ParseMemberDeclaration($$"""
public static Task<JsonElement?> {{GetServiceMethodName(serviceName)}}Async(this {{entityTypeName}} target, {{serviceArguments.TypeName}} data)
{
return target.CallServiceWithResponseAsync("{{serviceName}}", data);
}
""")!
.WithSummaryComment(service.Description);
}
else
{
return ParseMemberDeclaration($$"""
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target, {{serviceArguments.TypeName}} data)
{
target.CallService("{{serviceName}}", data);
}
""")!
.WithSummaryComment(service.Description);
.WithSummaryComment(service.Description);
}
}

private static MemberDeclarationSyntax ExtensionMethodWithSeparateArguments(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments)
static MemberDeclarationSyntax ExtensionMethodWithSeparateArguments(HassService service, string serviceName, string entityTypeName,
ServiceArguments serviceArguments, bool isAsync)
{
return ParseMemberDeclaration($$"""
if (isAsync)
{
return ParseMemberDeclaration($$"""
public static Task<JsonElement?> {{GetServiceMethodName(serviceName)}}Async(this {{entityTypeName}} target, {{serviceArguments.GetParametersList()}})
{
return target.CallServiceWithResponseAsync("{{serviceName}}", {{serviceArguments.GetNewServiceArgumentsTypeExpression()}});
}
""")!
.WithSummaryComment(service.Description)
.AppendParameterComment("target", $"The {entityTypeName} to call this service for")
.AppendParameterComments(serviceArguments);
}
else
{
return ParseMemberDeclaration($$"""
public static void {{GetServiceMethodName(serviceName)}}(this {{entityTypeName}} target, {{serviceArguments.GetParametersList()}})
{
target.CallService("{{serviceName}}", {{serviceArguments.GetNewServiceArgumentsTypeExpression()}});
}
""")!
.WithSummaryComment(service.Description)
.AppendParameterComment("target", $"The {entityTypeName} to call this service for")
.AppendParameterComments(serviceArguments);
.WithSummaryComment(service.Description)
.AppendParameterComment("target", $"The {entityTypeName} to call this service for")
.AppendParameterComments(serviceArguments);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,36 +98,98 @@ private static IEnumerable<MemberDeclarationSyntax> GenerateServiceMethod(string
if (serviceArguments is null)
{
// method without arguments
yield return ParseMemberDeclaration($$"""
foreach (var method in GenerateServiceMethodWithoutArguments(serviceMethodName, targetParam, targetComment, targetArg, domain, serviceName, haContextVariableName, service))
{
yield return method;
}
}
else
{
// method using arguments object
foreach (var method in GenerateServiceMethodWithArguments(serviceMethodName, serviceArguments, targetParam, targetComment, targetArg, domain, serviceName, haContextVariableName, service))
{
yield return method;
}
}
}

private static IEnumerable<MemberDeclarationSyntax> GenerateServiceMethodWithoutArguments(
string serviceMethodName, string? targetParam,
SyntaxTrivia? targetComment, string targetArg, string domain, string serviceName,
string haContextVariableName, HassService service)
{
yield return ParseMemberDeclaration($$"""
void {{serviceMethodName}}({{CommaSeparateNonEmpty(targetParam, "object? data = null")}})
{
{{haContextVariableName}}.CallService("{{domain}}", "{{serviceName}}", {{CommaSeparateNonEmpty(targetArg, "data")}});
}
""")!
.ToPublic()
.WithSummaryComment(service.Description)
.AppendTrivia(targetComment);

if (service.Response is not null)
{
yield return ParseMemberDeclaration($$"""
Task<JsonElement?> {{serviceMethodName}}Async({{CommaSeparateNonEmpty(targetParam, "object? data = null")}})
{
return {{haContextVariableName}}.CallServiceWithResponseAsync("{{domain}}", "{{serviceName}}", {{CommaSeparateNonEmpty(targetArg, "data")}});
}
""")!
.ToPublic()
.WithSummaryComment(service.Description)
.AppendTrivia(targetComment);
}
else
{
// method using arguments object
yield return ParseMemberDeclaration($$"""
}

private static IEnumerable<MemberDeclarationSyntax> GenerateServiceMethodWithArguments(
string serviceMethodName, ServiceArguments serviceArguments, string? targetParam,
SyntaxTrivia? targetComment, string targetArg, string domain, string serviceName,
string haContextVariableName, HassService service)
{
// method using arguments object
yield return ParseMemberDeclaration($$"""
void {{serviceMethodName}}({{CommaSeparateNonEmpty(targetParam, serviceArguments.TypeName)}} data)
{
{{haContextVariableName}}.CallService("{{domain}}", "{{serviceName}}", {{targetArg}}, data);
}
""")!
.ToPublic()
.WithSummaryComment(service.Description)
.AppendTrivia(targetComment);
.ToPublic()
.WithSummaryComment(service.Description)
.AppendTrivia(targetComment);

// method using arguments as separate parameters
yield return ParseMemberDeclaration($$"""
// method using arguments as separate parameters
yield return ParseMemberDeclaration($$"""
void {{serviceMethodName}}({{CommaSeparateNonEmpty(targetParam, serviceArguments.GetParametersList())}})
{
{{haContextVariableName}}.CallService("{{domain}}", "{{serviceName}}", {{targetArg}}, {{serviceArguments.GetNewServiceArgumentsTypeExpression()}});
}
""")!
.ToPublic()
.WithSummaryComment(service.Description)

.AppendParameterComments(serviceArguments);

if (service.Response is not null)
{
// method using arguments object
yield return ParseMemberDeclaration($$"""
Task<JsonElement?> {{serviceMethodName}}Async({{CommaSeparateNonEmpty(targetParam, serviceArguments.TypeName)}} data)
{
return {{haContextVariableName}}.CallServiceWithResponseAsync("{{domain}}", "{{serviceName}}", {{targetArg}}, data);
}
""")!
.ToPublic()
.WithSummaryComment(service.Description)
.AppendTrivia(targetComment);

// method using arguments as separate parameters
yield return ParseMemberDeclaration($$"""
Task<JsonElement?> {{serviceMethodName}}Async({{CommaSeparateNonEmpty(targetParam, serviceArguments.GetParametersList())}})
{
return {{haContextVariableName}}.CallServiceWithResponseAsync("{{domain}}", "{{serviceName}}", {{targetArg}}, {{serviceArguments.GetNewServiceArgumentsTypeExpression()}});
}
""")!
.ToPublic()
.WithSummaryComment(service.Description)
.AppendTrivia(targetComment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ public static string SimplifyTypeName(Type type)
"System",
"System.Linq",
"System.Collections.Generic",
"System.Threading.Tasks",
"Microsoft.Extensions.DependencyInjection",
typeof(System.Text.Json.JsonElement).Namespace!,
typeof(JsonPropertyNameAttribute).Namespace!,
typeof(IHaContext).Namespace!,
typeof(Entity).Namespace!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ internal record HassService
[JsonIgnore]
public IReadOnlyCollection<HassServiceField>? Fields { get; init; }
public TargetSelector? Target { get; init; }
}
public Response? Response { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal record HassServiceField
[JsonConverter(typeof(NullableBoolJsonConverter))]
public bool? Required { get; init; }
public object? Example { get; init; }

[JsonConverter(typeof(SelectorConverter))]
public Selector? Selector { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

using System.Text.Json.Serialization;

namespace NetDaemon.HassModel.CodeGenerator.Model;

internal record Response()
{
public bool Optional { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,44 @@ public void TestSomeBasicServicesCanBeParsed()
res.First().Services.ElementAt(1).Target!.Entity.SelectMany(e=>e.Domain).Should().BeEmpty();
}

[Fact]
public void TestServicesWithReturnValueCanBeParsed()
{
var sample = """
{
"weather": {
"foo_with_return_value": {
"name": "Foo",
"description": "Foo with return value",
"fields": {},
"target": {
"entity": {}
},
"response": {
"optional": false
}
},
"foo_with_optional_return_value": {
"name": "Foo optional",
"description": "Foo with optional return value",
"fields": {},
"target": {
"entity": {}
},
"response": {
"optional": true
}
}
}
}
""";
var res = Parse(sample);
res.Should().HaveCount(1);
res.First().Domain.Should().Be("weather");
res.First().Services.ElementAt(0).Response!.Optional.Should().BeFalse();
res.First().Services.ElementAt(1).Response!.Optional.Should().BeTrue();
}

[Fact]
public void TestMultiDomainTarget()
{
Expand Down
Loading

0 comments on commit 0869be2

Please sign in to comment.