Last active
February 9, 2022 16:00
-
-
Save jeroenvandijk/85f57c168ef9a39cc9af7dfa360cee61 to your computer and use it in GitHub Desktop.
An example of composing ring middleware in a more robust and intuitive way?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; When middleware comes with a certain dependency order things get a bit tedious and error prone. | |
;; Consider example 1 below: | |
(defn add-cmd [req cmd] | |
(update req :cmd (fnil conj []) cmd)) | |
(defn add-cmd-handler [cmd] | |
(fn [req] | |
(add-cmd req cmd))) | |
(defn call-app [middleware] | |
(let [app (ring/ring-handler | |
(ring/router | |
[["/" {:middleware middleware | |
:handler (add-cmd-handler :handler)}]]))] | |
(-> (app {:uri "/" :request-method :get}) | |
:cmd))) | |
;; Example 1: manually sorted middleware | |
(require '[reitit.ring :as ring]) | |
(let [add-pre-middleware | |
(fn [cmd] | |
(fn [handler] | |
(fn [req] | |
(handler (add-cmd req cmd))))) | |
add-post-middleware | |
(fn [cmd] | |
(fn [handler] | |
(fn [req] | |
(add-cmd (handler req) cmd)))) | |
sorted-middleware | |
[(add-pre-middleware :a) | |
(add-pre-middleware :b) | |
;; For post endpoint middleware we need to reverse the expected order ... | |
(add-post-middleware :e) | |
(add-post-middleware :d) | |
(add-post-middleware :c)]] | |
(call-app sorted-middleware)) ;=> [:a :b :middle :c :d :e] | |
;; What if we could at least make the ordering of the "middleware" more intuitively. See example 2: | |
(defn half-interceptors->middleware [sorted-half-interceptors] | |
(let [{:keys [enter leave]} | |
(group-by (fn [m] | |
(let [ks (filter m [:leave :enter])] | |
(if (= (count ks) 1) | |
(first ks) | |
(throw (ex-info "Expected either :leave or :enter" {:ks ks}))))) | |
sorted-half-interceptors) | |
middleware | |
(concat | |
(map (fn [{layer :enter}] | |
(fn [handler] | |
(fn [req] | |
(handler (layer req))))) | |
enter) | |
(map (fn [{layer :leave}] | |
(fn [handler] | |
(fn [req] | |
(layer (handler req))))) | |
(reverse leave)))] | |
middleware)) | |
(defn add-pre-interceptor [cmd] | |
{:enter (fn [req] (add-cmd req cmd))}) | |
(defn add-post-interceptor [cmd] | |
{:leave (fn [req] (add-cmd req cmd))}) | |
;; Example 2: usage of a label :enter or :leave (something like a half-interceptor as it cannot have both) | |
(let [sorted-half-interceptors | |
[(add-pre-interceptor :a) | |
(add-pre-interceptor :b) | |
;; -- | |
(add-post-interceptor :c) | |
(add-post-interceptor :d) | |
(add-post-interceptor :e)] | |
middleware (half-interceptors->middleware sorted-half-interceptors)] | |
(call-app middleware)) ;=> [:a :b :middle :c :d :e] | |
;; Given enough half-interceptors this can still become tedious. What if we add dependency | |
;; information by adding `:requires` and `:provides` to enable automatic sorting. See example 3: | |
(require '[reitit.dependency :as d]) | |
(defn compose-half-interceptors [half-interceptors] | |
(let [sorted (d/post-order half-interceptors)] | |
(half-interceptors->middleware sorted))) | |
;; Example 3: | |
(let [middleware (compose-half-interceptors | |
(shuffle | |
[{:requires #{:a} :provides #{:b} | |
:enter (add-cmd-handler :b)} | |
{:provides #{:a} | |
:enter (add-cmd-handler :a)} | |
{:provides #{:d} :requires #{:c} | |
:leave (add-cmd-handler :d)} | |
{:provides #{:c} | |
:leave (add-cmd-handler :c)} | |
{:provides #{:e} :requires #{:d} | |
:leave (add-cmd-handler :e)}]))] | |
(call-app middleware)) ;=> [:a :b :middle :c :d :e] | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment