Skip to content

Commit

Permalink
Handler to take single param as event of request w/ context & ret w/ …
Browse files Browse the repository at this point in the history
…compojure render
  • Loading branch information
ccfontes authored Aug 30, 2023
1 parent fd06989 commit a24c37d
Show file tree
Hide file tree
Showing 20 changed files with 885 additions and 129 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/faas_fn_build_invoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ jobs:
(docker stop bb-http-map || exit 0)
(docker rm bb-http-map || exit 0)
docker run -d --name bb-http-map ghcr.io/${{ github.repository_owner }}/bb-http-map:latest bb --main index
if [ "$(docker exec bb-http-map curl -X POST -d '{"foo": "bar", "spam": "eggs"}' -H 'Content-Type: application/json' --retry 3 --retry-delay 2 --retry-connrefused http://127.0.0.1:8082)" != '[["foo","spam"],["bar","eggs"]]' ]; then
if [ "$(docker exec bb-http-map curl -X POST -d '{"foo": "bar", "spam": "eggs"}' -H 'content-type: application/json' --retry 3 --retry-delay 2 --retry-connrefused http://127.0.0.1:8082)" != '[["foo","spam"],["bar","eggs"]]' ]; then
exit 6
fi
(docker stop bb-http-map-context || exit 0)
(docker rm bb-http-map-context || exit 0)
docker run -d --name bb-http-map-context ghcr.io/${{ github.repository_owner }}/bb-http-map-context:latest bb --main index
if [ "$(docker exec bb-http-map-context curl -X POST -d '{"foo": "bar", "spam": "eggs"}' -H 'Content-Type: application/json' --retry 3 --retry-delay 2 --retry-connrefused http://127.0.0.1:8082)" != '[["foo","spam"],["bar","eggs"],"application/json","http://127.0.0.1:8082"]' ]; then
if [ "$(docker exec bb-http-map-context curl -X POST -d '{"foo": "bar", "spam": "eggs"}' -H 'content-type: application/json' --retry 3 --retry-delay 2 --retry-connrefused http://127.0.0.1:8082)" != '[["foo","spam"],["bar","eggs"],"application/json","http://127.0.0.1:8082"]' ]; then
exit 7
fi
56 changes: 31 additions & 25 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,33 +48,36 @@ Although the Function namespace is called `function.handler`, the files themselv
my-function:
handler: ./anything/my-function
...
----
----

The `handler` function can be defined with one or two arguments.
=== Defining Function handler

Defining `handler` with one argument containing the request payload:
In `bb` language:
[source, clojure]
----
This works for both `bb` and `bb-streaming` languages.
(defn handler [payload] ...)
(defn handler [{:keys [headers body context] :as event}]
...)
----
Defining `handler` with two arguments containing the request payload and context:
`event` is a map containing `:headers`, `:body` and `:context` keys.

`:headers` contains headers, as such:
[source, clojure]
----
(defn handler [payload context] ...)
{:content-type "application/json"}
----
This works for `bb` language only.

The `context` will contain a map of `:headers` and `:env` keys.

The `:headers` key will contain a map of the request headers, as such:
[source, clojure]
`:body` is the payload body. When passing a JSON payload and using `bb` language, the payload will be automatically parsed as a Clojure map with keyword keys. There are cases where it's preferable to have string keys in the payload body, and it's possible to support them by setting `keywords: false` in the Function in `stack.yml`:
[source, yml]
----
{:content-type "application/json"}
my-function:
lang: bb
handler: ./anything/my-function
image: ${DOCKER_REGISTRY_IMG_ORG_PATH}/my-function
environment:
keywords: false
----

The `:env` map contains a map with the environment variables. Additional environment variables can be defined in the `stack.yml` file, as such:
`:context` contains environment variables. Additional environment variables can be defined in the `stack.yml` file, as such:
[source, yml]
----
my-function:
Expand All @@ -85,23 +88,19 @@ my-function:
MY_ENV1: foo
MY_ENV2: 2
----
The `:env` key will contain:
`:context` will contain:
[source, clojure]
----
{:my-env1 "foo"
:my-env2 2}
----

When passing a JSON payload and using `bb` language, the payload will be automatically parsed as a Clojure map with keyword keys. There are cases where string keys are preferable, and it's possible to support them by setting `keywords: false` in the Function in `stack.yml`:
[source, yml]
In `bb-streaming` language:
[source, clojure]
----
my-function:
lang: bb
handler: ./anything/my-function
image: ${DOCKER_REGISTRY_IMG_ORG_PATH}/my-function
environment:
keywords: false
(defn handler [event] ...)
----
The `event` is the payload body, and the function return is the payload body for the response.


== link:examples[Function examples]
Expand Down Expand Up @@ -132,7 +131,14 @@ The template may benefit from some common middleware functions, such as those of

== Third party code

link:template/bb/ring/middleware/json.clj[ring.middleware.json] is derived from https://github.com/ring-clojure/ring-json/blob/master/src/ring/middleware/json.clj[ring-son] to work with Babashka, originally authored by James Reeves and used under the MIT license.
The following files are derived from https://github.com/ring-clojure[ring] to work with Babashka, originally authored by James Reeves and contributors, and used under the MIT license:

- link:template/bb/lib/ring/middleware/json.clj[ring.middleware.json]
- link:template/bb/lib/ring/util/io.clj[ring.util.io]
- link:template/bb/lib/ring/util/mime_type.clj[ring.util.mime-type]
- link:template/bb/lib/ring/util/parsing.clj[ring.util.parsing]
- link:template/bb/lib/ring/util/response.clj[ring.util.response]
- link:template/bb/lib/ring/util/time.clj[ring.util.time]

== link:LICENSE[License]

Expand Down
2 changes: 1 addition & 1 deletion examples/http/bb-hello/handler.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(ns function.handler)

(defn handler [{:keys [body]}]
{:body (str "Hello, " (slurp body))})
{:body (str "Hello, " body)})
4 changes: 2 additions & 2 deletions examples/http/bb-map-context/handler.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(ns function.handler)

(defn handler [content {:keys [headers env]}]
[(keys content) (vals content) (:content-type headers) (:upstream-url env)])
(defn handler [{:keys [body headers context]}]
[(keys body) (vals body) (:content-type headers) (:upstream-url context)])
4 changes: 2 additions & 2 deletions examples/http/bb-map/handler.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(ns function.handler)

(defn handler [content]
[(keys content) (vals content)])
(defn handler [{:keys [body]}]
[(keys body) (vals body)])
4 changes: 2 additions & 2 deletions template/bb/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ USER app
WORKDIR $HOME

COPY index.clj function/bb.edn ./
COPY ring ./ring
COPY lib ./
COPY function function

RUN bb prepare
RUN bb --deps-root function prepare && bb --deps-root function print-deps

ENV mode="http"
ENV upstream_url="http://127.0.0.1:8082"
Expand Down
2 changes: 1 addition & 1 deletion template/bb/bb.edn
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
{:paths ["."]
{:paths ["." "lib"]
:deps {eg/eg {:mvn/version "0.5.6-alpha"}}}
76 changes: 26 additions & 50 deletions template/bb/index.clj
Original file line number Diff line number Diff line change
@@ -1,58 +1,34 @@
(ns index ^{:author "Carlos da Cunha Fontes"
:url "https://github.com/ccfontes/faas-bb"
:license {:name "Distributed under the MIT License"
:url "https://github.com/ccfontes/faas-bb/blob/main/LICENSE"}}
(ns index
^{:author "Carlos da Cunha Fontes"
:url "https://github.com/ccfontes/faas-bb"
:license {:name "Distributed under the MIT License"
:url "https://github.com/ccfontes/faas-bb/blob/main/LICENSE"}}
(:require
[clojure.walk :refer [keywordize-keys]]
[clojure.string :as str :refer [lower-case]]
[clojure.edn :as edn]
[org.httpkit.server :refer [run-server]]
[ring.middleware.json :as json-middleware]
[ring.middleware.json :refer [wrap-json-body]]
[ring.middleware.text :refer [wrap-text-body]]
[ring.middleware.headers :refer [wrap-lowercase-headers wrap-friendly-headers]]
[ring.middleware.headers]
[ring.util.walk :as ring-walk]
[compojure.response :as response]
[function.handler :as function]))

(defn read-string [s]
(try
(let [res (edn/read-string s)]
(if (symbol? res) (str res) res))
(catch Exception _
s)))
(def keywords? #(if (nil? %) true %))

(defn keywords? [env-val]
(if-some [keywords (edn/read-string env-val)]
keywords
true))
(defn ->handler [f env]
(fn [request]
(response/render f
(assoc request :context env))))

(defn ->kebab-case [s]
(lower-case (str/replace s #"_" "-")))

(def fn-arg-cnt (comp count first :arglists meta))

(defn format-context [m]
(->> m
(map (fn [[k v]] [(->kebab-case k) (read-string v)]))
(into {})
(keywordize-keys)))

(defn ->context [headers env]
{:headers (format-context headers)
:env (format-context env)})

(def response {:status 200})

(defn ->handler [f-var env]
(let [f (var-get f-var)
faas-fn (case (fn-arg-cnt f-var)
1 (comp f :body)
2 #(f (:body %) (->context (:headers %) env)))]
(fn [request]
(merge {:body (faas-fn request)} response))))

(defn ->app [f-var env]
(-> (->handler f-var env)
(json-middleware/wrap-json-body {:keywords? (keywords? (get env "keywords"))})
(json-middleware/wrap-json-response)))
(defn ->app [f env]
(-> (->handler f env)
(wrap-friendly-headers)
(wrap-text-body)
(wrap-json-body {:keywords? (-> env :keywords keywords?)})
(wrap-lowercase-headers)))

(defn -main []
(run-server (->app #'function/handler (System/getenv))
{:port 8082})
@(promise))
(let [env (ring-walk/format-context (System/getenv))]
(run-server (->app function/handler env)
{:port 8082})
@(promise)))
80 changes: 80 additions & 0 deletions template/bb/lib/compojure/response.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
(ns compojure.response
"A protocol for generating Ring response maps"
{:author "James Reeves"
:contributors "Modified by Carlos da Cunha Fontes to work with Babashka"
:url "https://github.com/weavejester/compojure"
:license {:name "Distributed under the MIT License, the same as Ring."}}
(:refer-clojure :exclude [send])
(:require [ring.util.mime-type :as mime]
[ring.util.response :as response]))

(defprotocol Renderable
"A protocol that tells Compojure how to handle the return value of routes
defined by [[GET]], [[POST]], etc.
This protocol supports rendering strings, maps, functions, refs, files, seqs,
input streams and URLs by default, and may be extended to cover many custom
types."
(render [x request]
"Render `x` into a form suitable for the given request map."))

(defprotocol Sendable
"A protocol that tells Compojure how to handle the return value of
asynchronous routes, should they require special attention."
(send* [x request respond raise]))

(defn send
"Send `x` as a Ring response. Checks to see if `x` satisfies [[Sendable]],
and if not, falls back to [[Renderable]]."
[x request respond raise]
(if (satisfies? Sendable x)
(send* x request respond raise)
(respond (render x request))))

(defn- guess-content-type [response name]
(if-let [mime-type (mime/ext-mime-type (str name))]
(response/content-type response mime-type)
response))

(extend-protocol Renderable
nil
(render [_ _] nil)
String
(render [body _]
(-> (response/response body)
(response/content-type "text/html; charset=utf-8")))
clojure.lang.IPersistentMap
(render [resp-map _]
(merge (with-meta (response/response "") (meta resp-map))
resp-map))
clojure.lang.Fn
(render [func request] (render (func request) request))
clojure.lang.MultiFn
(render [func request] (render (func request) request))
clojure.lang.IDeref
(render [ref request] (render (deref ref) request))
java.io.File
(render [file _]
(-> (response/file-response (str file))
(guess-content-type file)))
clojure.lang.ISeq
(render [coll _]
(-> (response/response coll)))
clojure.lang.PersistentVector
(render [coll _]
(-> (response/response coll)))
java.io.InputStream
(render [stream _] (response/response stream))
java.net.URL
(render [url _]
(-> (response/url-response url)
(guess-content-type url))))

(extend-protocol Sendable
clojure.lang.Fn
(send* [func request respond raise]
(func request #(send % request respond raise) raise))
clojure.lang.MultiFn
(send* [func request respond raise]
(func request #(send % request respond raise) raise)))

30 changes: 30 additions & 0 deletions template/bb/lib/ring/middleware/headers.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
(ns ring.middleware.headers
{:author "Carlos da Cunha Fontes"
:url "https://github.com/ccfontes/faas-bb"
:license {:name "Distributed under the MIT License"
:url "https://github.com/ccfontes/faas-bb/blob/main/LICENSE"}}
(:require [clojure.walk :refer [keywordize-keys stringify-keys]]
[ring.util.walk :as ring-walk]))

(defn wrap-friendly-headers
"Middleware that converts all header:
keys to keywords in the request, and back to strings in the response.
values to clojure values in the request, and back to strings in the response.
Purposed to be used right after the set defined handler, at the beginning of the middleware stack, or first of '->'"
[handler]
(fn [request]
(let [->friendly-headers-req (comp ring-walk/read-val-strings keywordize-keys)
->friendly-headers-resp (comp stringify-keys ring-walk/write-val-strings)
response (handler (update request :headers ->friendly-headers-req))]
(update response :headers ->friendly-headers-resp))))

(defn wrap-lowercase-headers
"Middleware that converts all header keys in ring request and response to lowercase strings.
Assumes that all middleware applied before this is configured with lower-case.
Prevents outside world from breaking this stack prepared for HTTP/2.
Prevents any user or middleware set headers from breaking HTTP/2 in the outside world.
Purposed to be used at the end of the middleware stack, or last of '->'"
[handler]
(fn [request]
(let [response (handler (update request :headers ring-walk/lowerify-keys))]
(update response :headers ring-walk/lowerify-keys))))
Loading

0 comments on commit a24c37d

Please sign in to comment.