Skip to content

Instantly share code, notes, and snippets.

@jeroenvandijk
Last active February 9, 2022 16:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeroenvandijk/85f57c168ef9a39cc9af7dfa360cee61 to your computer and use it in GitHub Desktop.
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?
;; 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