2018-07-18 10:09:16 +00:00
# Composing Routers
2018-07-26 07:40:04 +00:00
Data-driven approach in `reitit` allows us to compose routes, route data, route specs, middleware and interceptors chains. We can compose routers too. This is needed to achieve dynamic routing like in [Compojure ](https://github.com/weavejester/compojure ).
## Immutatability
Once a router is created, the routing tree is immutable and cannot be changed. To change the routing, we need to create a new router with changed routes and/or options. For this, the `Router` protocol exposes it's resolved routes via `r/routes` and options via `r/options` .
2018-07-18 10:09:16 +00:00
2018-07-25 06:10:04 +00:00
## Adding routes
2018-07-18 10:09:16 +00:00
2018-07-25 06:10:04 +00:00
Let's create a router:
2018-07-18 10:09:16 +00:00
```clj
(require '[reitit.core :as r])
2018-07-25 06:10:04 +00:00
(def router
(r/router
[["/foo" ::foo]
["/bar/:id" ::bar]]))
```
2018-07-28 09:01:12 +00:00
We can query the resolved routes and options:
2018-07-18 10:09:16 +00:00
2018-07-25 06:10:04 +00:00
```clj
(r/routes router)
;[["/foo" {:name :user/foo}]
2018-07-25 08:13:25 +00:00
; ["/bar/:id" {:name :user/bar}]]
2018-07-25 06:10:04 +00:00
(r/options router)
;{:lookup #object [...]
; :expand #object [...]
; :coerce #object [...]
; :compile #object [...]
; :conflicts #object [...]}
2018-07-18 10:09:16 +00:00
```
2018-07-26 07:40:04 +00:00
Let's add a helper function to create a new router with extra routes:
2018-07-18 10:09:16 +00:00
```clj
2018-07-25 06:10:04 +00:00
(defn add-routes [router routes]
2018-07-18 10:09:16 +00:00
(r/router
2018-07-25 06:10:04 +00:00
(into (r/routes router) routes)
2018-07-18 10:09:16 +00:00
(r/options router)))
```
2018-07-26 07:40:04 +00:00
We can now create a new router with an extra routes:
2018-07-18 10:09:16 +00:00
```clj
2018-07-25 06:10:04 +00:00
(def router2
(add-routes
router
[["/baz/:id/:subid" ::baz]]))
2018-07-18 10:09:16 +00:00
2018-07-25 06:10:04 +00:00
(r/routes router2)
2018-07-25 08:08:11 +00:00
;[["/foo" {:name :user/foo}]
2018-07-25 06:10:04 +00:00
; ["/bar/:id" {:name :user/bar}]
; ["/baz/:id/:subid" {:name :user/baz}]]
2018-07-18 10:09:16 +00:00
```
2018-07-26 07:40:04 +00:00
The original router was not changed:
```clj
(r/routes router)
;[["/foo" {:name :user/foo}]
; ["/bar/:id" {:name :user/bar}]]
```
When a new router is created, all rules are applied, including the conflict resolution:
2018-07-18 10:09:16 +00:00
```clj
2018-07-25 06:10:04 +00:00
(add-routes
router2
[["/:this/should/:fail" ::fail]])
;CompilerException clojure.lang.ExceptionInfo: Router contains conflicting route paths:
2018-07-18 10:09:16 +00:00
;
2018-07-25 06:10:04 +00:00
; /baz/:id/:subid
;-> /:this/should/:fail
2018-07-18 10:09:16 +00:00
```
## Merging routers
2018-07-26 07:40:04 +00:00
Let's create a helper function to merge routers:
2018-07-25 06:10:04 +00:00
```clj
(defn merge-routers [& routers]
(r/router
(apply merge (map r/routes routers))
(apply merge (map r/options routers))))
```
2018-07-26 07:40:04 +00:00
We can now merge multiple routers into one:
2018-07-18 10:09:16 +00:00
```clj
2018-07-25 06:10:04 +00:00
(def router
(merge-routers
(r/router ["/route1" ::route1])
(r/router ["/route2" ::route2])
(r/router ["/route3" ::route3])))
(r/routes router)
;[["/route1" {:name :user/route1}]
; ["/route2" {:name :user/route2}]
; ["/route3" {:name :user/route3}]]
2018-07-18 10:09:16 +00:00
```
2018-07-25 06:10:04 +00:00
## Nesting routers
2018-07-26 07:40:04 +00:00
Routers can be nested using the catch-all parameter.
2018-07-25 06:10:04 +00:00
2018-07-26 07:40:04 +00:00
Here's a router with deeply nested routers under a `:router` key in the route data:
2018-07-25 06:10:04 +00:00
```clj
(def router
(r/router
[["/ping" :ping]
["/olipa/*" {:name :olipa
:router (r/router
[["/olut" :olut]
["/makkara" :makkara]
["/kerran/*" {:name :kerran
:router (r/router
[["/avaruus" :avaruus]
["/ihminen" :ihminen]])}]])}]]))
```
Matching by path:
```clj
(r/match-by-path router "/olipa/kerran/iso/kala")
;#Match{:template "/olipa/*"
; :data {:name :olipa
; :router #object [reitit.core$mixed_router]}
; :result nil
; :path-params {: "kerran/iso/kala"}
; :path "/olipa/iso/kala"}
```
2018-07-28 09:01:12 +00:00
That didn't work as we wanted, as the nested routers don't have such a route. The core routing doesn't understand anything the `:router` key, so it only matched against the top-level router, which gave a match for the catch-all path.
2018-07-25 06:10:04 +00:00
2018-07-26 07:40:04 +00:00
As the `Match` contains all the route data, we can create a new matching function that understands the `:router` key. Below is a function that does recursive matching using the subrouters. It returns either `nil` or a vector of mathces.
2018-07-18 10:09:16 +00:00
```clj
2018-07-25 06:10:04 +00:00
(require '[clojure.string :as str])
2018-07-18 10:09:16 +00:00
2018-07-25 06:10:04 +00:00
(defn recursive-match-by-path [router path]
(if-let [match (r/match-by-path router path)]
(if-let [subrouter (-> match :data :router)]
2018-07-25 08:13:25 +00:00
(let [subpath (subs path (str/last-index-of (:template match) "/"))]
(if-let [submatch (recursive-match-by-path subrouter subpath)]
(cons match submatch)))
(list match))))
2018-07-18 10:09:16 +00:00
```
2018-07-25 06:10:04 +00:00
With invalid nested path we get now `nil` as expected:
```clj
(recursive-match-by-path router "/olipa/kerran/iso/kala")
; nil
```
With valid path we get all the nested matches:
```clj
(recursive-match-by-path router "/olipa/kerran/avaruus")
;[#reitit.core.Match{:template "/olipa/*"
; :data {:name :olipa
; :router #object [reitit.core$mixed_router]}
; :result nil
; :path-params {: "kerran/avaruus"}
; :path "/olipa/kerran/avaruus"}
; #reitit .core.Match{:template "/kerran/*"
; :data {:name :kerran
; :router #object [reitit.core$lookup_router]}
; :result nil
; :path-params {: "avaruus"}
; :path "/kerran/avaruus"}
; #reitit .core.Match{:template "/avaruus"
; :data {:name :avaruus}
; :result nil
; :path-params {}
; :path "/avaruus"}]
```
2018-07-26 07:40:04 +00:00
Let's create a helper to get only the route names for matches:
2018-07-25 06:10:04 +00:00
```clj
(defn name-path [router path]
(some->> (recursive-match-by-path router path)
(mapv (comp :name :data))))
(name-path router "/olipa/kerran/avaruus")
; [:olipa :kerran :avaruus]
```
2018-07-26 07:40:04 +00:00
So, we can nest routers, but why would we do that?
2018-07-25 06:10:04 +00:00
## Dynamic routing
2018-07-26 07:40:04 +00:00
In all the examples above, the routers were created ahead of time, making the whole route tree effective static. To have more dynamic routing, we can use router references allowing the router to be swapped over time. We can also create fully dynamic routers where the router is re-created for each request. Let's walk through both cases.
2018-07-25 06:10:04 +00:00
First, we need to modify our matching function to support router references:
```clj
2018-07-26 07:40:04 +00:00
(defn- < < [x]
(if (instance? clojure.lang.IDeref x)
(deref x) x))
2018-07-25 06:10:04 +00:00
(defn recursive-match-by-path [router path]
(if-let [match (r/match-by-path (< < router ) path ) ]
(if-let [subrouter (-> match :data :router < < )]
2018-07-25 08:13:25 +00:00
(let [subpath (subs path (str/last-index-of (:template match) "/"))]
(if-let [submatch (recursive-match-by-path subrouter subpath)]
(cons match submatch)))
(list match))))
2018-07-25 06:10:04 +00:00
```
2018-07-28 09:01:12 +00:00
Then, we need some routers.
2018-07-26 07:40:04 +00:00
2018-07-28 09:01:12 +00:00
First, a reference to a router that can be updated on background, for example when a new entry in inserted into a database. We'll wrap the router into a `atom` :
2018-07-25 06:10:04 +00:00
```clj
(def beer-router
(atom
(r/router
[["/lager" :lager]])))
```
2018-07-26 07:40:04 +00:00
Second, a reference to router, which is re-created on each routing request:
2018-07-25 06:10:04 +00:00
```clj
(def dynamic-router
(reify clojure.lang.IDeref
(deref [_]
(r/router
2018-07-26 07:40:04 +00:00
["/duo" (keyword (str "duo" (rand-int 100)))]))))
2018-07-25 06:10:04 +00:00
```
2018-07-26 07:40:04 +00:00
We can compose the routers into a system-level static root router:
2018-07-25 06:10:04 +00:00
```clj
(def router
(r/router
[["/gin/napue" :napue]
["/ciders/*" :ciders]
["/beers/*" {:name :beers
:router beer-router}]
2018-07-26 07:40:04 +00:00
["/dynamic/*" {:name :dynamic
2018-07-25 06:10:04 +00:00
:router dynamic-router}]]))
```
Matching root routes:
```clj
2018-07-28 09:01:12 +00:00
(name-path router "/vodka/russian")
2018-07-25 06:10:04 +00:00
; nil
2018-07-28 09:01:12 +00:00
(name-path router "/gin/napue")
2018-07-25 06:10:04 +00:00
; [:napue]
```
Matching (nested) beer routes:
```clj
2018-07-28 09:01:12 +00:00
(name-path router "/beers/lager")
2018-07-25 06:10:04 +00:00
; [:beers :lager]
2018-07-28 09:01:12 +00:00
(name-path router "/beers/saison")
2018-07-25 06:10:04 +00:00
; nil
```
No saison!? Let's add the route:
```clj
(swap! beer-router add-routes [["/saison" :saison]])
```
There we have it:
```clj
2018-07-28 09:01:12 +00:00
(name-path router "/beers/saison")
2018-07-25 06:10:04 +00:00
; [:beers :saison]
```
2018-07-26 07:40:04 +00:00
We can't add conflicting routes:
2018-07-25 06:10:04 +00:00
```clj
(swap! beer-router add-routes [["/saison" :saison]])
;CompilerException clojure.lang.ExceptionInfo: Router contains conflicting route paths:
;
; /saison
;-> /saison
```
The dynamic routes are re-created on every request:
```clj
2018-07-28 09:01:12 +00:00
(name-path router "/dynamic/duo")
2018-07-26 07:40:04 +00:00
; [:dynamic :duo71]
2018-07-25 06:10:04 +00:00
2018-07-28 09:01:12 +00:00
(name-path router "/dynamic/duo")
2018-07-26 07:40:04 +00:00
; [:dynamic :duo55]
2018-07-25 06:10:04 +00:00
```
### Performance
2018-07-28 09:01:12 +00:00
With nested routers, instead of having to do just one route match, matching is recursive, which adds a small cost. All nested routers need to be of type catch-all at top-level, which is order of magnitude slower than fully static routes. Dynamic routes are the slowest ones, at least two orders of magnitude slower, as the router needs to be recreated for each request.
2018-07-25 06:10:04 +00:00
2018-07-26 07:40:04 +00:00
A quick benchmark on the recursive lookups:
2018-07-25 06:10:04 +00:00
| path | time | type
|------------------|---------|-----------------------
| `/gin/napue` | 40ns | static
| `/ciders/weston` | 440ns | catch-all
| `/beers/saison` | 600ns | catch-all + static
2018-07-25 07:57:37 +00:00
| `/dynamic/duo` | 12000ns | catch-all + dynamic
2018-07-25 06:10:04 +00:00
2018-07-26 07:40:04 +00:00
The non-recursive lookup for `/gin/napue` is around 23ns.
Comparing the dynamic routing performance with Compojure:
```clj
(require '[compojure.core :refer [context])
(def app
(context "/dynamic" [] (constantly :duo)))
(app {:uri "/dynamic/duo" :request-method :get})
; :duo
```
| path | time | type
|------------------|---------|-----------------------
| `/dynamic/duo` | 20000ns | compojure
2018-07-28 09:01:12 +00:00
Can we make the nester routing faster? Sure. We could use the Router `:compile` hook to compile the nested routers for better performance. We could also allow router creation rules to be disabled, to get the dynamic routing much faster.
2018-07-26 07:40:04 +00:00
### When to use nested routers?
2018-07-28 09:01:12 +00:00
Nesting routers is not trivial and because of that, should be avoided. For dynamic (request-time) route generation, it's the only choise. For other cases, nested routes are most likely a better option.
2018-07-26 07:40:04 +00:00
2018-07-28 09:01:12 +00:00
Let's re-create the previous example with normal route nesting/composition.
2018-07-26 07:40:04 +00:00
2018-07-28 09:01:12 +00:00
A helper to the root router:
2018-07-26 07:40:04 +00:00
```clj
2018-07-28 09:01:12 +00:00
(defn create-router [beers]
2018-07-26 07:40:04 +00:00
(r/router
[["/gin/napue" :napue]
["/ciders/*" :ciders]
2018-07-28 09:01:12 +00:00
["/beers" (for [beer beers]
[(str "/" beer) (keyword "beer" beer)])]
2018-07-26 07:40:04 +00:00
["/dynamic/*" {:name :dynamic
:router dynamic-router}]]))
```
New new root router *reference* and a helper to reset it:
```clj
(def router
(atom (create-router nil)))
(defn reset-router! [beers]
2018-07-28 09:01:12 +00:00
(reset! router (create-router beers)))
2018-07-26 07:40:04 +00:00
```
The routing tree:
```clj
(r/routes @router )
;[["/gin/napue" {:name :napue}]
; ["/ciders/*" {:name :ciders}]
; ["/dynamic/*" {:name :dynamic,
; :router #object [user$reify__24359]}]]
```
Let's reset the router with some beers:
```clj
(reset-router! ["lager" "sahti" "bock"])
```
We can see that the beer routes are now embedded into the core router:
```clj
(r/routes @router )
;[["/gin/napue" {:name :napue}]
; ["/ciders/*" {:name :ciders}]
; ["/beers/lager" {:name :beer/lager}]
; ["/beers/sahti" {:name :beer/sahti}]
; ["/beers/bock" {:name :beer/bock}]
; ["/dynamic/*" {:name :dynamic,
; :router #object [user$reify__24359]}]]
```
And the routing works:
```clj
(name-path @router "/beers/sahti")
;[:beer/sahti]
```
2018-07-28 09:01:12 +00:00
All the beer-routes now match in constant time.
2018-07-26 07:40:04 +00:00
| path | time | type
|-----------------|---------|-----------------------
| `/beers/sahti` | 40ns | static
2018-07-25 06:10:04 +00:00
2018-07-18 10:09:16 +00:00
## TODO
2018-07-26 07:40:04 +00:00
* add an example how to do dynamic routing with `reitit-ring`
* maybe create a `recursive-router` into a separate ns with all `Router` functions implemented correctly? maybe not...
* add `reitit.core/merge-routes` to effectively merge routes with route data
2018-07-18 10:09:16 +00:00