|
|
||
|---|---|---|
| perf-test/clj/reitit | ||
| scripts | ||
| src/reitit | ||
| test | ||
| .gitignore | ||
| .travis.yml | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| project.clj | ||
| README.md | ||
reitit

A friendly data-driven router for Clojure(Script).
- Simple data-driven route syntax
- First-class route meta-data
- Generic, not tied to HTTP
- Route conflict resolution
- Extendable
- Fast
Ships with example router for Ring. See Issues for roadmap.
Latest version
Route Syntax
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:
["/ping"]
Two routes:
[["/ping"]
["/pong"]]
Routes with meta-data:
[["/ping" ::ping]
["/pong" {:name ::pong}]]
Routes with path and catch-all parameters:
[["/users/:user-id"]
["/public/*path"]]
Nested routes with meta-data:
["/api"
["/admin" {:middleware [::admin]}
["/user" ::user]
["/db" ::db]
["/ping" ::ping]]
Same routes flattened:
[["/api/admin/user" {:middleware [::admin], :name ::user}
["/api/admin/db" {:middleware [::admin], :name ::db}
["/api/ping" ::ping]]
Routing
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 implementation.
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. The actual Router implementation is selected based on the route tree or can be selected with the :router option. Router support both fast path- and name-based lookups.
Creating a router:
(require '[reitit.core :as reitit])
(def router
(reitit/router
[["/api"
["/ping" ::ping]
["/user/:id" ::user]]]))
:mixed-router is created (both static & wild routes are used):
(reitit/router-type router)
; :mixed-router
The expanded routes:
(reitit/routes router)
; [["/api/ping" {:name :user/ping}]
; ["/api/user/:id" {:name :user/user}]]
Route names:
(reitit/route-names router)
; [:user/ping :user/user]
Path-based routing
(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"}}
Name-based (reverse) routing
(reitit/match-by-name router ::user)
; #PartialMatch{:template "/api/user/:id",
; :meta {:name :user/user},
; :result nil,
; :params nil,
; :required #{:id}}
(reitit/partial-match? (reitit/match-by-name router ::user))
; true
Only a partial match. Let's provide the path-parameters:
(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"}}
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.
A router based on nested route tree:
(def router
(reitit/router
["/api" {:interceptors [::api]}
["/ping" ::ping]
["/public/*path" ::resources]
["/user/:id" {:name ::get-user
:parameters {:id String}}
["/orders" ::user-orders]]
["/admin" {:interceptors [::admin]
:roles #{:admin}}
["/root" {:name ::root
:roles ^:replace #{:root}}]
["/db" {:name ::db
:interceptors [::db]}]]]))
Resolved route tree:
(reitit/routes router)
; [["/api/ping" {:name :user/ping
; :interceptors [::api]}]
; ["/api/public/*path" {:name :user/resources
; :interceptors [::api]}]
; ["/api/user/:id/orders" {:name :user/user-orders
; :interceptors [::api]
; :parameters {:id String}}]
; ["/api/admin/root" {:name :user/root
; :interceptors [::api ::admin]
; :roles #{:root}}]
; ["/api/admin/db" {:name :user/db
; :interceptors [::api ::admin ::db]
; :roles #{:admin}}]]
Path-based routing:
(reitit/match-by-path router "/api/admin/root")
; #Match{:template "/api/admin/root"
; :meta {:name :user/root
; :interceptors [::api ::admin]
; :roles #{:root}}
; :path "/api/admin/root"
; :result nil
; :params {}}
On match, route meta-data is returned and can interpreted by the application.
Routers also support meta-data compilation enabling things like fast Ring or Pedestal -style handlers. Compilation results are found under :result in the match. See configuring routers for details.
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.
(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
;
; /bulk/:bulk-id
; -> /:version/status
;
; /public/*path
; -> /:version/status
;
Ring
Ring-router adds support for handlers, middleware and routing based on :request-method.
Simple Ring app:
(require '[reitit.ring :as ring])
(defn handler [_]
{:status 200, :body "ok"})
(def app
(ring/ring-handler
(ring/router
["/ping" handler])))
The expanded routes:
(-> app (ring/get-router) (reitit/routes))
; [["/ping" {:handler #object[...]}]]
Applying the handler:
(app {:request-method :get, :uri "/favicon.ico"})
; nil
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}
Request-method based routing
(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:
(-> app
(ring/get-router)
(reitit/match-by-name ::ping)
:path)
; "/ping"
Middleware
Let's define some middleware and a handler:
(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:
(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:
(app {:request-method :delete, :uri "/api/ping"})
; {:status 200, :body [:api :handler]}
Nested middleware works too:
(app {:request-method :delete, :uri "/api/admin/db"})
; {:status 200, :body [:api :admin :db :delete :handler]}
Async Ring
Ring-router supports also 3-arity Async Ring, so it can be used on Node.js too.
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.
A middleware to guard routes:
(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):
(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:
(app {:request-method :get, :uri "/api/ping"})
; {:status 200, :body "ok"}
Anonymous access to guarded route:
(app {:request-method :get, :uri "/api/admin/ping"})
; {:status 403, :body "forbidden"}
Authorized access to guarded route:
(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}})
; {:status 200, :body "ok"}
Merging route-trees
TODO
Validating meta-data
TODO
Schema, Spec, Swagger & Openapi
TODO
Interceptors
TODO
Configuring Routers
Routers can be configured via options. Options allow things like clojure.spec validation for meta-data and fast, compiled handlers. The following options are available for the reitit.core/router:
| key | description |
|---|---|
:path |
Base-path for routes (default "") |
: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, Bide, Bidi, Compojure and Pedestal.
License
Copyright © 2017 Metosin Oy
Distributed under the Eclipse Public License, the same as Clojure.