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/perf-test/clj/reitit/perf_test.clj b/perf-test/clj/reitit/perf_test.clj index 0ae01ce1..abf917ab 100644 --- a/perf-test/clj/reitit/perf_test.clj +++ b/perf-test/clj/reitit/perf_test.clj @@ -93,7 +93,7 @@ ;; 2.4µs (-84%) (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)) (cc/quick-bench (call))) diff --git a/src/reitit/core.cljc b/src/reitit/core.cljc index 2021af29..2ef79975 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -55,18 +55,25 @@ (meta-merge acc {k v})) {} x)) -(defn resolve-routes [data {:keys [coerce] :or {coerce identity} :as opts}] - (->> (walk data opts) - (map-meta merge-meta) - (mapv (partial coerce)) - (filterv identity))) +(defn resolve-routes [data {:keys [coerce] :as opts}] + (cond-> (->> (walk data opts) + (map-meta merge-meta)) + coerce (->> (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]) + +(def default-router-options + {:coerce identity + :compile (comp :handler second)}) (defrecord LinearRouter [routes data lookup] Routing @@ -76,23 +83,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 opts] + (let [{:keys [compile]} (meta-merge default-router-options opts) + compiled (map (partial compile-route compile) routes) + [data lookup] (reduce + (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] Routing @@ -105,28 +118,44 @@ (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 opts] + (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}))) + (let [{:keys [compile]} (meta-merge default-router-options opts) + compiled (map (partial compile-route compile) routes) + [data lookup] (reduce + (fn [[data lookup] [p {:keys [name] :as meta} handler]] + [(assoc data p (->Match p meta p handler {})) + (if name + (assoc lookup name #(->Match p meta p handler %)) + lookup)]) [{} {}] compiled)] + (->LookupRouter routes data lookup)))) (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] (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..38fb5577 100644 --- a/src/reitit/impl.cljc +++ b/src/reitit/impl.cljc @@ -10,7 +10,7 @@ ; ; 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] [clojure.set :as set]) (:import #?(:clj (java.util.regex Pattern)))) @@ -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..2da36f71 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -55,25 +55,44 @@ (reitit/resolve-routes ["/api/:version/ping"] {}))))))) - (testing "route coercion" - (let [coerce (fn [[path meta]] - (if-not (:invalid? meta) - [path (assoc meta :path path)])) - router (reitit/router - ["/api" {:roles #{:admin}} - ["/ping" ::ping] - ["/pong" ::pong] - ["/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))))) + (testing "route coercion & compilation" + (testing "custom compile" + (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] + ["/pong" ::pong] + ["/hidden" {:invalid? true} + ["/utter"] + ["/crap"]]] + {: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 "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" (let [routes [["/auth/login" :auth/login] @@ -107,4 +126,3 @@ :path "/api/user/1/2" :params {:id "1", :sub-id "2"}}) (reitit/match-by-path router "/api/user/1/2")))))) -