The dynamic extensions are an easy way to extend the system. To enable fast lookup of route data, we can compile them into any shape (records, functions etc.), enabling fast access at request-time.
But, we can do much better. As we know the exact route that a middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware at creation-time. It can do local reasoning: Extract and transform relevant data just for it and pass the optimized data into the actual request-handler via a closure - yielding much faster runtime processing. A middleware can also decide not to mount itself by returning nil
. (E.g. Why mount a wrap-enforce-roles
middleware for a route if there are no roles required for it?)
To enable this we use middleware records :compile
key instead of the normal :wrap
. :compile
expects a function of route-data router-opts => ?IntoMiddleware
.
To demonstrate the two approaches, below is the response coercion middleware written as normal ring middleware function and as middleware record with :compile
.
(defn wrap-coerce-response
"Middleware for pluggable response coercion.
Expects a :coercion of type `reitit.coercion/Coercion`
and :responses from route data, otherwise will do nothing."
[handler]
(fn
([request]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :data :responses)
coercion (-> match :data :coercion)
opts (-> match :data :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(coerce-response coercers request response))
response)))
([request respond raise]
(let [method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :data :responses)
coercion (-> match :data :coercion)
opts (-> match :data :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(handler request #(respond (coerce-response coercers request %))))
(handler request respond raise))))))
:coercion
and :responses
are defined for the route:responses
for the route data validation.(require '[reitit.spec :as rs])
(def coerce-response-middleware
"Middleware for pluggable response coercion.
Expects a :coercion of type `reitit.coercion/Coercion`
and :responses from route data, otherwise does not mount."
{:name ::coerce-response
:spec ::rs/responses
:compile (fn [{:keys [coercion responses]} opts]
(if (and coercion responses)
(let [coercers (coercion/response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]
(coercion/coerce-response coercers request (handler request)))
([request respond raise]
(handler request #(respond (coercion/coerce-response coercers request %)) raise)))))))})
It has 50% less code, it's much easier to reason about and is much faster.
Often it is useful to require a route to provide a specific key.
(require '[buddy.auth.accessrules :as accessrules])
(s/def ::authorize
(s/or :handler :accessrules/handler :rule :accessrules/rule))
(def authorization-middleware
{:name ::authorization
:spec (s/keys :req-un [::authorize])
:compile
(fn [route-data _opts]
(when-let [rule (:authorize route-data)]
(fn [handler]
(accessrules/wrap-access-rules handler {:rules [rule]}))))})
In the example above the :spec
expresses that each route is required to provide the :authorize
key. However, in this case the compile function returns nil
when that key is missing, which means the middleware will not be mounted, the spec will not be considered, and the compiler will not enforce this requirement as intended.
If you just want to enforce the spec return a map without :wrap
or :compile
keys, e.g. an empty map, {}
.
(def authorization-middleware
{:name ::authorization
:spec (s/keys :req-un [::authorize])
:compile
(fn [route-data _opts]
(if-let [rule (:authorize route-data)]
(fn [handler]
(accessrules/wrap-access-rules handler {:rules [rule]}))
;; return empty map just to enforce spec
{}))})
The middleware (and associated spec) will still be part of the chain, but will not process the request.
Can you improve this documentation? These fine people already did:
Tommi Reiman, Phil Hofmann & Cody CanningEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close