reitit/doc/ring/compiling_middleware.md

113 lines
5 KiB
Markdown
Raw Normal View History

2017-09-18 05:30:03 +00:00
# Compiling Middleware
2017-09-14 13:33:36 +00:00
2021-07-27 16:33:17 +00:00
The [dynamic extensions](dynamic_extensions.md) 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.
2017-09-14 13:33:36 +00:00
2021-07-27 16:33:17 +00:00
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?)
2017-09-14 13:33:36 +00:00
2018-02-11 17:15:25 +00:00
To enable this we use [middleware records](data_driven_middleware.md) `:compile` key instead of the normal `:wrap`. `:compile` expects a function of `route-data router-opts => ?IntoMiddleware`.
2017-09-14 13:33:36 +00:00
2021-07-27 16:33:17 +00:00
To demonstrate the two approaches, below is the response coercion middleware written as normal ring middleware function and as middleware record with `:compile`.
2017-09-14 13:33:36 +00:00
2017-11-27 06:02:35 +00:00
## Normal Middleware
2017-09-14 13:33:36 +00:00
2018-02-11 17:15:25 +00:00
* Reads the compiled route information on every request. Everything is done at request-time.
2017-09-14 13:33:36 +00:00
```clj
(defn wrap-coerce-response
2017-12-15 06:20:53 +00:00
"Middleware for pluggable response coercion.
Expects a :coercion of type `reitit.coercion/Coercion`
2017-11-18 10:47:16 +00:00
and :responses from route data, otherwise will do nothing."
2017-09-14 13:33:36 +00:00
[handler]
(fn
([request]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
2017-11-18 10:47:16 +00:00
responses (-> match :result method :data :responses)
coercion (-> match :data :coercion)
opts (-> match :data :opts)]
2017-09-14 13:33:36 +00:00
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(coerce-response coercers request response))
response)))
2017-09-14 13:33:36 +00:00
([request respond raise]
(let [method (:request-method request)
2017-09-14 13:33:36 +00:00
match (ring/get-match request)
2017-11-18 10:47:16 +00:00
responses (-> match :result method :data :responses)
coercion (-> match :data :coercion)
opts (-> match :data :opts)]
2017-09-14 13:33:36 +00:00
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
2017-09-14 13:33:36 +00:00
(handler request #(respond (coerce-response coercers request %))))
(handler request respond raise))))))
```
2017-11-27 06:02:35 +00:00
## Compiled Middleware
2017-09-14 13:33:36 +00:00
2018-02-11 17:15:25 +00:00
* Route information is provided at creation-time
* Coercers are compiled at creation-time
* Middleware mounts only if `:coercion` and `:responses` are defined for the route
2017-12-31 10:06:21 +00:00
* Also defines spec for the route data `:responses` for the [route data validation](route_data_validation.md).
2017-09-14 13:33:36 +00:00
```clj
2017-12-31 10:06:21 +00:00
(require '[reitit.spec :as rs])
2017-12-03 15:28:24 +00:00
(def coerce-response-middleware
2017-11-27 06:02:35 +00:00
"Middleware for pluggable response coercion.
2017-12-15 06:20:53 +00:00
Expects a :coercion of type `reitit.coercion/Coercion`
2017-11-18 10:47:16 +00:00
and :responses from route data, otherwise does not mount."
2017-12-15 06:20:53 +00:00
{:name ::coerce-response
2017-12-31 10:06:21 +00:00
:spec ::rs/responses
2017-12-15 06:20:53 +00:00
: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)))))))})
2017-09-14 13:33:36 +00:00
```
2018-02-11 17:15:25 +00:00
It has 50% less code, it's much easier to reason about and is much faster.
### Require Keys on Routes at Creation Time
Often it is useful to require a route to provide a specific key.
```clj
(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, `{}`.
```clj
(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.