From 1835ffc681cf228a98590621ff12869bf8af1d82 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 12 Aug 2017 17:36:50 +0300 Subject: [PATCH] Support route compilation (fixes #14) * also, so docs --- README.md | 7 ++- src/reitit/core.cljc | 87 +++++++++++++++++++++------------ src/reitit/impl.cljc | 8 +-- test/cljc/reitit/core_test.cljc | 32 ++++++++---- 4 files changed, 90 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b6c86c77..accf4970 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Path-based routing: ; #Match{:template "/api/user/:id" ; :meta {:name :user/user} ; :path "/api/user/1" +; :handler nil ; :params {:id "1"}} ``` @@ -110,6 +111,7 @@ Oh, that didn't work, retry: ; #Match{:template "/api/user/:id" ; :meta {:name :user/user} ; :path "/api/user/1" +; :handler nil ; :params {:id "1"}} ``` @@ -164,10 +166,13 @@ Path-based routing: ; :middleware [:api-mw :admin-mw] ; :roles #{:root}} ; :path "/api/admin/root" +; :handler nil ; :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 diff --git a/src/reitit/core.cljc b/src/reitit/core.cljc index 2021af29..9f8e39f1 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -61,12 +61,15 @@ (mapv (partial coerce)) (filterv identity))) +(defn compile-route [compile [p m :as route]] + [p m (if compile (compile route))]) + (defprotocol Routing (routes [this]) (match-by-path [this path]) (match-by-name [this name] [this name parameters])) -(defrecord Match [template meta path params]) +(defrecord Match [template meta path handler params]) (defrecord LinearRouter [routes data lookup] Routing @@ -76,23 +79,29 @@ (reduce (fn [acc ^Route route] (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)) (match-by-name [_ name] ((lookup name) nil)) (match-by-name [_ name params] ((lookup name) params))) -(defn linear-router [routes] - (->LinearRouter - routes - (mapv (partial apply impl/create) routes) - (->> (for [[p {:keys [name] :as meta}] routes - :when name - :let [route (impl/create p meta)]] - [name (fn [params] - (->Match p meta (impl/path-for route params) params))]) - (into {})))) +(defn linear-router + "Creates a [[LinearRouter]] from routes and optional options. + See [[router]] for available options" + ([routes] + (linear-router routes {})) + ([routes {:keys [compile]}] + (let [compiled (map (partial compile-route compile) routes)] + (->LinearRouter + routes + (mapv (partial impl/create) compiled) + (->> (for [[p {:keys [name] :as meta} handler] compiled + :when name + :let [route (impl/create [p meta handler])]] + [name (fn [params] + (->Match p meta (impl/path-for route params) handler params))]) + (into {})))))) (defrecord LookupRouter [routes data lookup] Routing @@ -105,28 +114,46 @@ (match-by-name [_ name params] ((lookup name) params))) -(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 - routes - (->> (for [[p meta] routes] - [p (->Match p meta p {})]) - (into {})) - (->> (for [[p {:keys [name] :as meta}] routes - :when name] - [name (fn [params] - (->Match p meta p params))]) - (into {})))) +(defn lookup-router + "Creates a [[LookupRouter]] from routes and optional options. + See [[router]] for available options" + ([routes] + (lookup-router routes {})) + ([routes {:keys [compile]}] + (let [compiled (map (partial compile-route compile) 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 + routes + (->> (for [[p meta handler] compiled] + [p (->Match p meta p handler {})]) + (into {})) + (->> (for [[p {:keys [name] :as meta} handler] compiled + :when name] + [name (fn [params] + (->Match p meta p handler params))]) + (into {})))))) (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" ([data] (router data {})) ([data opts] (let [routes (resolve-routes data opts)] ((if (some impl/contains-wilds? (map first routes)) - linear-router lookup-router) routes)))) + linear-router lookup-router) routes opts)))) diff --git a/src/reitit/impl.cljc b/src/reitit/impl.cljc index 5114ae61..fad07a33 100644 --- a/src/reitit/impl.cljc +++ b/src/reitit/impl.cljc @@ -101,14 +101,15 @@ ;; 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) (as-> (parse-path path) $ (assoc $ :path-re (path-regex $)) (merge $ {:path path :matcher (path-matcher $) + :handler handler :meta meta}) (dissoc $ :path-re :path-constraints) (update $ :path-params set) @@ -117,7 +118,8 @@ (map->Route $)) (map->Route {:path path :meta meta - :matcher #(if (= path %) {})}))) + :matcher #(if (= path %) {}) + :handler handler}))) (defn path-for [^Route route params] (when-not (every? #(contains? params %) (:params route)) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index e00cf429..738a5d7c 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -55,10 +55,14 @@ (reitit/resolve-routes ["/api/:version/ping"] {}))))))) - (testing "route coercion" - (let [coerce (fn [[path meta]] + (testing "route coercion & compilation" + (let [compile-times (atom 0) + coerce (fn [[path meta]] (if-not (:invalid? meta) [path (assoc meta :path path)])) + compile (fn [[path meta]] + (swap! compile-times inc) + (constantly path)) router (reitit/router ["/api" {:roles #{:admin}} ["/ping" ::ping] @@ -66,14 +70,22 @@ ["/hidden" {:invalid? true} ["/utter"] ["/crap"]]] - {:coerce coerce})] - (is (= [["/api/ping" {:name ::ping - :path "/api/ping", - :roles #{:admin}}] - ["/api/pong" {:name ::pong - :path "/api/pong", - :roles #{:admin}}]] - (reitit/routes router))))) + {:coerce coerce + :compile compile})] + (testing "routes are coerced" + (is (= [["/api/ping" {:name ::ping + :path "/api/ping", + :roles #{:admin}}] + ["/api/pong" {:name ::pong + :path "/api/pong", + :roles #{:admin}}]] + (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 "bide sample" (let [routes [["/auth/login" :auth/login]