mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 08:21:11 +00:00
Merge pull request #15 from metosin/CompileRoutes
Support route compilation (fixes #14)
This commit is contained in:
commit
d74b98af2f
5 changed files with 115 additions and 61 deletions
|
|
@ -93,6 +93,7 @@ Path-based routing:
|
||||||
; #Match{:template "/api/user/:id"
|
; #Match{:template "/api/user/:id"
|
||||||
; :meta {:name :user/user}
|
; :meta {:name :user/user}
|
||||||
; :path "/api/user/1"
|
; :path "/api/user/1"
|
||||||
|
; :handler nil
|
||||||
; :params {:id "1"}}
|
; :params {:id "1"}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -110,6 +111,7 @@ Oh, that didn't work, retry:
|
||||||
; #Match{:template "/api/user/:id"
|
; #Match{:template "/api/user/:id"
|
||||||
; :meta {:name :user/user}
|
; :meta {:name :user/user}
|
||||||
; :path "/api/user/1"
|
; :path "/api/user/1"
|
||||||
|
; :handler nil
|
||||||
; :params {:id "1"}}
|
; :params {:id "1"}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -164,10 +166,13 @@ Path-based routing:
|
||||||
; :middleware [:api-mw :admin-mw]
|
; :middleware [:api-mw :admin-mw]
|
||||||
; :roles #{:root}}
|
; :roles #{:root}}
|
||||||
; :path "/api/admin/root"
|
; :path "/api/admin/root"
|
||||||
|
; :handler nil
|
||||||
; :params {}}
|
; :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/).
|
Route meta-data is just data and the actual interpretation is left to the application. Custom coercion and route compilation can be defined via router options enabling things like [`clojure.spec`](https://clojure.org/about/spec) validation for route-meta data and pre-compiled route handlers ([Ring](https://github.com/ring-clojure/ring)-handlers or [Pedestal](pedestal.io)-style interceptors).
|
||||||
|
|
||||||
|
**TODO**: examples / implementations of different kind of routers. See [Open issues](https://github.com/metosin/reitit/issues/).
|
||||||
|
|
||||||
## Special thanks
|
## Special thanks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
|
|
||||||
;; 2.4µs (-84%)
|
;; 2.4µs (-84%)
|
||||||
(title "pedestal - map-tree => prefix-tree")
|
(title "pedestal - map-tree => prefix-tree")
|
||||||
(let [call #(pedestal/find-route pedestal-routes {:path-info "/workspace/1/1" :request-method :get})]
|
(let [call #(pedestal/find-route pedestal-router {:path-info "/workspace/1/1" :request-method :get})]
|
||||||
(assert (call))
|
(assert (call))
|
||||||
(cc/quick-bench
|
(cc/quick-bench
|
||||||
(call)))
|
(call)))
|
||||||
|
|
|
||||||
|
|
@ -55,18 +55,25 @@
|
||||||
(meta-merge acc {k v}))
|
(meta-merge acc {k v}))
|
||||||
{} x))
|
{} x))
|
||||||
|
|
||||||
(defn resolve-routes [data {:keys [coerce] :or {coerce identity} :as opts}]
|
(defn resolve-routes [data {:keys [coerce] :as opts}]
|
||||||
(->> (walk data opts)
|
(cond-> (->> (walk data opts)
|
||||||
(map-meta merge-meta)
|
(map-meta merge-meta))
|
||||||
(mapv (partial coerce))
|
coerce (->> (mapv (partial coerce))
|
||||||
(filterv identity)))
|
(filterv identity))))
|
||||||
|
|
||||||
|
(defn compile-route [compile [p m :as route]]
|
||||||
|
[p m (if compile (compile route))])
|
||||||
|
|
||||||
(defprotocol Routing
|
(defprotocol Routing
|
||||||
(routes [this])
|
(routes [this])
|
||||||
(match-by-path [this path])
|
(match-by-path [this path])
|
||||||
(match-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 handler params])
|
||||||
|
|
||||||
|
(def default-router-options
|
||||||
|
{:coerce identity
|
||||||
|
:compile (comp :handler second)})
|
||||||
|
|
||||||
(defrecord LinearRouter [routes data lookup]
|
(defrecord LinearRouter [routes data lookup]
|
||||||
Routing
|
Routing
|
||||||
|
|
@ -76,23 +83,29 @@
|
||||||
(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 (:handler route) params))))
|
||||||
nil data))
|
nil data))
|
||||||
(match-by-name [_ name]
|
(match-by-name [_ name]
|
||||||
((lookup name) nil))
|
((lookup name) nil))
|
||||||
(match-by-name [_ name params]
|
(match-by-name [_ name params]
|
||||||
((lookup name) params)))
|
((lookup name) params)))
|
||||||
|
|
||||||
(defn linear-router [routes]
|
(defn linear-router
|
||||||
(->LinearRouter
|
"Creates a [[LinearRouter]] from routes and optional options.
|
||||||
routes
|
See [[router]] for available options"
|
||||||
(mapv (partial apply impl/create) routes)
|
([routes]
|
||||||
(->> (for [[p {:keys [name] :as meta}] routes
|
(linear-router routes {}))
|
||||||
:when name
|
([routes opts]
|
||||||
:let [route (impl/create p meta)]]
|
(let [{:keys [compile]} (meta-merge default-router-options opts)
|
||||||
[name (fn [params]
|
compiled (map (partial compile-route compile) routes)
|
||||||
(->Match p meta (impl/path-for route params) params))])
|
[data lookup] (reduce
|
||||||
(into {}))))
|
(fn [[data lookup] [p {:keys [name] :as meta} handler]]
|
||||||
|
(let [route (impl/create [p meta handler])]
|
||||||
|
[(conj data route)
|
||||||
|
(if name
|
||||||
|
(assoc lookup name #(->Match p meta (impl/path-for route %) handler %))
|
||||||
|
lookup)])) [[] {}] compiled)]
|
||||||
|
(->LinearRouter routes data lookup))))
|
||||||
|
|
||||||
(defrecord LookupRouter [routes data lookup]
|
(defrecord LookupRouter [routes data lookup]
|
||||||
Routing
|
Routing
|
||||||
|
|
@ -105,28 +118,44 @@
|
||||||
(match-by-name [_ name params]
|
(match-by-name [_ name params]
|
||||||
((lookup name) params)))
|
((lookup name) params)))
|
||||||
|
|
||||||
(defn lookup-router [routes]
|
(defn lookup-router
|
||||||
|
"Creates a [[LookupRouter]] from routes and optional options.
|
||||||
|
See [[router]] for available options"
|
||||||
|
([routes]
|
||||||
|
(lookup-router routes {}))
|
||||||
|
([routes opts]
|
||||||
(when-let [route (some impl/contains-wilds? (map first routes))]
|
(when-let [route (some impl/contains-wilds? (map first routes))]
|
||||||
(throw
|
(throw
|
||||||
(ex-info
|
(ex-info
|
||||||
(str "can't create LookupRouter with wildcard routes: " route)
|
(str "can't create LookupRouter with wildcard routes: " route)
|
||||||
{:route route
|
{:route route
|
||||||
:routes routes})))
|
:routes routes})))
|
||||||
(->LookupRouter
|
(let [{:keys [compile]} (meta-merge default-router-options opts)
|
||||||
routes
|
compiled (map (partial compile-route compile) routes)
|
||||||
(->> (for [[p meta] routes]
|
[data lookup] (reduce
|
||||||
[p (->Match p meta p {})])
|
(fn [[data lookup] [p {:keys [name] :as meta} handler]]
|
||||||
(into {}))
|
[(assoc data p (->Match p meta p handler {}))
|
||||||
(->> (for [[p {:keys [name] :as meta}] routes
|
(if name
|
||||||
:when name]
|
(assoc lookup name #(->Match p meta p handler %))
|
||||||
[name (fn [params]
|
lookup)]) [{} {}] compiled)]
|
||||||
(->Match p meta p params))])
|
(->LookupRouter routes data lookup))))
|
||||||
(into {}))))
|
|
||||||
|
|
||||||
(defn router
|
(defn router
|
||||||
|
"Create a [[Router]] from raw route data and optionally an options map.
|
||||||
|
If routes contain wildcards, a [[LinearRouter]] is used, otherwise a
|
||||||
|
[[LookupRouter]]. The following options are available:
|
||||||
|
|
||||||
|
| keys | description |
|
||||||
|
| -----------|-------------|
|
||||||
|
| `:path` | Base-path for routes (default `\"\"`)
|
||||||
|
| `:routes` | Initial resolved routes (default `[]`)
|
||||||
|
| `:meta` | Initial expanded route-meta vector (default `[]`)
|
||||||
|
| `:expand` | Function `arg => meta` to expand route arg to route meta-data (default `reitit.core/expand`)
|
||||||
|
| `:coerce` | Function `[path meta] => [path meta]` to coerce resolved route, can throw or return `nil` (default `identity`)
|
||||||
|
| `:compile` | Function `[path meta] => handler` to compile a route handler (default `(comp :handler second)`)"
|
||||||
([data]
|
([data]
|
||||||
(router data {}))
|
(router data {}))
|
||||||
([data opts]
|
([data opts]
|
||||||
(let [routes (resolve-routes data opts)]
|
(let [routes (resolve-routes data opts)]
|
||||||
((if (some impl/contains-wilds? (map first routes))
|
((if (some impl/contains-wilds? (map first routes))
|
||||||
linear-router lookup-router) routes))))
|
linear-router lookup-router) routes opts))))
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
;
|
;
|
||||||
; You must not remove this notice, or any other, from this software.
|
; You must not remove this notice, or any other, from this software.
|
||||||
|
|
||||||
(ns reitit.impl
|
(ns ^:no-doc reitit.impl
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[clojure.set :as set])
|
[clojure.set :as set])
|
||||||
(:import #?(:clj (java.util.regex Pattern))))
|
(:import #?(:clj (java.util.regex Pattern))))
|
||||||
|
|
@ -101,14 +101,15 @@
|
||||||
;; Routing (c) Metosin
|
;; Routing (c) Metosin
|
||||||
;;
|
;;
|
||||||
|
|
||||||
(defrecord Route [path matcher parts params meta])
|
(defrecord Route [path matcher parts params meta handler])
|
||||||
|
|
||||||
(defn create [path meta]
|
(defn create [[path meta handler]]
|
||||||
(if (contains-wilds? path)
|
(if (contains-wilds? path)
|
||||||
(as-> (parse-path path) $
|
(as-> (parse-path path) $
|
||||||
(assoc $ :path-re (path-regex $))
|
(assoc $ :path-re (path-regex $))
|
||||||
(merge $ {:path path
|
(merge $ {:path path
|
||||||
:matcher (path-matcher $)
|
:matcher (path-matcher $)
|
||||||
|
:handler handler
|
||||||
:meta meta})
|
:meta meta})
|
||||||
(dissoc $ :path-re :path-constraints)
|
(dissoc $ :path-re :path-constraints)
|
||||||
(update $ :path-params set)
|
(update $ :path-params set)
|
||||||
|
|
@ -117,7 +118,8 @@
|
||||||
(map->Route $))
|
(map->Route $))
|
||||||
(map->Route {:path path
|
(map->Route {:path path
|
||||||
:meta meta
|
:meta meta
|
||||||
:matcher #(if (= path %) {})})))
|
:matcher #(if (= path %) {})
|
||||||
|
:handler handler})))
|
||||||
|
|
||||||
(defn path-for [^Route route params]
|
(defn path-for [^Route route params]
|
||||||
(when-not (every? #(contains? params %) (:params route))
|
(when-not (every? #(contains? params %) (:params route))
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,15 @@
|
||||||
(reitit/resolve-routes
|
(reitit/resolve-routes
|
||||||
["/api/:version/ping"] {})))))))
|
["/api/:version/ping"] {})))))))
|
||||||
|
|
||||||
(testing "route coercion"
|
(testing "route coercion & compilation"
|
||||||
(let [coerce (fn [[path meta]]
|
(testing "custom compile"
|
||||||
|
(let [compile-times (atom 0)
|
||||||
|
coerce (fn [[path meta]]
|
||||||
(if-not (:invalid? meta)
|
(if-not (:invalid? meta)
|
||||||
[path (assoc meta :path path)]))
|
[path (assoc meta :path path)]))
|
||||||
|
compile (fn [[path meta]]
|
||||||
|
(swap! compile-times inc)
|
||||||
|
(constantly path))
|
||||||
router (reitit/router
|
router (reitit/router
|
||||||
["/api" {:roles #{:admin}}
|
["/api" {:roles #{:admin}}
|
||||||
["/ping" ::ping]
|
["/ping" ::ping]
|
||||||
|
|
@ -66,14 +71,28 @@
|
||||||
["/hidden" {:invalid? true}
|
["/hidden" {:invalid? true}
|
||||||
["/utter"]
|
["/utter"]
|
||||||
["/crap"]]]
|
["/crap"]]]
|
||||||
{:coerce coerce})]
|
{:coerce coerce
|
||||||
|
:compile compile})]
|
||||||
|
(testing "routes are coerced"
|
||||||
(is (= [["/api/ping" {:name ::ping
|
(is (= [["/api/ping" {:name ::ping
|
||||||
:path "/api/ping",
|
:path "/api/ping",
|
||||||
:roles #{:admin}}]
|
:roles #{:admin}}]
|
||||||
["/api/pong" {:name ::pong
|
["/api/pong" {:name ::pong
|
||||||
:path "/api/pong",
|
:path "/api/pong",
|
||||||
:roles #{:admin}}]]
|
:roles #{:admin}}]]
|
||||||
(reitit/routes router)))))
|
(reitit/routes router))))
|
||||||
|
(testing "route match contains compiled handler"
|
||||||
|
(is (= 2 @compile-times))
|
||||||
|
(let [{:keys [handler]} (reitit/match-by-path router "/api/pong")]
|
||||||
|
(is handler)
|
||||||
|
(is (= "/api/pong" (handler)))
|
||||||
|
(is (= 2 @compile-times))))))
|
||||||
|
(testing "default compile"
|
||||||
|
(let [router (reitit/router ["/ping" (constantly "ok")])]
|
||||||
|
(println (reitit/match-by-path router "/ping"))
|
||||||
|
(let [{:keys [handler]} (reitit/match-by-path router "/ping")]
|
||||||
|
(is handler)
|
||||||
|
(is (= "ok" (handler)))))))
|
||||||
|
|
||||||
(testing "bide sample"
|
(testing "bide sample"
|
||||||
(let [routes [["/auth/login" :auth/login]
|
(let [routes [["/auth/login" :auth/login]
|
||||||
|
|
@ -107,4 +126,3 @@
|
||||||
:path "/api/user/1/2"
|
:path "/api/user/1/2"
|
||||||
:params {:id "1", :sub-id "2"}})
|
:params {:id "1", :sub-id "2"}})
|
||||||
(reitit/match-by-path router "/api/user/1/2"))))))
|
(reitit/match-by-path router "/api/user/1/2"))))))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue