dark icon indicating copy to clipboard operation
dark copied to clipboard

Re-implement LibMiddleware

Open StachuDotNet opened this issue 3 years ago • 1 comments
trafficstars

We added a comment the other day to kill the LibMiddleware.fs file, and to convert that to an issue. Here's the Issue - shortly I'll paste the existing code.

StachuDotNet avatar Jun 13 '22 19:06 StachuDotNet

module LibExecutionStdLib.LibMiddleware

// CLEANUP: delete this file, after extracting commentary into an Issue. include sha of removal or something.

open System.Threading.Tasks
open FSharp.Control.Tasks

open Prelude
open LibExecution.VendoredTablecloth
open LibExecution.RuntimeTypes

module Interpreter = LibExecution.Interpreter
module Errors = LibExecution.Errors
module DvalReprExternal = LibExecution.DvalReprExternal

let fn = FQFnName.stdlibFnName

let err (str : string) = Ply(Dval.errStr str)

let incorrectArgs = Errors.incorrectArgs

let varA = TVariable "a"
let varB = TVariable "b"

// Middlewares are based on Ring: https://github.com/mmcgrana/ring/blob/master/SPEC

// Middlewares are typed functions that contribute a small, composible part of
// decoding a web request for the handler to use. Middlewares receive a
// request, and then based on the request, may choose to call the next
// middleware or simply return a response instead. As such, middlewares
// receive as parameters both the request so far, as well as the next
// middleware to call. They are responsible for calling the next middleware, possibly changing the request first and possible altering the response as well.
// This leads to middlewares having the following shape:

// let myMiddleware (arg : myMiddlewareArgType) next =
//   fun (req : 'req) ->
//     let doSomethingToRequest req = { req with someExtraField = someFunction req }
//     let doSomethingToResponse res = { res with someExtraField = someFunction res }
//     let shortCircuitResponse = { status = 404, body = "", headers = [] }
//     if someCondition req
//     then shortCircuitResponse
//     else
//       req
//       |> doSomethingToRequest
//       |> nextMiddleware
//       |> doSomethingToResponse

// A middleware returns a function which takes a request. A middleware takes
// whatever arguments it needs, as well as the next middleware to call. As
// such, a middleware stack looks like this:

// let middleware =
//   (\ctx -> handler ctx) // shown like this for clarity
//   |> addQueryParams url
//   |> addHeaders headers
//   |> readVarsFromURL
//   |> addJsonBody headers body
//   |> addFormBody headers body
//   |> addCookies headers
//   |> processErrorRail
//   |> optionsHanderMiddleware
//   |> headHandlerMiddleware
//   |> textPingMiddleware
//   |> sitemapFaviconMiddleware
// middleware emptyRequest

// Each middleware wraps the previous one, so the outermost middleware is last,
// and the handler comes first.

// EmptyRequest is an empty record, and each middleware adds fields to it until
// the request has the shape required by the handler. It then returns a
// response, which can also have fields added to it by middleware wishing to
// send those fields to other middlewares.

// As such, the types of the entire middleware have to add up to the type of
// the handler.

// This is returned by a middlware and passed into the next middleware

// CLEANUP: this started as an attempt to rewrite the http framework using
// middleware. It was sort-of working, but it was quite difficult to make work.
// Specifically, it was really hard to make it work exactly like the old OCaml code,
// and also quite hard to write and to figure out what it did. A better approach
// might be to try to write it in Dark using the package manager.

// let middlewareReturnType = TFn([ TVariable "req" ], TVariable "resp")

// let middlewareNextParameter = Param.make "next" middlewareReturnType ""


// let fns : List<BuiltInFn> =
//   [ { name = fn "Http" "parseQueryString" 0
//       parameters = [ Param.make "url" TStr "" ]
//       returnType = TDict TStr // This is always a string
//       description =
//         "Parse the query string into a dict. If there are copies of the same query param, the last one wins"
//       fn =
//         (function
//         | _, [ DStr url ] ->
//           // TODO: could do with some fuzzing here
//           let queryString = System.Uri(url).Query
//           let nvc = System.Web.HttpUtility.ParseQueryString queryString

//           nvc.AllKeys
//           |> Seq.map
//                (fun key ->
//                  let values = nvc.GetValues key

//                  let value =
//                    let split = values[values.Length - 1] |> String.split ","

//                    match split with
//                    | [] -> DNull
//                    | [ "" ] -> DNull // CLEANUP this should be a string
//                    | [ v ] -> DStr v
//                    | list -> DList(List.map DStr list)

//                  if isNull key then
//                    // All the values with no key are by GetValues, so make each one a value
//                    values |> Array.toList |> List.map (fun k -> (k, DNull))
//                  else
//                    [ (key, value) ])
//           |> List.concat
//           |> Map
//           |> DObj
//           |> Ply
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "addQueryParamsMiddleware" 0
//       parameters = [ middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description = ""
//       fn =
//         // fun req ->
//         //   let url = Http.parseQueryString_v0 req.url
//         //   let req = Dict.set_v0 req "queryParams" url
//         //   req |> next
//         let code =
//           eLambda
//             [ "req" ]
//             (eLet
//               "url"
//               (eFn "Http" "parseQueryString" 0 [ eFieldAccess (eVar "req") "url" ])
//               (eLet
//                 "req"
//                 (eFn "Dict" "set" 0 [ eVar "req"; eStr "queryParams"; eVar "url" ])
//                 (ePipeApply (eVar "next") [ eVar "req" ])))

//         (function
//         | state, [ DFnVal _ as next ] ->
//           let st = Map.empty |> Map.add "next" next
//           Interpreter.eval state st code
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "urlEncodeQueryParamString" 0
//       parameters = [ Param.make "url" TStr "" ]
//       returnType = TStr
//       // Uses the same conditions as the old OCaml implementation
//       description = "URL encode query param strings"
//       fn =
//         let urlSafe =
//           (let allowed =
//             // TODO: this adds extra safe characters ("=,&") to avoid having
//             // to parse the query string (using the .NET utilities to do so
//             // might throw away info we need, as well.) This is wrong and
//             // probably has edge cases. Needs fuzzing at least.
//             "/?:@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~=,&"

//            let mutable safe = (Array.init 256 (fun _ -> false))
//            Array.iter (fun b -> safe[int b] <- true) (UTF8.toBytes allowed)
//            safe)

//         (function
//         | _, [ DStr queryParams ] ->
//           let result = System.Text.StringBuilder()
//           // What if there;s a urlencoded ',' or '=' in the values?
//           queryParams
//           |> UTF8.toBytes
//           |> Array.iter
//                (fun b ->
//                  let (_ : System.Text.StringBuilder) =
//                    if urlSafe[int b] then
//                      result.Append(char b)
//                    else
//                      result.AppendFormat("%{0:X2}", b)

//                  ())

//           result |> string |> DStr |> Ply
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "canonicalizeUrlMiddleware" 0
//       parameters = [ middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description = "Lowercases the url and removes queryparams from it"
//       fn =
//         // fun req ->
//         //   let parts = req.url |> String.split_v0 "?"
//         //   let host = parts |> List.head_v2 |> Option.withDefault_v0 req.url |> String.toLowercase_v1
//         //   let query = parts |> List.getAt_v1 1 |> Option.map (fun q -> "?" ++ Http.urlEncodeQueryParamString_v0 q) |> Option.withDefault_v0 ""
//         //   let req = Dict.set_v0 req "url" (host ++ query)
//         //   req |> next
//         let code =
//           eLambda
//             [ "req" ]
//             (eLet
//               "parts"
//               (ePipeApply
//                 (eStdFnVal "String" "split" 0)
//                 [ eFieldAccess (eVar "req") "url"; eStr "?" ])
//               (eLet
//                 "host"
//                 (ePipeApply
//                   (eStdFnVal "String" "toLowercase" 1)
//                   [ ePipeApply
//                       (eStdFnVal "Option" "withDefault" 0)
//                       [ ePipeApply (eStdFnVal "List" "head" 2) [ eVar "parts" ]
//                         eFieldAccess (eVar "req") "url" ] ])
//                 (eLet
//                   "query"
//                   (ePipeApply
//                     (eStdFnVal "Option" "withDefault" 0)
//                     [ ePipeApply
//                         (eStdFnVal "Option" "map" 0)
//                         [ ePipeApply
//                             (eStdFnVal "List" "getAt" 1)
//                             [ eVar "parts"; eInt 1 ]
//                           eLambda
//                             [ "q" ]
//                             (eFn
//                               ""
//                               "++"
//                               0
//                               [ eStr "?"
//                                 eFn "Http" "urlEncodeQueryParamString" 0 [ eVar "q" ] ]) ]
//                       eStr "" ])
//                   (eLet
//                     "req"
//                     (eFn
//                       "Dict"
//                       "set"
//                       0
//                       [ eVar "req"
//                         eStr "url"
//                         eFn "" "++" 0 [ eVar "host"; eVar "query" ] ])
//                     (ePipeApply (eVar "next") [ eVar "req" ])))))

//         (function
//         | state, [ DFnVal _ as next ] ->
//           let st = Map.empty |> Map.add "next" next
//           Interpreter.eval state st code
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "parseHeadersMiddleware" 0
//       parameters =
//         [ Param.make "url" TStr ""
//           Param.make "req" (TVariable "a") ""
//           middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description =
//         "Parse the headers string into a dict. If multiple headers of the same key exist, the latest one wins."
//       fn =
//         (function
//         // TODO
//         | _, [] -> Ply(DObj(Map []))
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "addJSONBodyMiddleware" 0
//       parameters =
//         [ Param.make "url" TStr ""
//           Param.make "req" (TVariable "a") ""
//           middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description =
//         "Parse the headers string into a dict. If multiple headers of the same key exist, the latest one wins."
//       fn =
//         (function
//         // TODO
//         | _, [] -> Ply(DObj(Map []))
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "setHeader" 0
//       parameters =
//         [ Param.make "response" (THttpResponse varA) ""
//           Param.make "name" TStr ""
//           Param.make "value" TStr "" ]
//       returnType = THttpResponse varA
//       description = "Set a header in the HTTP response"
//       fn =
//         (function
//         | _, [ DHttpResponse response; DStr name; DStr value ] ->
//           match response with
//           | Response (code, headers, responseVal) ->
//             Response(code, headers @ [ name, value ], responseVal)
//             |> DHttpResponse
//             |> Ply
//           | Redirect _ -> Ply(DHttpResponse response)
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "setHeaderUnlessPresent" 0
//       parameters =
//         [ Param.make "response" (THttpResponse varA) ""
//           Param.make "name" TStr ""
//           Param.make "value" TStr "" ]
//       returnType = THttpResponse varA
//       description = "Set a header in the HTTP response"
//       fn =
//         (function
//         | _, [ DHttpResponse response; DStr headerName; DStr value ] ->
//           match response with
//           | Response (code, headers, responseVal) ->
//             let existingHeader =
//               headers
//               |> List.tryFind (fun (name, _) -> String.toLowercase name = headerName)

//             let headers =
//               if existingHeader = None then
//                 (headerName, value) :: headers
//               else
//                 headers

//             Response(code, headers, responseVal) |> DHttpResponse |> Ply
//           | Redirect _ -> Ply(DHttpResponse response)
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }

//     { name = fn "Http" "responseBody" 0
//       parameters = [ Param.make "response" (THttpResponse varA) "" ]
//       returnType = varA
//       description = "Return the body of a HTTP response"
//       fn =
//         (function
//         | _, [ DHttpResponse response ] ->
//           match response with
//           | Redirect _ -> Ply DNull
//           | Response (_, _, dv) -> Ply dv
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "addServerHeaderMiddleware" 0
//       parameters = [ middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description = "Add the darklang server header."
//       fn =
//         let code =
//           // (fun req ->
//           //   let response = req |> next in
//           //   Http.setHeader_v0 response "server" "darklang")
//           eLambda
//             [ "req" ]
//             (eLet
//               "response"
//               (ePipeApply (eVar "next") [ eVar "req" ])
//               (eFn
//                 "Http"
//                 "setHeaderUnlessPresent"
//                 0
//                 [ eVar "response"; eStr "server"; eStr "darklang" ]))

//         (function
//         | state, [ DFnVal _ as next ] ->
//           let st = Map.empty |> Map.add "next" next
//           Interpreter.eval state st code
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "createRequest" 0
//       parameters =
//         [ Param.make "url" TStr ""
//           Param.make "headers" (TDict TStr) ""
//           Param.make "body" TBytes "" ]
//       returnType = TRecord [ "url", TStr; "headers", TDict TStr; "body", TBytes ]
//       description = "Creates a request"
//       fn =
//         (function
//         | _, [ DStr _ as url; DObj headers; DBytes bodyBytes as body ] ->
//           let body = if bodyBytes.Length = 0 then DNull else body
//           let cookies = DObj Map.empty // TODO
//           let formBody = DNull // TODO
//           let fullBody = DStr ""
//           let jsonBody = DNull // TODO

//           let headers =
//             headers
//             |> Map.toList
//             |> List.map (fun (k, v) -> (String.toLowercase k, v))
//             |> (@) [ "user-agent", DStr "ocaml-cohttp/1.2.0" ] // TODO wtf
//             |> Map.ofList
//             |> DObj

//           Map.empty
//           |> Map.add "body" body
//           |> Map.add "url" url
//           |> Map.add "headers" headers
//           |> Map.add "cookies" cookies
//           |> Map.add "formBody" formBody
//           |> Map.add "fullBody" fullBody
//           |> Map.add "jsonBody" jsonBody

//           |> DObj
//           |> Ply
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "convertToResponseValue" 0
//       parameters =
//         [ Param.make
//             "response"
//             varA
//             "A HTTP response to be returned to the client. May be any type, and will automatically be converted to an appropriate HTTP response" ]
//       returnType = THttpResponse TBytes
//       description = "Return a HTTPResponse based on the input"
//       // This does a lot more than we'd hope it does, and ideally we'd have
//       // split this into multiple middleware functions.  However, there is a
//       // need to make it match the OCaml version.  In OCaml, if the value was
//       // not wrapped in a response, we would always use toPrettyMachineJson to
//       // display it, regardless of the inferred headers. As a result, we cannot
//       // separate these from each other: if we convert to bytes we lose type to
//       // infer the content-type from. If we add the content-type first, we lose
//       // whether it's a result or not.
//       fn =
//         (function
//         | _, [ response ] ->
//           // let req ->
//           //   if not wrapped in a response
//           //     use machinejson
//           //     infer header based on type (list/obj uses JSON, else use textplain)
//           //   if in a response:
//           //      use existing header or infer from type.
//           //      if output is bytes, print direct
//           //      if text/html or text/plain or applcation/xml, use enduser_readable_text
//           //      if application/json, convert to pretty_machine_json_v1
//           //   note: string with no response: machine printout and text/plain (dev mode)
//           //         string in http response: enduser_readable_text and text/plain
//           let inferContentType dv =
//             match dv with
//             | DObj _
//             | DList _ -> "application/json; charset=utf-8"
//             | _ -> "text/plain; charset=utf-8"

//           match response with
//           | DHttpResponse (Response (code, headers, dv)) ->
//             let inferredCT = inferContentType dv

//             let existingContentType =
//               headers
//               |> List.tryFind
//                    (fun (name, _) -> String.toLowercase name = "content-type")

//             let headers =
//               if existingContentType = None then
//                 ("Content-type", inferredCT) :: headers
//               else
//                 headers

//             let contentType =
//               existingContentType
//               |> Option.map (fun (k, v) -> v)
//               |> Option.defaultValue inferredCT
//               |> String.split ";"
//               |> Seq.tryHead

//             let asBytes =
//               match dv, contentType with
//               | DBytes bytes, _ -> bytes
//               | _, Some "text/plain"
//               | _, Some "application/xml"
//               | _, Some "text/html" ->
//                 dv |> DvalReprExternal.toEnduserReadableTextV0 |> UTF8.toBytes
//               | _ -> dv |> DvalReprExternal.toPrettyMachineJsonStringV1 |> UTF8.toBytes

//             Ply(DHttpResponse(Response(code, headers, DBytes asBytes)))
//           | DHttpResponse (Redirect _) as resp -> Ply resp
//           | response ->
//             let bytes =
//               response |> DvalReprExternal.toPrettyMachineJsonStringV1 |> UTF8.toBytes

//             let headers = [ "content-type", inferContentType response ]
//             Ply(DHttpResponse(Response(200L, headers, DBytes bytes)))
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "convertToResponseMiddleware" 0
//       parameters = [ middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description =
//         "Takes a value that is expected to be returned to an end-user via HTTP.
//         If it is not a HttpResponse, it converts it into one."
//       fn =
//         let code =
//           // (fun req -> Http.convertToResponseValue (req |> next))
//           eLambda
//             [ "req" ]
//             (eFn
//               "Http"
//               "convertToResponseValue"
//               0
//               [ (ePipeApply (eVar "next") [ eVar "req" ]) ])

//         (function
//         | state, [ DFnVal _ as next ] ->
//           let st = Map.empty |> Map.add "next" next
//           Interpreter.eval state st code
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "addContentLengthResponseHeaderMiddleware" 0
//       parameters = [ middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description = "Take the HTTP result, and add a Content-length header to it."
//       fn =
//         let code =
//           // (fun req ->
//           //   let response = req |> next in
//           //   let body = Http.responseBody_v0 response in
//           //   Http.setHeader_v0 response "content-length" (toString (Bytes.length_v0 body)))
//           eLambda
//             [ "req" ]
//             (eLet
//               "response"
//               (ePipeApply (eVar "next") [ eVar "req" ])
//               (eLet
//                 "body"
//                 (eFn "Http" "responseBody" 0 [ eVar "response" ])
//                 (eFn
//                   "Http"
//                   "setHeader" // always set it
//                   0
//                   [ eVar "response"
//                     eStr "Content-Length"
//                     eFn "" "toString" 0 [ eFn "Bytes" "length" 0 [ eVar "body" ] ] ])))

//         (function
//         | state, [ DFnVal _ as next ] ->
//           let st = Map.empty |> Map.add "next" next
//           Interpreter.eval state st code
//         | _, _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "respondToTextPingMiddleware" 0
//       parameters = [ middlewareNextParameter ]
//       returnType = middlewareReturnType
//       description =
//         "If the header has content-type of `text/ping`, return a status code of 418"
//       fn =
//         let code =
//           // (fun req ->
//           //   let contentType  = Dict.get_v2 req.headers "content-type"
//           //   if contentType = "text/ping"
//           //   then
//           //     Http::response_v0 Bytes.empty_v0 418
//           //   else
//           //     req |> next
//           eLambda
//             [ "req" ]
//             (eLet
//               "contentType"
//               (eFn
//                 "Dict"
//                 "get"
//                 2
//                 [ eFieldAccess (eVar "req") "headers"; eStr "content-type" ])
//               (eIf
//                 (eFn
//                   ""
//                   "=="
//                   0
//                   [ eVar "contentType"; eConstructor "Just" [ eStr "text/ping" ] ])
//                 (eFn "Http" "response" 0 [ eFn "Bytes" "empty" 0 []; eInt 418 ])
//                 (ePipeApply (eVar "next") [ eVar "req" ])))

//         (function
//         | state, [ DFnVal _ as next ] ->
//           let st = Map.empty |> Map.add "next" next
//           Interpreter.eval state st code
//         | _, _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     { name = fn "Http" "middleware" 0
//       parameters =
//         [ Param.make "url" TStr ""
//           Param.make "body" TBytes ""
//           Param.make "headers" (TDict TStr) ""
//           middlewareNextParameter ]
//       returnType = THttpResponse TBytes
//       description =
//         "Call the middleware stack, returning a response which can be sent
//         to the browser. Each function in the middleware stack receives the next
//         middleware element, and returns a function to be called on the request,
//         returning a response. The stack is executed with the last elements executed
//         first on the request and last on the response. "
//       fn =
//         let code =
//           // let fns = [Http.wrapInResponseValue_v0 ; Http.addserverHeaderMiddleware_v0]
//           // let app = List.fold_v0 fns handler (fun accum curr -> accum |> curr)
//           // request |> app
//           eLet
//             "fns"
//             (eList [ eStdFnVal "Http" "convertToResponseMiddleware" 0
//                      eStdFnVal "Http" "canonicalizeUrlMiddleware" 0
//                      eStdFnVal "Http" "addQueryParamsMiddleware" 0 // must be after canonicalizeUrlMiddleware
//                      // Shortcircuit
//                      eStdFnVal "Http" "respondToTextPingMiddleware" 0
//                      // Everything gets `Content-Length` and `Server` headers
//                      eStdFnVal "Http" "addServerHeaderMiddleware" 0
//                      eStdFnVal "Http" "addContentLengthResponseHeaderMiddleware" 0 ])
//             (eLet
//               "app"
//               (eFn
//                 "List"
//                 "fold"
//                 0
//                 [ eVar "fns"
//                   eVar "handler"
//                   eLambda
//                     [ "accum"; "curr" ]
//                     (ePipeApply (eVar "curr") [ eVar "accum" ]) ])
//               (eLet
//                 "initialRequest"
//                 (eFn
//                   "Http"
//                   "createRequest"
//                   0
//                   [ eVar "url"; eVar "headers"; eVar "body" ])

//                 (ePipeApply (eVar "app") [ eVar "initialRequest" ])))


//         (function
//         | state, [ DStr _ as url; DBytes _ as body; headers; DFnVal _ as handler ] ->
//           let st =
//             Map.empty
//             |> Map.add "url" url
//             |> Map.add "body" body
//             |> Map.add "headers" headers
//             |> Map.add "handler" handler

//           Interpreter.eval state st code
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated }
//     // CLEANUP: move into LibBytes. It's here to help with testing
//     { name = fn "Bytes" "empty" 0
//       parameters = []
//       returnType = TBytes
//       description = "Returns an empty Bytes value"
//       fn =
//         (function
//         | _, [] -> Ply(DBytes [||])
//         | _ -> incorrectArgs ())
//       sqlSpec = NotYetImplementedTODO
//       previewable = Pure
//       deprecated = NotDeprecated } ]

// let httpMiddleware_v0 (body : string)
//                       (headers : string)
//                       (url : string)
//                       (handler)
//                       : response =
//   handler
//   |> addQueryParams
//   |> parseRawBody
//   |> addJsonBody body
//   |> addFormBody body
//   |> addCookies headers
//   |> processErrorRail
//   |> optionsHanderMiddleware
//   |> textPingMiddleware
//   |> sitemapFaviconMiddleware

StachuDotNet avatar Jun 13 '22 19:06 StachuDotNet

folding this into a larger Issue shortly

StachuDotNet avatar Jan 14 '24 20:01 StachuDotNet