A fast data-driven routing library for Clojure/Script
Find a file
2017-09-08 08:43:59 +03:00
perf-test/clj/reitit README & small fixes 2017-08-30 13:28:49 +03:00
scripts Initial commit 2017-08-07 14:15:45 +03:00
src/reitit The new ::spec/routes 2017-09-08 08:29:31 +03:00
test The new ::spec/routes 2017-09-08 08:29:31 +03:00
.gitignore Initial commit 2017-08-07 14:15:45 +03:00
.travis.yml Initial commit 2017-08-07 14:15:45 +03:00
CHANGELOG.md Initial commit 2017-08-07 14:15:45 +03:00
CONTRIBUTING.md Initial commit 2017-08-07 14:15:45 +03:00
LICENSE Initial commit 2017-08-07 14:15:45 +03:00
project.clj Clojure 1.9.0-alpha20 2017-09-08 08:43:59 +03:00
README.md Clojure 1.9.0-alpha20 2017-09-08 08:43:59 +03:00

reitit Build Status Dependencies Status

A friendly data-driven router for Clojure(Script).

Ships with example router for Ring. See Issues for roadmap.

Latest version

Clojars Project

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. Actual Router implementation is selected automatically but can be defined with a :router option. Router support both 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 found):

(reitit/router-name 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]
     ["/admin" {:roles #{:admin}}
      ["/users" ::users]
      ["/db" {:interceptors [::db]
              :roles ^:replace #{:db-admin}}
       ["/:db" {:parameters {:db String}}
        ["/drop" ::drop-db]
        ["/stats" ::db-stats]]]]]))

Resolved route tree:

(reitit/routes router)
; [["/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:

(reitit/match-by-path router "/api/admin/users")
; #Match{:template "/api/admin/users"
;        :meta {:interceptors [::api]
;               :roles #{:admin}
;               :name ::users}
;        :result nil
;        :params {}
;        :path "/api/admin/users"}

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 ring concepts like handlers, 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:

(require '[reitit.ring :as ring])

(defn handler [_]
  {:status 200, :body "ok"})

(def app
  (ring/ring-handler
    (ring/router
      ["/ping" handler])))

Applying the handler:

(app {:request-method :get, :uri "/favicon.ico"})
; nil

(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}

The expanded routes:

(-> app (ring/get-router) (reitit/routes))
; [["/ping"
;   {:handler #object[user$handler 0x5c312d6f "user$handler@5c312d6f"]}
;   #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.

(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

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.

Let's define some middleware and a handler:

(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:

(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]}
(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.

Behind the scenes, when routes are compiled, all middleware are first expanded into Middleware and stored as such in compilation results to be used for api-docs etc. For actual request processing, they are unwrapped into normal middleware functions producing zero runtime performance penalty. Thanks to the reitit.middleware/IntoMiddleware protocol, plain clojure(script) maps can also be used.

A Record:

(require '[reitit.middleware :as middleware])

(def wrap2
  (middleware/create
    {:name ::wrap2
     :description "a nice little mw, takes 1 arg."
     :wrap wrap}))

As plain map:

;; 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 the 2 and 3-arity, so they work with Async Ring 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.

A middleware to guard routes based on user roles:

(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"}

Parameter coercion

Reitit provides pluggable parameter coercion via reitit.coercion.protocol/Coercion protocol, originally introduced in compojure-api. Reitit ships with reitit.coercion.spec/SpecCoercion providing implemenation for clojure.spec and data-specs.

NOTE: Before Clojure 1.9.0 is shipped, to use the spec-coercion, one needs to add the following dependencies manually to the project:

[org.clojure/clojure "1.9.0-alpha20"]
[org.clojure/spec.alpha "0.1.123"]
[metosin/spec-tools "0.3.3"]

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:
  • :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.
  1. Define a Coercion to route meta-data under :coercion
  2. 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

(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}})))
(app
  {:request-method :get
   :uri "/api/ping"
   :body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}

Example with specs

(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}})))
(app
  {:request-method :get
   :uri "/api/ping"
   :body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}

Compiling Middleware

The meta-data extensions are a easy way to extend the system. Routes meta-data can be transformed into any shape (records, functions etc.) in route compilation, enabling fast access at request-time.

Still, we can do better. As we know the exact route that 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 - yielding faster runtime processing.

To do this we use middleware records :gen hook instead of the normal :wrap. :gen expects a function of route-meta router-opts => wrap. Middleware can also return nil, which effective unmounts the middleware. Why mount a wrap-enforce-roles middleware for a route if there are no roles required for it?

To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with :gen. These are the actual codes are from reitit.coercion:

Naive

  • Extracts the compiled route information on every request.
(defn wrap-coerce-response
  "Pluggable response coercion middleware.
  Expects a :coercion of type `reitit.coercion.protocol/Coercion`
  and :responses 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 coercion
         (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 coercion
         (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
(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)))))))}))

The :gen -version has 50% less code, is easier to reason about and is 2-4x faster on basic perf tests.

Merging route-trees

TODO

Validating meta-data

TODO

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
: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.

Also to Compojure-api, Kekkonen and Ring-swagger and for the data-driven syntax, coercion & stuff.

And some Yada too.

License

Copyright © 2017 Metosin Oy

Distributed under the Eclipse Public License, the same as Clojure.