Skip to content

Commit c3102a3

Browse files
Merge pull request #2982 from JasperFx/feat-2969-fsharp-http-results
F# codegen: AsyncMode-aware abort + terminal IResult endpoint (GH-2969)
2 parents 3bd824e + 744c148 commit c3102a3

6 files changed

Lines changed: 61 additions & 9 deletions

File tree

src/Http/Wolverine.Http/CodeGen/IReadHttpFrame.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using JasperFx.CodeGeneration.Model;
44
using JasperFx.Core.Reflection;
55
using Microsoft.AspNetCore.Http;
6+
using Wolverine.Configuration;
67

78
namespace Wolverine.Http.CodeGen;
89

@@ -133,7 +134,7 @@ private void writeStringValueFSharp(GeneratedMethod method, ISourceWriter writer
133134
// A missing required route value 404s and aborts. F# has no early return, so the rest of
134135
// the chain renders inside the else branch.
135136
writer.Write($"BLOCK:if isNull {Variable.Usage} then");
136-
writeStatusAbort(writer);
137+
writeStatusAbort(method, writer);
137138
writer.FinishBlock();
138139
writer.Write("BLOCK:else");
139140
WriteNextOrUnit(method, writer);
@@ -167,15 +168,15 @@ private void writeParsedValueFSharp(GeneratedMethod method, ISourceWriter writer
167168
// Required route value: null or parse-failure 404s + aborts; success binds the variable and
168169
// renders the rest of the chain in the success match arm (no F# early return).
169170
writer.Write($"BLOCK:if isNull {raw} then");
170-
writeStatusAbort(writer);
171+
writeStatusAbort(method, writer);
171172
writer.FinishBlock();
172173
writer.Write("BLOCK:else");
173174
writer.Write($"BLOCK:match {fsharpTryParse(raw)} with");
174175
writer.Write($"BLOCK:| true, {Variable.Usage} ->");
175176
WriteNextOrUnit(method, writer);
176177
writer.FinishBlock();
177178
writer.Write("BLOCK:| _ ->");
178-
writeStatusAbort(writer);
179+
writeStatusAbort(method, writer);
179180
writer.FinishBlock();
180181
writer.FinishBlock(); // match
181182
writer.FinishBlock(); // else
@@ -197,14 +198,14 @@ private void WriteNextOrUnit(GeneratedMethod method, ISourceWriter writer)
197198
}
198199
else
199200
{
200-
writer.Write("()");
201+
writer.Write(FSharpEmitHelpers.AbortExpression(method));
201202
}
202203
}
203204

204-
private static void writeStatusAbort(ISourceWriter writer)
205+
private static void writeStatusAbort(GeneratedMethod method, ISourceWriter writer)
205206
{
206207
writer.Write($"httpContext.Response.{nameof(HttpResponse.StatusCode)} <- 404");
207-
writer.Write("()");
208+
writer.Write(FSharpEmitHelpers.AbortExpression(method));
208209
}
209210

210211
// The F# tuple form of the C# out-parameter TryParse (F# auto-tuples the out arg).

src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,9 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
7373

7474
Next?.GenerateCode(method, writer);
7575
}
76+
77+
// NOTE: F# emit for MaybeEndWithResultFrame is deferred. The frame is added by ResultContinuationPolicy
78+
// during a full endpoint compile (MapWolverineEndpoints); the no-host ChainFor harness used by the F#
79+
// fixture doesn't apply continuation policies, so it can't exercise this frame yet. It will be done
80+
// with the behavioural (host-based) harness (GH-2969).
7681
}

src/Testing/Wolverine.Http.FSharpContracts/Endpoints.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Microsoft.AspNetCore.Http;
2+
13
namespace Wolverine.Http.FSharpContracts;
24

35
/// <summary>The JSON body bound by <see cref="ThingEndpoints.Create" />.</summary>
@@ -52,4 +54,13 @@ public string Paged(int page)
5254
{
5355
return $"page {page}";
5456
}
57+
58+
// IResult return: a terminal IResult endpoint. The handler's IResult is executed directly as the
59+
// returned Task (ReturnFromLastNode); combined with a route value it exercises the AsyncMode-aware
60+
// abort (a missing route value yields Task.CompletedTask, not unit).
61+
[WolverineGet("/fsharp/result/{id}")]
62+
public IResult GetResult(string id)
63+
{
64+
return string.IsNullOrEmpty(id) ? Results.NotFound() : Results.Ok($"thing {id}");
65+
}
5566
}

src/Testing/Wolverine.Http.FSharpFixture/Generated.fs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,20 @@ type GET_fsharp_paged(wolverineHttpOptions: Wolverine.Http.WolverineHttpOptions)
122122
do! Wolverine.Http.HttpHandler.WriteString(httpContext, result_of_Paged)
123123
}
124124

125+
type GET_fsharp_result_id(wolverineHttpOptions: Wolverine.Http.WolverineHttpOptions) =
126+
inherit Wolverine.Http.HttpHandler(wolverineHttpOptions)
127+
let _wolverineHttpOptions = wolverineHttpOptions
128+
129+
override this.Handle(httpContext: Microsoft.AspNetCore.Http.HttpContext) : System.Threading.Tasks.Task =
130+
let id = (httpContext.GetRouteValue("id") :?> string)
131+
if isNull id then
132+
httpContext.Response.StatusCode <- 404
133+
System.Threading.Tasks.Task.CompletedTask
134+
else
135+
let thingEndpoints = Wolverine.Http.FSharpContracts.ThingEndpoints()
136+
137+
// The actual HTTP request handler execution
138+
let result = thingEndpoints.GetResult(id)
139+
140+
result.ExecuteAsync(httpContext)
141+

src/Testing/Wolverine.Http.FSharpTests/HttpFSharpCodegenSample.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public static string GenerateCode()
3737
HttpChain.ChainFor<ThingEndpoints>(x => x.GetById(null!), httpGraph),
3838
HttpChain.ChainFor<ThingEndpoints>(x => x.Search(null!), httpGraph),
3939
HttpChain.ChainFor<ThingEndpoints>(x => x.GetItems(null!, 0), httpGraph), // typed int route value
40-
HttpChain.ChainFor<ThingEndpoints>(x => x.Paged(0), httpGraph) // typed int query value
40+
HttpChain.ChainFor<ThingEndpoints>(x => x.Paged(0), httpGraph), // typed int query value
41+
HttpChain.ChainFor<ThingEndpoints>(x => x.GetResult(null!), httpGraph) // terminal IResult + route value
4142
};
4243

4344
var generatedAssembly = httpGraph.StartAssembly(httpGraph.Rules);

src/Wolverine/Configuration/FSharpEmitHelpers.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using JasperFx.CodeGeneration;
22
using JasperFx.CodeGeneration.Frames;
3+
using JasperFx.CodeGeneration.Model;
34

45
namespace Wolverine.Configuration;
56

@@ -22,8 +23,10 @@ internal static class FSharpEmitHelpers
2223
public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method, string conditionExpression,
2324
Frame? next)
2425
{
26+
var abort = AbortExpression(method);
27+
2528
writer.Write($"BLOCK:if {conditionExpression} then");
26-
writer.Write("()");
29+
writer.Write(abort);
2730
writer.FinishBlock();
2831

2932
writer.Write("BLOCK:else");
@@ -33,9 +36,23 @@ public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method,
3336
}
3437
else
3538
{
36-
writer.Write("()");
39+
writer.Write(abort);
3740
}
3841

3942
writer.FinishBlock();
4043
}
44+
45+
/// <summary>
46+
/// The expression a branch yields when it does NOT continue the chain (abort / no-op). In a
47+
/// <c>task { }</c> body (AsyncMode.AsyncTask) or a synchronous-Task method (AsyncMode.None, where
48+
/// the machinery appends a trailing <c>Task.CompletedTask</c>) that's just <c>()</c>. But when the
49+
/// method body IS a bare trailing Task expression (AsyncMode.ReturnFromLastNode) every branch must
50+
/// itself be a <c>Task</c>, so the no-op branch yields <c>Task.CompletedTask</c>.
51+
/// </summary>
52+
public static string AbortExpression(GeneratedMethod method)
53+
{
54+
return method.AsyncMode == AsyncMode.ReturnFromLastNode
55+
? "System.Threading.Tasks.Task.CompletedTask"
56+
: "()";
57+
}
4158
}

0 commit comments

Comments
 (0)