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

Add aws lambda support #359

Merged
merged 1 commit into from
Jan 20, 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
24 changes: 24 additions & 0 deletions Fable.Remoting.AwsLambda/Fable.Remoting.AwsLambda.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Description>AWS Lambda-Fable adapter that generates routes for shared server spec with a Fable client. Client must use Fable.Remoting.Client</Description>
<PackageProjectUrl>https://github.com/Zaid-Ajaj/Fable.Remoting</PackageProjectUrl>
<RepositoryUrl>https://github.com/Zaid-Ajaj/Fable.Remoting.git</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Zaid-Ajaj/Fable.Remoting/blob/master/LICENSE</PackageLicenseUrl>
<PackageIconUrl></PackageIconUrl>
<PackageTags>fsharp;fable;remoting;rpc;webserver;serverless;azure functions</PackageTags>
<Authors>Zaid Ajaj;Roman Provaznik</Authors>
<Version>1.11.0</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReleaseNotes>Update Microsoft.IO.RecyclableMemoryStream to v3.0</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
<Compile Include="FableLambdaAdapter.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fable.Remoting.Server\Fable.Remoting.Server.fsproj" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />

</Project>
173 changes: 173 additions & 0 deletions Fable.Remoting.AwsLambda/FableLambdaAdapter.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
namespace Fable.Remoting.AwsLambda.Worker

open System
open System.Net
open System.Text
open System.Threading.Tasks
open System.IO
open Fable.Remoting.Server
open Fable.Remoting.Server.Proxy
open Amazon.Lambda.APIGatewayEvents
open Newtonsoft.Json

type HttpRequestData = APIGatewayHttpApiV2ProxyRequest
type HttpResponseData = APIGatewayHttpApiV2ProxyResponse
Comment on lines +13 to +14
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've aliased the AWS types for parity with the Azure Functions implementation to make comparison easier, but they don't need to remain.


module private FuncsUtil =

let private htmlString (html: string) (req: HttpRequestData) : Task<HttpResponseData option> =
task {
let resp = HttpResponseData(StatusCode = int HttpStatusCode.OK, Body = html)
resp.SetHeaderValues("Content-Type", "text/html; charset=utf-8", false)

return Some resp
}

let text (str: string) (req: HttpRequestData) : Task<HttpResponseData option> =
task {
let resp = HttpResponseData(StatusCode = int HttpStatusCode.OK, Body = str)
resp.SetHeaderValues("Content-Type", "text/plain; charset=utf-8", false)
return Some resp
}

let private path (r: HttpRequestData) = r.RawPath

let setJsonBody
(res: HttpResponseData)
(response: obj)
(logger: Option<string -> unit>)
(req: HttpRequestData)
: Task<HttpResponseData option> =
task {
use ms = new MemoryStream()
jsonSerialize response ms
let responseBody = System.Text.Encoding.UTF8.GetString(ms.ToArray())
Diagnostics.outputPhase logger responseBody
res.SetHeaderValues("Content-Type", "application/json; charset=utf-8", false)
res.Body <- responseBody
return Some res
}

/// Handles thrown exceptions
let fail
(ex: exn)
(routeInfo: RouteInfo<HttpRequestData>)
(options: RemotingOptions<HttpRequestData, 't>)
(req: HttpRequestData)
: Task<HttpResponseData option> =
let resp = HttpResponseData(StatusCode = int HttpStatusCode.InternalServerError)
let logger = options.DiagnosticsLogger

match options.ErrorHandler with
| None -> setJsonBody resp (Errors.unhandled routeInfo.methodName) logger req
| Some errorHandler ->
match errorHandler ex routeInfo with
| Ignore -> setJsonBody resp (Errors.ignored routeInfo.methodName) logger req
| Propagate error -> setJsonBody resp (Errors.propagated error) logger req

let halt: HttpResponseData option = None

let buildFromImplementation<'impl>
(implBuilder: HttpRequestData -> 'impl)
(options: RemotingOptions<HttpRequestData, 'impl>)
=
let proxy = makeApiProxy options

let rmsManager =
options.RmsManager
|> Option.defaultWith (fun _ -> recyclableMemoryStreamManager.Value)

fun (req: HttpRequestData) ->
task {
let isProxyHeaderPresent = req.Headers.Keys.Contains "x-remoting-proxy"
use output = rmsManager.GetStream "remoting-output-stream"

let isBinaryEncoded =
match req.Headers.TryGetValue "Content-Type" with
| true, "application/octet-stream" -> true
| _ -> false

let bodyAsStream =
if String.IsNullOrEmpty req.Body then
new MemoryStream()
else
new MemoryStream(Encoding.UTF8.GetBytes(req.Body))

let props =
{ ImplementationBuilder = (fun () -> implBuilder req)
EndpointName = path req
Input = bodyAsStream
IsProxyHeaderPresent = isProxyHeaderPresent
HttpVerb = req.RequestContext.Http.Method.ToUpper()
IsContentBinaryEncoded = isBinaryEncoded
Output = output }

match! proxy props with
| Success isBinaryOutput ->
let resp = HttpResponseData(StatusCode = int HttpStatusCode.OK)

if isBinaryOutput && isProxyHeaderPresent then
resp.SetHeaderValues("Content-Type", "application/octet-stream", false)
elif options.ResponseSerialization = SerializationType.Json then
resp.SetHeaderValues("Content-Type", "application/json; charset=utf-8", false)
else
resp.SetHeaderValues("Content-Type", "application/msgpack", false)

let result = Encoding.UTF8.GetString(output.ToArray())
resp.Body <- result

return Some resp
| Exception(e, functionName, requestBodyText) ->
let routeInfo =
{ methodName = functionName
path = path req
httpContext = req
requestBodyText = requestBodyText }

return! fail e routeInfo options req
| InvalidHttpVerb -> return halt
| EndpointNotFound ->
match req.RequestContext.Http.Method.ToUpper(), options.Docs with
| "GET", (Some docsUrl, Some docs) when docsUrl = (path req) ->
let (Documentation(docsName, docsRoutes)) = docs
let schema = Docs.makeDocsSchema typeof<'impl> docs options.RouteBuilder
let docsApp = DocsApp.embedded docsName docsUrl schema
return! htmlString docsApp req
| "OPTIONS", (Some docsUrl, Some docs) when
sprintf "/%s/$schema" docsUrl = (path req)
|| sprintf "%s/$schema" docsUrl = (path req)
->
let schema = Docs.makeDocsSchema typeof<'impl> docs options.RouteBuilder
let serializedSchema = schema.ToString(Formatting.None)
return! text serializedSchema req
| _ -> return halt
}

module Remoting =

/// Builds a HttpRequestData -> HttpResponseData option function from the given implementation and options
/// Please see HttpResponseData.fromRequestHandler for using output of this function
let buildRequestHandler (options: RemotingOptions<HttpRequestData, 't>) =
match options.Implementation with
| StaticValue impl -> FuncsUtil.buildFromImplementation (fun _ -> impl) options
| FromContext createImplementationFrom -> FuncsUtil.buildFromImplementation createImplementationFrom options
| Empty -> fun _ -> Task.FromResult None

module FunctionsRouteBuilder =
/// Default RouteBuilder for Azure Functions running HttpTrigger on /api prefix
let apiPrefix = sprintf "/api/%s/%s"
/// RouteBuilder for Azure Functions running HttpTrigger without any prefix
let noPrefix = sprintf "/%s/%s"

module HttpResponseData =

/// Build HttpResponseData from single builder function and HttpRequestData
let fromRequestHandler
(req: HttpRequestData)
(fn: HttpRequestData -> Task<HttpResponseData option>)
: Task<HttpResponseData> =
task {
match! fn req with
| Some r -> return r
| None -> return HttpResponseData(StatusCode = int HttpStatusCode.NotFound, Body = "")
}
4 changes: 4 additions & 0 deletions Fable.Remoting.AwsLambda/paket.references
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
group AwsLambda

FSharp.Core
Amazon.Lambda.APIGatewayEvents
14 changes: 14 additions & 0 deletions Fable.Remoting.sln
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fable.Remoting.AzureFunctio
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Build", "build\Build.fsproj", "{30D42110-503D-4671-B58C-C4FE9BD2BEB2}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Remoting.AwsLambda", "Fable.Remoting.AwsLambda\Fable.Remoting.AwsLambda.fsproj", "{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -334,6 +336,18 @@ Global
{30D42110-503D-4671-B58C-C4FE9BD2BEB2}.Release|x64.Build.0 = Release|Any CPU
{30D42110-503D-4671-B58C-C4FE9BD2BEB2}.Release|x86.ActiveCfg = Release|Any CPU
{30D42110-503D-4671-B58C-C4FE9BD2BEB2}.Release|x86.Build.0 = Release|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Debug|x64.ActiveCfg = Debug|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Debug|x64.Build.0 = Debug|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Debug|x86.ActiveCfg = Debug|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Debug|x86.Build.0 = Debug|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Release|Any CPU.Build.0 = Release|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Release|x64.ActiveCfg = Release|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Release|x64.Build.0 = Release|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Release|x86.ActiveCfg = Release|Any CPU
{2D66CF7F-43B7-4EE5-9E0C-2DFC5315DFCF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
8 changes: 8 additions & 0 deletions paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ nuget PuppeteerSharp >= 7.1
nuget Suave >= 2.6.0
nuget Giraffe >= 4.1

group AwsLambda
source https://api.nuget.org/v3/index.json
framework: net5
storage: none
lowest_matching: true
nuget FSharp.Core >= 6.0.0
nuget Amazon.Lambda.APIGatewayEvents 2.6.0

group FunctionsWorker
source https://api.nuget.org/v3/index.json
framework: net5
Expand Down
9 changes: 9 additions & 0 deletions paket.lock
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,15 @@ NUGET
System.Threading.Tasks.Extensions (>= 4.4) - restriction: || (&& (>= net45) (< netstandard2.0)) (&& (< net45) (>= netstandard2.0)) (>= net47)
System.ValueTuple (>= 4.4) - restriction: || (&& (>= net45) (< netstandard2.0)) (&& (< net45) (>= netstandard2.0)) (>= net47)

GROUP AwsLambda
STORAGE: NONE
LOWEST_MATCHING: TRUE
RESTRICTION: == net5.0
NUGET
remote: https://api.nuget.org/v3/index.json
Amazon.Lambda.APIGatewayEvents (2.6)
FSharp.Core (6.0)

GROUP Client
LOWEST_MATCHING: TRUE
NUGET
Expand Down