reitit/README.md

379 lines
9.5 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)
Snappy data-driven router for Clojure(Script).
* Simple data-driven route syntax
* First-class route meta-data
* Generic, not tied to HTTP
* Extendable
* Fast
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 as the first element, then optional meta-data (non-vector) 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]]
```
Previous example flattened:
```clj
[["/api/admin/user" {:middleware [::admin], :name ::user}
["/api/admin/db" {:middleware [::admin], :name ::db}
["/api/ping" ::ping]]
```
## Routers
For actual routing, we need to create a `Router`. Reitit ships with 2 different router implementations: `LinearRouter` and `LookupRouter`, both based on the awesome [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation.
`Router` is created with `reitit.core/router`, which takes routes and optionally an options map as arguments. The route-tree gets expanded, optionally coerced and compiled to support both fast 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-11 05:14:15 +00:00
["/user/:id" ::user]]))
2017-08-15 05:57:57 +00:00
```
2017-08-15 06:14:02 +00:00
`LinearRouter` is created (as there are wildcard):
2017-08-15 05:57:57 +00:00
```clj
(class router)
; reitit.core.LinearRouter
```
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}]]
```
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"
; :handler nil
; :params {:id "1"}}
```
Name-based (reverse) routing:
```clj
(reitit/match-by-name router ::user)
; ExceptionInfo missing path-params for route '/api/user/:id': #{:id}
(reitit/match-by-name router ::user {:id "1"})
; #Match{:template "/api/user/:id"
; :meta {:name :user/user}
; :path "/api/user/1"
; :handler nil
; :params {:id "1"}}
```
## 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
2017-08-15 05:57:57 +00:00
(def router
(reitit/router
2017-08-15 06:14:02 +00:00
["/api" {:interceptors [::api]}
["/ping" ::ping]
["/public/*path" ::resources]
["/user/:id" {:name ::get-user
:parameters {:id String}}
["/orders" ::user-orders]]
2017-08-15 06:14:02 +00:00
["/admin" {:interceptors [::admin]
:roles #{:admin}}
["/root" {:name ::root
:roles ^:replace #{:root}}]
["/db" {:name ::db
2017-08-15 06:14:02 +00:00
:interceptors [::db]}]]]))
```
2017-08-15 05:57:57 +00:00
Resolved route tree:
```clj
2017-08-15 06:14:02 +00:00
(reitit/routes router)
; [["/api/ping" {:name :user/ping
2017-08-15 06:14:02 +00:00
; :interceptors [::api]}]
; ["/api/public/*path" {:name :user/resources
2017-08-15 06:14:02 +00:00
; :interceptors [::api]}]
; ["/api/user/:id/orders" {:name :user/user-orders
2017-08-15 06:14:02 +00:00
; :interceptors [::api]
; :parameters {:id String}}]
; ["/api/admin/root" {:name :user/root
2017-08-15 06:14:02 +00:00
; :interceptors [::api ::admin]
; :roles #{:root}}]
; ["/api/admin/db" {:name :user/db
2017-08-15 06:14:02 +00:00
; :interceptors [::api ::admin ::db]
; :roles #{:admin}}]]
```
Path-based routing:
```clj
2017-08-15 06:14:02 +00:00
(reitit/match-by-path router "/api/admin/root")
; #Match{:template "/api/admin/root"
; :meta {:name :user/root
2017-08-15 06:14:02 +00:00
; :interceptors [::api ::admin]
; :roles #{:root}}
; :path "/api/admin/root"
; :handler nil
; :params {}}
```
2017-08-15 05:57:57 +00:00
On match, route meta-data is returned and can interpreted by the application.
2017-08-15 08:06:26 +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 `:handler` in the match. See [configuring routers](#configuring-routers) for details.
2017-08-15 05:57:57 +00:00
## Ring
2017-08-15 06:14:02 +00:00
Simple [Ring](https://github.com/ring-clojure/ring)-based routing 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])))
```
2017-08-15 06:14:02 +00:00
It's backed by a `LookupRouter` (no wildcards!)
```clj
(-> app (ring/get-router) class)
; reitit.core.LookupRouter
```
2017-08-15 05:57:57 +00:00
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"}
```
Routing based on `:request-method`:
```clj
(def app
(ring/ring-handler
(ring/router
["/ping" {:get handler
:post handler}])))
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}
(app {:request-method :put, :uri "/ping"})
; nil
```
2017-08-15 06:14:02 +00:00
Define some middleware and a new 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]}
```
Nested middleware works too:
```clj
(app {:request-method :delete, :uri "/api/admin/db"})
; {:status 200, :body [:api :admin :db :delete :handler]}
```
2017-08-15 06:14:02 +00:00
Ring-router supports also 3-arity [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html), so it can be used on [Node.js](https://nodejs.org/en/) 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` helper. It can be used to build dynamic extensions to the system.
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-15 05:57:57 +00:00
## Validating route-tree
**TODO**
## Merging route-trees
**TODO**
## Schema, Spec, Swagger & Openapi
**TODO**
## Interceptors
**TODO**
## Configuring Routers
Routers can be configured via options to do things like custom coercion and compilatin of meta-data. These can be used to do things like [`clojure.spec`](https://clojure.org/about/spec) validation of meta-data and fast, compiled [Ring](https://github.com/ring-clojure/ring/wiki/Concepts) or [Pedestal](http://pedestal.io/) -style handlers.
The following options are available for the `Router`:
2017-08-15 05:57:57 +00:00
| key | description |
| -----------|-------------|
| `:path` | Base-path for routes (default `""`)
| `:routes` | Initial resolved routes (default `[]`)
| `:meta` | Initial expanded route-meta vector (default `[]`)
| `:expand` | Function of `arg => meta` to expand route arg to route meta-data (default `reitit.core/expand`)
| `:coerce` | Function of `[path meta] opts => [path meta]` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `[path meta] opts => handler` to compile a route handler
## 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.