reitit/README.md

652 lines
20 KiB
Markdown
Raw Normal View History

2017-08-07 11:08:39 +00:00
# reitit [![Build Status](https://travis-ci.org/metosin/reitit.svg?branch=master)](https://travis-ci.org/metosin/reitit) [![Dependencies Status](https://jarkeeper.com/metosin/reitit/status.svg)](https://jarkeeper.com/metosin/reitit)
A friendly data-driven router for Clojure(Script).
2017-08-07 11:08:39 +00:00
* Simple data-driven route syntax
* First-class route meta-data
* Generic, not tied to HTTP
2017-08-22 15:33:02 +00:00
* [Route conflict resolution](#route-conflicts)
2017-08-30 10:24:01 +00:00
* [Pluggable coercion](#parameter-coercion) ([clojure.spec](https://clojure.org/about/spec))
* both Middleware & Interceptors
* Extendable
* Fast
2017-08-17 06:24:23 +00:00
Ships with example router for [Ring](#ring). See [Issues](https://github.com/metosin/reitit/issues) for roadmap.
2017-08-07 11:08:39 +00:00
## Latest version
[![Clojars Project](http://clojars.org/metosin/reitit/latest-version.svg)](http://clojars.org/metosin/reitit)
## Route Syntax
2017-08-07 11:08:39 +00:00
Routes are defined as vectors, which String path, optional (non-vector) route argument and optional child routes. Routes can be wrapped in vectors.
Simple route:
```clj
["/ping"]
```
Two routes:
```clj
2017-08-15 05:57:57 +00:00
[["/ping"]
["/pong"]]
```
Routes with meta-data:
```clj
2017-08-15 05:57:57 +00:00
[["/ping" ::ping]
["/pong" {:name ::pong}]]
```
Routes with path and catch-all parameters:
```clj
[["/users/:user-id"]
["/public/*path"]]
```
Nested routes with meta-data:
```clj
["/api"
["/admin" {:middleware [::admin]}
["/user" ::user]
["/db" ::db]
["/ping" ::ping]]
```
Same routes flattened:
```clj
[["/api/admin/user" {:middleware [::admin], :name ::user}
["/api/admin/db" {:middleware [::admin], :name ::db}
["/api/ping" ::ping]]
```
2017-08-19 13:04:44 +00:00
## Routing
2017-08-22 15:33:02 +00:00
For routing, a `Router` is needed. Reitit ships with several different router implementations: `:linear-router`, `:lookup-router` and `:mixed-router`, based on the awesome [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation.
2017-08-30 10:24:01 +00:00
`Router` is created with `reitit.core/router`, which takes routes and optional options map as arguments. The route tree gets expanded, optionally coerced and compiled. Actual `Router` implementation is selected automatically but can be defined with a `:router` option. `Router` support both path- and name-based lookups.
2017-08-15 05:57:57 +00:00
Creating a router:
```clj
(require '[reitit.core :as reitit])
(def router
(reitit/router
[["/api"
["/ping" ::ping]
2017-08-19 13:04:44 +00:00
["/user/:id" ::user]]]))
2017-08-15 05:57:57 +00:00
```
2017-08-30 10:24:01 +00:00
`:mixed-router` is created (both static & wild routes are found):
2017-08-15 05:57:57 +00:00
```clj
2017-09-08 05:17:45 +00:00
(reitit/router-name router)
2017-08-22 15:33:02 +00:00
; :mixed-router
```
2017-08-15 05:57:57 +00:00
The expanded routes:
```clj
(reitit/routes router)
; [["/api/ping" {:name :user/ping}]
; ["/api/user/:id" {:name :user/user}]]
```
2017-08-19 13:04:13 +00:00
Route names:
```clj
(reitit/route-names router)
; [:user/ping :user/user]
```
2017-08-22 15:33:02 +00:00
### Path-based routing
```clj
(reitit/match-by-path router "/hello")
; nil
(reitit/match-by-path router "/api/user/1")
; #Match{:template "/api/user/:id"
; :meta {:name :user/user}
; :path "/api/user/1"
; :result nil
; :params {:id "1"}}
```
2017-08-22 15:33:02 +00:00
### Name-based (reverse) routing
```clj
(reitit/match-by-name router ::user)
2017-08-19 13:04:13 +00:00
; #PartialMatch{:template "/api/user/:id",
; :meta {:name :user/user},
; :result nil,
2017-08-19 13:04:13 +00:00
; :params nil,
; :required #{:id}}
(reitit/partial-match? (reitit/match-by-name router ::user))
; true
```
2017-08-22 15:33:02 +00:00
Only a partial match. Let's provide the path-parameters:
2017-08-19 13:04:13 +00:00
```clj
(reitit/match-by-name router ::user {:id "1"})
; #Match{:template "/api/user/:id"
; :meta {:name :user/user}
; :path "/api/user/1"
; :result nil
; :params {:id "1"}}
```
2017-08-19 13:04:13 +00:00
There is also a exception throwing version:
```
(reitit/match-by-name! router ::user)
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
```
## Route meta-data
Routes can have arbitrary meta-data. For nested routes, the meta-data is accumulated from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge).
A router based on nested route tree:
```clj
(reitit/router
2017-08-15 06:14:02 +00:00
["/api" {:interceptors [::api]}
["/ping" ::ping]
2017-08-30 10:24:01 +00:00
["/admin" {:roles #{:admin}}
["/users" ::users]
["/db" {:interceptors [::db], :roles ^:replace #{:db-admin}}
["/:db" {:parameters {:db String}}
["/drop" ::drop-db]
["/stats" ::db-stats]]]]]))
```
2017-08-15 05:57:57 +00:00
Resolved route tree:
```clj
2017-08-15 06:14:02 +00:00
(reitit/routes router)
2017-08-30 10:24:01 +00:00
; [["/api/ping" {:interceptors [::api]
; :name ::ping}]
; ["/api/admin/users" {:interceptors [::api]
; :roles #{:admin}
; :name ::users}]
; ["/api/admin/db/:db/drop" {:interceptors [::api ::db]
; :roles #{:db-admin}
; :parameters {:db String}
; :name ::drop-db}]
; ["/api/admin/db/:db/stats" {:interceptors [::api ::db]
; :roles #{:db-admin}
; :parameters {:db String}
; :name ::db-stats}]]
```
Path-based routing:
```clj
2017-08-30 10:24:01 +00:00
(reitit/match-by-path router "/api/admin/users")
; #Match{:template "/api/admin/users"
; :meta {:interceptors [::api]
; :roles #{:admin}
; :name ::users}
; :result nil
2017-08-30 10:24:01 +00:00
; :params {}
; :path "/api/admin/users"}
```
2017-08-22 15:33:02 +00:00
On match, route meta-data is returned and can interpreted by the application.
2017-08-15 05:57:57 +00:00
Routers also support meta-data compilation enabling things like fast [Ring](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:result` in the match. See [configuring routers](#configuring-routers) for details.
2017-08-15 05:57:57 +00:00
2017-08-22 15:33:02 +00:00
## Route conflicts
Route trees should not have multiple routes that match to a single (request) path. `router` checks the route tree at creation for conflicts and calls a registered `:conflicts` option callback with the found conflicts. Default implementation throws `ex-info` with a descriptive message.
```clj
(reitit/router
[["/ping"]
["/:user-id/orders"]
["/bulk/:bulk-id"]
["/public/*path"]
["/:version/status"]])
; CompilerException clojure.lang.ExceptionInfo: router contains conflicting routes:
;
; /:user-id/orders
; -> /public/*path
; -> /bulk/:bulk-id
;
2017-08-22 15:33:02 +00:00
; /bulk/:bulk-id
; -> /:version/status
;
2017-08-22 15:33:02 +00:00
; /public/*path
; -> /:version/status
;
```
2017-08-15 05:57:57 +00:00
## Ring
[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.
2017-08-22 15:33:02 +00:00
Simple Ring app:
2017-08-15 05:57:57 +00:00
```clj
(require '[reitit.ring :as ring])
(defn handler [_]
{:status 200, :body "ok"})
(def app
(ring/ring-handler
(ring/router
["/ping" handler])))
```
The expanded routes:
```clj
(-> app (ring/get-router) (reitit/routes))
; [["/ping" {:handler #object[...]}]]
```
Applying the handler:
```clj
(app {:request-method :get, :uri "/favicon.ico"})
; nil
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}
```
2017-08-22 15:33:02 +00:00
### Request-method based routing
2017-08-15 05:57:57 +00:00
```clj
(def app
(ring/ring-handler
(ring/router
2017-08-19 13:04:44 +00:00
["/ping" {:name ::ping
:get handler
2017-08-15 05:57:57 +00:00
:post handler}])))
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}
(app {:request-method :put, :uri "/ping"})
; nil
```
2017-08-19 13:04:44 +00:00
Reverse routing:
```clj
(-> app
(ring/get-router)
(reitit/match-by-name ::ping)
:path)
; "/ping"
```
2017-08-22 15:33:02 +00:00
### 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
2017-08-22 15:33:02 +00:00
Let's define some middleware and a handler:
2017-08-15 05:57:57 +00:00
```clj
(defn wrap [handler id]
(fn [request]
(handler (update request ::acc (fnil conj []) id))))
(defn wrap-api [handler]
(wrap handler :api))
(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}}]]])))
```
2017-08-15 06:14:02 +00:00
Middleware is applied correctly:
2017-08-15 05:57:57 +00:00
```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
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))})
```
2017-08-22 15:33:02 +00:00
### Async Ring
2017-08-30 10:24:01 +00:00
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.
2017-08-15 05:57:57 +00:00
2017-08-15 08:06:26 +00:00
### Meta-data based extensions
The routing `Match` is injected into a request and can be extracted with `reitit.ring/get-match`. It can be used to build dynamic extensions to the system.
2017-08-15 08:06:26 +00:00
A middleware to guard routes:
```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"}
```
2017-08-30 10:24:01 +00:00
## Parameter coercion
Reitit ships with pluggable parameter coercion via `reitit.coercion.protocol/Coercion` protocol. `reitit.coercion.spec/SpecCoercion` provides implements it for [clojure.spec](https://clojure.org/about/spec) & [data-specs](https://github.com/metosin/spec-tools#data-specs).
**NOTE**: to use the spec-coercion, one needs to add the following dependencies manually to the project:
```clj
[org.clojure/clojure "1.9.0-alpha19"]
2017-08-30 10:24:01 +00:00
[org.clojure/spec.alpha "0.1.123"]
[metosin/spec-tools "0.3.3"]
2017-08-30 10:24:01 +00:00
```
### Ring request and response coercion
To use `Coercion` with Ring, one needs to do the following:
1. Define parameters and responses as data into route meta-data, in format adopted from [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example):
* `:parameters` map, with submaps for different parameters: `:query`, `:body`, `:form`, `:header` and `:path`. Parameters are defined in the format understood by the `Coercion`.
* `:responses` map, with response status codes as keys (or `:default` for "everything else") with maps with `:schema` and optionally `:description` as values.
2. Define a `Coercion` to route meta-data under `:coercion`
3. Mount request & response coercion middleware to the routes.
If the request coercion succeeds, the coerced parameters are injected into request under `:parameters`.
If either request or response coercion fails, an descriptive error is thrown.
#### Example with data-specs
```clj
(require '[reitit.ring :as ring])
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.spec :as spec])
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" {:parameters {:body {:x int?, :y int?}}
:responses {200 {:schema {:total pos-int?}}}
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
{:meta {:middleware [coercion/gen-wrap-coerce-parameters
coercion/gen-wrap-coerce-response]
:coercion spec/coercion}})))
```
```clj
(app
{:request-method :get
:uri "/api/ping"
:body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}
```
#### Example with specs
```clj
(require '[reitit.ring :as ring])
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.spec :as spec])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])
(s/def ::x (st/spec int?))
(s/def ::y (st/spec int?))
(s/def ::total int?)
(s/def ::request (s/keys :req-un [::x ::y]))
(s/def ::response (s/keys :req-un [::total]))
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" {:parameters {:body ::request}
:responses {200 {:schema ::response}}
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
{:meta {:middleware [coercion/gen-wrap-coerce-parameters
coercion/gen-wrap-coerce-response]
:coercion spec/coercion}})))
```
```clj
(app
{:request-method :get
:uri "/api/ping"
:body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}
```
## Compiling Middleware
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 faster runtime processing.
2017-08-30 10:24:01 +00:00
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?
2017-08-30 10:24:01 +00:00
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`:
2017-08-30 10:24:01 +00:00
### Naive
* Extracts the compiled route information on every request.
```clj
(defn wrap-coerce-response
"Pluggable response coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responeses from route meta, otherwise does not mount."
[handler]
(fn
([request]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(coerce-response coercers request (handler request)))
(handler request))))
([request respond raise]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(handler request #(respond (coerce-response coercers request %))))
(handler request respond raise))))))
```
### Compiled
* Route information is provided via a closure
* Pre-compiled coercers
* Mounts only if `:coercion` and `:responses` are defined for the route
```clj
(def gen-wrap-coerce-response
"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/create
{:name ::coerce-response
:gen (fn [{:keys [responses coercion 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)))))))}))
2017-08-30 10:24:01 +00:00
```
The `:gen` -version is both much easier to understand but also 2-4x faster on basic perf tests.
## Merging route-trees
2017-08-15 05:57:57 +00:00
*TODO*
2017-08-15 05:57:57 +00:00
## Validating meta-data
2017-08-15 05:57:57 +00:00
*TODO*
2017-08-15 05:57:57 +00:00
2017-08-30 10:24:01 +00:00
## Swagger & Openapi
2017-08-15 05:57:57 +00:00
*TODO*
2017-08-15 05:57:57 +00:00
## Interceptors
*TODO*
2017-08-15 05:57:57 +00:00
## Configuring Routers
Routers can be configured via options. Options allow things like [`clojure.spec`](https://clojure.org/about/spec) validation for meta-data and fast, compiled handlers. The following options are available for the `reitit.core/router`:
2017-08-21 06:02:03 +00:00
| key | description |
| -------------|-------------|
| `:path` | Base-path for routes
2017-08-21 06:02:03 +00:00
| `: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`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler
| `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`)
| `:router` | Function of `routes opts => router` to override the actual router implementation
## Special thanks
To all Clojure(Script) routing libs out there, expecially to
[Ataraxy](https://github.com/weavejester/ataraxy), [Bide](https://github.com/funcool/bide), [Bidi](https://github.com/juxt/bidi), [Compojure](https://github.com/weavejester/compojure) and
2017-08-09 07:36:57 +00:00
[Pedestal](https://github.com/pedestal/pedestal/tree/master/route).
2017-08-07 11:08:39 +00:00
## License
Copyright © 2017 [Metosin Oy](http://www.metosin.fi)
2017-08-07 11:08:39 +00:00
Distributed under the Eclipse Public License, the same as Clojure.