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

Handler to take single event param & ret w/ compojure dispatch #24

Merged
Merged
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
39 changes: 23 additions & 16 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,20 @@ 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 `handler` with one argument containing the request payload:
[source, clojure]
----
This works for both `bb` and `bb-streaming` languages.

(defn handler [payload] ...)
----
Defining `handler` with two arguments containing the request payload and context:
=== Defining Function handler

In `bb` language:
[source, clojure]
----
(defn handler [payload context] ...)
(defn handler [{:keys [headers body context] :as event}] ...)
----
This works for `bb` language only.
The `event` is a map containing the request `:headers`, `:body` and `:context` keys.

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

The `:headers` key will contain a map of the request headers, as such:
The `:headers` key contains headers, as such:
[source, clojure]
----
{:content-type "application/json"}
Expand All @@ -92,7 +85,7 @@ The `:env` key will contain:
: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`:
There are cases where string keys are preferable in the payload body, and it's possible to support them by setting `keywords: false` in the Function in `stack.yml`:
[source, yml]
----
my-function:
Expand All @@ -103,6 +96,13 @@ my-function:
keywords: false
----

In `bb-streaming` language:
[source, clojure]
----
(defn handler [event] ...)
----
The `event` is the payload body.


== link:examples[Function examples]

Expand Down Expand Up @@ -132,7 +132,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/ring/middleware/json.clj[ring.middleware.json]
- link:template/bb/ring/util/io.clj[ring.util.io]
- link:template/bb/ring/util/mime_type.clj[ring.util.mime-type]
- link:template/bb/ring/util/parsing.clj[ring.util.parsing]
- link:template/bb/ring/util/response.clj[ring.util.response]
- link:template/bb/ring/util/time.clj[ring.util.time]

== link:LICENSE[License]

Expand Down
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 env]}]
[(keys body) (vals body) (:content-type headers) (:upstream-url env)])
2 changes: 1 addition & 1 deletion template/bb/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ COPY index.clj function/bb.edn ./
COPY ring ./ring
COPY function function

RUN bb prepare
RUN bb prepare && bb 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"}}}
74 changes: 24 additions & 50 deletions template/bb/index.clj
Original file line number Diff line number Diff line change
@@ -1,58 +1,32 @@
(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.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-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))))
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
(assoc-in resp [:headers name] (str value)))

(defn content-type
"Returns an updated Ring response with the a Content-Type header corresponding
"Returns an updated Ring response with the a 'content-type header corresponding
to the given content-type."
[resp content-type]
(header resp "Content-Type" content-type))
(header resp "content-type" content-type))

(defn- json-request? [request]
(when-let [type (get-in request [:headers "content-type"])]
Expand All @@ -65,7 +65,7 @@
(def ^{:doc "The default response to return when a JSON request is malformed."}
default-malformed-response
{:status 400
:headers {"Content-Type" "text/plain"}
:headers {"content-type" "text/plain"}
:body "Malformed JSON in request body."})

(defn json-body-request
Expand All @@ -76,7 +76,7 @@
(when valid? (assoc request :body json))
request))

(defn wrap-json-body
(defn wrap-json-body-request
"Middleware that parses the body of JSON request maps, and replaces the :body
key with the parsed data structure. Requests without a JSON content type are
unaffected.
Expand Down Expand Up @@ -149,7 +149,7 @@
[response options]
(if (coll? (:body response))
(let [json-resp (update-in response [:body] json/generate-string options)]
(if (contains? (:headers response) "Content-Type")
(if (contains? (:headers response) "content-type")
json-resp
(content-type json-resp "application/json; charset=utf-8")))
response))
Expand All @@ -171,3 +171,5 @@
(json-response (handler request) options))
([request respond raise]
(handler request (fn [response] (respond (json-response response options))) raise))))

(def wrap-json-body (comp wrap-json-response wrap-json-body-request))
Loading
Loading