Skip to content
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
79 changes: 79 additions & 0 deletions src/Testing/Wolverine.Core.FSharpContracts/Contracts.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Wolverine.Attributes;
using Wolverine.Runtime;

namespace Wolverine.Core.FSharpContracts;
Expand All @@ -17,3 +18,81 @@ public interface IFoundationProbe
{
void Run(IWolverineRuntime runtime);
}

// -----------------------------------------------------------------------------
// Phase A handler surface (issue GH-2969): the smallest in-process handler that
// exercises message extraction, simple validation (abort), and a cascading
// message. The driver discovers NameHandler, renders its real Wolverine handler
// chain to F#, and the fixture compiles the generated adapter against these
// public types. (Authoring handlers in F# is the separate concern of #2968;
// this audit only proves the codegen frames emit F#.)
// -----------------------------------------------------------------------------

/// <summary>The command handled by <see cref="NameHandler" />. The <c>[Audit]</c> member also
/// exercises <c>AuditToActivityFrame</c>.</summary>
public record CreateName([property: Audit] string Name);

/// <summary>The event cascaded back out by <see cref="NameHandler" />.</summary>
public record NameCreated(string Name);

/// <summary>
/// A minimal async in-process handler with simple validation + a cascading return. Produces, in
/// chain order: message extraction, OTel tags, a <c>Validate</c> continuation (abort-if-invalid),
/// the handler call, and a cascaded <see cref="NameCreated" />. Exercises the abort guard inside a
/// <c>task { }</c> body.
/// </summary>
public class NameHandler
{
public IEnumerable<string> Validate(CreateName command)
{
return string.IsNullOrWhiteSpace(command.Name)
? new[] { "Name is required" }
: Array.Empty<string>();
}

public NameCreated Handle(CreateName command)
{
return new NameCreated(command.Name);
}
}

/// <summary>The command handled by <see cref="CheckThingHandler" />.</summary>
public record CheckThing(string Value);

/// <summary>
/// A synchronous handler whose <c>Before</c> returns a <see cref="RequirementResult" />, exercising
/// <c>RequirementResultHandlerFrame</c> and the non-<c>task { }</c> abort path (the method returns
/// <c>Task</c> and the abort branch yields <c>Task.CompletedTask</c>).
/// </summary>
public class CheckThingHandler
{
public RequirementResult Before(CheckThing command)
{
return string.IsNullOrEmpty(command.Value)
? new RequirementResult(HandlerContinuation.Stop, new[] { "Value is required" })
: RequirementResult.AllGood();
}

public void Handle(CheckThing command)
{
}
}

/// <summary>The command handled by <see cref="GateHandler" />.</summary>
public record Gate(bool Ok);

/// <summary>
/// A synchronous handler whose <c>Before</c> returns a <see cref="HandlerContinuation" />,
/// exercising <c>HandlerContinuationFrame</c>.
/// </summary>
public class GateHandler
{
public HandlerContinuation Before(Gate command)
{
return command.Ok ? HandlerContinuation.Continue : HandlerContinuation.Stop;
}

public void Handle(Gate command)
{
}
}
88 changes: 81 additions & 7 deletions src/Testing/Wolverine.Core.FSharpFixture/Generated.fs
Original file line number Diff line number Diff line change
@@ -1,15 +1,89 @@
// <auto-generated/>

namespace Wolverine.Core.FSharpFixture.Generated
namespace Internal.Generated.WolverineHandlers

open Microsoft.Extensions.Logging
open System
open Wolverine.Core.FSharpContracts
open System.Threading
open System.Threading.Tasks
open Wolverine.Runtime
open Wolverine.Runtime.Handlers

type GeneratedFoundationProbe() =
interface Wolverine.Core.FSharpContracts.IFoundationProbe with
member _.Run(runtime: Wolverine.Runtime.IWolverineRuntime) : unit =
// Generated by Wolverine F# code generation (GH-2969 foundation probe)
let messageContext = Wolverine.Runtime.MessageContext(runtime)
type CheckThingHandler649476295(loggerForMessage: Microsoft.Extensions.Logging.ILogger<Wolverine.Core.FSharpContracts.CheckThing>) =
inherit Wolverine.Runtime.Handlers.MessageHandler()
let _loggerForMessage = loggerForMessage

override _.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
// The actual message body
let checkThing = context.Envelope.Message :?> Wolverine.Core.FSharpContracts.CheckThing

Wolverine.Runtime.WolverineTracing.ApplyExecutionDiagnosticTags(System.Diagnostics.Activity.Current, context.Envelope)
if not (isNull System.Diagnostics.Activity.Current) then
System.Diagnostics.Activity.Current.SetTag("message.handler", "Wolverine.Core.FSharpContracts.CheckThingHandler") |> ignore
System.Diagnostics.Activity.Current.SetTag("handler.type", "Wolverine.Core.FSharpContracts.CheckThingHandler") |> ignore
let checkThingHandler = Wolverine.Core.FSharpContracts.CheckThingHandler()
let requirementResult1 = checkThingHandler.Before(checkThing)
// Check RequirementResult and abort if Branch == Stop
if Wolverine.Middleware.RequirementResultContinuationPolicy.ShouldStop((_loggerForMessage :> Microsoft.Extensions.Logging.ILogger), requirementResult1) then
()
else

// The actual message execution
checkThingHandler.Handle(checkThing)

System.Threading.Tasks.Task.CompletedTask

type CreateNameHandler1923366998(loggerForMessage: Microsoft.Extensions.Logging.ILogger<Wolverine.Core.FSharpContracts.CreateName>) =
inherit Wolverine.Runtime.Handlers.MessageHandler()
let _loggerForMessage = loggerForMessage

override _.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
task {
// The actual message body
let createName = context.Envelope.Message :?> Wolverine.Core.FSharpContracts.CreateName

Wolverine.Runtime.WolverineTracing.ApplyExecutionDiagnosticTags(System.Diagnostics.Activity.Current, context.Envelope)
// Application-specific Open Telemetry auditing
if not (isNull System.Diagnostics.Activity.Current) then
System.Diagnostics.Activity.Current.SetTag("name", createName.Name) |> ignore
if not (isNull System.Diagnostics.Activity.Current) then
System.Diagnostics.Activity.Current.SetTag("message.handler", "Wolverine.Core.FSharpContracts.NameHandler") |> ignore
System.Diagnostics.Activity.Current.SetTag("handler.type", "Wolverine.Core.FSharpContracts.NameHandler") |> ignore
let nameHandler = Wolverine.Core.FSharpContracts.NameHandler()
let stringValueIEnumerable1 = nameHandler.Validate(createName)
// Check for any simple validation messages and abort if any exist
if Wolverine.Middleware.SimpleValidationContinuationPolicy.LogValidationMessages((_loggerForMessage :> Microsoft.Extensions.Logging.ILogger), stringValueIEnumerable1) then
()
else

// The actual message execution
let outgoing1 = nameHandler.Handle(createName)


// Outgoing, cascaded message
do! context.EnqueueCascadingAsync(outgoing1)

}

type GateHandler1696712162() =
inherit Wolverine.Runtime.Handlers.MessageHandler()
override _.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
// The actual message body
let gate = context.Envelope.Message :?> Wolverine.Core.FSharpContracts.Gate

Wolverine.Runtime.WolverineTracing.ApplyExecutionDiagnosticTags(System.Diagnostics.Activity.Current, context.Envelope)
if not (isNull System.Diagnostics.Activity.Current) then
System.Diagnostics.Activity.Current.SetTag("message.handler", "Wolverine.Core.FSharpContracts.GateHandler") |> ignore
System.Diagnostics.Activity.Current.SetTag("handler.type", "Wolverine.Core.FSharpContracts.GateHandler") |> ignore
let gateHandler = Wolverine.Core.FSharpContracts.GateHandler()
let result_of_Before1 = gateHandler.Before(gate)
// Evaluate whether or not the execution should stop based on the HandlerContinuation value
if result_of_Before1 = Wolverine.HandlerContinuation.Stop then
()
else

// The actual message execution
gateHandler.Handle(gate)

System.Threading.Tasks.Task.CompletedTask

86 changes: 57 additions & 29 deletions src/Testing/Wolverine.Core.FSharpTests/FSharpCodegenSample.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,78 @@
using System.Runtime.CompilerServices;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Wolverine;
using Wolverine.Core.FSharpContracts;
using Wolverine.Runtime.Handlers;

namespace Wolverine.Core.FSharpTests;

/// <summary>
/// Builds the "milestone 0" <see cref="GeneratedAssembly" /> for Wolverine's F# code generation
/// (issue GH-2969): a single generated type that implements <see cref="IFoundationProbe" /> and
/// exercises the smallest real Wolverine frame — <see cref="MessageContextFrame" /> — alongside
/// the JasperFx-provided <see cref="CommentFrame" /> and <see cref="ReturnFrame" />.
/// Renders the Phase A handler chain (issue GH-2969) as F#. Builds a minimal in-memory Wolverine
/// host that discovers <see cref="NameHandler" />, compiles its real handler chain (message
/// extraction → simple validation abort → handler call → cascaded message), and emits the adapter
/// as F# via <see cref="GeneratedAssembly.GenerateFSharpCode" /> — the same path
/// <c>WolverineDiagnosticsCommand.GenerateSingleFileCode</c> uses for C#, swapping
/// <c>GenerateCode</c> for <c>GenerateFSharpCode</c>.
/// </summary>
/// <remarks>
/// This is the foundation harness: it proves the regenerate → compile pipeline end-to-end with a
/// deterministic, fully-controlled assembly. Phase A replaces this hand-built assembly with a real
/// <c>HandlerGraph</c> rendering (<c>handlerGraph.StartAssembly(rules)</c> +
/// <c>file.AssembleTypes(assembly)</c> + <c>assembly.GenerateFSharpCode()</c>) and richer handlers.
/// </remarks>
public static class FSharpCodegenSample
{
public const string GeneratedNamespace = "Wolverine.Core.FSharpFixture.Generated";

public static GeneratedAssembly BuildSampleAssembly()
/// <summary>Builds the real handler chain for <see cref="CreateName" /> and renders it as F# source.</summary>
public static string GenerateCode()
{
var assembly = new GeneratedAssembly(new GenerationRules(GeneratedNamespace));
// Apply lightweight codegen mode so the host stands up without transports / persistence and so
// resolving the code-file collections compiles the handler graph without starting it.
DynamicCodeBuilder.WithinCodegenCommand = true;
try
{
using var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery.DisableConventionalDiscovery();
opts.Discovery.IncludeType<NameHandler>();
opts.Discovery.IncludeType<CheckThingHandler>();
opts.Discovery.IncludeType<GateHandler>();

var type = assembly.AddType("GeneratedFoundationProbe", typeof(IFoundationProbe));
var method = type.MethodFor(nameof(IFoundationProbe.Run));
// Inserts ApplyExecutionDiagnosticTagsFrame at the head of every chain.
opts.Tracking.HandlerExecutionDiagnosticsEnabled = true;
})
.Build();

// A leading comment (CommentFrame emits identical F# / C#).
method.Frames.Add(new CommentFrame("Generated by Wolverine F# code generation (GH-2969 foundation probe)"));
// Force HandlerGraph.Compile() to run *without starting the host* (no Roslyn, no
// transport/persistence connections) — exactly as the describe-handlers command does.
_ = host.Services.GetServices<ICodeFileCollection>().ToArray();

// The headline Wolverine frame under test: `let messageContext = MessageContext(runtime)`.
method.Frames.Add(new MessageContextFrame());
var handlerGraph = host.Services.GetRequiredService<HandlerGraph>();
var serviceVariableSource = host.Services.GetService<IServiceVariableSource>();
var generatedAssembly = handlerGraph.StartAssembly(handlerGraph.Rules);

// A trailing unit expression so the F# member body is complete: `()`.
method.Frames.Add(new ReturnFrame());
// Render every handler chain defined in the contracts assembly into one Generated.fs so the
// compile gate exercises the whole Phase A frame set (validation, requirement-result, the
// HandlerContinuation gate, OTel/audit tags, cascading) in one build.
var contractsAssembly = typeof(CreateName).Assembly;
var chains = handlerGraph.AllChains()
.Where(c => c.MessageType.Assembly == contractsAssembly)
.OrderBy(c => c.MessageType.Name)
.ToArray();

return assembly;
}
if (chains.Length == 0)
{
throw new InvalidOperationException("No handler chains were built for the contracts assembly.");
}

/// <summary>Builds the sample and renders it as F# source.</summary>
public static string GenerateCode()
{
return BuildSampleAssembly().GenerateFSharpCode();
foreach (var chain in chains)
{
((ICodeFile)chain).AssembleTypes(generatedAssembly);
}

return generatedAssembly.GenerateFSharpCode(serviceVariableSource);
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}

/// <summary>
Expand Down
60 changes: 60 additions & 0 deletions src/Wolverine/Configuration/FSharpEmitHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;

namespace Wolverine.Configuration;

/// <summary>
/// Small shared helpers for emitting F# from Wolverine frames (issue GH-2969). These cover gaps in
/// the JasperFx-layer model where a value's C# rendering is not valid F#, plus the recurring
/// "abort-or-continue" continuation shape.
/// </summary>
internal static class FSharpEmitHelpers
{
/// <summary>
/// Emits the F# form of a C# "abort the handler if <paramref name="conditionExpression" /> is
/// true, otherwise run the rest of the chain" guard. F# has no early <c>return</c>, so the
/// remainder of the chain (<paramref name="next" />) is rendered inside the <c>else</c> branch.
/// The abort branch is a no-op <c>()</c>: the method's <c>Task</c> result comes from the enclosing
/// <c>task { }</c> body or the machinery-appended trailing <c>Task.CompletedTask</c>, so both
/// branches are <c>unit</c> and the guard sits cleanly in statement position. Mirrors the C#
/// <c>if (cond) return; &lt;next&gt;</c>.
/// </summary>
public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method, string conditionExpression,
Frame? next)
{
writer.Write($"BLOCK:if {conditionExpression} then");
writer.Write("()");
writer.FinishBlock();

writer.Write("BLOCK:else");
if (next != null)
{
next.GenerateFSharpCode(method, writer);
}
else
{
writer.Write("()");
}

writer.FinishBlock();
}

/// <summary>
/// The F# rendering of a variable's usage. F# has no C-style cast, but
/// <see cref="CastVariable" /> bakes a C# <c>((Type)x)</c> cast into its <see cref="Variable.Usage" />
/// (e.g. the injected <c>ILogger&lt;TMessage&gt;</c> handed to validation frames as <c>ILogger</c>).
/// Rewrite that as an F# upcast <c>(x :&gt; Type)</c>; everything else uses its usage verbatim.
/// </summary>
/// <remarks>
/// The proper fix is an F#-aware usage on JasperFx's <c>CastVariable</c> itself; tracked as an
/// upstream JasperFx gap. Until then this keeps the audit moving without leaving Wolverine.
/// </remarks>
public static string FSharpUsage(Variable variable)
{
return variable is CastVariable cast
? $"({cast.Inner.Usage} :> {cast.VariableType.FSharpName()})"
: variable.Usage;
}
}
23 changes: 23 additions & 0 deletions src/Wolverine/Logging/AuditToActivityFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,27 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)

Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment("Application-specific Open Telemetry auditing");

// F# has no null-conditional operator, and SetTag returns the Activity (discarded), so guard
// Activity.Current once and pipe each tagging call to `ignore`. Skip the guard entirely when
// there are no audited members so the `if` body is never empty.
if (_members.Count > 0)
{
var current = $"{typeof(Activity).FSharpName()}.{nameof(Activity.Current)}";
writer.Write($"BLOCK:if not (isNull {current}) then");
foreach (var member in _members)
{
writer.Write(
$"{current}.{nameof(Activity.SetTag)}(\"{member.OpenTelemetryName}\", {FSharpEmitHelpers.FSharpUsage(_input!)}.{member.Member.Name}) |> ignore");
}

writer.FinishBlock();
}

Next?.GenerateFSharpCode(method, writer);
}
}
Loading