Merge pull request #54 from metosin/MiddlewareAddons

Middleware addons
This commit is contained in:
Tommi Reiman 2017-12-05 09:08:56 +02:00 committed by GitHub
commit 7b336bbe36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 118 additions and 93 deletions

View file

@ -4,9 +4,9 @@ The [dynamic extensions](dynamic_extensions.md) is a easy way to extend the syst
But, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware/interceptor at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding much faster runtime processing. It can also decide not to mount itself by returning `nil`. 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](data_driven_middleware.md) `:gen-wrap` key instead of the normal `:wrap`. `:gen-wrap` expects a function of `route-data router-opts => ?wrap`.
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 => ?wrap`.
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen-wrap`.
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:compile`.
## Normal Middleware
@ -49,7 +49,7 @@ To demonstrate the two approaches, below are response coercion middleware writte
* Mounts only if `:coercion` and `:responses` are defined for the route
```clj
(require '[reitit.ring.middleware :as middleware])
(require '[reitit.middleware :as middleware])
(def coerce-response-middleware
"Middleware for pluggable response coercion.
@ -57,15 +57,15 @@ To demonstrate the two approaches, below are response coercion middleware writte
and :responses from route data, otherwise does not mount."
(middleware/create
{:name ::coerce-response
:gen-wrap (fn [{:keys [coercion responses opts]} _]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]
(coerce-response coercers request (handler request)))
([request respond raise]
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
:compile (fn [{:keys [coercion responses opts]} _]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]
(coerce-response coercers request (handler request)))
([request respond raise]
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
```
The latter has 50% less code, is easier to reason about and is much faster.

View file

@ -11,7 +11,7 @@ Reitit defines middleware as data:
## Middleware as data
All values in the `:middleware` vector in the route data are coerced into `reitit.ring.middleware/Middleware` Records with using the `reitit.ring.middleware/IntoMiddleware` Protocol. By default, functions, maps and `Middleware` records are allowed.
All values in the `:middleware` vector in the route data are coerced into `reitit.middleware/Middleware` Records with using the `reitit.middleware/IntoMiddleware` Protocol. By default, functions, maps and `Middleware` records are allowed.
Records can have arbitrary keys, but the following keys have a special purpose:
@ -19,7 +19,7 @@ Records can have arbitrary keys, but the following keys have a special purpose:
| ---------------|-------------|
| `:name` | Name of the middleware as a qualified keyword (optional)
| `:wrap` | The actual middleware function of `handler & args => request => response`
| `:gen-wrap` | Middleware function generation function, see [compiling middleware](compiling_middleware.md).
| `:compile` | Middleware compilation function, see [compiling middleware](compiling_middleware.md).
Middleware Records are accessible in their raw form in the compiled route results, thus available for inventories, creating api-docs etc.
@ -40,7 +40,7 @@ The following produce identical middleware runtime function.
### Record
```clj
(require '[reitit.ring.middleware :as middleware])
(require '[reitit.middleware :as middleware])
(def wrap2
(middleware/create
@ -89,7 +89,7 @@ Middleware can be optimized against an endpoint using [middleware compilation](c
## Transforming the middleware chain
There is an extra option in ring-router (actually, in the undelaying middleware-router): `:reitit.ring.middleware/transform` to transform the middleware chain per endpoint. It sees the vector of compiled middleware and should return a new vector of middleware.
There is an extra option in ring-router (actually, in the undelaying middleware-router): `:reitit.middleware/transform` to transform the middleware chain per endpoint. It sees the vector of compiled middleware and should return a new vector of middleware.
#### Adding debug middleware between all other middleware

View file

@ -1,6 +1,6 @@
(ns example.server
(:require [ring.adapter.jetty :as jetty]
[reitit.ring.middleware :as middleware]
[reitit.middleware :as middleware]
[reitit.ring.coercion :as coercion]))
(defonce ^:private server (atom nil))

View file

@ -153,3 +153,6 @@
(defn fast-get
#?@(:clj [[^java.util.HashMap m k] (.get m k)]
:cljs [[m k] (m k)]))
(defn strip-nils [m]
(->> m (remove (comp nil? second)) (into {})))

View file

@ -1,6 +1,7 @@
(ns reitit.ring.middleware
(ns reitit.middleware
(:require [meta-merge.core :refer [meta-merge]]
[reitit.core :as r]))
[reitit.core :as r]
[reitit.impl :as impl]))
(defprotocol IntoMiddleware
(into-middleware [this data opts]))
@ -8,13 +9,15 @@
(defrecord Middleware [name wrap])
(defrecord Endpoint [data handler middleware])
(defn create [{:keys [name wrap gen-wrap] :as m}]
(when (and wrap gen-wrap)
(defn create [{:keys [name wrap compile] :as m}]
(when (and wrap compile)
(throw
(ex-info
(str "Middleware can't both :wrap and :gen-wrap defined " m) m)))
(str "Middleware can't have both :wrap and :compile defined " m) m)))
(map->Middleware m))
(def ^:dynamic *max-compile-depth* 10)
(extend-protocol IntoMiddleware
#?(:clj clojure.lang.APersistentVector
@ -40,14 +43,21 @@
(into-middleware (create this) data opts))
Middleware
(into-middleware [{:keys [wrap gen-wrap] :as this} data opts]
(if-not gen-wrap
(into-middleware [{:keys [compile] :as this} data opts]
(if-not compile
this
(if-let [wrap (gen-wrap data opts)]
(map->Middleware
(-> this
(dissoc :gen-wrap)
(assoc :wrap wrap))))))
(let [compiled (::compiled opts 0)
opts (assoc opts ::compiled (inc compiled))]
(when (>= compiled *max-compile-depth*)
(throw
(ex-info
(str "Too deep middleware compilation - " compiled)
{:this this, :data data, :opts opts})))
(if-let [middeware (into-middleware (compile data opts) data opts)]
(map->Middleware
(merge
(dissoc this :create)
(impl/strip-nils middeware)))))))
nil
(into-middleware [_ _ _]))
@ -68,13 +78,6 @@
(defn compile-handler [middleware handler]
((apply comp identity (keep :wrap middleware)) handler))
(compile-handler
[(map->Middleware
{:wrap
(fn [handler]
(fn [request]
(handler request)))})] identity)
(defn compile-result
([route opts]
(compile-result route opts nil))

View file

@ -1,6 +1,6 @@
(ns reitit.ring
(:require [meta-merge.core :refer [meta-merge]]
[reitit.ring.middleware :as middleware]
[reitit.middleware :as middleware]
[reitit.core :as r]
[reitit.impl :as impl]))
@ -82,7 +82,7 @@
:delete {:middleware [wrap-delete]
:handler delete-user}}]])
See router options from [[reitit.core/router]] and [[reitit.ring.middleware/router]]."
See router options from [[reitit.core/router]] and [[reitit.middleware/router]]."
([data]
(router data nil))
([data opts]

View file

@ -1,7 +1,7 @@
(ns reitit.ring.coercion
(:require [clojure.walk :as walk]
[spec-tools.core :as st]
[reitit.ring.middleware :as middleware]
[reitit.middleware :as middleware]
[reitit.ring.coercion.protocol :as protocol]
[reitit.ring :as ring]
[reitit.impl :as impl]))
@ -125,17 +125,17 @@
and :parameters from route data, otherwise does not mount."
(middleware/create
{:name ::coerce-parameters
:gen-wrap (fn [{:keys [coercion parameters]} opts]
(if (and coercion parameters)
(let [coercers (request-coercers coercion parameters opts)]
(fn [handler]
(fn
([request]
(let [coerced (coerce-request coercers request)]
(handler (impl/fast-assoc request :parameters coerced))))
([request respond raise]
(let [coerced (coerce-request coercers request)]
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
:compile (fn [{:keys [coercion parameters]} opts]
(if (and coercion parameters)
(let [coercers (request-coercers coercion parameters opts)]
(fn [handler]
(fn
([request]
(let [coerced (coerce-request coercers request)]
(handler (impl/fast-assoc request :parameters coerced))))
([request respond raise]
(let [coerced (coerce-request coercers request)]
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
(def coerce-response-middleware
"Middleware for pluggable response coercion.
@ -143,15 +143,15 @@
and :responses from route data, otherwise does not mount."
(middleware/create
{:name ::coerce-response
:gen-wrap (fn [{:keys [coercion responses]} opts]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]
(coerce-response coercers request (handler request)))
([request respond raise]
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
:compile (fn [{:keys [coercion responses]} opts]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]
(coerce-response coercers request (handler request)))
([request respond raise]
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
(def coerce-exceptions-middleware
"Middleware for handling coercion exceptions.
@ -159,17 +159,17 @@
and :parameters or :responses from route data, otherwise does not mount."
(middleware/create
{:name ::coerce-exceptions
:gen-wrap (fn [{:keys [coercion parameters responses]} _]
(if (and coercion (or parameters responses))
(fn [handler]
(fn
([request]
(try
(handler request)
(catch #?(:clj Exception :cljs js/Error) e
(handle-coercion-exception e identity #(throw %)))))
([request respond raise]
(try
(handler request respond #(handle-coercion-exception % respond raise))
(catch #?(:clj Exception :cljs js/Error) e
(handle-coercion-exception e respond raise))))))))}))
:compile (fn [{:keys [coercion parameters responses]} _]
(if (and coercion (or parameters responses))
(fn [handler]
(fn
([request]
(try
(handler request)
(catch #?(:clj Exception :cljs js/Error) e
(handle-coercion-exception e identity #(throw %)))))
([request respond raise]
(try
(handler request respond #(handle-coercion-exception % respond raise))
(catch #?(:clj Exception :cljs js/Error) e
(handle-coercion-exception e respond raise))))))))}))

View file

@ -7,3 +7,6 @@
(into [] (impl/segments "/api/ipa/beer/craft/bisse"))))
(is (= ["" "a" "" "b" "" "c" ""]
(into [] (impl/segments "/a//b//c/")))))
(deftest strip-nils-test
(is (= {:a 1, :c false} (impl/strip-nils {:a 1, :b nil, :c false}))))

View file

@ -1,6 +1,6 @@
(ns reitit.middleware-test
(:require [clojure.test :refer [deftest testing is are]]
[reitit.ring.middleware :as middleware]
[reitit.middleware :as middleware]
[clojure.set :as set]
[reitit.core :as r])
#?(:clj
@ -10,14 +10,14 @@
(testing "middleware records"
(testing ":wrap & :gen-wrap are exclusive"
(testing ":wrap & :compile are exclusive"
(is (thrown-with-msg?
ExceptionInfo
#"Middleware can't both :wrap and :gen-wrap defined"
#"Middleware can't have both :wrap and :compile defined"
(middleware/create
{:name ::test
:wrap identity
:gen-wrap (constantly identity)}))))
:compile (constantly identity)}))))
(testing "middleware"
(let [calls (atom 0)
@ -74,12 +74,17 @@
(testing "compiled Middleware"
(let [calls (atom 0)
mw {:gen-wrap (fn [data _]
mw {:compile (fn [data _]
(swap! calls inc)
(fn [handler value]
(swap! calls inc)
(fn [request]
[data value request])))}
mw3 {:compile (fn [data _]
(swap! calls inc)
(fn [handler value]
(swap! calls inc)
(fn [request]
[data value request])))}
{:compile (fn [data _]
(swap! calls inc)
mw)})}
->app (fn [ast handler]
(middleware/compile-handler
(middleware/expand ast :data {})
@ -99,9 +104,20 @@
(is (= [:data :value :request] (app :request)))
(is (= 2 @calls)))))
(testing "deeply compiled Middleware"
(reset! calls 0)
(let [app (->app [[(middleware/create mw3) :value]] identity)]
(dotimes [_ 10]
(is (= [:data :value :request] (app :request)))
(is (= 4 @calls)))))
(testing "too deeply compiled Middleware fails"
(binding [middleware/*max-compile-depth* 2]
(is (thrown? ExceptionInfo (->app [[(middleware/create mw3) :value]] identity)))))
(testing "nil unmounts the middleware"
(let [app (->app [{:gen-wrap (constantly nil)}
{:gen-wrap (constantly nil)}] identity)]
(let [app (->app [{:compile (constantly nil)}
{:compile (constantly nil)}] identity)]
(dotimes [_ 10]
(is (= :request (app :request))))))))))
@ -145,9 +161,9 @@
(testing "with nested middleware"
(is (= [:api :admin :ok :admin :api] (app "/api/admin/ping"))))
(testing ":gen-wrap middleware can be unmounted at creation-time"
(let [mw1 {:name ::mw1, :gen-wrap (constantly #(mw % ::mw1))}
mw2 {:name ::mw2, :gen-wrap (constantly nil)}
(testing ":compile middleware can be unmounted at creation-time"
(let [mw1 {:name ::mw1, :compile (constantly #(mw % ::mw1))}
mw2 {:name ::mw2, :compile (constantly nil)}
mw3 {:name ::mw3, :wrap #(mw % ::mw3)}
router (middleware/router
["/api" {:name ::api
@ -176,13 +192,13 @@
(let [mw (fn [handler value]
#(conj (handler (conj % value)) value))
handler #(conj % :ok)
mw1 {:gen-wrap (constantly #(mw % ::mw1))}
mw2 {:gen-wrap (constantly nil)}
mw1 {:compile (constantly #(mw % ::mw1))}
mw2 {:compile (constantly nil)}
mw3 {:wrap #(mw % ::mw3)}
mw4 #(mw % ::mw4)
mw5 {:gen-wrap (fn [{:keys [mount?]} _]
(when mount?
#(mw % ::mw5)))}
mw5 {:compile (fn [{:keys [mount?]} _]
(when mount?
#(mw % ::mw5)))}
chain1 (middleware/chain [mw1 mw2 mw3 mw4 mw5] handler {:mount? true})
chain2 (middleware/chain [mw1 mw2 mw3 mw4 mw5] handler {:mount? false})]
(is (= [::mw1 ::mw3 ::mw4 ::mw5 :ok ::mw5 ::mw4 ::mw3 ::mw1] (chain1 [])))

View file

@ -1,7 +1,7 @@
(ns reitit.ring-test
(:require [clojure.test :refer [deftest testing is]]
[clojure.set :as set]
[reitit.ring.middleware :as middleware]
[reitit.middleware :as middleware]
[reitit.ring :as ring]
[reitit.core :as r])
#?(:clj