Skip to content

Commit

Permalink
Merge pull request #359 from blair55/add-aws-lambda-support
Browse files Browse the repository at this point in the history
Add aws lambda support
  • Loading branch information
Zaid-Ajaj authored Jan 20, 2024
2 parents e1093b0 + f01d363 commit 630b0b0
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 0 deletions.
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

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

0 comments on commit 630b0b0

Please sign in to comment.