mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 17:01:11 +00:00
welcome, first class data-driven Middleware.
This commit is contained in:
parent
c7c4013f97
commit
76f7f28591
5 changed files with 210 additions and 112 deletions
74
README.md
74
README.md
|
|
@ -7,7 +7,7 @@ A friendly data-driven router for Clojure(Script).
|
|||
* Generic, not tied to HTTP
|
||||
* [Route conflict resolution](#route-conflicts)
|
||||
* [Pluggable coercion](#parameter-coercion) ([clojure.spec](https://clojure.org/about/spec))
|
||||
* Middleware & Interceptors
|
||||
* both Middleware & Interceptors
|
||||
* Extendable
|
||||
* Fast
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ Route trees should not have multiple routes that match to a single (request) pat
|
|||
|
||||
## Ring
|
||||
|
||||
[Ring](https://github.com/ring-clojure/ring)-router adds support for [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`.
|
||||
[Ring](https://github.com/ring-clojure/ring)-router adds support for ring [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It validates that all paths have a `:handler` defined and expands `:middleware` to create accumulated handlers for all request-methods. `reitit.ring/ring-handler` creates an actual ring handler out of a ring-router.
|
||||
|
||||
Simple Ring app:
|
||||
|
||||
|
|
@ -295,6 +295,11 @@ Reverse routing:
|
|||
|
||||
### Middleware
|
||||
|
||||
`:middleware` should be a vector of either of the following (expanded via the `reitit.middleware/ExpandMiddleware`:
|
||||
|
||||
1. a ring middleware function of `handler -> request -> response`
|
||||
2. a vector of middleware function (`handler args -> request -> response`) and it's args - actial middleware is created by applying function with handler and args
|
||||
|
||||
Let's define some middleware and a handler:
|
||||
|
||||
```clj
|
||||
|
|
@ -335,6 +340,50 @@ Middleware is applied correctly:
|
|||
; {:status 200, :body [:api :admin :db :delete :handler]}
|
||||
```
|
||||
|
||||
### Middleware Records
|
||||
|
||||
Besides just being opaque functions, middleware can be presented as first-class data entries, `reitit.middleware/Middleware` records. They are created with `reitit.middleware/create` function and must have a `:name` and either `:wrap` or `:gen` key with the actual middleware function or a [middleware generator function](#compiling-middleware).
|
||||
|
||||
When routes are compiled, middleware records are unwrapped into normal middleware functions producing no runtime performance penalty. Thanks to the `ExpandMiddleware` protocol, plain clojure(script) maps can also be used - they get expanded into middleware records.
|
||||
|
||||
The previous middleware re-written as records:
|
||||
|
||||
```clj
|
||||
(require '[reitit.middleware :as middleware])
|
||||
|
||||
(def wrap2
|
||||
(middleware/create
|
||||
{:name ::wrap
|
||||
:description "a nice little mw, takes 1 arg."
|
||||
:wrap wrap}))
|
||||
|
||||
(def wrap2-api
|
||||
{:name ::wrap-api
|
||||
:description "a nice little mw, :api as arg"
|
||||
:wrap (fn [handler]
|
||||
(wrap handler :api))})
|
||||
```
|
||||
|
||||
Or as maps:
|
||||
|
||||
```clj
|
||||
(require '[reitit.middleware :as middleware])
|
||||
|
||||
(def wrap3
|
||||
{:name ::wrap
|
||||
:description "a nice little mw, takes 1 arg."
|
||||
:wrap wrap})
|
||||
|
||||
(def wrap3-api
|
||||
{:name ::wrap-api
|
||||
:description "a nice little mw, :api as arg"
|
||||
:wrap (fn [handler]
|
||||
(wrap handler :api))})
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
### Async Ring
|
||||
|
||||
All built-in middleware provide both the 2 and 3-arity, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) too.
|
||||
|
|
@ -399,9 +448,9 @@ Reitit ships with pluggable parameter coercion via `reitit.coercion.protocol/Coe
|
|||
**NOTE**: to use the spec-coercion, one needs to add the following dependencies manually to the project:
|
||||
|
||||
```clj
|
||||
[org.clojure/clojure "1.9.0-alpha17"]
|
||||
[org.clojure/clojure "1.9.0-alpha19"]
|
||||
[org.clojure/spec.alpha "0.1.123"]
|
||||
[metosin/spec-tools "0.3.2"]
|
||||
[metosin/spec-tools "0.3.3"]
|
||||
```
|
||||
|
||||
### Ring request and response coercion
|
||||
|
|
@ -489,11 +538,11 @@ If either request or response coercion fails, an descriptive error is thrown.
|
|||
|
||||
The [meta-data extensions](#meta-data-based-extensions) are a easy way to extend the system. Routes meta-data can be trasnformed into any shape (records, functions etc.) in route compilation, enabling easy access at request-time.
|
||||
|
||||
Still, we can do better. As we know the exact route interceptor/middleware is linked to, we can pass the (compiled) route information into the interceptor/middleware at creation-time. It can extract and transform relevant data just for it and pass it into the actual request-handler via a closure. We can do all the static local computations forehand, yielding much lighter runtime processing.
|
||||
Still, we can do better. As we know the exact route interceptor/middleware is linked to, we can pass the (compiled) route information into the interceptor/middleware at creation-time. It can extract and transform relevant data just for it and pass it into the actual request-handler via a closure. We can do all the static local computations forehand, yielding faster runtime processing.
|
||||
|
||||
For middleware, there is a helper `reitit.middleware/gen` for this. It takes a function of `route-meta router-opts => middleware` and returns a special record extending the internal middleware protocols so it can be mounted as normal middleware. The compiled middleware can also decide no to mount itsef byt returning `nil`. Why mount `wrap-enforce-roles` if there are no roles required for that route?
|
||||
To do this we use [middleware records](#middleware-records) `:gen` hook instead of the normal `:wrap`. `:gen` expects a function of `route-meta router-opts => wrap`. Instead of returning the actual middleware function, the middleware record can also decide no to mount itsef byt returning `nil`. Why mount `wrap-enforce-roles` for a route if there are no roles required for it?
|
||||
|
||||
To demonstrate the two approaches, below are response coercion middleware written in both ways (found in `reitit.coercion`):
|
||||
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen`. The actual codes are from `reitit.coercion`:
|
||||
|
||||
### Naive
|
||||
|
||||
|
|
@ -543,8 +592,9 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
|||
"Generator for pluggable response coercion middleware.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :responses from route meta, otherwise does not mount."
|
||||
(middleware/gen
|
||||
(fn [{:keys [responses coercion opts]} _]
|
||||
(middleware/create
|
||||
{:name ::coerce-response
|
||||
:gen (fn [{:keys [responses coercion opts]} _]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
|
|
@ -552,9 +602,11 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
|||
([request]
|
||||
(coerce-response coercers request (handler request)))
|
||||
([request respond raise]
|
||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))))
|
||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||
```
|
||||
|
||||
The `:gen` -version is both much easier to understand but also 2-4x faster on basic perf tests.
|
||||
|
||||
## Merging route-trees
|
||||
|
||||
*TODO*
|
||||
|
|
@ -577,7 +629,7 @@ Routers can be configured via options. Options allow things like [`clojure.spec`
|
|||
|
||||
| key | description |
|
||||
| -------------|-------------|
|
||||
| `:path` | Base-path for routes (default `""`)
|
||||
| `:path` | Base-path for routes
|
||||
| `:routes` | Initial resolved routes (default `[]`)
|
||||
| `:meta` | Initial expanded route-meta vector (default `[]`)
|
||||
| `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@
|
|||
[lein-cloverage "1.0.9"]
|
||||
[lein-codox "0.10.3"]]
|
||||
:jvm-opts ^:replace ["-server"]
|
||||
:dependencies [[org.clojure/clojure "1.9.0-alpha17"]
|
||||
:dependencies [[org.clojure/clojure "1.9.0-alpha19"]
|
||||
[org.clojure/clojurescript "1.9.660"]
|
||||
|
||||
[metosin/spec-tools "0.3.2"]
|
||||
[metosin/spec-tools "0.3.3"]
|
||||
[org.clojure/spec.alpha "0.1.123"]
|
||||
|
||||
[criterium "0.4.4"]
|
||||
|
|
|
|||
|
|
@ -134,8 +134,9 @@
|
|||
"Generator for pluggable request coercion middleware.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :parameters from route meta, otherwise does not mount."
|
||||
(middleware/gen
|
||||
(fn [{:keys [parameters coercion]} _]
|
||||
(middleware/create
|
||||
{:name ::coerce-parameters
|
||||
:gen (fn [{:keys [parameters coercion]} _]
|
||||
(if (and coercion parameters)
|
||||
(let [coercers (request-coercers coercion parameters)]
|
||||
(fn [handler]
|
||||
|
|
@ -145,7 +146,7 @@
|
|||
(handler (impl/fast-assoc request :parameters coerced))))
|
||||
([request respond raise]
|
||||
(let [coerced (coerce-parameters coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))))
|
||||
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
|
||||
|
||||
(defn wrap-coerce-response
|
||||
"Pluggable response coercion middleware.
|
||||
|
|
@ -182,8 +183,9 @@
|
|||
"Generator for pluggable response coercion middleware.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :responses from route meta, otherwise does not mount."
|
||||
(middleware/gen
|
||||
(fn [{:keys [responses coercion opts]} _]
|
||||
(middleware/create
|
||||
{:name ::coerce-response
|
||||
:gen (fn [{:keys [responses coercion opts]} _]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
|
|
@ -191,5 +193,5 @@
|
|||
([request]
|
||||
(coerce-response coercers request (handler request)))
|
||||
([request respond raise]
|
||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))))
|
||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
(ns reitit.middleware
|
||||
(:require [meta-merge.core :refer [meta-merge]]
|
||||
[reitit.core :as reitit])
|
||||
#?(:clj
|
||||
(:import (clojure.lang IFn AFn))))
|
||||
[reitit.core :as reitit]))
|
||||
|
||||
(defprotocol ExpandMiddleware
|
||||
(expand-middleware [this meta opts]))
|
||||
|
||||
(defrecord MiddlewareGenerator [f args]
|
||||
IFn
|
||||
(invoke [_]
|
||||
(f nil nil))
|
||||
(invoke [_ meta]
|
||||
(f meta nil))
|
||||
(invoke [_ meta opts]
|
||||
(f meta opts))
|
||||
#?(:clj
|
||||
(applyTo [this args]
|
||||
(AFn/applyToHelper this args))))
|
||||
(defrecord Middleware [name wrap create])
|
||||
|
||||
(defn create [{:keys [name gen wrap] :as m}]
|
||||
(when-not name
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Middleware must have :name defined " m) m)))
|
||||
(when (and gen wrap)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Middleware can't both :wrap and :gen defined " m) m)))
|
||||
(map->Middleware m))
|
||||
|
||||
(extend-protocol ExpandMiddleware
|
||||
|
||||
|
|
@ -32,11 +31,24 @@
|
|||
:cljs function)
|
||||
(expand-middleware [this _ _] this)
|
||||
|
||||
MiddlewareGenerator
|
||||
#?(:clj clojure.lang.PersistentArrayMap
|
||||
:cljs cljs.core.PersistentArrayMap)
|
||||
(expand-middleware [this meta opts]
|
||||
(if-let [mw (this meta opts)]
|
||||
(expand-middleware (create this) meta opts))
|
||||
|
||||
#?(:clj clojure.lang.PersistentHashMap
|
||||
:cljs cljs.core.PersistentHashMap)
|
||||
(expand-middleware [this meta opts]
|
||||
(expand-middleware (create this) meta opts))
|
||||
|
||||
Middleware
|
||||
(expand-middleware [{:keys [wrap gen]} meta opts]
|
||||
(if gen
|
||||
(if-let [wrap (gen meta opts)]
|
||||
(fn [handler & args]
|
||||
(apply mw handler args))))
|
||||
(apply wrap handler args)))
|
||||
(fn [handler & args]
|
||||
(apply wrap handler args))))
|
||||
|
||||
nil
|
||||
(expand-middleware [_ _ _]))
|
||||
|
|
@ -56,9 +68,6 @@
|
|||
(keep identity)
|
||||
(apply comp identity)))
|
||||
|
||||
(defn gen [f & args]
|
||||
(->MiddlewareGenerator f args))
|
||||
|
||||
(defn compile-handler
|
||||
([route opts]
|
||||
(compile-handler route opts nil))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
(ns reitit.middleware-test
|
||||
(:require [clojure.test :refer [deftest testing is]]
|
||||
(:require [clojure.test :refer [deftest testing is are]]
|
||||
[reitit.middleware :as middleware]
|
||||
[clojure.set :as set]
|
||||
[reitit.core :as reitit])
|
||||
|
|
@ -26,61 +26,96 @@
|
|||
(respond (handler request))))
|
||||
|
||||
(deftest expand-middleware-test
|
||||
(testing "middleware generators"
|
||||
(let [calls (atom 0)]
|
||||
|
||||
(testing "record generator"
|
||||
(testing "middleware records"
|
||||
|
||||
(testing ":name is mandatory"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Middleware must have :name defined"
|
||||
(middleware/create
|
||||
{:wrap identity
|
||||
:gen (constantly identity)}))))
|
||||
|
||||
(testing ":wrap & :gen are exclusive"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Middleware can't both :wrap and :gen defined"
|
||||
(middleware/create
|
||||
{:name ::test
|
||||
:wrap identity
|
||||
:gen (constantly identity)}))))
|
||||
|
||||
(testing ":wrap"
|
||||
(let [calls (atom 0)
|
||||
data {:name ::test
|
||||
:wrap (fn [handler value]
|
||||
(swap! calls inc)
|
||||
(fn [request]
|
||||
[value request]))}]
|
||||
|
||||
(testing "as map"
|
||||
(reset! calls 0)
|
||||
(let [syntax [(middleware/gen
|
||||
(fn [meta _]
|
||||
(let [app ((middleware/compose-middleware [data] :meta {}) identity :value)]
|
||||
(dotimes [_ 10]
|
||||
(is (= [:value :request] (app :request)))
|
||||
(is (= 1 @calls)))))
|
||||
|
||||
(testing "direct"
|
||||
(reset! calls 0)
|
||||
(let [app ((middleware/compose-middleware [(middleware/create data)] :meta {}) identity :value)]
|
||||
(dotimes [_ 10]
|
||||
(is (= [:value :request] (app :request)))
|
||||
(is (= 1 @calls)))))
|
||||
|
||||
(testing "vector"
|
||||
(reset! calls 0)
|
||||
(let [app ((middleware/compose-middleware [[(middleware/create data) :value]] :meta {}) identity)]
|
||||
(dotimes [_ 10]
|
||||
(is (= [:value :request] (app :request)))
|
||||
(is (= 1 @calls)))))))
|
||||
|
||||
(testing ":gen"
|
||||
(let [calls (atom 0)
|
||||
data {:name ::test
|
||||
:gen (fn [meta _]
|
||||
(swap! calls inc)
|
||||
(fn [handler value]
|
||||
(swap! calls inc)
|
||||
(fn [request]
|
||||
[meta value request]))))]
|
||||
app ((middleware/compose-middleware syntax :meta {}) identity :value)]
|
||||
[meta value request])))}]
|
||||
|
||||
(testing "as map"
|
||||
(reset! calls 0)
|
||||
(let [app ((middleware/compose-middleware [data] :meta {}) identity :value)]
|
||||
(dotimes [_ 10]
|
||||
(is (= [:meta :value :request] (app :request)))
|
||||
(is (= 2 @calls)))))
|
||||
|
||||
(testing "middleware generator as function"
|
||||
(testing "direct"
|
||||
(reset! calls 0)
|
||||
(let [syntax (middleware/gen
|
||||
(fn [meta _]
|
||||
(swap! calls inc)
|
||||
(fn [handler value]
|
||||
(swap! calls inc)
|
||||
(fn [request]
|
||||
[meta value request]))))
|
||||
app ((syntax :meta nil) identity :value)]
|
||||
(let [app ((middleware/compose-middleware [(middleware/create data)] :meta {}) identity :value)]
|
||||
(dotimes [_ 10]
|
||||
(is (= [:meta :value :request] (app :request)))
|
||||
(is (= 2 @calls)))))
|
||||
|
||||
(testing "generator vector"
|
||||
(testing "vector"
|
||||
(reset! calls 0)
|
||||
(let [syntax [[(middleware/gen
|
||||
(fn [meta _]
|
||||
(swap! calls inc)
|
||||
(fn [handler value]
|
||||
(swap! calls inc)
|
||||
(fn [request]
|
||||
[meta value request])))) :value]]
|
||||
app ((middleware/compose-middleware syntax :meta {}) identity)]
|
||||
(let [app ((middleware/compose-middleware [[(middleware/create data) :value]] :meta {}) identity)]
|
||||
(is (= [:meta :value :request] (app :request)))
|
||||
(dotimes [_ 10]
|
||||
(is (= [:meta :value :request] (app :request)))
|
||||
(is (= 2 @calls)))))
|
||||
|
||||
(testing "generator can return nil"
|
||||
(testing "nil unmounts the middleware"
|
||||
(reset! calls 0)
|
||||
(let [syntax [[(middleware/gen
|
||||
(fn [meta _])) :value]]
|
||||
(let [syntax [[(middleware/create
|
||||
{:name ::test
|
||||
:gen (fn [meta _])}) :value]]
|
||||
app ((middleware/compose-middleware syntax :meta {}) identity)]
|
||||
(is (= :request (app :request)))
|
||||
(dotimes [_ 10]
|
||||
(is (= :request (app :request)))))))))
|
||||
|
||||
(is (= :request (app :request))))))))))
|
||||
|
||||
(deftest middleware-router-test
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue