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 should take clj body w/ optional headers & env as context #23

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cf8c68a
Handler should take clj body w/ optional headers & env as context
ccfontes Aug 17, 2023
6f2540c
🔧 chore(.clj-kondo/config.edn): update linter configuration for tests…
ccfontes Aug 19, 2023
d868917
🚀 feat(bb.edn): remove unnecessary tasks key from bb.edn file
ccfontes Aug 20, 2023
20aa51f
🐛 fix(workflows): fix docker run command in faas_fn_build_invoke.yml
ccfontes Aug 20, 2023
9226377
🔧 chore(broken-link-checker.yml): update asciidoc-link-check version …
ccfontes Aug 20, 2023
d7668cc
🐛 fix(broken-link-checker.yml): fix typo in asciidoc-link-check packa…
ccfontes Aug 20, 2023
0666ef4
🔧 chore(faas_fn_build_invoke.yml): remove redundant docker logs commands
ccfontes Aug 20, 2023
9367ec9
🐛 fix(workflows): fix curl command in faas_fn_build_invoke.yml
ccfontes Aug 20, 2023
4909549
🔧 chore(faas_fn_build_invoke.yml): remove unnecessary docker logs and…
ccfontes Aug 20, 2023
18b25f8
🔧 chore(faas_fn_build_invoke.yml): stop and remove containers before …
ccfontes Aug 20, 2023
dd60921
🐛 fix(faas_fn_build_invoke.yml): increase retry count from 3 to 6 for…
ccfontes Aug 20, 2023
9009a69
🐛 fix(faas_fn_build_invoke.yml): add sleep command before starting co…
ccfontes Aug 20, 2023
7b3ef2d
Handler should take clj body w/ optional headers & env as context
ccfontes Aug 20, 2023
0abd8df
🐛 fix(faas_fn_build_invoke.yml): increase retry count for curl comman…
ccfontes Aug 20, 2023
32c4c3e
🐛 fix(faas_fn_build_invoke.yml): add sleep command before starting th…
ccfontes Aug 20, 2023
07c23f8
🔧 chore(faas_fn_build_invoke.yml): remove unnecessary docker stop and…
ccfontes Aug 20, 2023
e1cd4a2
🔧 chore(handler.clj): refactor handler function to include context pa…
ccfontes Aug 20, 2023
9a44c67
🔀 refactor(handler.clj): change the name of the `:keywords` environme…
ccfontes Aug 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{:linters
{:redefined-var {:level :off}
:duplicate-require {:level :off}
:namespace-name-mismatch {:level :off}}}
:namespace-name-mismatch {:level :off}}
:config-in-ns {tests {:linters {:unresolved-symbol {:exclude [(eg/eg)
(eg/ex)
(plumula.mimolette.alpha/defspec-test [spec-check-index])]}}}
index {:linters {:invalid-arity {:level :off}}}}}
12 changes: 8 additions & 4 deletions .github/workflows/faas_fn_build_invoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@ jobs:
exit 3
fi
docker run -i ghcr.io/${{ github.repository_owner }}/bb-streaming-lib:latest function/handler-test.clj
(docker stop bb-http-hello || exit 0)
(docker rm bb-http-hello || exit 0)
docker run -d --name bb-http-hello ghcr.io/${{ github.repository_owner }}/bb-http-hello:latest ./index.clj
if [ "$(docker exec bb-http-hello curl -X POST --data-raw "world" --retry 3 --retry-delay 2 --retry-connrefused http://127.0.0.1:8082)" != "Hello, world" ]; then
(docker stop bb-http || exit 0)
(docker rm bb-http || exit 0)
docker run -d --name bb-http ghcr.io/${{ github.repository_owner }}/bb-http:latest ./index.clj
if [ "$(docker exec bb-http curl -X POST --data-raw "world" --retry 3 --retry-delay 2 --retry-connrefused http://127.0.0.1:8082)" != "Hello, world" ]; then
exit 5
fi
# 1. one arg: text -> text, string with environment var
# 2. two args: json -> json, update map with header
# 2.1. keywords=false
# 2.2. keywords=true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
examples/build
examples/template
.clj-kondo/.cache
1 change: 1 addition & 0 deletions examples/http/bb-map/bb.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:paths ["."]}
6 changes: 6 additions & 0 deletions examples/http/bb-map/handler.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
(ns function.handler)

(defn handler [content context]
(println "content" content)
(println "context" context)
(update content :bar str "spam"))
6 changes: 6 additions & 0 deletions examples/stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ functions:
lang: bb
handler: ./http/bb-hello
image: ${DOCKER_REGISTRY_IMG_ORG_PATH}/bb-http-hello
bb-http-map:
lang: bb
handler: ./http/bb-map
image: ${DOCKER_REGISTRY_IMG_ORG_PATH}/bb-http-map
environment:
keywords: true
1 change: 1 addition & 0 deletions template/bb/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ USER app
WORKDIR $HOME

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

RUN bb prepare
Expand Down
3 changes: 3 additions & 0 deletions template/bb/bb.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{:paths ["."]
:deps {eg/eg {:mvn/version "0.5.6-alpha"}}
:tasks {test tests/-main}}
62 changes: 56 additions & 6 deletions template/bb/index.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,59 @@
#!/usr/bin/env bb
(ns index
(:require
[function.handler :as function]
[org.httpkit.server :refer [run-server]]
[ring.middleware.json :as json-middleware]
[clojure.walk :refer [keywordize-keys]]
[clojure.string :as str :refer [lower-case]]
[clojure.edn :as edn]))

(require
'[function.handler :as function]
'[org.httpkit.server :refer [run-server]])
(defn read-string [s]
(try
(let [res (edn/read-string s)]
(if (symbol? res)
(str res)
res))
(catch Exception _
s)))

(run-server function/handler {:port 8082})
(defn keywords? [env-val]
(if-some [keywords (edn/read-string env-val)]
keywords
true))

@(promise)
(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]
(fn [request]
(let [f (var-get f-var)
faas-fn (case (fn-arg-cnt f-var)
1 (comp f :body)
2 #(f (:body %) (->context (:headers %) env)))]
; TODO replace {} with request, but need to remove troublesome keys
(merge (assoc {} :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 -main []
(run-server (->app #'function/handler (System/getenv))
{:port 8082})
@(promise))
169 changes: 169 additions & 0 deletions template/bb/ring/middleware/json.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
(ns ring.middleware.json
"Ring middleware for parsing JSON requests and generating JSON responses."
(:require [cheshire.core :as json])
(:import [java.io InputStream]))

(def ^{:doc "HTTP token: 1*<any CHAR except CTLs or tspecials>. See RFC2068"}
re-token
#"[!#$%&'*\-+.0-9A-Z\^_`a-z\|~]+")

(def ^{:doc "HTTP quoted-string: <\"> *<any TEXT except \"> <\">. See RFC2068."}
re-quoted
#"\"((?:\\\"|[^\"])*)\"")

(def ^{:doc "HTTP value: token | quoted-string. See RFC2109"}
re-value
(str "(" re-token ")|" re-quoted))

(def ^{:doc "Pattern for pulling the charset out of the content-type header"}
re-charset
(re-pattern (str ";(?:.*\\s)?(?i:charset)=(?:" re-value ")\\s*(?:;|$)")))

(defn find-content-type-charset
"Return the charset of a given a content-type string."
[s]
(when-let [m (re-find re-charset s)]
(or (m 1) (m 2))))

(defn character-encoding
"Return the character encoding for the request, or nil if it is not set."
[request]
(some-> (get-in request [:headers "content-type"])
find-content-type-charset))

(defn header
"Returns an updated Ring response with the specified header added."
[resp name value]
(assoc-in resp [:headers name] (str value)))

(defn content-type
"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))

(defn- json-request? [request]
(when-let [type (get-in request [:headers "content-type"])]
(seq (re-find #"^application/(.+\+)?json" type))))

(defn- read-json [request & [{:keys [keywords? key-fn]}]]
(when (json-request? request)
(when-let [^InputStream body (:body request)]
(let [^String encoding (or (character-encoding request)
"UTF-8")
body-reader (java.io.InputStreamReader. body encoding)]
(try
[true (json/parse-stream body-reader (or key-fn keywords?))]
(catch Exception _
(println "Error parsing json stream")
[false nil]))))))

(def ^{:doc "The default response to return when a JSON request is malformed."}
default-malformed-response
{:status 400
:headers {"Content-Type" "text/plain"}
:body "Malformed JSON in request body."})

(defn json-body-request
"Parse a JSON request body and assoc it back into the :body key. Returns nil
if the JSON is malformed. See: wrap-json-body."
[request options]
(if-let [[valid? json] (read-json request options)]
(when valid? (assoc request :body json))
request))

(defn wrap-json-body
"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.

Accepts the following options:

:key-fn - function that will be applied to each key
:keywords? - true if the keys of maps should be turned into keywords
:bigdecimals? - true if BigDecimals should be used instead of Doubles
:malformed-response - a response map to return when the JSON is malformed"
{:arglists '([handler] [handler options])}
[handler & [{:keys [malformed-response]
:or {malformed-response default-malformed-response}
:as options}]]
(fn
([request]
(if-let [request (json-body-request request options)]
(handler request)
malformed-response))
([request respond raise]
(if-let [request (json-body-request request options)]
(handler request respond raise)
(respond malformed-response)))))

(defn- assoc-json-params [request json]
(if (map? json)
(-> request
(assoc :json-params json)
(update-in [:params] merge json))
request))

(defn json-params-request
"Parse the body of JSON requests into a map of parameters, which are added
to the request map on the :json-params and :params keys. Returns nil if the
JSON is malformed. See: wrap-json-params."
[request options]
(if-let [[valid? json] (read-json request options)]
(if valid? (assoc-json-params request json))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[clj-kondo] reported by reviewdog 🐶
Missing else branch.

request))

(defn wrap-json-params
"Middleware that parses the body of JSON requests into a map of parameters,
which are added to the request map on the :json-params and :params keys.

Accepts the following options:

:key-fn - function that will be applied to each key
:bigdecimals? - true if BigDecimals should be used instead of Doubles
:malformed-response - a response map to return when the JSON is malformed

Use the standard Ring middleware, ring.middleware.keyword-params, to
convert the parameters into keywords."
{:arglists '([handler] [handler options])}
[handler & [{:keys [malformed-response]
:or {malformed-response default-malformed-response}
:as options}]]
(fn
([request]
(if-let [request (json-params-request request options)]
(handler request)
malformed-response))
([request respond raise]
(if-let [request (json-params-request request options)]
(handler request respond raise)
(respond malformed-response)))))

(defn json-response
"Converts responses with a map or a vector for a body into a JSON response.
See: wrap-json-response."
[response options]
(if (coll? (:body response))
(let [json-resp (update-in response [:body] json/generate-string options)]
(if (contains? (:headers response) "Content-Type")
json-resp
(content-type json-resp "application/json; charset=utf-8")))
response))

(defn wrap-json-response
"Middleware that converts responses with a map or a vector for a body into a
JSON response.

Accepts the following options:

:key-fn - function that will be applied to each key
:pretty - true if the JSON should be pretty-printed
:escape-non-ascii - true if non-ASCII characters should be escaped with \\u
:stream? - true to create JSON body as stream rather than string"
{:arglists '([handler] [handler options])}
[handler & [{:as options}]]
(fn
([request]
(json-response (handler request) options))
([request respond raise]
(handler request (fn [response] (respond (json-response response options))) raise))))
4 changes: 2 additions & 2 deletions template/bb/template.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
language: bb
fprocess: ./index.clj
fprocess: bb --main index
welcome_message: |
You have created a new Function which uses Babashka
You have created a new HTTP Function which uses Babashka
53 changes: 53 additions & 0 deletions template/bb/tests.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
(ns tests
(:require
[index]
[clojure.test :refer [run-tests]]
[eg :refer [eg]]))

(eg index/keywords?
"true" => true
"false" => false
nil => true)

(eg index/read-string
"0A" => "0A"
"0" => 0
"abc" => string?)

(eg index/->kebab-case
"" => ""
"Boo_baR" => "boo-bar")

(eg index/format-context
{} => {}
{"Foo_baR" "false"} => {:foo-bar false})

(eg index/->context
[{} {}] => {:headers {} :env {}}
[{"Foo_baR" "[]"} {"eggs" "4.3"}] => {:headers {:foo-bar []}
:env {:eggs 4.3}})

(defn arity-2-handler [{:keys [bar] :as a} {:keys [headers env]}]
ccfontes marked this conversation as resolved.
Show resolved Hide resolved
[bar (get headers :content-type) (:my-env env)])

(def handler (index/->handler (var arity-2-handler) {"my-env" "env-val"}))

(eg handler
{:headers {} :body {}} => {:body [nil nil "env-val"] :status 200}
{:headers {"content-type" "application/json"}, :body {:bar "foo"}} => {:body ["foo" "application/json" "env-val"] :status 200})

(def app (index/->app (var arity-2-handler) {"MY_ENV" "env-val"}))

(def str->stream #(-> % (.getBytes "UTF-8") (java.io.ByteArrayInputStream.)))

(def resp-fixture {:headers {"Content-Type" "application/json; charset=utf-8"}
:body "[\"spam\",\"application/json\",\"env-val\"]"
:status 200})

(eg app
{:headers {"content-type" "application/json"}, :body (str->stream "{\"bar\": \"spam\"}")} => resp-fixture)

; TODO check examples of ring apps

(defn -main []
(run-tests 'tests))
Loading