Merge pull request #15 from metosin/CompileRoutes

Support route compilation (fixes #14)
This commit is contained in:
Tommi Reiman 2017-08-14 09:38:23 +03:00 committed by GitHub
commit d74b98af2f
5 changed files with 115 additions and 61 deletions

View file

@ -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

View file

@ -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)))

View file

@ -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))))

View file

@ -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))

View file

@ -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"))))))