diff --git a/advanced/composing_routers.html b/advanced/composing_routers.html index a8f0dcd6..cfbc08bf 100644 --- a/advanced/composing_routers.html +++ b/advanced/composing_routers.html @@ -1189,7 +1189,7 @@ diff --git a/advanced/configuring_routers.html b/advanced/configuring_routers.html index cb86dc4e..db27941f 100644 --- a/advanced/configuring_routers.html +++ b/advanced/configuring_routers.html @@ -903,7 +903,7 @@ diff --git a/advanced/dev_workflow.html b/advanced/dev_workflow.html index 0c4e7842..e51b4c45 100644 --- a/advanced/dev_workflow.html +++ b/advanced/dev_workflow.html @@ -954,7 +954,7 @@ diff --git a/advanced/different_routers.html b/advanced/different_routers.html index 4fb52d42..4087377b 100644 --- a/advanced/different_routers.html +++ b/advanced/different_routers.html @@ -902,7 +902,7 @@ diff --git a/advanced/index.html b/advanced/index.html deleted file mode 100644 index 062b331e..00000000 --- a/advanced/index.html +++ /dev/null @@ -1,839 +0,0 @@ - - - -
- - -Reitit also supports Pedestal-style interceptors.
-Many routing libraries allow single path lookup could match multiple routes. Usually, first match is used. This is not good, especially if route tree is merged from multiple sources - routes might regress to be unreachable without a warning.
-Reitit resolves this by running explicit conflicit resolution when a Router is created. Conflicting routes are passed into a :conflicts callback. Default implementation throws ex-info with a descriptive message.
Examples routes with conflicts:
-(require '[reitit.core :as reitit])
-
-(def routes
- [["/ping"]
- ["/:user-id/orders"]
- ["/bulk/:bulk-id"]
- ["/public/*path"]
- ["/:version/status"]])
-
-By default, ExceptionInfo is thrown:
(reitit/router routes)
-; 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
-;
-
-Just logging the conflicts:
-(reitit/router
- routes
- {:conflicts (comp println reitit/conflicts-str)})
-; Router contains conflicting routes:
-;
-; /:user-id/orders
-; -> /public/*path
-; -> /bulk/:bulk-id
-;
-; /bulk/:bulk-id
-; -> /:version/status
-;
-; /public/*path
-; -> /:version/status
-;
-
-
-
- Raw routes are defined as vectors, which have a String path, optional (non-sequential) route argument and optional child routes. Routes can be wrapped in vectors and lists and nil routes are ignored. Paths can have path-parameters (:id) or catch-all-parameters (*path).
Simple route:
-["/ping"]
-
-Two routes:
-[["/ping"]
- ["/pong"]]
-
-Routes with route arguments:
-[["/ping" ::ping]
- ["/pong" {:name ::pong}]]
-
-Routes with path parameters:
-[["/users/:user-id"]
- ["/api/:version/ping"]]
-
-Route with catch-all parameter:
-["/public/*path"]
-
-Nested routes:
-["/api"
- ["/admin" {:middleware [::admin]}
- ["" ::admin]
- ["/db" ::db]]
- ["/ping" ::ping]]
-
-Same routes flattened:
-[["/api/admin" {:middleware [::admin], :name ::admin}]
- ["/api/admin/db" {:middleware [::admin], :name ::db}]
- ["/api/ping" {:name ::ping}]]
-
-As routes are just data, it's easy to create them programamtically:
-(defn cqrs-routes [actions dev-mode?]
- ["/api" {:interceptors [::api ::db]}
- (for [[type interceptor] actions
- :let [path (str "/" (name interceptor))
- method (condp = type
- :query :get
- :command :post)]]
- [path {method {:interceptors [interceptor]}}])
- (if dev-mode? ["/dev-tools" ::dev-tools])])
-
-(cqrs-routes
- [[:query 'get-user]
- [:command 'add-user]
- [:command 'add-order]]
- false)
-; ["/api" {:interceptors [::api ::db]}
-; (["/get-user" {:get {:interceptors [get-user]}}]
-; ["/add-user" {:post {:interceptors [add-user]}}]
-; ["/add-order" {:post {:interceptors [add-order]}}])
-; nil]
-
-Routes are just data and to do actual routing, we need a Router satisfying the reitit.core/Router protocol. Routers are created with reitit.core/router function, taking the raw routes and optionally an options map. Raw routes gets expanded and optionally coerced and compiled.
Router protocol:
(defprotocol Router
- (router-name [this])
- (routes [this])
- (options [this])
- (route-names [this])
- (match-by-path [this path])
- (match-by-name [this name] [this name params]))
-
-Creating a router:
-(require '[reitit.core :as r])
-
-(def router
- (r/router
- [["/api"
- ["/ping" ::ping]
- ["/user/:id" ::user]]]))
-
-Router flattens the raw routes and expands the route arguments using reitit.core/Expand protocol. By default, Keywords are expanded to :name and functions are expaned to :handler. nil routes are removed. The expanded routes can be retrieved with router:
(r/routes router)
-; [["/api/ping" {:name :user/ping}]
-; ["/api/user/:id" {:name :user/user}]]
-
-Path-based routing is done using the reitit.core/match-by-path function. It takes the router and path as arguments and returns one of the following:
nil, no matchPartialMatch, path matched, missing path-parameters (only in reverse-routing)Match, exact match(r/match-by-path router "/hello")
-; nil
-
-(r/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"}}
-
-All routes which :name route data defined, can be matched by name.
Listing all route names:
-(r/route-names router)
-; [:user/ping :user/user]
-
-Matching by name:
-(r/match-by-name router ::user)
-; #PartialMatch{:template "/api/user/:id",
-; :meta {:name :user/user},
-; :result nil,
-; :params nil,
-; :required #{:id}}
-
-(r/partial-match? (r/match-by-name router ::user))
-; true
-
-We only got a partial match as we didn't provide the needed path-parameters. Let's provide the them too:
-(r/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:
-(r/match-by-name! router ::user)
-; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
-
-Routes can have arbitrary meta-data, interpreted by the router (via it's :compile hook) or the application itself. For nested routes, route data is accumulated recursively using meta-merge. By default, it appends collections, but it can be overridden to do :prepend, :replace or :displace.
An example router with nested data:
-(def router
- (r/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}]]
-
-Route data is returned with Match and the application can act based on it.
(r/match-by-path router "/api/admin/db/users/drop")
-; #Match{:template "/api/admin/db/:db/drop"
-; :meta {:interceptors [::api ::db]
-; :roles #{:db-admin}
-; :parameters {:db String}
-; :name ::drop-db}
-; :result nil
-; :params {:db "users"}
-; :path "/api/admin/db/users/drop"}
-
-Reitit ships with several different implementations for the Router protocol, originally based on the awesome Pedestal implementation. router selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using :router ROUTER OPTION.
| router | -description | -
|---|---|
:linear-router |
-Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. | -
:lookup-router |
-Fastest router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters. | -
:mixed-router |
-Creates internally a :linear-router and a :lookup-router and used them to effectively get best-of-both-worlds. Valid if there are no CONFLICTING ROUTES. |
-
:prefix-tree-router |
-TODO | -
The router name can be asked from the router
-(r/router-name router)
-; :mixed-router
-
-
-
- Reitit ships with several different implementations for the Router protocol, originally based on the awesome Pedestal implementation. router selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using :router ROUTER OPTION.
| router | -description | -
|---|---|
:linear-router |
-Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. | -
:lookup-router |
-Fastest router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters. | -
:mixed-router |
-Creates internally a :linear-router and a :lookup-router and used them to effectively get best-of-both-worlds. Valid if there are no CONFLICTING ROUTES. |
-
:prefix-tree-router |
-TODO | -
The router name can be asked from the router
-(r/router-name router)
-; :mixed-router
-
-
-
- s/keys accepts :opt-un to support optional keys.
diff --git a/coercion/coercion.html b/coercion/coercion.html
index bfd5d649..10d32ce3 100644
--- a/coercion/coercion.html
+++ b/coercion/coercion.html
@@ -979,7 +979,7 @@
diff --git a/coercion/data_spec_coercion.html b/coercion/data_spec_coercion.html
index 2976f13a..8b5105ab 100644
--- a/coercion/data_spec_coercion.html
+++ b/coercion/data_spec_coercion.html
@@ -877,7 +877,7 @@
diff --git a/coercion/index.html b/coercion/index.html
deleted file mode 100644
index 478248e9..00000000
--- a/coercion/index.html
+++ /dev/null
@@ -1,839 +0,0 @@
-
-
-
-
-
-
- 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:
(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 (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))))))
-
-: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.
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 |
-
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.
Example 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"}
-
-
-
- Reitit also support for Pedestal-style interceptors as an alternative to Middleware. Basic interceptor handling is implemented in reitit.interceptor package. There is no interceptor executor shipped, but you can use libraries like Pedestal Interceptor or Sieppari to execute the chains.
An alternative to reitit-ring, using interceptors instead of middleware. Currently not finalized, you can track progress in here.
(require '[reitit.interceptor.sieppari :as sieppari])
-(require '[reitit.http.coercion :as coercion])
-(require '[reitit.http :as http])
-(require '[reitit.ring :as ring])
-(require '[reitit.coercion.spec])
-(require '[clojure.set :as set])
-(require '[manifold.deferred :as d])
-(require '[ring.adapter.jetty :as jetty])
-
-(def auth-interceptor
- "Interceptor that mounts itself if route has `:roles` data. Expects `:roles`
- to be a set of keyword and the context to have `[:user :roles]` with user roles.
- responds with HTTP 403 if user doesn't have the roles defined, otherwise no-op."
- {:name ::auth
- :compile (fn [{:keys [roles]} _]
- (if (seq roles)
- {:description (str "requires roles " roles)
- :spec {:roles #{keyword?}}
- :context-spec {:user {:roles #{keyword}}}
- :enter (fn [{{user-roles :roles} :user :as ctx}]
- (if (not (set/subset? roles user-roles))
- (assoc ctx :response {:status 403, :body "forbidden"})
- ctx))}))})
-
-(def async-interceptor
- {:enter (fn [ctx] (d/future ctx))})
-
-(def app
- (http/ring-handler
- (http/router
- ["/api" {:interceptors [async-interceptor auth-interceptor]}
- ["/ping" {:name ::ping
- :get (constantly
- {:status 200
- :body "pong"})}]
- ["/plus/:z" {:name ::plus
- :post {:parameters {:query {:x int?}
- :body {:y int?}
- :path {:z int?}}
- :responses {200 {:body {:total pos-int?}}}
- :roles #{:admin}
- :handler (fn [{:keys [parameters]}]
- (let [total (+ (-> parameters :query :x)
- (-> parameters :body :y)
- (-> parameters :path :z))]
- {:status 200
- :body {:total total}}))}}]]
- {:data {:coercion reitit.coercion.spec/coercion
- :interceptors [coercion/coerce-exceptions-interceptor
- coercion/coerce-request-interceptor
- coercion/coerce-response-interceptor]}})
- (ring/create-default-handler)
- {:executor sieppari/executor}))
-
-(jetty/run-jetty #'app {:port 3000, :join? false, :async? true})
-
-TODO
- - -Goal is to support both Swagger & OpenAPI for route documentation. Documentation is extracted from existing coercion definitions :parameters, :responses and from a set of new doumentation keys.
Swagger-support draft works, but only for Clojure.
-Current reitit-swagger draft (with reitit-ring & data-specs):
(require '[reitit.ring :as ring])
-(require '[reitit.ring.swagger :as swagger])
-(require '[reitit.ring.coercion :as rrc])
-(require '[reitit.coercion.spec :as spec])
-
-(def app
- (ring/ring-handler
- (ring/router
- ["/api"
- ;; identify a swagger api
- ;; there can be several in a routing tree
- {:swagger {:id :math}}
-
- ;; the (undocumented) swagger spec endpoint
- ["/swagger.json"
- {:get {:no-doc true
- :swagger {:info {:title "my-api"}}
- :handler swagger/swagger-spec-handler}}]
-
- ["/minus"
- {:get {:summary "minus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (- x y)}})}}]
-
- ["/plus"
- {:get {:summary "plus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (+ x y)}})}}]]
-
- {:data {:middleware [;; does not particiate in request processing
- ;; just defines specs for the extra keys
- swagger/swagger-middleware
- rrc/coerce-exceptions-middleware
- rrc/coerce-request-middleware
- rrc/coerce-response-middleware]
- :coercion spec/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"]
-
-To use Coercion with Ring, one needs to do the following:
: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.Coercion to route meta-data under :coercionIf 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.
-(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}}
-
-(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}}
-
-
-
- As reitit-core works with both Clojure & ClojureScript, one can have a shared routing table for both the frontend and the backend application, using the Clujore Common Files.
For backend, you need to define a :handler for the request processing, for fronend, :name enables the use of reverse routing.
There are multiple options to use shared routing table.
-;; define the handlers for for clojure
-#?(:clj (declare get-kikka))
-#?(:clj (declare post-kikka))
-
-;; :name for both, :handler just for clojure
-(def routes
- ["/kikka"
- {:name ::kikka
- :get #?(:clj {:handler get-kikka})
- :post #?(:clj {:handler post-kikka})}]
-
-raw-routes can have any non-sequential data as a route argument, which gets expanded using the :expand option given to the reitit.core.router function. It defaults to reitit.core/expand multimethod.
First, define the common routes (in a .cljc file):
(def routes
- [["/kikka" ::kikka]
- ["/bar" ::bar]])
-
-Those can be used as-is from ClojureScript:
-(require '[reitit.core :as r])
-
-(def router
- (r/router routes))
-
-(r/match-by-name router ::kikka)
-;#Match{:template "/kikka"
-; :data {:name :user/kikka}
-; :result nil
-; :path-params nil
-; :path "/kikka"}
-
-For the backend, we can use a custom-expander to expand the routes:
-(require '[reitit.ring :as ring])
-
-(defn my-expand [registry]
- (fn [data opts]
- (or (if (keyword? data)
- (some-> data
- registry
- (r/expand opts)
- (assoc :name data)))
- (r/expand data opts))))
-
-;; the handler functions
-(defn get-kikka [_] {:status 200, :body "get"})
-(defn post-kikka [_] {:status 200, :body "post"})
-(defn bar [_] {:status 200, :body "bar"})
-
-(def app
- (ring/ring-handler
- (ring/router
- [["/kikka" ::kikka]
- ["/bar" ::bar]]
- ;; use a custom expander
- {:expand (my-expand
- {::kikka {:get get-kikka
- :post post-kikka}
- ::bar bar})})))
-
-(app {:request-method :post, :uri "/kikka"})
-; {:status 200, :body "post"}
-
-
-
- Ring-router adds support for 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[...]}
-; #Methods{:any #Endpoint{:meta {:handler #object[...]},
-; :handler #object[...],
-; :middleware []}}]]
-
-Note that the compiled resuts as third element in the route vector.
-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 can be added with a :middleware key, with a vector value of the following:
handler -> request -> responsehandler ?args -> request -> response and optinally it's args.A 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]}
-
-All built-in middleware provide both 2 and 3-arity and are compiled for both Clojure & ClojureScript, so they work with Async Ring and Node.js too.
- - -Reitit provides pluggable parameter coercion via reitit.ring.coercion.protocol/Coercion protocol, originally introduced in compojure-api. Reitit ships with reitit.ring.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-beta2"]
-[org.clojure/spec.alpha "0.1.123"]
-[metosin/spec-tools "0.4.0"]
-
-To use Coercion with Ring, one needs to do the following:
: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.Coercion to route data under :coercionreitit.ring.coercion/gen-wrap-coerce-parametersgen-wrap-coerce-parameters/gen-wrap-coerce-responsesIf 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.
-(require '[reitit.ring :as ring])
-(require '[reitit.ring.coercion :as coercion])
-(require '[reitit.ring.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)}})}}]]
- {:data {: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}}
-
-Currently, clojure.spec doesn't support runtime transformations via conforming, so one needs to wrap all specs with spec-tools.core/spec.
(require '[reitit.ring :as ring])
-(require '[reitit.ring.coercion :as coercion])
-(require '[reitit.ring.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)}})}}]]
- {:data {: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}}
-
-
-
- Many routing libraries allow single path lookup could match multiple routes. Usually, first match is used. This is not good, especially if route tree is merged from multiple sources - routes might regress to be unreachable without a warning.
-Reitit resolves this by running explicit conflicit resolution when a Router is created. Conflicting routes are passed into a :conflicts callback. Default implementation throws ex-info with a descriptive message.
Examples routes with conflicts:
-(require '[reitit.core :as reitit])
-
-(def routes
- [["/ping"]
- ["/:user-id/orders"]
- ["/bulk/:bulk-id"]
- ["/public/*path"]
- ["/:version/status"]])
-
-By default, ExceptionInfo is thrown:
(reitit/router routes)
-; 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
-;
-
-Just logging the conflicts:
-(reitit/router
- routes
- {:conflicts (comp println reitit/conflicts-str)})
-; Router contains conflicting routes:
-;
-; /:user-id/orders
-; -> /public/*path
-; -> /bulk/:bulk-id
-;
-; /bulk/:bulk-id
-; -> /:version/status
-;
-; /public/*path
-; -> /:version/status
-;
-
-
-
- Namespace reitit.spec contains clojure.spec definitions for raw-routes, routes, router and router options.
NOTE: Use of specs requires to use one of the following:
-[org.clojure/clojurescript "1.9.660"][org.clojure/clojure "1.9.0-alpha19"][clojure-future-spec "1.9.0-alpha17"] (Clojure 1.8)If route trees are generated at runtime (e.g. from external source like the database), one can use directly the clojure.spec functions.
(require '[clojure.spec.alpha :as s])
-(require '[reitit.spec :as spec])
-
-(def routes-from-db
- ["tenant1" ::tenant1])
-
-(s/valid? ::spec/raw-routes routes-from-db)
-; false
-
-(s/explain ::spec/raw-routes routes-from-db)
-; In: [0] val: "tenant1" fails spec: :reitit.spec/path at: [:route :path] predicate: (or (blank? %) (starts-with? % "/"))
-; In: [0] val: "tenant1" fails spec: :reitit.spec/raw-route at: [:routes] predicate: (cat :path :reitit.spec/path :arg (? :reitit.spec/arg) :childs (* (and (nilable :reitit.spec/raw-route))))
-; In: [1] val: :user/tenant1 fails spec: :reitit.spec/raw-route at: [:routes] predicate: (cat :path :reitit.spec/path :arg (? :reitit.spec/arg) :childs (* (and (nilable :reitit.spec/raw-route))))
-; :clojure.spec.alpha/spec :reitit.spec/raw-routes
-; :clojure.spec.alpha/value ["tenant1" :user/tenant1]
-
-reitit.core/router can be instrumented and use something like expound to pretty-print the spec problems.
First add a :dev dependency to:
[expound "0.3.0"]
-
-Some bootstrapping:
-(require '[clojure.spec.test.alpha :as stest])
-(require '[expound.alpha :as expound])
-(require '[clojure.spec.alpha :as s])
-(require '[reitit.spec])
-
-(stest/instrument `reitit/router)
-(set! s/*explain-out* expound/printer)
-
-And we are ready to go:
-
-(reitit/router
- ["/api"
- ["/public"
- ["/ping"]
- ["pong"]]])
-
-; CompilerException clojure.lang.ExceptionInfo: Call to #'reitit.core/router did not conform to spec:
-;
-; -- Spec failed --------------------
-;
-; Function arguments
-;
-; (["/api" ...])
-; ^^^^^^
-;
-; should satisfy
-;
-; (clojure.spec.alpha/cat
-; :path
-; :reitit.spec/path
-; :arg
-; (clojure.spec.alpha/? :reitit.spec/arg)
-; :childs
-; (clojure.spec.alpha/*
-; (clojure.spec.alpha/and
-; (clojure.spec.alpha/nilable :reitit.spec/raw-route))))
-;
-; or
-;
-; (clojure.spec.alpha/cat
-; :path
-; :reitit.spec/path
-; :arg
-; (clojure.spec.alpha/? :reitit.spec/arg)
-; :childs
-; (clojure.spec.alpha/*
-; (clojure.spec.alpha/and
-; (clojure.spec.alpha/nilable :reitit.spec/raw-route))))
-;
-; -- Relevant specs -------
-;
-; :reitit.spec/raw-route:
-; (clojure.spec.alpha/cat
-; :path
-; :reitit.spec/path
-; :arg
-; (clojure.spec.alpha/? :reitit.spec/arg)
-; :childs
-; (clojure.spec.alpha/*
-; (clojure.spec.alpha/and
-; (clojure.spec.alpha/nilable :reitit.spec/raw-route))))
-; :reitit.spec/raw-routes:
-; (clojure.spec.alpha/or
-; :route
-; :reitit.spec/raw-route
-; :routes
-; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into []))
-;
-; -- Spec failed --------------------
-;
-; Function arguments
-;
-; ([... [... ... ["pong"]]])
-; ^^^^^^
-;
-; should satisfy
-;
-; (fn
-; [%]
-; (or
-; (clojure.string/blank? %)
-; (clojure.string/starts-with? % "/")))
-;
-; or
-;
-; (fn
-; [%]
-; (or
-; (clojure.string/blank? %)
-; (clojure.string/starts-with? % "/")))
-;
-; -- Relevant specs -------
-;
-; :reitit.spec/path:
-; (clojure.spec.alpha/and
-; clojure.core/string?
-; (clojure.core/fn
-; [%]
-; (clojure.core/or
-; (clojure.string/blank? %)
-; (clojure.string/starts-with? % "/"))))
-; :reitit.spec/raw-route:
-; (clojure.spec.alpha/cat
-; :path
-; :reitit.spec/path
-; :arg
-; (clojure.spec.alpha/? :reitit.spec/arg)
-; :childs
-; (clojure.spec.alpha/*
-; (clojure.spec.alpha/and
-; (clojure.spec.alpha/nilable :reitit.spec/raw-route))))
-; :reitit.spec/raw-routes:
-; (clojure.spec.alpha/or
-; :route
-; :reitit.spec/raw-route
-; :routes
-; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into []))
-;
-; -------------------------
-; Detected 2 errors
-
-TODO
- - -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
-;
-
-
-
- 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.
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]]
-
-
-
- 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]
-
-(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"}}
-
-(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}
-
-
-
- Reitit supports Swagger to generate route documentation. Documentation is extracted from existing coercion definitions :parameters, :responses and from a set of new doumentation keys.
Current reitit-swagger draft (with reitit-ring & data-specs):
(require '[reitit.ring :as ring])
-(require '[reitit.ring.swagger :as swagger])
-(require '[reitit.ring.coercion :as rrc])
-(require '[reitit.coercion.spec :as spec])
-
-(def app
- (ring/ring-handler
- (ring/router
- ["/api"
-
- ;; identify a swagger api
- ;; there can be several in a routing tree
- {:swagger {:id :math}}
-
- ;; the (undocumented) swagger spec endpoint
- ["/swagger.json"
- {:get {:no-doc true
- :swagger {:info {:title "my-api"}}
- :handler (swagger/create-swagger-handler)}}]
-
- ;; the (undocumented) swagger-ui
- ;; [org.webjars/swagger-ui "3.13.4"]
- ["/docs/*"
- {:get {:no-doc true
- :handler (ring/create-resource-handler
- {:root "META-INF/resources/webjars/swagger-ui"})}}]
-
- ["/minus"
- {:get {:summary "minus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (- x y)}})}}]
-
- ["/plus"
- {:get {:summary "plus"
- :parameters {:query {:x int?, :y int?}}
- :responses {200 {:body {:total int?}}}
- :handler (fn [{{{:keys [x y]} :query} :parameters}]
- {:status 200, :body {:total (+ x y)}})}}]]
-
- {:data {:middleware [rrc/coerce-exceptions-middleware
- rrc/coerce-request-middleware
- rrc/coerce-response-middleware
- ;; provides just route data specs
- swagger/swagger-feature]
- :coercion spec/coercion}})))
-
-
-
- Namespace reitit.spec contains specs for routes, router and router options.
To enable spec-validation of router inputs & outputs at development time, one can do the following:
; add to dependencies:
-; [expound "0.3.0"]
-
-(require '[clojure.spec.test.alpha :as st])
-(require '[expound.alpha :as expound])
-(require '[clojure.spec.alpha :as s])
-(require '[reitit.spec])
-
-(st/instrument `reitit/router)
-(set! s/*explain-out* expound/printer)
-
-(reitit/router
- ["/api"
- ["/publuc"
- ["/ping"]
- ["pong"]]])
-; -- Spec failed --------------------
-;
-; ["/api" ...]
-; ^^^^^^
-;
-; should satisfy
-;
-; (clojure.spec.alpha/cat
-; :path
-; :reitit.spec/path
-; :arg
-; (clojure.spec.alpha/? :reitit.spec/arg)
-; :childs
-; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route)))
-;
-; -- Relevant specs -------
-;
-; :reitit.spec/raw-route:
-; (clojure.spec.alpha/cat
-; :path
-; :reitit.spec/path
-; :arg
-; (clojure.spec.alpha/? :reitit.spec/arg)
-; :childs
-; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route)))
-; :reitit.spec/raw-routes:
-; (clojure.spec.alpha/or
-; :route
-; :reitit.spec/raw-route
-; :routes
-; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into []))
-;
-; -- Spec failed --------------------
-;
-; [... [... ... ["pong"]]]
-; ^^^^^^
-;
-; should satisfy
-;
-; (fn [%] (clojure.string/starts-with? % "/"))
-;
-; -- Relevant specs -------
-;
-; :reitit.spec/path:
-; (clojure.spec.alpha/and
-; clojure.core/string?
-; (clojure.core/fn [%] (clojure.string/starts-with? % "/")))
-; :reitit.spec/raw-route:
-; (clojure.spec.alpha/cat
-; :path
-; :reitit.spec/path
-; :arg
-; (clojure.spec.alpha/? :reitit.spec/arg)
-; :childs
-; (clojure.spec.alpha/* (clojure.spec.alpha/and :reitit.spec/raw-route)))
-; :reitit.spec/raw-routes:
-; (clojure.spec.alpha/or
-; :route
-; :reitit.spec/raw-route
-; :routes
-; (clojure.spec.alpha/coll-of :reitit.spec/raw-route :into []))
-;
-; -------------------------
-; Detected 2 errors
-
-TODO
- - -