Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support events. #94

Merged
merged 2 commits into from
Jul 4, 2024
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
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>

<PropertyGroup>
<VersionPrefix>2.28.2</VersionPrefix>
<PackageValidationBaselineVersion>2.28.0</PackageValidationBaselineVersion>
<VersionPrefix>2.29.0</VersionPrefix>
<PackageValidationBaselineVersion>2.28.2</PackageValidationBaselineVersion>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
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
4 changes: 4 additions & 0 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Notes

## 2.29.0

* Support events. (Must opt-in via `FsdParserSettings.SupportsEvents`.)

## 2.28.2

* Don't use `ToArray` unless needed for JSON source generation.
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
Loading