mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 08:51:12 +00:00
Update README, more perf-tests, cleanup
* `match` => `match-by-path` * `by-name` = > `match-by-name` * `lookup-router` can't be created with wildcard routes * `match-by-name` initial perf tests
This commit is contained in:
parent
5646494388
commit
69ee59cbd2
4 changed files with 228 additions and 54 deletions
167
README.md
167
README.md
|
|
@ -3,6 +3,7 @@
|
||||||
Snappy data-driven router for Clojure(Script).
|
Snappy data-driven router for Clojure(Script).
|
||||||
|
|
||||||
* Simple data-driven route syntax
|
* Simple data-driven route syntax
|
||||||
|
* First-class route meta-data
|
||||||
* Generic, not tied to HTTP
|
* Generic, not tied to HTTP
|
||||||
* Extendable
|
* Extendable
|
||||||
* Fast
|
* Fast
|
||||||
|
|
@ -11,49 +12,163 @@ Snappy data-driven router for Clojure(Script).
|
||||||
|
|
||||||
[](http://clojars.org/metosin/reitit)
|
[](http://clojars.org/metosin/reitit)
|
||||||
|
|
||||||
## Usage
|
## Route Syntax
|
||||||
|
|
||||||
Named routes (example from [bide](https://github.com/funcool/bide#why-another-routing-library)).
|
Routes are defined as vectors, which String path as the first element, then optional meta-data (non-vector) 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}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested routes with meta-data:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
["/api"
|
||||||
|
["/admin" {:middleware [::admin]}
|
||||||
|
["/user" ::user]
|
||||||
|
["/db" ::db]
|
||||||
|
["/ping" ::ping]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Previous example flattened:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[["/api/admin/user" {:middleware [::admin], :name ::user}
|
||||||
|
["/api/admin/db" {:middleware [::admin], :name ::db}
|
||||||
|
["/api/ping" ::ping]]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routers
|
||||||
|
|
||||||
|
For actual routing, we need to create a `Router`. Reitit ships with 2 different router implementations: `LinearRouter` and `LookupRouter`, both 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 optionally an options map as arguments. The route-tree gets expanded, optionally coerced and compiled to support both fast path- and name-based lookups.
|
||||||
|
|
||||||
|
Create a router:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[reitit.core :as reitit])
|
(require '[reitit.core :as reitit])
|
||||||
|
|
||||||
(def router
|
(def router
|
||||||
(reitit/router
|
(reitit/router
|
||||||
[["/auth/login" :auth/login]
|
[["/api"
|
||||||
["/auth/recovery/token/:token" :auth/recovery]
|
["/ping" ::ping]
|
||||||
["/workspace/:project-uuid/:page-uuid" :workspace/page]]))
|
["/user/:id ::user]]))
|
||||||
|
|
||||||
(reitit/match-route router "/workspace/1/2")
|
(class router)
|
||||||
; {:name :workspace/page
|
; reitit.core.LinearRouter
|
||||||
; :route-params {:project-uuid "1", :page-uuid "2"}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Nested routes with [meta-merged](https://github.com/weavejester/meta-merge) meta-data:
|
Get the expanded routes:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(def handler (constantly "ok"))
|
(reitit/routes router)
|
||||||
|
; [["/api/ping" {:name :user/ping}]
|
||||||
|
; ["/api/user/:id" {:name :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"
|
||||||
|
; :params {:id "1"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Name-based (reverse) routing:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(reitit/match-by-name router ::user)
|
||||||
|
; ExceptionInfo missing path-params for route '/api/user/:id': #{:id}
|
||||||
|
```
|
||||||
|
|
||||||
|
Oh, that didn't work, retry:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(reitit/match-by-name router ::user {:id "1"})
|
||||||
|
; #Match{:template "/api/user/:id"
|
||||||
|
; :meta {:name :user/user}
|
||||||
|
; :path "/api/user/1"
|
||||||
|
; :params {:id "1"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 ring-router
|
(def ring-router
|
||||||
(reitit/router
|
(reitit/router
|
||||||
["/api" {:middleware [:api]}
|
["/api" {:middleware [:api-mw]}
|
||||||
["/ping" handler]
|
["/ping" ::ping]
|
||||||
["/public/*path" handler]
|
["/public/*path" ::resources]
|
||||||
["/user/:id" {:parameters {:id String}
|
["/user/:id" {:name ::get-user
|
||||||
:handler handler}]
|
:parameters {:id String}}
|
||||||
["/admin" {:middleware [:admin] :roles #{:admin}}
|
["/orders" ::user-orders]]
|
||||||
["/root" {:roles ^:replace #{:root}
|
["/admin" {:middleware [:admin-mw]
|
||||||
:handler handler}]
|
:roles #{:admin}}
|
||||||
["/db" {:middleware [:db]
|
["/root" {:name ::root
|
||||||
:handler handler}]]]))
|
:roles ^:replace #{:root}}]
|
||||||
|
["/db" {:name ::db
|
||||||
(reitit/match-route ring-router "/api/admin/db")
|
:middleware [:db-mw]}]]]))
|
||||||
; {:middleware [:api :admin :db]
|
|
||||||
; :roles #{:admin}
|
|
||||||
; :handler #object[...]
|
|
||||||
; :route-params {}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Expanded and merged route tree:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(reitit/routes ring-router)
|
||||||
|
; [["/api/ping" {:name :user/ping
|
||||||
|
; :middleware [:api-mw]}]
|
||||||
|
; ["/api/public/*path" {:name :user/resources
|
||||||
|
; :middleware [:api-mw]}]
|
||||||
|
; ["/api/user/:id/orders" {:name :user/user-orders
|
||||||
|
; :middleware [:api-mw]
|
||||||
|
; :parameters {:id String}}]
|
||||||
|
; ["/api/admin/root" {:name :user/root
|
||||||
|
; :middleware [:api-mw :admin-mw]
|
||||||
|
; :roles #{:root}}]
|
||||||
|
; ["/api/admin/db" {:name :user/db
|
||||||
|
; :middleware [:api-mw :admin-mw :db-mw]
|
||||||
|
; :roles #{:admin}}]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Path-based routing:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(reitit/match-by-path ring-router "/api/admin/root")
|
||||||
|
; #Match{:template "/api/admin/root"
|
||||||
|
; :meta {:name :user/root
|
||||||
|
; :middleware [:api-mw :admin-mw]
|
||||||
|
; :roles #{:root}}
|
||||||
|
; :path "/api/admin/root"
|
||||||
|
; :params {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Route meta-data is just data and the actual interpretation is left to the application. `Router` will get more options in the future to do things like [`clojure.spec`](https://clojure.org/about/spec) validation and custom route compilation (into into [Ring](https://github.com/ring-clojure/ring)-handlers or [Pedestal](pedestal.io)-style interceptors). See [Open issues](https://github.com/metosin/reitit/issues/).
|
||||||
|
|
||||||
## Special thanks
|
## Special thanks
|
||||||
|
|
||||||
To all Clojure(Script) routing libs out there, expecially to
|
To all Clojure(Script) routing libs out there, expecially to
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@
|
||||||
[reitit.core :as reitit]
|
[reitit.core :as reitit]
|
||||||
|
|
||||||
[bidi.bidi :as bidi]
|
[bidi.bidi :as bidi]
|
||||||
[compojure.api.core :refer [routes GET]]
|
[compojure.api.sweet :refer [api routes GET]]
|
||||||
|
[compojure.api.routes :as routes]
|
||||||
[ataraxy.core :as ataraxy]
|
[ataraxy.core :as ataraxy]
|
||||||
|
|
||||||
[io.pedestal.http.route.definition.table :as table]
|
[io.pedestal.http.route.definition.table :as table]
|
||||||
[io.pedestal.http.route.map-tree :as map-tree]
|
[io.pedestal.http.route.map-tree :as map-tree]
|
||||||
[io.pedestal.http.route.router :as pedestal]))
|
[io.pedestal.http.route.router :as pedestal]
|
||||||
|
[io.pedestal.http.route :as route]))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; start repl with `lein perf repl`
|
;; start repl with `lein perf repl`
|
||||||
|
|
@ -40,9 +42,12 @@
|
||||||
|
|
||||||
(def compojure-api-routes
|
(def compojure-api-routes
|
||||||
(routes
|
(routes
|
||||||
(GET "/auth/login" [] (constantly ""))
|
(GET "/auth/login" [] :name :auth/login (constantly ""))
|
||||||
(GET "/auth/recovery/token/:token" [] (constantly ""))
|
(GET "/auth/recovery/token/:token" [] :name :auth/recovery (constantly ""))
|
||||||
(GET "/workspace/:project/:page" [] (constantly ""))))
|
(GET "/workspace/:project/:page" [] :name :workspace/page (constantly ""))))
|
||||||
|
|
||||||
|
(def compojure-api-request
|
||||||
|
{:compojure.api.request/lookup (routes/route-lookup-table (routes/get-routes (api compojure-api-routes)))})
|
||||||
|
|
||||||
(def ataraxy-routes
|
(def ataraxy-routes
|
||||||
(ataraxy/compile
|
(ataraxy/compile
|
||||||
|
|
@ -51,11 +56,16 @@
|
||||||
["/workspace/" project "/" token] [:workspace/page project token]}))
|
["/workspace/" project "/" token] [:workspace/page project token]}))
|
||||||
|
|
||||||
(def pedestal-routes
|
(def pedestal-routes
|
||||||
|
(table/table-routes
|
||||||
|
[["/auth/login" :get (constantly "") :route-name :auth/login]
|
||||||
|
["/auth/recovery/token/:token" :get (constantly "") :route-name :auth/recovery]
|
||||||
|
["/workspace/:project/:page" :get (constantly "") :route-name :workspace/page]]))
|
||||||
|
|
||||||
|
(def pedestal-router
|
||||||
(map-tree/router
|
(map-tree/router
|
||||||
(table/table-routes
|
pedestal-routes))
|
||||||
[["/auth/login" :get (constantly "") :route-name :auth/login]
|
|
||||||
["/auth/recovery/token/:token" :get (constantly "") :route-name :auth/recovery]
|
(def pedestal-url-for (route/url-for-routes pedestal-routes))
|
||||||
["/workspace/:project/:page" :get (constantly "") :route-name :workspace/page]])))
|
|
||||||
|
|
||||||
(def reitit-routes
|
(def reitit-routes
|
||||||
(reitit/router
|
(reitit/router
|
||||||
|
|
@ -98,10 +108,45 @@
|
||||||
;; 1.0µs (-94%)
|
;; 1.0µs (-94%)
|
||||||
;; 770ns (-95%, -23%)
|
;; 770ns (-95%, -23%)
|
||||||
(title "reitit")
|
(title "reitit")
|
||||||
(let [call #(reitit/match reitit-routes "/workspace/1/1")]
|
(let [call #(reitit/match-by-path reitit-routes "/workspace/1/1")]
|
||||||
|
(assert (call))
|
||||||
|
(cc/quick-bench
|
||||||
|
(call))))
|
||||||
|
|
||||||
|
(defn reverse-routing-test []
|
||||||
|
|
||||||
|
(suite "reverse routing")
|
||||||
|
|
||||||
|
;; 2.2µs (-56%)
|
||||||
|
(title "bidi")
|
||||||
|
(let [call #(bidi/path-for bidi-routes :workspace/page :project "1" :page "1")]
|
||||||
|
(assert (= "/workspace/1/1" (call)))
|
||||||
|
(cc/quick-bench
|
||||||
|
(call)))
|
||||||
|
|
||||||
|
(title "ataraxy doesn't support reverse routing :(")
|
||||||
|
|
||||||
|
;; 3.8µs (-25%)
|
||||||
|
(title "pedestal - map-tree => prefix-tree")
|
||||||
|
(let [call #(pedestal-url-for :workspace/page :path-params {:project "1" :page "1"})]
|
||||||
|
(assert (= "/workspace/1/1" (call)))
|
||||||
|
(cc/quick-bench
|
||||||
|
(call)))
|
||||||
|
|
||||||
|
;; 5.1µs
|
||||||
|
(title "compojure-api")
|
||||||
|
(let [call #(routes/path-for* :workspace/page compojure-api-request {:project "1", :page "1"})]
|
||||||
|
(assert (= "/workspace/1/1" (call)))
|
||||||
|
(cc/quick-bench
|
||||||
|
(call)))
|
||||||
|
|
||||||
|
;; 850ns (-83%)
|
||||||
|
(title "reitit")
|
||||||
|
(let [call #(reitit/match-by-name reitit-routes :workspace/page {:project "1", :page "1"})]
|
||||||
(assert (call))
|
(assert (call))
|
||||||
(cc/quick-bench
|
(cc/quick-bench
|
||||||
(call))))
|
(call))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(routing-test))
|
(routing-test)
|
||||||
|
(reverse-routing-test))
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@
|
||||||
|
|
||||||
(defprotocol Routing
|
(defprotocol Routing
|
||||||
(routes [this])
|
(routes [this])
|
||||||
(match [this path])
|
(match-by-path [this path])
|
||||||
(by-name [this name] [this name parameters]))
|
(match-by-name [this name] [this name parameters]))
|
||||||
|
|
||||||
(defrecord Match [template meta path params])
|
(defrecord Match [template meta path params])
|
||||||
|
|
||||||
|
|
@ -69,15 +69,15 @@
|
||||||
Routing
|
Routing
|
||||||
(routes [_]
|
(routes [_]
|
||||||
routes)
|
routes)
|
||||||
(match [_ path]
|
(match-by-path [_ path]
|
||||||
(reduce
|
(reduce
|
||||||
(fn [acc ^Route route]
|
(fn [acc ^Route route]
|
||||||
(if-let [params ((:matcher route) path)]
|
(if-let [params ((:matcher route) path)]
|
||||||
(reduced (->Match (:path route) (:meta route) path params))))
|
(reduced (->Match (:path route) (:meta route) path params))))
|
||||||
nil data))
|
nil data))
|
||||||
(by-name [_ name]
|
(match-by-name [_ name]
|
||||||
((lookup name) nil))
|
((lookup name) nil))
|
||||||
(by-name [_ name params]
|
(match-by-name [_ name params]
|
||||||
((lookup name) params)))
|
((lookup name) params)))
|
||||||
|
|
||||||
(defn linear-router [routes]
|
(defn linear-router [routes]
|
||||||
|
|
@ -95,14 +95,20 @@
|
||||||
Routing
|
Routing
|
||||||
(routes [_]
|
(routes [_]
|
||||||
routes)
|
routes)
|
||||||
(match [_ path]
|
(match-by-path [_ path]
|
||||||
(data path))
|
(data path))
|
||||||
(by-name [_ name]
|
(match-by-name [_ name]
|
||||||
((lookup name) nil))
|
((lookup name) nil))
|
||||||
(by-name [_ name params]
|
(match-by-name [_ name params]
|
||||||
((lookup name) params)))
|
((lookup name) params)))
|
||||||
|
|
||||||
(defn lookup-router [routes]
|
(defn lookup-router [routes]
|
||||||
|
(when-let [route (some impl/contains-wilds? (map first routes))]
|
||||||
|
(throw
|
||||||
|
(ex-info
|
||||||
|
(str "can't create LookupRouter with wildcard routes: " route)
|
||||||
|
{:route route
|
||||||
|
:routes routes})))
|
||||||
(->LookupRouter
|
(->LookupRouter
|
||||||
routes
|
routes
|
||||||
(->> (for [[p meta] routes]
|
(->> (for [[p meta] routes]
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,18 @@
|
||||||
:meta {:name ::beer}
|
:meta {:name ::beer}
|
||||||
:path "/api/ipa/large"
|
:path "/api/ipa/large"
|
||||||
:params {:size "large"}})
|
:params {:size "large"}})
|
||||||
(reitit/match router "/api/ipa/large")))
|
(reitit/match-by-path router "/api/ipa/large")))
|
||||||
(is (= (reitit/map->Match
|
(is (= (reitit/map->Match
|
||||||
{:template "/api/ipa/:size"
|
{:template "/api/ipa/:size"
|
||||||
:meta {:name ::beer}
|
:meta {:name ::beer}
|
||||||
:path "/api/ipa/large"
|
:path "/api/ipa/large"
|
||||||
:params {:size "large"}})
|
:params {:size "large"}})
|
||||||
(reitit/by-name router ::beer {:size "large"})))
|
(reitit/match-by-name router ::beer {:size "large"})))
|
||||||
(is (thrown-with-msg?
|
(testing "name-based routing at runtime for missing parameters"
|
||||||
ExceptionInfo
|
(is (thrown-with-msg?
|
||||||
#"^missing path-params for route '/api/ipa/:size': \#\{:size\}$"
|
ExceptionInfo
|
||||||
(reitit/by-name router ::beer)))))
|
#"^missing path-params for route '/api/ipa/:size': \#\{:size\}$"
|
||||||
|
(reitit/match-by-name router ::beer))))))
|
||||||
|
|
||||||
(testing "lookup router"
|
(testing "lookup router"
|
||||||
(let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])]
|
(let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])]
|
||||||
|
|
@ -39,13 +40,20 @@
|
||||||
:meta {:name ::beer}
|
:meta {:name ::beer}
|
||||||
:path "/api/ipa/large"
|
:path "/api/ipa/large"
|
||||||
:params {}})
|
:params {}})
|
||||||
(reitit/match router "/api/ipa/large")))
|
(reitit/match-by-path router "/api/ipa/large")))
|
||||||
(is (= (reitit/map->Match
|
(is (= (reitit/map->Match
|
||||||
{:template "/api/ipa/large"
|
{:template "/api/ipa/large"
|
||||||
:meta {:name ::beer}
|
:meta {:name ::beer}
|
||||||
:path "/api/ipa/large"
|
:path "/api/ipa/large"
|
||||||
:params {:size "large"}})
|
:params {:size "large"}})
|
||||||
(reitit/by-name router ::beer {:size "large"})))))
|
(reitit/match-by-name router ::beer {:size "large"})))
|
||||||
|
(testing "can't be created with wildcard routes"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"can't create LookupRouter with wildcard routes"
|
||||||
|
(reitit/lookup-router
|
||||||
|
(reitit/resolve-routes
|
||||||
|
["/api/:version/ping"] {})))))))
|
||||||
|
|
||||||
(testing "bide sample"
|
(testing "bide sample"
|
||||||
(let [routes [["/auth/login" :auth/login]
|
(let [routes [["/auth/login" :auth/login]
|
||||||
|
|
@ -78,5 +86,5 @@
|
||||||
:meta {:mw [:api], :parameters {:id String, :sub-id String}}
|
:meta {:mw [:api], :parameters {:id String, :sub-id String}}
|
||||||
:path "/api/user/1/2"
|
:path "/api/user/1/2"
|
||||||
:params {:id "1", :sub-id "2"}})
|
:params {:id "1", :sub-id "2"}})
|
||||||
(reitit/match router "/api/user/1/2"))))))
|
(reitit/match-by-path router "/api/user/1/2"))))))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue