Allow middleware to be compiled (fixes #26)

Match :handler => :result
This commit is contained in:
Tommi Reiman 2017-08-30 08:14:06 +03:00
parent 18f8bdfbec
commit 4e22fd2f53
8 changed files with 197 additions and 88 deletions

View file

@ -114,7 +114,7 @@ Route names:
; #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 ; :result nil
; :params {:id "1"}} ; :params {:id "1"}}
``` ```
@ -124,7 +124,7 @@ Route names:
(reitit/match-by-name router ::user) (reitit/match-by-name router ::user)
; #PartialMatch{:template "/api/user/:id", ; #PartialMatch{:template "/api/user/:id",
; :meta {:name :user/user}, ; :meta {:name :user/user},
; :handler nil, ; :result nil,
; :params nil, ; :params nil,
; :required #{:id}} ; :required #{:id}}
@ -139,7 +139,7 @@ Only a partial match. Let's provide the path-parameters:
; #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 ; :result nil
; :params {:id "1"}} ; :params {:id "1"}}
``` ```
@ -201,13 +201,13 @@ Path-based routing:
; :interceptors [::api ::admin] ; :interceptors [::api ::admin]
; :roles #{:root}} ; :roles #{:root}}
; :path "/api/admin/root" ; :path "/api/admin/root"
; :handler nil ; :result nil
; :params {}} ; :params {}}
``` ```
On match, route meta-data is returned and can interpreted by the application. 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 ## Route conflicts
@ -225,10 +225,10 @@ Route trees should not have multiple routes that match to a single (request) pat
; /:user-id/orders ; /:user-id/orders
; -> /public/*path ; -> /public/*path
; -> /bulk/:bulk-id ; -> /bulk/:bulk-id
; ;
; /bulk/:bulk-id ; /bulk/:bulk-id
; -> /:version/status ; -> /:version/status
; ;
; /public/*path ; /public/*path
; -> /:version/status ; -> /: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 `[]`) | `: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`) | `: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` | `: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!`) | `: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 | `:router` | Function of `routes opts => router` to override the actual router implementation

View file

@ -46,7 +46,7 @@
(if (seq childs) (if (seq childs)
(walk-many (str pacc path) macc childs) (walk-many (str pacc path) macc childs)
[[(str pacc path) macc]]))))] [[(str pacc path) macc]]))))]
(walk-one path meta data))) (walk-one path (mapv identity meta) data)))
(defn map-meta [f routes] (defn map-meta [f routes]
(mapv #(update % 1 f) routes)) (mapv #(update % 1 f) routes))
@ -100,8 +100,8 @@
(match-by-path [this path]) (match-by-path [this path])
(match-by-name [this name] [this name params])) (match-by-name [this name] [this name params]))
(defrecord Match [template meta handler params path]) (defrecord Match [template meta result params path])
(defrecord PartialMatch [template meta handler params required]) (defrecord PartialMatch [template meta result params required])
(defn partial-match? [x] (defn partial-match? [x]
(instance? PartialMatch x)) (instance? PartialMatch x))
@ -132,11 +132,11 @@
(let [compiled (map #(compile-route % opts) routes) (let [compiled (map #(compile-route % opts) routes)
names (find-names routes opts) names (find-names routes opts)
[data lookup] (reduce [data lookup] (reduce
(fn [[data lookup] [p {:keys [name] :as meta} handler]] (fn [[data lookup] [p {:keys [name] :as meta} result]]
(let [{:keys [params] :as route} (impl/create [p meta handler]) (let [{:keys [params] :as route} (impl/create [p meta result])
f #(if-let [path (impl/path-for route %)] f #(if-let [path (impl/path-for route %)]
(->Match p meta handler % path) (->Match p meta result % path)
(->PartialMatch p meta handler % params))] (->PartialMatch p meta result % params))]
[(conj data route) [(conj data route)
(if name (assoc lookup name f) lookup)])) (if name (assoc lookup name f) lookup)]))
[[] {}] compiled) [[] {}] compiled)
@ -155,7 +155,7 @@
(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) (:handler route) params path)))) (reduced (->Match (:path route) (:meta route) (:result route) params path))))
nil data)) nil data))
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
@ -179,10 +179,10 @@
(let [compiled (map #(compile-route % opts) routes) (let [compiled (map #(compile-route % opts) routes)
names (find-names routes opts) names (find-names routes opts)
[data lookup] (reduce [data lookup] (reduce
(fn [[data lookup] [p {:keys [name] :as meta} handler]] (fn [[data lookup] [p {:keys [name] :as meta} result]]
[(assoc data p (->Match p meta handler {} p)) [(assoc data p (->Match p meta result {} p))
(if name (if name
(assoc lookup name #(->Match p meta handler % p)) (assoc lookup name #(->Match p meta result % p))
lookup)]) [{} {}] compiled) lookup)]) [{} {}] compiled)
data (impl/fast-map data) data (impl/fast-map data)
lookup (impl/fast-map lookup)] lookup (impl/fast-map lookup)]
@ -244,10 +244,10 @@
| -------------|-------------| | -------------|-------------|
| `:path` | Base-path for routes (default `\"\"`) | `:path` | Base-path for routes (default `\"\"`)
| `:routes` | Initial resolved 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`) | `: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` | `: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!`) | `: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" | `:router` | Function of `routes opts => router` to override the actual router implementation"
([data] ([data]

View file

@ -101,15 +101,15 @@
;; Routing (c) Metosin ;; 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) (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 :result result
:meta meta}) :meta meta})
(dissoc $ :path-re :path-constraints) (dissoc $ :path-re :path-constraints)
(update $ :path-params set) (update $ :path-params set)
@ -119,7 +119,7 @@
(map->Route {:path path (map->Route {:path path
:meta meta :meta meta
:matcher #(if (= path %) {}) :matcher #(if (= path %) {})
:handler handler}))) :result result})))
(defn segments [path] (defn segments [path]
(let [ss (-> (str/split path #"/") rest vec)] (let [ss (-> (str/split path #"/") rest vec)]

View file

@ -1,24 +1,45 @@
(ns reitit.middleware (ns reitit.middleware
(:require [meta-merge.core :refer [meta-merge]] (:require [meta-merge.core :refer [meta-merge]]
[reitit.core :as reitit])) [reitit.core :as reitit])
#?(:clj
(:import (clojure.lang IFn AFn))))
(defprotocol ExpandMiddleware (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 (extend-protocol ExpandMiddleware
#?(:clj clojure.lang.APersistentVector #?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector) :cljs cljs.core.PersistentVector)
(expand-middleware [[f & args] _] (expand-middleware [[f & args] meta opts]
(fn [handler] (if-let [mw (expand-middleware f meta opts)]
(apply f handler args))) (fn [handler]
(apply mw handler args))))
#?(:clj clojure.lang.Fn #?(:clj clojure.lang.Fn
:cljs function) :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 nil
(expand-middleware [_ _])) (expand-middleware [_ _ _]))
(defn- ensure-handler! [path meta scope] (defn- ensure-handler! [path meta scope]
(when-not (:handler meta) (when-not (:handler meta)
@ -28,18 +49,22 @@
(merge {:path path, :meta meta} (merge {:path path, :meta meta}
(if scope {:scope scope})))))) (if scope {:scope scope}))))))
(defn compose-middleware [middleware opts] (defn compose-middleware [middleware meta opts]
(->> middleware (->> middleware
(keep identity) (keep identity)
(map #(expand-middleware % opts)) (map #(expand-middleware % meta opts))
(keep identity)
(apply comp identity))) (apply comp identity)))
(defn gen [f & args]
(->MiddlewareGenerator f args))
(defn compile-handler (defn compile-handler
([route opts] ([route opts]
(compile-handler route opts nil)) (compile-handler route opts nil))
([[path {:keys [middleware handler] :as meta}] opts scope] ([[path {:keys [middleware handler] :as meta}] opts scope]
(ensure-handler! path meta scope) (ensure-handler! path meta scope)
((compose-middleware middleware opts) handler))) ((compose-middleware middleware meta opts) handler)))
(defn router (defn router
([data] ([data]

View file

@ -60,6 +60,5 @@
([data] ([data]
(router data nil)) (router data nil))
([data opts] ([data opts]
(let [opts (meta-merge {:coerce coerce-handler (let [opts (meta-merge {:coerce coerce-handler, :compile compile-handler} opts)]
:compile compile-handler} opts)]
(reitit/router data opts)))) (reitit/router data opts))))

View file

@ -96,15 +96,15 @@
(reitit/routes router)))) (reitit/routes router))))
(testing "route match contains compiled handler" (testing "route match contains compiled handler"
(is (= 2 @compile-times)) (is (= 2 @compile-times))
(let [{:keys [handler]} (reitit/match-by-path router "/api/pong")] (let [{:keys [result]} (reitit/match-by-path router "/api/pong")]
(is handler) (is result)
(is (= "/api/pong" (handler))) (is (= "/api/pong" (result)))
(is (= 2 @compile-times)))))) (is (= 2 @compile-times))))))
(testing "default compile" (testing "default compile"
(let [router (reitit/router ["/ping" (constantly "ok")])] (let [router (reitit/router ["/ping" (constantly "ok")])]
(let [{:keys [handler]} (reitit/match-by-path router "/ping")] (let [{:keys [result]} (reitit/match-by-path router "/ping")]
(is handler) (is result)
(is (= "ok" (handler))))))) (is (= "ok" (result)))))))
(testing "custom router" (testing "custom router"
(let [router (reitit/router ["/ping"] {:router (fn [_ _] (let [router (reitit/router ["/ping"] {:router (fn [_ _]

View file

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

View file

@ -26,50 +26,6 @@
([request respond raise] ([request respond raise]
(respond (handler request)))) (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 (deftest ring-router-test
(testing "all paths should have a handler" (testing "all paths should have a handler"
@ -142,7 +98,7 @@
(testing "only top-level route names are matched" (testing "only top-level route names are matched"
(is (= [::all ::get ::users] (is (= [::all ::get ::users]
(reitit/route-names router)))) (reitit/route-names router))))
(testing "all named routes can be matched" (testing "all named routes can be matched"
(doseq [name (reitit/route-names router)] (doseq [name (reitit/route-names router)]