Skip to content

Commit

Permalink
Support events.
Browse files Browse the repository at this point in the history
  • Loading branch information
ejball committed Jul 4, 2024
1 parent 56354b3 commit 0b5a335
Show file tree
Hide file tree
Showing 32 changed files with 1,626 additions and 121 deletions.
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<PackageVersion Include="ArgsReading" Version="2.3.2" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="Facility.AspNetCore" Version="3.8.0" />
<PackageVersion Include="Facility.Definition" Version="2.13.0" />
<PackageVersion Include="Facility.CodeGen.Console" Version="2.13.0" />
<PackageVersion Include="Facility.Definition" Version="2.14.0" />
<PackageVersion Include="Facility.CodeGen.Console" Version="2.14.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="MessagePack" Version="2.5.140" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
Expand Down
9 changes: 9 additions & 0 deletions conformance/ConformanceApi.fsd
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,15 @@ service ConformanceApi
[http(from: body, type: "application/x-output")] content: bytes;
}

[http(method: GET)]
event fibonacci
{
count: int32!;
}:
{
value: int32!;
}

data Any
{
string: string;
Expand Down
107 changes: 83 additions & 24 deletions src/Facility.CodeGen.CSharp/CSharpGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Globalization;
using Facility.Definition;
using Facility.Definition.CodeGen;
using Facility.Definition.Fsd;
using Facility.Definition.Http;

namespace Facility.CodeGen.CSharp;
Expand All @@ -13,8 +14,18 @@ public sealed class CSharpGenerator : CodeGenerator
/// <summary>
/// Generates C#.
/// </summary>
/// <param name="parser">The parser.</param>
/// <param name="settings">The settings.</param>
/// <returns>The number of updated files.</returns>
public static int GenerateCSharp(ServiceParser parser, CSharpGeneratorSettings settings) =>
FileGenerator.GenerateFiles(parser, new CSharpGenerator { GeneratorName = nameof(CSharpGenerator) }, settings);

/// <summary>
/// Generates C#.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns>The number of updated files.</returns>
[Obsolete("Use the overload that takes a parser.")]
public static int GenerateCSharp(CSharpGeneratorSettings settings) =>
FileGenerator.GenerateFiles(new CSharpGenerator { GeneratorName = nameof(CSharpGenerator) }, settings);

Expand Down Expand Up @@ -74,11 +85,11 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service)
foreach (var dtoInfo in service.Dtos)
outputFiles.Add(GenerateDto(dtoInfo, context));

if (service.Methods.Count != 0)
if (service.AllMethods.Count != 0)
{
outputFiles.Add(GenerateInterface(service, context));

foreach (var methodInfo in service.Methods)
foreach (var methodInfo in service.AllMethods)
outputFiles.AddRange(GenerateMethodDtos(methodInfo, context));

outputFiles.Add(GenerateMethodInfos(service, context));
Expand All @@ -90,7 +101,7 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service)
foreach (var httpErrorSetInfo in httpServiceInfo.ErrorSets)
outputFiles.Add(GenerateHttpErrors(httpErrorSetInfo, context));

if (httpServiceInfo.Methods.Count != 0)
if (httpServiceInfo.AllMethods.Count != 0)
{
outputFiles.Add(GenerateHttpMapping(httpServiceInfo, context));
outputFiles.Add(GenerateHttpClient(httpServiceInfo, context));
Expand Down Expand Up @@ -716,7 +727,7 @@ private CodeGenFile GenerateHttpMapping(HttpServiceInfo httpServiceInfo, Context
code.WriteLine($"public static partial class {httpMappingName}");
using (code.Block())
{
foreach (var httpMethodInfo in httpServiceInfo.Methods)
foreach (var httpMethodInfo in httpServiceInfo.AllMethods)
{
var methodInfo = httpMethodInfo.ServiceMethod;
var csharpInfo = context.CSharpServiceInfo;
Expand Down Expand Up @@ -1092,6 +1103,8 @@ private CodeGenFile GenerateHttpClient(HttpServiceInfo httpServiceInfo, Context
"Facility.Core",
"Facility.Core.Http",
};
if (serviceInfo.Events.Count != 0)
usings.Add("System.Collections.Generic");
CSharpUtility.WriteUsings(code, usings, namespaceName);

code.WriteLine($"namespace {namespaceName}");
Expand All @@ -1110,19 +1123,22 @@ private CodeGenFile GenerateHttpClient(HttpServiceInfo httpServiceInfo, Context
code.WriteLine(": base(settings, s_defaults)");
code.Block().Dispose();

foreach (var httpMethodInfo in httpServiceInfo.Methods)
foreach (var httpMethodInfo in httpServiceInfo.AllMethods)
{
var methodInfo = httpMethodInfo.ServiceMethod;
var methodName = csharpInfo.GetMethodName(methodInfo);
var requestTypeName = csharpInfo.GetRequestDtoName(methodInfo);
var responseTypeName = csharpInfo.GetResponseDtoName(methodInfo);
var isEvent = methodInfo.Kind == ServiceMethodKind.Event;
if (isEvent)
responseTypeName = $"IAsyncEnumerable<ServiceResult<{responseTypeName}>>";

code.WriteLine();
CSharpUtility.WriteSummary(code, methodInfo.Summary);
CSharpUtility.WriteObsoleteAttribute(code, methodInfo);
code.WriteLine($"public Task<ServiceResult<{responseTypeName}>> {methodName}Async({requestTypeName} request, CancellationToken cancellationToken = default) =>");
using (code.Indent())
code.WriteLine($"TrySendRequestAsync({httpMappingName}.{methodName}Mapping, request, cancellationToken);");
code.WriteLine($"TrySend{(isEvent ? "Event" : "")}RequestAsync({httpMappingName}.{methodName}Mapping, request, cancellationToken);");
}

code.WriteLine();
Expand Down Expand Up @@ -1167,7 +1183,7 @@ private CodeGenFile GenerateHttpHandler(HttpServiceInfo httpServiceInfo, Context
};
CSharpUtility.WriteUsings(code, usings, namespaceName);

if (serviceInfo.Methods.Any(x => x.IsObsolete))
if (serviceInfo.AllMethods.Any(x => x.IsObsolete))
{
CSharpUtility.WriteObsoletePragma(code);
code.WriteLine();
Expand Down Expand Up @@ -1206,7 +1222,7 @@ private CodeGenFile GenerateHttpHandler(HttpServiceInfo httpServiceInfo, Context
// check 'widgets/get' before 'widgets/{id}'
IDisposable? indent = null;
code.Write("return ");
foreach (var httpServiceMethod in httpServiceInfo.Methods.OrderBy(x => x, HttpMethodInfo.ByRouteComparer))
foreach (var httpServiceMethod in httpServiceInfo.AllMethods.OrderBy(x => x, HttpMethodInfo.ByRouteComparer))
{
if (indent != null)
code.WriteLine(" ??");
Expand All @@ -1218,17 +1234,18 @@ private CodeGenFile GenerateHttpHandler(HttpServiceInfo httpServiceInfo, Context
indent?.Dispose();
}

foreach (var httpMethodInfo in httpServiceInfo.Methods)
foreach (var httpMethodInfo in httpServiceInfo.AllMethods)
{
var methodInfo = httpMethodInfo.ServiceMethod;
var methodName = csharpInfo.GetMethodName(methodInfo);
var isEvent = methodInfo.Kind == ServiceMethodKind.Event;

code.WriteLine();
CSharpUtility.WriteSummary(code, methodInfo.Summary);
CSharpUtility.WriteObsoleteAttribute(code, methodInfo);
code.WriteLine($"public Task<HttpResponseMessage{NullableReferenceSuffix}> TryHandle{methodName}Async(HttpRequestMessage httpRequest, CancellationToken cancellationToken = default) =>");
using (code.Indent())
code.WriteLine($"TryHandleServiceMethodAsync({httpMappingName}.{methodName}Mapping, httpRequest, GetService(httpRequest).{methodName}Async, cancellationToken);");
code.WriteLine($"TryHandleService{(isEvent ? "Event" : "Method")}Async({httpMappingName}.{methodName}Mapping, httpRequest, GetService(httpRequest).{methodName}Async, cancellationToken);");
}

if (httpServiceInfo.ErrorSets.Count != 0)
Expand Down Expand Up @@ -1387,6 +1404,8 @@ private CodeGenFile GenerateInterface(ServiceInfo serviceInfo, Context context)
"System.Threading.Tasks",
"Facility.Core",
};
if (serviceInfo.Events.Count != 0)
usings.Add("System.Collections.Generic");
CSharpUtility.WriteUsings(code, usings, context.NamespaceName);

code.WriteLine($"namespace {context.NamespaceName}");
Expand All @@ -1399,12 +1418,17 @@ private CodeGenFile GenerateInterface(ServiceInfo serviceInfo, Context context)
code.WriteLine($"public partial interface {interfaceName}");
using (code.Block())
{
foreach (var methodInfo in serviceInfo.Methods)
foreach (var methodInfo in serviceInfo.AllMethods)
{
var responseTypeName = csharpInfo.GetResponseDtoName(methodInfo);
var isEvent = methodInfo.Kind == ServiceMethodKind.Event;
if (isEvent)
responseTypeName = $"IAsyncEnumerable<ServiceResult<{responseTypeName}>>";

code.WriteLineSkipOnce();
CSharpUtility.WriteSummary(code, methodInfo.Summary);
CSharpUtility.WriteObsoleteAttribute(code, methodInfo);
code.WriteLine($"Task<ServiceResult<{csharpInfo.GetResponseDtoName(methodInfo)}>> {csharpInfo.GetMethodName(methodInfo)}Async(" +
code.WriteLine($"Task<ServiceResult<{responseTypeName}>> {csharpInfo.GetMethodName(methodInfo)}Async(" +
$"{csharpInfo.GetRequestDtoName(methodInfo)} request, CancellationToken cancellationToken = default);");
}
}
Expand Down Expand Up @@ -1438,14 +1462,15 @@ private CodeGenFile GenerateMethodInfos(ServiceInfo serviceInfo, Context context
code.WriteLine($"internal static class {className}");
using (code.Block())
{
foreach (var methodInfo in serviceInfo.Methods)
foreach (var methodInfo in serviceInfo.AllMethods)
{
var isEvent = methodInfo.Kind == ServiceMethodKind.Event;
code.WriteLineSkipOnce();
CSharpUtility.WriteObsoleteAttribute(code, methodInfo);
code.WriteLine($"public static readonly IServiceMethodInfo {csharpInfo.GetMethodName(methodInfo)} =");
code.WriteLine($"public static readonly IService{(isEvent ? "Event" : "Method")}Info {csharpInfo.GetMethodName(methodInfo)} =");
using (code.Indent())
{
code.WriteLine($"ServiceMethodInfo.Create<{interfaceName}, {csharpInfo.GetRequestDtoName(methodInfo)}, {csharpInfo.GetResponseDtoName(methodInfo)}>(");
code.WriteLine($"Service{(isEvent ? "Event" : "Method")}Info.Create<{interfaceName}, {csharpInfo.GetRequestDtoName(methodInfo)}, {csharpInfo.GetResponseDtoName(methodInfo)}>(");
using (code.Indent())
code.WriteLine($"{CSharpUtility.CreateString(methodInfo.Name)}, {CSharpUtility.CreateString(serviceInfo.Name)}, x => x.{csharpInfo.GetMethodName(methodInfo)}Async);");
}
Expand Down Expand Up @@ -1473,6 +1498,11 @@ private CodeGenFile GenerateDelegatingService(ServiceInfo serviceInfo, Context c
"System.Threading.Tasks",
"Facility.Core",
};
if (serviceInfo.Events.Count != 0)
{
usings.Add("System.Collections.Generic");
usings.Add("System.Runtime.CompilerServices");
}
CSharpUtility.WriteUsings(code, usings, context.NamespaceName);

code.WriteLine($"namespace {context.NamespaceName}");
Expand All @@ -1485,23 +1515,52 @@ private CodeGenFile GenerateDelegatingService(ServiceInfo serviceInfo, Context c
code.WriteLine($"public partial class {className} : {interfaceName}");
using (code.Block())
{
CSharpUtility.WriteSummary(code, "Creates an instance with the specified service delegate.");
code.WriteLine($"public {className}(ServiceDelegate serviceDelegate) =>");
using (code.Indent())
code.WriteLine("m_serviceDelegate = serviceDelegate ?? throw new ArgumentNullException(nameof(serviceDelegate));");

code.WriteLine();
CSharpUtility.WriteSummary(code, "Creates an instance with the specified delegator.");
code.WriteLine("""[Obsolete("Use the constructor that accepts a ServiceDelegate.")]""");
code.WriteLine($"public {className}(ServiceDelegator delegator) =>");
using (code.Indent())
code.WriteLine("m_delegator = delegator ?? throw new ArgumentNullException(nameof(delegator));");
code.WriteLine("m_serviceDelegate = ServiceDelegate.FromDelegator(delegator);");

foreach (var methodInfo in serviceInfo.Methods)
foreach (var methodInfo in serviceInfo.AllMethods)
{
code.WriteLine();
CSharpUtility.WriteSummary(code, methodInfo.Summary);
CSharpUtility.WriteObsoleteAttribute(code, methodInfo);
code.WriteLine($"public virtual async Task<ServiceResult<{csharpInfo.GetResponseDtoName(methodInfo)}>> {csharpInfo.GetMethodName(methodInfo)}Async({csharpInfo.GetRequestDtoName(methodInfo)} request, CancellationToken cancellationToken = default) =>");
using (code.Indent())
code.WriteLine($"(await m_delegator({methodsClassName}.{csharpInfo.GetMethodName(methodInfo)}, request, cancellationToken).ConfigureAwait(false)).Cast<{csharpInfo.GetResponseDtoName(methodInfo)}>();");

if (methodInfo.Kind == ServiceMethodKind.Event)
{
code.WriteLine($"public virtual async Task<ServiceResult<IAsyncEnumerable<ServiceResult<{csharpInfo.GetResponseDtoName(methodInfo)}>>>> {csharpInfo.GetMethodName(methodInfo)}Async({csharpInfo.GetRequestDtoName(methodInfo)} request, CancellationToken cancellationToken = default)");
using (code.Block())
{
code.WriteLine($"var result = await m_serviceDelegate.InvokeEventAsync({methodsClassName}.{csharpInfo.GetMethodName(methodInfo)}, request, cancellationToken).ConfigureAwait(false);");
code.WriteLine("return result.IsFailure ? result.ToFailure() : ServiceResult.Success(Enumerate(result.Value, cancellationToken));");

code.WriteLine();
code.WriteLine($"static async IAsyncEnumerable<ServiceResult<{csharpInfo.GetResponseDtoName(methodInfo)}>> Enumerate(IAsyncEnumerable<ServiceResult<ServiceDto>> enumerable, [EnumeratorCancellation] CancellationToken cancellationToken)");
using (code.Block())
{
code.WriteLine("await foreach (var result in enumerable.WithCancellation(cancellationToken))");
using (code.Indent())
code.WriteLine($"yield return result.Cast<{csharpInfo.GetResponseDtoName(methodInfo)}>();");
}
}
}
else
{
code.WriteLine($"public virtual async Task<ServiceResult<{csharpInfo.GetResponseDtoName(methodInfo)}>> {csharpInfo.GetMethodName(methodInfo)}Async({csharpInfo.GetRequestDtoName(methodInfo)} request, CancellationToken cancellationToken = default) =>");
using (code.Indent())
code.WriteLine($"(await m_serviceDelegate.InvokeMethodAsync({methodsClassName}.{csharpInfo.GetMethodName(methodInfo)}, request, cancellationToken).ConfigureAwait(false)).Cast<{csharpInfo.GetResponseDtoName(methodInfo)}>();");
}
}

code.WriteLine();
code.WriteLine("private readonly ServiceDelegator m_delegator;");
code.WriteLine("private readonly ServiceDelegate m_serviceDelegate;");
}
}
});
Expand Down Expand Up @@ -1539,7 +1598,7 @@ private CodeGenFile GenerateJsonSerializerContext(ServiceInfo serviceInfo, HttpS
"ServiceObject",
};

foreach (var methodInfo in serviceInfo.Methods)
foreach (var methodInfo in serviceInfo.AllMethods)
{
serializables.Add(csharpInfo.GetRequestDtoName(methodInfo));
serializables.Add(csharpInfo.GetResponseDtoName(methodInfo));
Expand All @@ -1548,8 +1607,8 @@ private CodeGenFile GenerateJsonSerializerContext(ServiceInfo serviceInfo, HttpS
foreach (var dtoInfo in serviceInfo.Dtos)
serializables.Add(csharpInfo.GetDtoName(dtoInfo));

foreach (var bodyFieldType in httpServiceInfo.Methods.Select(x => x.RequestBodyField)
.Concat(httpServiceInfo.Methods.SelectMany(x => x.ValidResponses).Select(x => x.BodyField))
foreach (var bodyFieldType in httpServiceInfo.AllMethods.Select(x => x.RequestBodyField)
.Concat(httpServiceInfo.AllMethods.SelectMany(x => x.ValidResponses).Select(x => x.BodyField))
.Where(x => x != null)
.Select(x => context.GetFieldType(x!.ServiceField))
.Where(x => x.Kind == ServiceTypeKind.Array))
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/Facility.ConformanceApi/ConformanceApiMethods.g.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 0b5a335

Please sign in to comment.