diff --git a/Directory.Build.props b/Directory.Build.props index 5fa02c8..86d4457 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ JasperFx.Events, JasperFx.Events.SourceGenerator) set $(JasperFxVersion) so they always release together. Bump this one value to release the whole set. --> - 2.2.6 + 2.2.7 13 1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618 Jeremy D. Miller;Jaedyn Tonee diff --git a/src/CodegenTests.FSharp/FSharpCodegenSample.cs b/src/CodegenTests.FSharp/FSharpCodegenSample.cs index 6c107dd..730962f 100644 --- a/src/CodegenTests.FSharp/FSharpCodegenSample.cs +++ b/src/CodegenTests.FSharp/FSharpCodegenSample.cs @@ -62,10 +62,125 @@ public static GeneratedAssembly BuildSampleAssembly() AddCalculatorType(assembly); AddCastType(assembly); AddScopedConsumerType(assembly); + AddActivityEmitterType(assembly); + AddNowHandlerType(assembly); + AddValueTaskHandlerType(assembly); + AddMemberAccessType(assembly); + AddArrayHandlerType(assembly); + AddLazyHandlerType(assembly); return assembly; } + /// + /// Appends an activity event — exercises . F# has no + /// null-conditional operator, so the body emits an explicit if not (isNull Activity.Current) + /// guard and pipes the chained AddEvent result to ignore. + /// + private static void AddActivityEmitterType(GeneratedAssembly assembly) + { + var type = assembly.AddType("GeneratedActivityEmitter", typeof(IActivityEmitter)); + var method = type.MethodFor(nameof(IActivityEmitter.Emit)); + method.Frames.Add(new AppendActivityEventFrame("sample.activity.event")); + } + + /// + /// Resolves the current time through and passes it to a + /// service — exercises (let now = System.DateTime.UtcNow). + /// + private static void AddNowHandlerType(GeneratedAssembly assembly) + { + var type = assembly.AddType("GeneratedNowHandler", typeof(INowHandler)); + var method = type.MethodFor(nameof(INowHandler.Stamp)); + method.Sources.Add(new NowTimeVariableSource()); + + var service = new InjectedField(typeof(ClockService), "clockService"); + var call = new MethodCall(typeof(ClockService), nameof(ClockService.Stamp)) + { + Target = service, + ReturnAction = ReturnAction.Return + }; + method.Frames.Add(call); + } + + /// + /// A synchronous body behind a signature — exercises + /// : the trailing F# expression is ValueTask<string>(result). + /// + private static void AddValueTaskHandlerType(GeneratedAssembly assembly) + { + var type = assembly.AddType("GeneratedValueTaskHandler", typeof(IValueTaskHandler)); + var method = type.MethodFor(nameof(IValueTaskHandler.HandleAsync)); + + var service = new InjectedField(typeof(ControlFlowService), "controlFlowService"); + var call = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Fallback)) + { + Target = service + }; + method.Frames.Add(call); + method.Frames.Add(new ReturnValueTask(typeof(string))); + } + + /// + /// Reads a member off a constructed object — exercises + /// (let value = mutableBox.Value). + /// + private static void AddMemberAccessType(GeneratedAssembly assembly) + { + var type = assembly.AddType("GeneratedMemberAccessHandler", typeof(IMemberAccessHandler)); + var method = type.MethodFor(nameof(IMemberAccessHandler.Read)); + + var ctor = new ConstructorFrame(typeof(MutableBox), typeof(MutableBox).GetConstructors().Single()); + method.Frames.Add(ctor); + + var member = typeof(MutableBox).GetProperty(nameof(MutableBox.Value))!; + var access = new MemberAccessFrame(typeof(MutableBox), member, "value"); + method.Frames.Add(access); + + method.Frames.Add(new ReturnFrame(access.Variable)); + } + + /// + /// Builds an array literal from two injected elements — exercises + /// (let things = [| thingA; thingB |]). + /// + private static void AddArrayHandlerType(GeneratedAssembly assembly) + { + var type = assembly.AddType("GeneratedArrayHandler", typeof(IArrayHandler)); + var method = type.MethodFor(nameof(IArrayHandler.Build)); + + var thingA = new InjectedField(typeof(Thing), "thingA"); + var thingB = new InjectedField(typeof(Thing), "thingB"); + var array = new CreateArrayFrame(typeof(Thing[]), typeof(Thing), new Variable[] { thingA, thingB }); + method.Frames.Add(array); + + method.Frames.Add(new ReturnFrame(array.Variable)); + } + + /// + /// Resolves a service flagged for service location off an injected + /// — exercises + /// (let controlFlowService = …GetRequiredService<ControlFlowService>(provider)). + /// + private static void AddLazyHandlerType(GeneratedAssembly assembly) + { + var type = assembly.AddType("GeneratedLazyHandler", typeof(ILazyHandler)); + var method = type.MethodFor(nameof(ILazyHandler.Handle)); + + // The injected IServiceProvider is the scope the LazyServiceLocationFrame resolves against; + // register it as a constructor field so FindVariable(IServiceProvider) resolves. + type.AllInjectedFields.Add(new InjectedField(typeof(IServiceProvider), "provider")); + var lazy = new LazyServiceLocationFrame(typeof(ControlFlowService)); + method.Frames.Add(lazy); + + var call = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Fallback)) + { + Target = lazy.Variable, + ReturnAction = ReturnAction.Return + }; + method.Frames.Add(call); + } + /// /// A handler that resolves a service-located and awaits it. /// Exercises the scoped-DI frames: use serviceScope = …CreateAsyncScope(…) + diff --git a/src/CodegenTests.FSharpFixture/Generated.fs b/src/CodegenTests.FSharpFixture/Generated.fs index ae8bb1e..94d9f08 100644 --- a/src/CodegenTests.FSharpFixture/Generated.fs +++ b/src/CodegenTests.FSharpFixture/Generated.fs @@ -125,3 +125,49 @@ type GeneratedScopedConsumer(serviceScopeFactory: Microsoft.Extensions.Dependenc do! scopedThing.DoAsync() } +type GeneratedActivityEmitter() = + interface FSharpCodegenTarget.IActivityEmitter with + member this.Emit() : unit = + if not (isNull System.Diagnostics.Activity.Current) then System.Diagnostics.Activity.Current.AddEvent(System.Diagnostics.ActivityEvent("sample.activity.event")) |> ignore + +type GeneratedNowHandler(clockService: FSharpCodegenTarget.ClockService) = + let _clockService = clockService + + interface FSharpCodegenTarget.INowHandler with + member this.Stamp() : string = + let now = System.DateTime.UtcNow + _clockService.Stamp(now) + +type GeneratedValueTaskHandler(controlFlowService: FSharpCodegenTarget.ControlFlowService) = + let _controlFlowService = controlFlowService + + interface FSharpCodegenTarget.IValueTaskHandler with + member this.HandleAsync() : System.Threading.Tasks.ValueTask = + let result_of_Fallback = _controlFlowService.Fallback() + System.Threading.Tasks.ValueTask(result_of_Fallback) + +type GeneratedMemberAccessHandler() = + interface FSharpCodegenTarget.IMemberAccessHandler with + member this.Read() : int = + let mutableBox = FSharpCodegenTarget.MutableBox() + let value = mutableBox.Value + value + +type GeneratedArrayHandler(thingA: FSharpCodegenTarget.Thing, thingB: FSharpCodegenTarget.Thing) = + let _thingA = thingA + let _thingB = thingB + + interface FSharpCodegenTarget.IArrayHandler with + member this.Build() : FSharpCodegenTarget.Thing[] = + let thingArray = [| _thingA; _thingB |] + thingArray + +type GeneratedLazyHandler(provider: System.IServiceProvider) = + let _provider = provider + + interface FSharpCodegenTarget.ILazyHandler with + member this.Handle() : string = + // This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else + let controlFlowService = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(_provider) + controlFlowService.Fallback() + diff --git a/src/FSharpCodegenTarget/Contracts.cs b/src/FSharpCodegenTarget/Contracts.cs index 45fd7f3..eba932f 100644 --- a/src/FSharpCodegenTarget/Contracts.cs +++ b/src/FSharpCodegenTarget/Contracts.cs @@ -102,6 +102,71 @@ public interface IScopedConsumerHandler Task Handle(); } +// --- Contracts for the remaining JasperFx F# frame surface (jasperfx#399) --- + +/// +/// A sync void handler used to exercise AppendActivityEventFrame: the generated body emits a +/// guarded Activity.Current.AddEvent(...) (F# has no ?., so an explicit isNull guard). +/// +public interface IActivityEmitter +{ + void Emit(); +} + +/// +/// A handler that stamps the current time. The injected resolves through +/// NowTimeVariableSourceNowFetchFrame (let now = System.DateTime.UtcNow). +/// +public interface INowHandler +{ + string Stamp(); +} + +public class ClockService +{ + public string Stamp(DateTime now) + { + return now.ToString("O"); + } +} + +/// +/// A handler whose signature returns with a synchronous body — exercises +/// ReturnValueTask: the trailing F# expression constructs a ValueTask<string>(result). +/// +public interface IValueTaskHandler +{ + ValueTask HandleAsync(); +} + +/// +/// A handler that reads a member off a constructed object — exercises MemberAccessFrame +/// (let value = mutableBox.Value). +/// +public interface IMemberAccessHandler +{ + int Read(); +} + +/// +/// A handler that builds an array of injected elements — exercises CreateArrayFrame's F# array +/// literal (let things = [| thingA; thingB |]). +/// +public interface IArrayHandler +{ + Thing[] Build(); +} + +/// +/// A handler that resolves a service flagged AlwaysUseServiceLocationFor off an injected +/// — exercises LazyServiceLocationFrame +/// (let controlFlowService = ServiceProviderServiceExtensions.GetRequiredService<…>(provider)). +/// +public interface ILazyHandler +{ + string Handle(); +} + /// /// A base class with a public instance method () and an abstract method to /// override (). The generated subclass overrides Compute and calls the diff --git a/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs b/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs index 8f06813..defba48 100644 --- a/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs +++ b/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs @@ -38,5 +38,15 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) Next?.GenerateCode(method, writer); } + public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer) + { + // F# has no null-conditional operator; guard Activity.Current explicitly. AddEvent returns the + // Activity for chaining, so pipe the result to ignore. + writer.Write( + $"if not (isNull {typeof(Activity).FSharpName()}.{nameof(Activity.Current)}) then {typeof(Activity).FSharpName()}.{nameof(Activity.Current)}.AddEvent({typeof(ActivityEvent).FSharpName()}(\"{_eventName}\")) |> ignore"); + + Next?.GenerateFSharpCode(method, writer); + } + public override IEnumerable FindVariables(IMethodVariables chain) => []; } diff --git a/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs b/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs index 9f1808b..d420f53 100644 --- a/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs +++ b/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs @@ -27,4 +27,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) writer.WriteLine( $"return new {typeof(ValueTask).FullNameInCode()}<{_variableType.FullNameInCode()}>({_returnValue.Usage});"); } + + public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer) + { + // Terminal frame: in F# the trailing expression IS the return value (no `return` keyword). + // ValueTask is a struct, so it is constructed without `new`. + writer.WriteLine( + $"{typeof(ValueTask).FSharpName()}<{_variableType.FSharpName()}>({_returnValue.FSharpUsage})"); + } } \ No newline at end of file diff --git a/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs b/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs index c27386b..3464622 100644 --- a/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs +++ b/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs @@ -1,4 +1,5 @@ using JasperFx.CodeGeneration.Frames; +using JasperFx.Core.Reflection; namespace JasperFx.CodeGeneration.Model; @@ -47,6 +48,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) writer.WriteLine($"var {Variable.Usage} = {Variable.VariableType.FullName}.{nameof(DateTime.UtcNow)};"); Next?.GenerateCode(method, writer); } + + public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer) + { + writer.WriteLine($"{Variable.FSharpAssignmentUsage} = {Variable.VariableType.FSharpName()}.{nameof(DateTime.UtcNow)}"); + Next?.GenerateFSharpCode(method, writer); + } } #endregion \ No newline at end of file diff --git a/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs b/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs index 4cf2a9b..7a69997 100644 --- a/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs +++ b/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs @@ -131,4 +131,23 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) writer.WriteLine($"{_serviceType.FullNameInCode()} {Variable.Usage} = new {_elementType.FullNameInCode()}[]{{{_elements.Select(x => x.Usage).Join(", ")}}};"); Next?.GenerateCode(method, writer); } + + public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer) + { + // Mirror the C# fail-fast null guard for a missing keyed singleton mirror (jasperfx#381). + foreach (var element in _elements) + { + if (element is InjectedSingleton { Descriptor: { IsKeyedService: true } descriptor } + && EnumerableSingletons.IsMirrorKey(descriptor.ServiceKey)) + { + var message = EnumerableSingletons.MissingMirrorMessage(_elementType, descriptor.ServiceKey); + writer.WriteLine( + $"if isNull {element.FSharpUsage} then raise({typeof(InvalidOperationException).FSharpName()}({CodeFormatter.Write(message)}))"); + } + } + + // F# array literal: [| e1; e2 |]. The element type is inferred, so no explicit annotation. + writer.WriteLine($"{Variable.FSharpAssignmentUsage} = [| {_elements.Select(x => x.FSharpUsage).Join("; ")} |]"); + Next?.GenerateFSharpCode(method, writer); + } } \ No newline at end of file diff --git a/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs b/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs index 7b24280..b217de4 100644 --- a/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs +++ b/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs @@ -53,4 +53,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) $"var {Variable.Usage} = {typeof(ServiceProviderServiceExtensions).FullNameInCode()}.{nameof(ServiceProviderServiceExtensions.GetRequiredService)}<{Variable.VariableType.FullNameInCode()}>({_scoped.Usage});"); Next?.GenerateCode(method, writer); } + + public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer) + { + writer.WriteComment("This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else"); + writer.Write( + $"{Variable.FSharpAssignmentUsage} = {typeof(ServiceProviderServiceExtensions).FSharpName()}.{nameof(ServiceProviderServiceExtensions.GetRequiredService)}<{Variable.VariableType.FSharpName()}>({_scoped.FSharpUsage})"); + Next?.GenerateFSharpCode(method, writer); + } } \ No newline at end of file diff --git a/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs b/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs index 46a9bcf..416da9d 100644 --- a/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs +++ b/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs @@ -29,6 +29,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) Next?.GenerateCode(method, writer); } + public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer) + { + writer.Write($"{Variable.FSharpAssignmentUsage} = {_parent.FSharpUsage}.{_member.Name}"); + Next?.GenerateFSharpCode(method, writer); + } + public override IEnumerable FindVariables(IMethodVariables chain) { _parent = chain.FindVariable(_targetType);