A fast data-driven routing library for Clojure/Script
Find a file
2017-09-05 20:13:28 +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 simplify specs 2017-09-04 16:09:19 +03:00
test use fn? instead of fspec with router options 2017-09-04 08:33:49 +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 Initial specs for reitit.core 2017-09-04 08:33:49 +03:00
README.md welcome, first class data-driven Middleware. 2017-09-04 08:24:42 +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-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:

  (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 handlers, 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.

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

: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

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

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:

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

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

Async Ring

All built-in middleware provide both the 2 and 3-arity, so they work with Async Ring 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"}

Parameter coercion

Reitit ships with pluggable parameter coercion via reitit.coercion.protocol/Coercion protocol. reitit.coercion.spec/SpecCoercion provides implements it for clojure.spec & data-specs.

NOTE: to use the spec-coercion, one needs to add the following dependencies manually to the project:

[org.clojure/clojure "1.9.0-alpha19"]
[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 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.

To do this we use 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?

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:

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 :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
(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 is both much easier to understand but also 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.

License

Copyright © 2017 Metosin Oy

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