reitit/doc/basics.md
2017-09-18 08:30:03 +03:00

254 lines
7 KiB
Markdown

# 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
```