diff --git a/README.md b/README.md index f73c3178..c24c9bfb 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Route names: ; #Match{:template "/api/user/:id" ; :meta {:name :user/user} ; :path "/api/user/1" -; :handler nil +; :result nil ; :params {:id "1"}} ``` @@ -124,7 +124,7 @@ Route names: (reitit/match-by-name router ::user) ; #PartialMatch{:template "/api/user/:id", ; :meta {:name :user/user}, -; :handler nil, +; :result nil, ; :params nil, ; :required #{:id}} @@ -139,7 +139,7 @@ Only a partial match. Let's provide the path-parameters: ; #Match{:template "/api/user/:id" ; :meta {:name :user/user} ; :path "/api/user/1" -; :handler nil +; :result nil ; :params {:id "1"}} ``` @@ -201,13 +201,13 @@ Path-based routing: ; :interceptors [::api ::admin] ; :roles #{:root}} ; :path "/api/admin/root" -; :handler nil +; :result nil ; :params {}} ``` On match, route meta-data is returned and can interpreted by the application. -Routers also support meta-data compilation enabling things like fast [Ring](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:handler` in the match. See [configuring routers](#configuring-routers) for details. +Routers also support meta-data compilation enabling things like fast [Ring](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:result` in the match. See [configuring routers](#configuring-routers) for details. ## Route conflicts @@ -225,10 +225,10 @@ Route trees should not have multiple routes that match to a single (request) pat ; /:user-id/orders ; -> /public/*path ; -> /bulk/:bulk-id -; +; ; /bulk/:bulk-id ; -> /:version/status -; +; ; /public/*path ; -> /:version/status ; @@ -424,7 +424,7 @@ Routers can be configured via options. Options allow things like [`clojure.spec` | `:meta` | Initial expanded route-meta vector (default `[]`) | `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`) | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` - | `:compile` | Function of `route opts => handler` to compile a route handler + | `:compile` | Function of `route opts => result` to compile a route handler | `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) | `:router` | Function of `routes opts => router` to override the actual router implementation diff --git a/src/reitit/core.cljc b/src/reitit/core.cljc index 003c4040..ae1282fb 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -46,7 +46,7 @@ (if (seq childs) (walk-many (str pacc path) macc childs) [[(str pacc path) macc]]))))] - (walk-one path meta data))) + (walk-one path (mapv identity meta) data))) (defn map-meta [f routes] (mapv #(update % 1 f) routes)) @@ -100,8 +100,8 @@ (match-by-path [this path]) (match-by-name [this name] [this name params])) -(defrecord Match [template meta handler params path]) -(defrecord PartialMatch [template meta handler params required]) +(defrecord Match [template meta result params path]) +(defrecord PartialMatch [template meta result params required]) (defn partial-match? [x] (instance? PartialMatch x)) @@ -132,11 +132,11 @@ (let [compiled (map #(compile-route % opts) routes) names (find-names routes opts) [data lookup] (reduce - (fn [[data lookup] [p {:keys [name] :as meta} handler]] - (let [{:keys [params] :as route} (impl/create [p meta handler]) + (fn [[data lookup] [p {:keys [name] :as meta} result]] + (let [{:keys [params] :as route} (impl/create [p meta result]) f #(if-let [path (impl/path-for route %)] - (->Match p meta handler % path) - (->PartialMatch p meta handler % params))] + (->Match p meta result % path) + (->PartialMatch p meta result % params))] [(conj data route) (if name (assoc lookup name f) lookup)])) [[] {}] compiled) @@ -155,7 +155,7 @@ (reduce (fn [acc ^Route route] (if-let [params ((:matcher route) path)] - (reduced (->Match (:path route) (:meta route) (:handler route) params path)))) + (reduced (->Match (:path route) (:meta route) (:result route) params path)))) nil data)) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] @@ -179,10 +179,10 @@ (let [compiled (map #(compile-route % opts) routes) names (find-names routes opts) [data lookup] (reduce - (fn [[data lookup] [p {:keys [name] :as meta} handler]] - [(assoc data p (->Match p meta handler {} p)) + (fn [[data lookup] [p {:keys [name] :as meta} result]] + [(assoc data p (->Match p meta result {} p)) (if name - (assoc lookup name #(->Match p meta handler % p)) + (assoc lookup name #(->Match p meta result % p)) lookup)]) [{} {}] compiled) data (impl/fast-map data) lookup (impl/fast-map lookup)] @@ -244,10 +244,10 @@ | -------------|-------------| | `:path` | Base-path for routes (default `\"\"`) | `:routes` | Initial resolved routes (default `[]`) - | `:meta` | Initial expanded route-meta vector (default `[]`) + | `:meta` | Initial route meta (default `{}`) | `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`) | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` - | `:compile` | Function of `route opts => handler` to compile a route handler + | `:compile` | Function of `route opts => result` to compile a route handler | `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`) | `:router` | Function of `routes opts => router` to override the actual router implementation" ([data] diff --git a/src/reitit/impl.cljc b/src/reitit/impl.cljc index 217a48fb..be1e610c 100644 --- a/src/reitit/impl.cljc +++ b/src/reitit/impl.cljc @@ -101,15 +101,15 @@ ;; Routing (c) Metosin ;; -(defrecord Route [path matcher parts params meta handler]) +(defrecord Route [path matcher parts params meta result]) -(defn create [[path meta handler]] +(defn create [[path meta result]] (if (contains-wilds? path) (as-> (parse-path path) $ (assoc $ :path-re (path-regex $)) (merge $ {:path path :matcher (path-matcher $) - :handler handler + :result result :meta meta}) (dissoc $ :path-re :path-constraints) (update $ :path-params set) @@ -119,7 +119,7 @@ (map->Route {:path path :meta meta :matcher #(if (= path %) {}) - :handler handler}))) + :result result}))) (defn segments [path] (let [ss (-> (str/split path #"/") rest vec)] diff --git a/src/reitit/middleware.cljc b/src/reitit/middleware.cljc index 0dd97da7..853fc50d 100644 --- a/src/reitit/middleware.cljc +++ b/src/reitit/middleware.cljc @@ -1,24 +1,45 @@ (ns reitit.middleware (:require [meta-merge.core :refer [meta-merge]] - [reitit.core :as reitit])) + [reitit.core :as reitit]) + #?(:clj + (:import (clojure.lang IFn AFn)))) (defprotocol ExpandMiddleware - (expand-middleware [this opts])) + (expand-middleware [this meta opts])) + +(defrecord MiddlewareGenerator [f args] + IFn + (invoke [_] + (f nil nil)) + (invoke [_ meta] + (f meta nil)) + (invoke [_ meta opts] + (f meta opts)) + #?(:clj + (applyTo [this args] + (AFn/applyToHelper this args)))) (extend-protocol ExpandMiddleware #?(:clj clojure.lang.APersistentVector :cljs cljs.core.PersistentVector) - (expand-middleware [[f & args] _] - (fn [handler] - (apply f handler args))) + (expand-middleware [[f & args] meta opts] + (if-let [mw (expand-middleware f meta opts)] + (fn [handler] + (apply mw handler args)))) #?(:clj clojure.lang.Fn :cljs function) - (expand-middleware [this _] this) + (expand-middleware [this _ _] this) + + MiddlewareGenerator + (expand-middleware [this meta opts] + (if-let [mw (this meta opts)] + (fn [handler & args] + (apply mw handler args)))) nil - (expand-middleware [_ _])) + (expand-middleware [_ _ _])) (defn- ensure-handler! [path meta scope] (when-not (:handler meta) @@ -28,18 +49,22 @@ (merge {:path path, :meta meta} (if scope {:scope scope})))))) -(defn compose-middleware [middleware opts] +(defn compose-middleware [middleware meta opts] (->> middleware (keep identity) - (map #(expand-middleware % opts)) + (map #(expand-middleware % meta opts)) + (keep identity) (apply comp identity))) +(defn gen [f & args] + (->MiddlewareGenerator f args)) + (defn compile-handler ([route opts] (compile-handler route opts nil)) ([[path {:keys [middleware handler] :as meta}] opts scope] (ensure-handler! path meta scope) - ((compose-middleware middleware opts) handler))) + ((compose-middleware middleware meta opts) handler))) (defn router ([data] diff --git a/src/reitit/ring.cljc b/src/reitit/ring.cljc index 2badd934..5ff83a98 100644 --- a/src/reitit/ring.cljc +++ b/src/reitit/ring.cljc @@ -60,6 +60,5 @@ ([data] (router data nil)) ([data opts] - (let [opts (meta-merge {:coerce coerce-handler - :compile compile-handler} opts)] + (let [opts (meta-merge {:coerce coerce-handler, :compile compile-handler} opts)] (reitit/router data opts)))) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index d292fc23..5dfe38f2 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -96,15 +96,15 @@ (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))) + (let [{:keys [result]} (reitit/match-by-path router "/api/pong")] + (is result) + (is (= "/api/pong" (result))) (is (= 2 @compile-times)))))) (testing "default compile" (let [router (reitit/router ["/ping" (constantly "ok")])] - (let [{:keys [handler]} (reitit/match-by-path router "/ping")] - (is handler) - (is (= "ok" (handler))))))) + (let [{:keys [result]} (reitit/match-by-path router "/ping")] + (is result) + (is (= "ok" (result))))))) (testing "custom router" (let [router (reitit/router ["/ping"] {:router (fn [_ _] diff --git a/test/cljc/reitit/middleware_test.cljc b/test/cljc/reitit/middleware_test.cljc new file mode 100644 index 00000000..3c57b2aa --- /dev/null +++ b/test/cljc/reitit/middleware_test.cljc @@ -0,0 +1,129 @@ +(ns reitit.middleware-test + (:require [clojure.test :refer [deftest testing is]] + [reitit.middleware :as middleware] + [clojure.set :as set] + [reitit.core :as reitit]) + #?(:clj + (:import (clojure.lang ExceptionInfo)))) + +(defn mw [handler name] + (fn + ([request] + (-> request + (update ::mw (fnil conj []) name) + (handler) + (update :body (fnil conj []) name))) + ([request respond raise] + (handler + (update request ::mw (fnil conj []) name) + #(respond (update % :body (fnil conj []) name)) + raise)))) + +(defn handler + ([{:keys [::mw]}] + {:status 200 :body (conj mw :ok)}) + ([request respond raise] + (respond (handler request)))) + +(deftest expand-middleware-test + (testing "middleware generators" + (let [calls (atom 0)] + + (testing "record generator" + (reset! calls 0) + (let [syntax [(middleware/gen + (fn [meta _] + (swap! calls inc) + (fn [handler value] + (swap! calls inc) + (fn [request] + [meta value request]))))] + app ((middleware/compose-middleware syntax :meta {}) identity :value)] + (dotimes [_ 10] + (is (= [:meta :value :request] (app :request))) + (is (= 2 @calls))))) + + (testing "middleware generator as function" + (reset! calls 0) + (let [syntax (middleware/gen + (fn [meta _] + (swap! calls inc) + (fn [handler value] + (swap! calls inc) + (fn [request] + [meta value request])))) + app ((syntax :meta nil) identity :value)] + (dotimes [_ 10] + (is (= [:meta :value :request] (app :request))) + (is (= 2 @calls))))) + + (testing "generator vector" + (reset! calls 0) + (let [syntax [[(middleware/gen + (fn [meta _] + (swap! calls inc) + (fn [handler value] + (swap! calls inc) + (fn [request] + [meta value request])))) :value]] + app ((middleware/compose-middleware syntax :meta {}) identity)] + (is (= [:meta :value :request] (app :request))) + (dotimes [_ 10] + (is (= [:meta :value :request] (app :request))) + (is (= 2 @calls))))) + + (testing "generator can return nil" + (reset! calls 0) + (let [syntax [[(middleware/gen + (fn [meta _])) :value]] + app ((middleware/compose-middleware syntax :meta {}) identity)] + (is (= :request (app :request))) + (dotimes [_ 10] + (is (= :request (app :request))))))))) + + +(deftest middleware-router-test + + (testing "all paths should have a handler" + (is (thrown-with-msg? + ExceptionInfo + #"path \"/ping\" doesn't have a :handler defined" + (middleware/router ["/ping"])))) + + (testing "ring-handler" + (let [api-mw #(mw % :api) + router (middleware/router + [["/ping" handler] + ["/api" {:middleware [api-mw]} + ["/ping" handler] + ["/admin" {:middleware [[mw :admin]]} + ["/ping" handler]]]]) + app (fn + ([{:keys [uri] :as request}] + (if-let [handler (:result (reitit/match-by-path router uri))] + (handler request))) + ([{:keys [uri] :as request} respond raise] + (if-let [handler (:result (reitit/match-by-path router uri))] + (handler request respond raise))))] + + (testing "not found" + (is (= nil (app {:uri "/favicon.ico"})))) + + (testing "normal handler" + (is (= {:status 200, :body [:ok]} + (app {:uri "/ping"})))) + + (testing "with middleware" + (is (= {:status 200, :body [:api :ok :api]} + (app {:uri "/api/ping"})))) + + (testing "with nested middleware" + (is (= {:status 200, :body [:api :admin :ok :admin :api]} + (app {:uri "/api/admin/ping"})))) + + (testing "3-arity" + (let [result (atom nil) + respond (partial reset! result), raise ::not-called] + (app {:uri "/api/admin/ping"} respond raise) + (is (= {:status 200, :body [:api :admin :ok :admin :api]} + @result))))))) diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index 641c40a7..8029d55f 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -26,50 +26,6 @@ ([request respond raise] (respond (handler request)))) -(deftest middleware-router-test - - (testing "all paths should have a handler" - (is (thrown-with-msg? - ExceptionInfo - #"path \"/ping\" doesn't have a :handler defined" - (middleware/router ["/ping"])))) - - (testing "ring-handler" - (let [api-mw #(mw % :api) - router (middleware/router - [["/ping" handler] - ["/api" {:middleware [api-mw]} - ["/ping" handler] - ["/admin" {:middleware [[mw :admin]]} - ["/ping" handler]]]]) - app (ring/ring-handler router)] - - (testing "router can be extracted" - (is (= router (ring/get-router app)))) - - (testing "not found" - (is (= nil (app {:uri "/favicon.ico"})))) - - (testing "normal handler" - (is (= {:status 200, :body [:ok]} - (app {:uri "/ping"})))) - - (testing "with middleware" - (is (= {:status 200, :body [:api :ok :api]} - (app {:uri "/api/ping"})))) - - (testing "with nested middleware" - (is (= {:status 200, :body [:api :admin :ok :admin :api]} - (app {:uri "/api/admin/ping"})))) - - (testing "3-arity" - (let [result (atom nil) - respond (partial reset! result), raise ::not-called] - (app {:uri "/api/admin/ping"} respond raise) - (is (= {:status 200, :body [:api :admin :ok :admin :api]} - @result))))))) - - (deftest ring-router-test (testing "all paths should have a handler" @@ -142,7 +98,7 @@ (testing "only top-level route names are matched" (is (= [::all ::get ::users] - (reitit/route-names router)))) + (reitit/route-names router)))) (testing "all named routes can be matched" (doseq [name (reitit/route-names router)]