mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 08:21:11 +00:00
Re-organize docs
This commit is contained in:
parent
8e2e0c07f6
commit
eb22bae047
14 changed files with 715 additions and 417 deletions
129
doc/README.md
129
doc/README.md
|
|
@ -1,16 +1,133 @@
|
||||||
# reitit
|
# Introduction
|
||||||
|
|
||||||
[reitit](https://github.com/metosin/reitit) is a friendly data-driven router for Clojure(Script).
|
[Reitit](https://github.com/metosin/reitit) is a small Clojure(Script) library for data-driven routing.
|
||||||
|
|
||||||
* Simple data-driven [route syntax](./routing/route_syntax.md)
|
* Simple data-driven [route syntax](./routing/route_syntax.md)
|
||||||
* First-class [route meta-data](./routing/route_metadata.md)
|
* First-class [route meta-data](./routing/route_metadata.md)
|
||||||
* Generic, not tied to HTTP
|
* Bi-directional-routing
|
||||||
* [Route conflict resolution](./routing/route_conflicts.md)
|
* [Route conflict resolution](./routing/route_conflicts.md)
|
||||||
* [Pluggable coercion](./parameter-coercion.md) ([clojure.spec](https://clojure.org/about/spec))
|
* [Pluggable coercion](./parameter-coercion.md) ([clojure.spec](https://clojure.org/about/spec))
|
||||||
* both [Middleware](./ring.md#middleware) & Interceptors
|
* Both [Middleware](./ring.md#middleware) & Interceptors
|
||||||
* Extendable
|
* Extendable
|
||||||
* Fast
|
* Fast
|
||||||
|
|
||||||
## Latest version
|
To use Reitit, add the following dependecy to your project:
|
||||||
|
|
||||||
[](http://clojars.org/metosin/reitit)
|
```clj
|
||||||
|
[metosin/reitit "0.1.0-SNAPSHOT"]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Simple router
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.core :as r])
|
||||||
|
|
||||||
|
(def router
|
||||||
|
(r/router
|
||||||
|
[["/api/ping" ::ping]
|
||||||
|
["/api/orders/:id" ::order-by-id]]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Routing:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/match-by-path router "/api/ipa")
|
||||||
|
; nil
|
||||||
|
|
||||||
|
(r/match-by-path router "/api/ping")
|
||||||
|
; #Match{:template "/api/ping"
|
||||||
|
; :meta {:name ::ping}
|
||||||
|
; :result nil
|
||||||
|
; :params {}
|
||||||
|
; :path "/api/ping"}
|
||||||
|
|
||||||
|
(r/match-by-path router "/api/orders/1")
|
||||||
|
; #Match{:template "/api/orders/:id"
|
||||||
|
; :meta {:name ::order-by-id}
|
||||||
|
; :result nil
|
||||||
|
; :params {:id "1"}
|
||||||
|
; :path "/api/orders/1"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse-routing:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/match-by-name router ::ipa)
|
||||||
|
; nil
|
||||||
|
|
||||||
|
(r/match-by-name router ::ping)
|
||||||
|
; #Match{:template "/api/ping"
|
||||||
|
; :meta {:name ::ping}
|
||||||
|
; :result nil
|
||||||
|
; :params {}
|
||||||
|
; :path "/api/ping"}
|
||||||
|
|
||||||
|
(r/match-by-name router ::order-by-id)
|
||||||
|
; #PartialMatch{:template "/api/orders/:id"
|
||||||
|
; :meta {:name :user/order-by-id}
|
||||||
|
; :result nil
|
||||||
|
; :params nil
|
||||||
|
; :required #{:id}}
|
||||||
|
|
||||||
|
(r/partial-match? (r/match-by-name router ::order-by-id))
|
||||||
|
; true
|
||||||
|
|
||||||
|
(r/match-by-name router ::order-by-id {:id 2})
|
||||||
|
; #Match{:template "/api/orders/:id",
|
||||||
|
; :meta {:name ::order-by-id},
|
||||||
|
; :result nil,
|
||||||
|
; :params {:id 2},
|
||||||
|
; :path "/api/orders/2"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ring-router
|
||||||
|
|
||||||
|
Ring-router adds support for `:handler` functions, `:middleware` and routing based on `:request-method`. It also supports pluggable parameter coercion (`clojure.spec`), data-driven middleware, route and middleware compilation, dynamic extensions and more.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring :as ring])
|
||||||
|
|
||||||
|
(def handler [_]
|
||||||
|
{:status 200, :body "ok"})
|
||||||
|
|
||||||
|
(defn wrap [handler id]
|
||||||
|
(fn [request]
|
||||||
|
(update (handler request) :wrap (fnil conj '()) id)))
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api" {:middleware [[wrap :api]]}
|
||||||
|
["/ping" {:get handler
|
||||||
|
:name ::ping}]
|
||||||
|
["/admin" {:middleware [[wrap :admin]]}
|
||||||
|
["/users" {:get handler
|
||||||
|
:post handler}]]])))
|
||||||
|
```
|
||||||
|
|
||||||
|
Routing:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(app {:request-method :get, :uri "/api/admin/users"})
|
||||||
|
; {:status 200, :body "ok", :wrap (:api :admin}
|
||||||
|
|
||||||
|
(app {:request-method :put, :uri "/api/admin/users"})
|
||||||
|
; nil
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse-routing:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.core :as r])
|
||||||
|
|
||||||
|
(-> app (ring/get-router) (r/match-by-name ::ping))
|
||||||
|
; #Match{:template "/api/ping"
|
||||||
|
; :meta {:middleware [[#object[user$wrap] :api]]
|
||||||
|
; :get {:handler #object[user$handler]}
|
||||||
|
; :name ::ping}
|
||||||
|
; :result #Methods{...}
|
||||||
|
; :params nil
|
||||||
|
; :path "/api/ping"}
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
# Summary
|
# Summary
|
||||||
|
|
||||||
* [Introduction](README.md)
|
* [Introduction](README.md)
|
||||||
* Routing
|
* Basics
|
||||||
* [Route syntax](routing/route_syntax.md)
|
* [Route syntax](basics.md#route-syntax)
|
||||||
* [Routers](routing/routers.md)
|
* [Router](basics.md#router)
|
||||||
* [Route metadata](routing/route_metadata.md)
|
* [Path-based Routing](basics.md#path-based-routing)
|
||||||
* [Route conflicts](routing/route_conflicts.md)
|
* [Name-based Routing](basics.md#name-based-routing)
|
||||||
* [Ring support](ring.md)
|
* [Route data](basics.md#route-data)
|
||||||
* [Parameter coercion](parameter_coercion.md)
|
* [Different Routers](basics.md#different-routers)
|
||||||
* [Compiling middleware](compiling_middleware.md)
|
* Advanced
|
||||||
* [Validating route-trees](validating.md)
|
* [Route conflicts](route_conflicts.md)
|
||||||
* [Configuring routers](configuring_routers.md)
|
* [Route Validation](route_validation.md)
|
||||||
* TODO: Merging route-trees
|
* [Configuring routers](configuring_routers.md)
|
||||||
|
* Ring
|
||||||
|
* [Ring-router](ring.md)
|
||||||
|
* [Dynamic extensions](dynamic_extensions.md)
|
||||||
|
* [Parameter coercion](parameter_coercion.md)
|
||||||
|
* [Compiling middleware](compiling_middleware.md)
|
||||||
* TODO: Swagger & OpenAPI
|
* TODO: Swagger & OpenAPI
|
||||||
* TODO: Interceptors
|
* TODO: Interceptors
|
||||||
|
|
|
||||||
254
doc/basics.md
Normal file
254
doc/basics.md
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
# Route Syntax
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
["/ping"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Two routes:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[["/ping"]
|
||||||
|
["/pong"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Routes with route arguments:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[["/ping" ::ping]
|
||||||
|
["/pong" {:name ::pong}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Routes with path parameters:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[["/users/:user-id"]
|
||||||
|
["/api/:version/ping"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Route with catch-all parameter:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
["/public/*path"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested routes:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
["/api"
|
||||||
|
["/admin" {:middleware [::admin]}
|
||||||
|
["" ::admin]
|
||||||
|
["/db" ::db]]
|
||||||
|
["/ping" ::ping]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Same routes flattened:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[["/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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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])])
|
||||||
|
```
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Router
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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, `Keyword`s are expanded to `:name` and functions are expaned to `:handler`. `nil` routes are removed. The expanded routes can be retrieved with router:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/routes router)
|
||||||
|
; [["/api/ping" {:name :user/ping}]
|
||||||
|
; ["/api/user/:id" {:name :user/user}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path-based routing
|
||||||
|
|
||||||
|
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 match
|
||||||
|
* `PartialMatch`, path matched, missing path-parameters (only in reverse-routing)
|
||||||
|
* `Match`, exact match
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/match-by-path router "/hello")
|
||||||
|
; nil
|
||||||
|
```
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Name-based routing
|
||||||
|
|
||||||
|
All routes which `:name` route data defined, can be matched by name.
|
||||||
|
|
||||||
|
Listing all route names:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/route-names router)
|
||||||
|
; [:user/ping :user/user]
|
||||||
|
```
|
||||||
|
|
||||||
|
Matching by name:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/match-by-name! router ::user)
|
||||||
|
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Route data
|
||||||
|
|
||||||
|
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](https://github.com/weavejester/meta-merge). By default, it appends collections, but it can be overridden to do `:prepend`, `:replace` or `:displace`.
|
||||||
|
|
||||||
|
An example router with nested data:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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"}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Different Routers
|
||||||
|
|
||||||
|
Reitit ships with several different implementations for the `Router` protocol, originally based on the awesome [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) 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](https://github.com/julienschmidt/httprouter#how-does-it-work)
|
||||||
|
|
||||||
|
The router name can be asked from the router
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/router-name router)
|
||||||
|
; :mixed-router
|
||||||
|
```
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
## Compiling Middleware
|
# Compiling Middleware
|
||||||
|
|
||||||
The [meta-data extensions](ring.md#meta-data-based-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.
|
The [meta-data extensions](ring.md#meta-data-based-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.
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ To do this we use [middleware records](ring.md#middleware-records) `:gen` hook i
|
||||||
|
|
||||||
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`](https://github.com/metosin/reitit/blob/master/src/reitit/coercion.cljc):
|
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`](https://github.com/metosin/reitit/blob/master/src/reitit/coercion.cljc):
|
||||||
|
|
||||||
### Naive
|
## Naive
|
||||||
|
|
||||||
* Extracts the compiled route information on every request.
|
* Extracts the compiled route information on every request.
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
||||||
(handler request respond raise))))))
|
(handler request respond raise))))))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiled
|
## Compiled
|
||||||
|
|
||||||
* Route information is provided via a closure
|
* Route information is provided via a closure
|
||||||
* Pre-compiled coercers
|
* Pre-compiled coercers
|
||||||
|
|
|
||||||
52
doc/dynamic_extensions.md
Normal file
52
doc/dynamic_extensions.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Dynamic 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.
|
||||||
|
|
||||||
|
Example middleware to guard routes based on user roles:
|
||||||
|
|
||||||
|
```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"}
|
||||||
|
```
|
||||||
34
doc/middleware_records.md
Normal file
34
doc/middleware_records.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# 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](#compiling-middleware).
|
||||||
|
|
||||||
|
When routes are compiled, all middleware are expanded (and optionally compiled) into `Middleware` and stored in compilation results for later use (api-docs etc). For actual request processing, they are unwrapped into normal middleware functions producing zero runtime performance penalty. Middleware expansion is backed by `reitit.middleware/IntoMiddleware` protocol, enabling plain clojure(script) maps to be used.
|
||||||
|
|
||||||
|
A Record:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.middleware :as middleware])
|
||||||
|
|
||||||
|
(def wrap2
|
||||||
|
(middleware/create
|
||||||
|
{:name ::wrap2
|
||||||
|
:description "a nice little mw, takes 1 arg."
|
||||||
|
:wrap wrap}))
|
||||||
|
```
|
||||||
|
|
||||||
|
As plain map:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
;; plain map
|
||||||
|
(def wrap3
|
||||||
|
{:name ::wrap3
|
||||||
|
:description "a nice little mw, :api as arg"
|
||||||
|
:wrap (fn [handler]
|
||||||
|
(wrap handler :api))})
|
||||||
|
```
|
||||||
98
doc/ring.md
98
doc/ring.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Ring support
|
# Ring Router
|
||||||
|
|
||||||
[Ring](https://github.com/ring-clojure/ring)-router adds support for ring concepts like [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#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.
|
[Ring](https://github.com/ring-clojure/ring)-router adds support for [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#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:
|
Simple Ring app:
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ The expanded routes:
|
||||||
|
|
||||||
Note that the compiled resuts as third element in the route vector.
|
Note that the compiled resuts as third element in the route vector.
|
||||||
|
|
||||||
### Request-method based routing
|
# 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.
|
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.
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ Reverse routing:
|
||||||
; "/ping"
|
; "/ping"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Middleware
|
# Middleware
|
||||||
|
|
||||||
Middleware can be added with a `:middleware` key, with a vector value of the following:
|
Middleware can be added with a `:middleware` key, with a vector value of the following:
|
||||||
|
|
||||||
|
|
@ -112,94 +112,6 @@ Middleware is applied correctly:
|
||||||
; {:status 200, :body [:api :admin :db :delete :handler]}
|
; {:status 200, :body [:api :admin :db :delete :handler]}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Middleware Records
|
# Async Ring
|
||||||
|
|
||||||
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](#compiling-middleware).
|
|
||||||
|
|
||||||
When routes are compiled, all middleware are expanded (and optionally compiled) into `Middleware` and stored in compilation results for later use (api-docs etc). For actual request processing, they are unwrapped into normal middleware functions producing zero runtime performance penalty. Middleware expansion is backed by `reitit.middleware/IntoMiddleware` protocol, enabling plain clojure(script) maps to be used.
|
|
||||||
|
|
||||||
A Record:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(require '[reitit.middleware :as middleware])
|
|
||||||
|
|
||||||
(def wrap2
|
|
||||||
(middleware/create
|
|
||||||
{:name ::wrap2
|
|
||||||
:description "a nice little mw, takes 1 arg."
|
|
||||||
:wrap wrap}))
|
|
||||||
```
|
|
||||||
|
|
||||||
As plain map:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
;; 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 2 and 3-arity and are compiled for both Clojure & ClojureScript, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) and [Node.js](https://nodejs.org) too.
|
All built-in middleware provide both 2 and 3-arity and are compiled for both Clojure & ClojureScript, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) and [Node.js](https://nodejs.org) 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.
|
|
||||||
|
|
||||||
Example middleware to guard routes based on user roles:
|
|
||||||
|
|
||||||
```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"}
|
|
||||||
```
|
|
||||||
|
|
|
||||||
56
doc/route_conflicts.md
Normal file
56
doc/route_conflicts.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Route conflicts
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.core :as reitit])
|
||||||
|
|
||||||
|
(def routes
|
||||||
|
[["/ping"]
|
||||||
|
["/:user-id/orders"]
|
||||||
|
["/bulk/:bulk-id"]
|
||||||
|
["/public/*path"]
|
||||||
|
["/:version/status"]])
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, `ExceptionInfo` is thrown:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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
|
||||||
|
;
|
||||||
|
```
|
||||||
172
doc/route_validation.md
Normal file
172
doc/route_validation.md
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
# Route validation
|
||||||
|
|
||||||
|
Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/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)
|
||||||
|
|
||||||
|
## At runtime
|
||||||
|
If route trees are generated at runtime (e.g. from external source like the database), one can use directly the `clojure.spec` functions.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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]
|
||||||
|
```
|
||||||
|
|
||||||
|
## At development time
|
||||||
|
|
||||||
|
`reitit.core/router` can be instrumented and use something like [expound](https://github.com/bhb/expound) to pretty-print the spec problems.
|
||||||
|
|
||||||
|
First add a `:dev` dependency to:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[expound "0.3.0"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Some bootstrapping:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(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:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
|
||||||
|
(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
|
||||||
|
```
|
||||||
|
|
||||||
|
# Validating route data
|
||||||
|
|
||||||
|
*TODO*
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
## 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.
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(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
|
|
||||||
;
|
|
||||||
```
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
# 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
|
|
||||||
(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:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(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:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(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](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:result` in the match. See [configuring routers](../configuring_routers.md) for details.
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# 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:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
["/ping"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Two routes:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
[["/ping"]
|
|
||||||
["/pong"]]
|
|
||||||
```
|
|
||||||
|
|
||||||
Routes with meta-data:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
[["/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]]
|
|
||||||
```
|
|
||||||
|
|
||||||
Same routes flattened:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
[["/api/admin/user" {:middleware [::admin], :name ::user}
|
|
||||||
["/api/admin/db" {:middleware [::admin], :name ::db}
|
|
||||||
["/api/ping" ::ping]]
|
|
||||||
```
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
# Routers
|
|
||||||
|
|
||||||
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](https://github.com/pedestal/pedestal/tree/master/route) 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:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(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):
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(reitit/router-name router)
|
|
||||||
; :mixed-router
|
|
||||||
```
|
|
||||||
|
|
||||||
The expanded routes:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(reitit/routes router)
|
|
||||||
; [["/api/ping" {:name :user/ping}]
|
|
||||||
; ["/api/user/:id" {:name :user/user}]]
|
|
||||||
```
|
|
||||||
|
|
||||||
Route names:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(reitit/route-names router)
|
|
||||||
; [:user/ping :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"
|
|
||||||
; :result nil
|
|
||||||
; :params {:id "1"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Name-based (reverse) routing
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(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:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(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:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
(reitit/match-by-name! router ::user)
|
|
||||||
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
|
|
||||||
```
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# Validating route-trees
|
|
||||||
|
|
||||||
Namespace `reitit.spec` contains [specs](https://clojure.org/about/spec) for routes, router and router options.
|
|
||||||
|
|
||||||
To enable spec-validation of `router` inputs & outputs at development time, one can do the following:
|
|
||||||
|
|
||||||
```clj
|
|
||||||
; 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validating meta-data
|
|
||||||
|
|
||||||
*TODO*
|
|
||||||
Loading…
Reference in a new issue