-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathFableAzureFunctionsAdapter.fs
More file actions
146 lines (127 loc) · 7.36 KB
/
FableAzureFunctionsAdapter.fs
File metadata and controls
146 lines (127 loc) · 7.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
namespace Fable.Remoting.AzureFunctions.Worker
open System.Net
open System.IO
open System.Linq
open System.Threading.Tasks
open Newtonsoft.Json
open Microsoft.Azure.Functions.Worker.Http
open Fable.Remoting.Server
open Fable.Remoting.Server.Proxy
module private FuncsUtil =
let private setContentType (t:string) (res:HttpResponseData) =
res.Headers.Add("Content-Type", t)
res
let private htmlString (html:string) (req:HttpRequestData) : Task<HttpResponseData option> =
task {
let bytes = System.Text.Encoding.UTF8.GetBytes html
let resp = req.CreateResponse(HttpStatusCode.OK) |> setContentType "text/html; charset=utf-8"
do! resp.WriteBytesAsync bytes
return Some resp
}
let text (str:string) (req:HttpRequestData) : Task<HttpResponseData option> =
task {
let bytes = System.Text.Encoding.UTF8.GetBytes str
let resp = req.CreateResponse(HttpStatusCode.OK) |> setContentType "text/plain; charset=utf-8"
do! resp.WriteBytesAsync bytes
return Some resp
}
let private path (r:HttpRequestData) = r.Url.PathAndQuery.Split("?").[0]
let setJsonBody (backend: JsonSerializerBackend) (res:HttpResponseData) (response: obj) (logger: Option<string -> unit>) (req:HttpRequestData) : Task<HttpResponseData option> =
task {
use ms = new MemoryStream ()
jsonSerializeWithBackend backend response ms
let responseBody = System.Text.Encoding.UTF8.GetString (ms.ToArray ())
Diagnostics.outputPhase logger responseBody
let res = res |> setContentType "application/json; charset=utf-8"
do! res.WriteStringAsync 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 = req.CreateResponse(HttpStatusCode.InternalServerError)
let logger = options.DiagnosticsLogger
let backend = options.JsonSerializer
match options.ErrorHandler with
| None -> setJsonBody backend resp (Errors.unhandled routeInfo.methodName) logger req
| Some errorHandler ->
match errorHandler ex routeInfo with
| Ignore -> setJsonBody backend resp (Errors.ignored routeInfo.methodName) logger req
| Propagate error -> setJsonBody backend 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 = getRecyclableMemoryStreamManager options
fun (req:HttpRequestData) ->
task {
let isProxyHeaderPresent = req.Headers.Contains "x-remoting-proxy"
use output = rmsManager.GetStream "remoting-output-stream"
let contentType =
match req.Headers.TryGetValues "Content-Type" with
| true, values when values.Any () -> values.First ()
| _ -> ""
let props = { ImplementationBuilder = (fun () -> implBuilder req); EndpointName = path req; Input = req.Body; IsProxyHeaderPresent = isProxyHeaderPresent;
HttpVerb = req.Method; InputContentType = contentType; Output = output }
match! proxy props with
| Success isBinaryOutput ->
let resp =
req.CreateResponse(HttpStatusCode.OK)
|> (fun r ->
if isBinaryOutput && isProxyHeaderPresent then
r |> setContentType "application/octet-stream"
elif options.ResponseSerialization.IsJson then
r |> setContentType "application/json; charset=utf-8"
else
r |> setContentType "application/vnd.msgpack"
)
do! output.CopyToAsync resp.Body
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.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 =
let rec private chooseHttpResponse (fns:(HttpRequestData -> Task<HttpResponseData option>) list) =
fun (req:HttpRequestData) ->
task {
match fns with
| [] -> return req.CreateResponse(HttpStatusCode.NotFound)
| func :: tail ->
match! func req with
| Some r -> return r
| None -> return! chooseHttpResponse tail req
}
/// Build HttpResponseData from builder functions and HttpRequestData
/// This functionality is very similar to choose function from Giraffe
let fromRequestHandlers (req:HttpRequestData) (fns:(HttpRequestData -> Task<HttpResponseData option>) list) : Task<HttpResponseData> =
chooseHttpResponse fns req
/// Build HttpResponseData from single builder function and HttpRequestData
let fromRequestHandler (req:HttpRequestData) (fn:HttpRequestData -> Task<HttpResponseData option>) : Task<HttpResponseData> =
fromRequestHandlers req [fn]