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"
; :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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 [_ _]

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]
(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)]