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
13 changes: 7 additions & 6 deletions src/Http/Wolverine.Http/CodeGen/IReadHttpFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Http;
using Wolverine.Configuration;

namespace Wolverine.Http.CodeGen;

Expand Down Expand Up @@ -133,7 +134,7 @@ private void writeStringValueFSharp(GeneratedMethod method, ISourceWriter writer
// A missing required route value 404s and aborts. F# has no early return, so the rest of
// the chain renders inside the else branch.
writer.Write($"BLOCK:if isNull {Variable.Usage} then");
writeStatusAbort(writer);
writeStatusAbort(method, writer);
writer.FinishBlock();
writer.Write("BLOCK:else");
WriteNextOrUnit(method, writer);
Expand Down Expand Up @@ -167,15 +168,15 @@ private void writeParsedValueFSharp(GeneratedMethod method, ISourceWriter writer
// Required route value: null or parse-failure 404s + aborts; success binds the variable and
// renders the rest of the chain in the success match arm (no F# early return).
writer.Write($"BLOCK:if isNull {raw} then");
writeStatusAbort(writer);
writeStatusAbort(method, writer);
writer.FinishBlock();
writer.Write("BLOCK:else");
writer.Write($"BLOCK:match {fsharpTryParse(raw)} with");
writer.Write($"BLOCK:| true, {Variable.Usage} ->");
WriteNextOrUnit(method, writer);
writer.FinishBlock();
writer.Write("BLOCK:| _ ->");
writeStatusAbort(writer);
writeStatusAbort(method, writer);
writer.FinishBlock();
writer.FinishBlock(); // match
writer.FinishBlock(); // else
Expand All @@ -197,14 +198,14 @@ private void WriteNextOrUnit(GeneratedMethod method, ISourceWriter writer)
}
else
{
writer.Write("()");
writer.Write(FSharpEmitHelpers.AbortExpression(method));
}
}

private static void writeStatusAbort(ISourceWriter writer)
private static void writeStatusAbort(GeneratedMethod method, ISourceWriter writer)
{
writer.Write($"httpContext.Response.{nameof(HttpResponse.StatusCode)} <- 404");
writer.Write("()");
writer.Write(FSharpEmitHelpers.AbortExpression(method));
}

// The F# tuple form of the C# out-parameter TryParse (F# auto-tuples the out arg).
Expand Down
5 changes: 5 additions & 0 deletions src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,9 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)

Next?.GenerateCode(method, writer);
}

// NOTE: F# emit for MaybeEndWithResultFrame is deferred. The frame is added by ResultContinuationPolicy
// during a full endpoint compile (MapWolverineEndpoints); the no-host ChainFor harness used by the F#
// fixture doesn't apply continuation policies, so it can't exercise this frame yet. It will be done
// with the behavioural (host-based) harness (GH-2969).
}
11 changes: 11 additions & 0 deletions src/Testing/Wolverine.Http.FSharpContracts/Endpoints.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Http;

namespace Wolverine.Http.FSharpContracts;

/// <summary>The JSON body bound by <see cref="ThingEndpoints.Create" />.</summary>
Expand Down Expand Up @@ -52,4 +54,13 @@ public string Paged(int page)
{
return $"page {page}";
}

// IResult return: a terminal IResult endpoint. The handler's IResult is executed directly as the
// returned Task (ReturnFromLastNode); combined with a route value it exercises the AsyncMode-aware
// abort (a missing route value yields Task.CompletedTask, not unit).
[WolverineGet("/fsharp/result/{id}")]
public IResult GetResult(string id)
{
return string.IsNullOrEmpty(id) ? Results.NotFound() : Results.Ok($"thing {id}");
}
}
17 changes: 17 additions & 0 deletions src/Testing/Wolverine.Http.FSharpFixture/Generated.fs
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,20 @@ type GET_fsharp_paged(wolverineHttpOptions: Wolverine.Http.WolverineHttpOptions)
do! Wolverine.Http.HttpHandler.WriteString(httpContext, result_of_Paged)
}

type GET_fsharp_result_id(wolverineHttpOptions: Wolverine.Http.WolverineHttpOptions) =
inherit Wolverine.Http.HttpHandler(wolverineHttpOptions)
let _wolverineHttpOptions = wolverineHttpOptions

override this.Handle(httpContext: Microsoft.AspNetCore.Http.HttpContext) : System.Threading.Tasks.Task =
let id = (httpContext.GetRouteValue("id") :?> string)
if isNull id then
httpContext.Response.StatusCode <- 404
System.Threading.Tasks.Task.CompletedTask
else
let thingEndpoints = Wolverine.Http.FSharpContracts.ThingEndpoints()

// The actual HTTP request handler execution
let result = thingEndpoints.GetResult(id)

result.ExecuteAsync(httpContext)

Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public static string GenerateCode()
HttpChain.ChainFor<ThingEndpoints>(x => x.GetById(null!), httpGraph),
HttpChain.ChainFor<ThingEndpoints>(x => x.Search(null!), httpGraph),
HttpChain.ChainFor<ThingEndpoints>(x => x.GetItems(null!, 0), httpGraph), // typed int route value
HttpChain.ChainFor<ThingEndpoints>(x => x.Paged(0), httpGraph) // typed int query value
HttpChain.ChainFor<ThingEndpoints>(x => x.Paged(0), httpGraph), // typed int query value
HttpChain.ChainFor<ThingEndpoints>(x => x.GetResult(null!), httpGraph) // terminal IResult + route value
};

var generatedAssembly = httpGraph.StartAssembly(httpGraph.Rules);
Expand Down
21 changes: 19 additions & 2 deletions src/Wolverine/Configuration/FSharpEmitHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;

namespace Wolverine.Configuration;

Expand All @@ -22,8 +23,10 @@ internal static class FSharpEmitHelpers
public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method, string conditionExpression,
Frame? next)
{
var abort = AbortExpression(method);

writer.Write($"BLOCK:if {conditionExpression} then");
writer.Write("()");
writer.Write(abort);
writer.FinishBlock();

writer.Write("BLOCK:else");
Expand All @@ -33,9 +36,23 @@ public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method,
}
else
{
writer.Write("()");
writer.Write(abort);
}

writer.FinishBlock();
}

/// <summary>
/// The expression a branch yields when it does NOT continue the chain (abort / no-op). In a
/// <c>task { }</c> body (AsyncMode.AsyncTask) or a synchronous-Task method (AsyncMode.None, where
/// the machinery appends a trailing <c>Task.CompletedTask</c>) that's just <c>()</c>. But when the
/// method body IS a bare trailing Task expression (AsyncMode.ReturnFromLastNode) every branch must
/// itself be a <c>Task</c>, so the no-op branch yields <c>Task.CompletedTask</c>.
/// </summary>
public static string AbortExpression(GeneratedMethod method)
{
return method.AsyncMode == AsyncMode.ReturnFromLastNode
? "System.Threading.Tasks.Task.CompletedTask"
: "()";
}
}
Loading