mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 08:21:11 +00:00
206 lines
5.8 KiB
Markdown
206 lines
5.8 KiB
Markdown
|
|
# Ring support
|
||
|
|
|
||
|
|
[Ring](https://github.com/ring-clojure/ring)-router adds support for ring concepts like [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 runs a custom route compiler, creating a optimized stucture for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined.
|
||
|
|
|
||
|
|
Simple Ring app:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(require '[reitit.ring :as ring])
|
||
|
|
|
||
|
|
(defn handler [_]
|
||
|
|
{:status 200, :body "ok"})
|
||
|
|
|
||
|
|
(def app
|
||
|
|
(ring/ring-handler
|
||
|
|
(ring/router
|
||
|
|
["/ping" handler])))
|
||
|
|
```
|
||
|
|
|
||
|
|
Applying the handler:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(app {:request-method :get, :uri "/favicon.ico"})
|
||
|
|
; nil
|
||
|
|
|
||
|
|
(app {:request-method :get, :uri "/ping"})
|
||
|
|
; {:status 200, :body "ok"}
|
||
|
|
```
|
||
|
|
|
||
|
|
The expanded routes:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(-> app (ring/get-router) (reitit/routes))
|
||
|
|
; [["/ping"
|
||
|
|
; {:handler #object[...]}
|
||
|
|
; #Methods{:any #Endpoint{:meta {:handler #object[...]},
|
||
|
|
; :handler #object[...],
|
||
|
|
; :middleware []}}]]
|
||
|
|
```
|
||
|
|
|
||
|
|
Note that the compiled resuts as third element in the route vector.
|
||
|
|
|
||
|
|
### Request-method based routing
|
||
|
|
|
||
|
|
Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `:delete`, `:options`, `:post` or `:put`. Top-level handler is used if request-method based handler is not found.
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(def app
|
||
|
|
(ring/ring-handler
|
||
|
|
(ring/router
|
||
|
|
["/ping" {:name ::ping
|
||
|
|
:get handler
|
||
|
|
:post handler}])))
|
||
|
|
|
||
|
|
(app {:request-method :get, :uri "/ping"})
|
||
|
|
; {:status 200, :body "ok"}
|
||
|
|
|
||
|
|
(app {:request-method :put, :uri "/ping"})
|
||
|
|
; nil
|
||
|
|
```
|
||
|
|
|
||
|
|
Reverse routing:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(-> app
|
||
|
|
(ring/get-router)
|
||
|
|
(reitit/match-by-name ::ping)
|
||
|
|
:path)
|
||
|
|
; "/ping"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Middleware
|
||
|
|
|
||
|
|
Middleware can be added with a `:middleware` key, with a vector value of the following:
|
||
|
|
|
||
|
|
1. ring middleware function `handler -> request -> response`
|
||
|
|
2. vector of middleware function `handler ?args -> request -> response` and optinally it's args.
|
||
|
|
|
||
|
|
A middleware and a handler:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(defn wrap [handler id]
|
||
|
|
(fn [request]
|
||
|
|
(handler (update request ::acc (fnil conj []) id))))
|
||
|
|
|
||
|
|
(defn handler [{:keys [::acc]}]
|
||
|
|
{:status 200, :body (conj acc :handler)})
|
||
|
|
```
|
||
|
|
|
||
|
|
App with nested middleware:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(def app
|
||
|
|
(ring/ring-handler
|
||
|
|
(ring/router
|
||
|
|
["/api" {:middleware [#(wrap % :api)]}
|
||
|
|
["/ping" handler]
|
||
|
|
["/admin" {:middleware [[wrap :admin]]}
|
||
|
|
["/db" {:middleware [[wrap :db]]
|
||
|
|
:delete {:middleware [#(wrap % :delete)]
|
||
|
|
:handler handler}}]]])))
|
||
|
|
```
|
||
|
|
|
||
|
|
Middleware is applied correctly:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(app {:request-method :delete, :uri "/api/ping"})
|
||
|
|
; {:status 200, :body [:api :handler]}
|
||
|
|
```
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(app {:request-method :delete, :uri "/api/admin/db"})
|
||
|
|
; {:status 200, :body [:api :admin :db :delete :handler]}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Middleware Records
|
||
|
|
|
||
|
|
Reitit supports first-class data-driven middleware via `reitit.middleware/Middleware` records, created with `reitit.middleware/create` function. The following keys have special purpose:
|
||
|
|
|
||
|
|
| key | description |
|
||
|
|
| -----------|-------------|
|
||
|
|
| `:name` | Name of the middleware as qualified keyword (optional,recommended for libs)
|
||
|
|
| `:wrap` | The actual middleware function of `handler args? => request => response`
|
||
|
|
| `:gen` | Middleware compile function, see [compiling middleware](#compiling-middleware).
|
||
|
|
|
||
|
|
When routes are compiled, all middleware are expanded (and optionally compiled) into `Middleware` and stored in compilation results for later use (api-docs etc). For actual request processing, they are unwrapped into normal middleware functions producing zero runtime performance penalty. Middleware expansion is backed by `reitit.middleware/IntoMiddleware` protocol, enabling plain clojure(script) maps to be used.
|
||
|
|
|
||
|
|
A Record:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(require '[reitit.middleware :as middleware])
|
||
|
|
|
||
|
|
(def wrap2
|
||
|
|
(middleware/create
|
||
|
|
{:name ::wrap2
|
||
|
|
:description "a nice little mw, takes 1 arg."
|
||
|
|
:wrap wrap}))
|
||
|
|
```
|
||
|
|
|
||
|
|
As plain map:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
;; plain map
|
||
|
|
(def wrap3
|
||
|
|
{:name ::wrap3
|
||
|
|
:description "a nice little mw, :api as arg"
|
||
|
|
:wrap (fn [handler]
|
||
|
|
(wrap handler :api))})
|
||
|
|
```
|
||
|
|
|
||
|
|
### Async Ring
|
||
|
|
|
||
|
|
All built-in middleware provide both 2 and 3-arity and are compiled for both Clojure & ClojureScript, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) and [Node.js](https://nodejs.org) too.
|
||
|
|
|
||
|
|
### Meta-data based extensions
|
||
|
|
|
||
|
|
`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build dynamic extensions to the system.
|
||
|
|
|
||
|
|
Example middleware to guard routes based on user roles:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(require '[clojure.set :as set])
|
||
|
|
|
||
|
|
(defn wrap-enforce-roles [handler]
|
||
|
|
(fn [{:keys [::roles] :as request}]
|
||
|
|
(let [required (some-> request (ring/get-match) :meta ::roles)]
|
||
|
|
(if (and (seq required) (not (set/intersection required roles)))
|
||
|
|
{:status 403, :body "forbidden"}
|
||
|
|
(handler request)))))
|
||
|
|
```
|
||
|
|
|
||
|
|
Mounted to an app via router meta-data (effecting all routes):
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(def handler (constantly {:status 200, :body "ok"}))
|
||
|
|
|
||
|
|
(def app
|
||
|
|
(ring/ring-handler
|
||
|
|
(ring/router
|
||
|
|
[["/api"
|
||
|
|
["/ping" handler]
|
||
|
|
["/admin" {::roles #{:admin}}
|
||
|
|
["/ping" handler]]]]
|
||
|
|
{:meta {:middleware [wrap-enforce-roles]}})))
|
||
|
|
```
|
||
|
|
|
||
|
|
Anonymous access to public route:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(app {:request-method :get, :uri "/api/ping"})
|
||
|
|
; {:status 200, :body "ok"}
|
||
|
|
```
|
||
|
|
|
||
|
|
Anonymous access to guarded route:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(app {:request-method :get, :uri "/api/admin/ping"})
|
||
|
|
; {:status 403, :body "forbidden"}
|
||
|
|
```
|
||
|
|
|
||
|
|
Authorized access to guarded route:
|
||
|
|
|
||
|
|
```clj
|
||
|
|
(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}})
|
||
|
|
; {:status 200, :body "ok"}
|
||
|
|
```
|