fn -> handler in interceptors

This commit is contained in:
Tommi Reiman 2018-08-13 08:17:55 +03:00
parent 59cbb25688
commit 5e7be28eb7
2 changed files with 70 additions and 76 deletions

View file

@ -7,8 +7,12 @@
(defprotocol IntoInterceptor
(into-interceptor [this data opts]))
(defrecord Interceptor [name enter leave error])
(defrecord Interceptor [name handler? enter leave error])
(defrecord Endpoint [data interceptors])
(defrecord Context [request response exception])
(defn context [request]
(map->Context {:request request}))
(def ^:dynamic *max-compile-depth* 10)
@ -44,9 +48,12 @@
#?(:clj clojure.lang.Fn
:cljs function)
(into-interceptor [this _ _]
(map->Interceptor
{:enter this}))
(into-interceptor [this data opts]
(into-interceptor
{:handler? true
:enter (fn [ctx]
(assoc ctx :response (this (:request ctx))))}
data opts))
#?(:clj clojure.lang.PersistentArrayMap
:cljs cljs.core.PersistentArrayMap)
@ -78,43 +85,30 @@
nil
(into-interceptor [_ _ _]))
(defn- ensure-handler! [path data scope]
(when-not (:handler data)
(throw (ex-info
(str "path \"" path "\" doesn't have a :handler defined"
(if scope (str " for " scope)))
(merge {:path path, :data data}
(if scope {:scope scope}))))))
(defn- expand-and-transform
[interceptors data {:keys [::transform] :or {transform identity} :as opts}]
(->> interceptors
(keep #(into-interceptor % data opts))
(transform)
(keep #(into-interceptor % data opts))
(into [])))
;;
;; public api
;;
(defn chain
"Creates a Interceptor chain out of sequence of IntoInterceptor
and optionally a handler. Optionally takes route data and (Router) opts."
([interceptors handler data]
(chain interceptors handler data nil))
([interceptors handler data opts]
(let [interceptor (some-> (into-interceptor handler data opts)
(assoc :name (:name data)))]
(-> (expand-and-transform interceptors data opts)
(cond-> interceptor (conj interceptor))))))
Optionally takes route data and (Router) opts."
([interceptors]
(chain interceptors nil nil))
([interceptors data]
(chain interceptors data nil))
([interceptors data {:keys [::transform] :or {transform identity} :as opts}]
(->> interceptors
(keep #(into-interceptor % data opts))
(transform)
(keep #(into-interceptor % data opts))
(into []))))
(defn compile-result
([route opts]
(compile-result route opts nil))
([[_ {:keys [interceptors handler] :as data}] opts _]
(map->Endpoint
{:interceptors (chain interceptors handler data opts)
{:interceptors (chain (into (vec interceptors) [handler]) data opts)
:data data})))
(defn router

View file

@ -5,32 +5,39 @@
#?(:clj
(:import (clojure.lang ExceptionInfo))))
(def ctx (interceptor/context []))
(defn execute [interceptors ctx]
(as-> ctx $
(reduce #(%2 %1) $ (keep :enter interceptors))
(reduce #(%2 %1) $ (reverse (keep :leave interceptors)))))
(reduce #(%2 %1) $ (reverse (keep :leave interceptors)))
(:response $)))
(def ctx [])
(defn f [value ctx]
(update ctx :request conj value))
(defn kws [k qk]
(keyword (namespace qk) (str (name k) "_" (name qk))))
(defn interceptor [value]
{:name value
:enter #(conj % value)
:leave #(conj % value)})
:enter #(update % :request (fnil conj []) (kws :enter value))
:leave #(update % :response (fnil conj []) (kws :leave value))})
(defn enter [value]
{:name value
:enter #(conj % value)})
:enter (partial f value)})
(defn handler [ctx]
(conj ctx :ok))
(defn handler [request]
(conj request :ok))
(defn create
([interceptors]
(create interceptors nil))
(create interceptors nil))
([interceptors opts]
(let [chain (interceptor/chain
interceptors
handler :data opts)]
(conj interceptors handler)
:data opts)]
(partial execute chain))))
(deftest expand-interceptor-test
@ -41,8 +48,8 @@
(let [calls (atom 0)
enter (fn [value]
(swap! calls inc)
(fn [ctx]
(conj ctx value)))]
{:enter (fn [ctx]
(update ctx :request conj value))})]
(testing "as function"
(reset! calls 0)
@ -73,14 +80,14 @@
(testing "as map"
(reset! calls 0)
(let [app (create [{:enter (enter :value)}])]
(let [app (create [{:enter (:enter (enter :value))}])]
(dotimes [_ 10]
(is (= [:value :ok] (app ctx)))
(is (= 1 @calls)))))
(testing "as Interceptor"
(reset! calls 0)
(let [app (create [(interceptor/map->Interceptor {:enter (enter :value)})])]
(let [app (create [(interceptor/map->Interceptor {:enter (:enter (enter :value))})])]
(dotimes [_ 10]
(is (= [:value :ok] (app ctx)))
(is (= 1 @calls)))))))
@ -90,12 +97,12 @@
i1 (fn [value]
{:compile (fn [data _]
(swap! calls inc)
(fn [ctx]
(into ctx [data value])))})
{:enter (fn [ctx]
(update ctx :request into [data value]))})})
i3 (fn [value]
{:compile (fn [data _]
{:compile (fn [_ _]
(swap! calls inc)
{:compile (fn [data _]
{:compile (fn [_ _]
(swap! calls inc)
(i1 value))})})]
@ -137,16 +144,10 @@
(let [handler (interceptor/interceptor-handler router)]
(fn [path]
(when-let [interceptors (handler path)]
(execute interceptors [])))))
(execute interceptors ctx)))))
(deftest interceptor-handler-test
(testing "all paths should have a handler"
(is (thrown-with-msg?
ExceptionInfo
#"path \"/ping\" doesn't have a :handler defined"
(interceptor/router ["/ping"]))))
(testing "interceptor-handler"
(let [api-interceptor (interceptor :api)
router (interceptor/router
@ -164,36 +165,35 @@
(is (= [:ok] (app "/ping"))))
(testing "with interceptor"
(is (= [:api :ok :api] (app "/api/ping"))))
(is (= [:enter_api :ok :leave_api] (app "/api/ping"))))
(testing "with nested interceptor"
(is (= [:api :admin :ok :admin :api] (app "/api/admin/ping"))))
(is (= [:enter_api :enter_admin :ok :leave_admin :leave_api] (app "/api/admin/ping"))))
(testing ":compile interceptor can be unmounted at creation-time"
(let [i1 {:name ::i1, :compile (constantly (interceptor ::i1))}
i2 {:name ::i2, :compile (constantly nil)}
i3 (interceptor ::i3)
router (interceptor/router
["/api" {:name ::api
:interceptors [i1 i2 i3 i2]
["/api" {:interceptors [i1 i2 i3 i2]
:handler handler}])
app (create-app router)]
(is (= [::i1 ::i3 :ok ::i3 ::i1] (app "/api")))
(is (= [::enter_i1 ::enter_i3 :ok ::leave_i3 ::leave_i1] (app "/api")))
(testing "routes contain list of actually applied interceptors"
(is (= [::i1 ::i3 ::api] (->> (r/compiled-routes router)
first
last
:interceptors
(map :name)))))
(is (= [::i1 ::i3 nil] (->> (r/compiled-routes router)
first
last
:interceptors
(map :name)))))
(testing "match contains list of actually applied interceptors"
(is (= [::i1 ::i3 ::api] (->> "/api"
(r/match-by-path router)
:result
:interceptors
(map :name))))))))))
(is (= [::i1 ::i3 nil] (->> "/api"
(r/match-by-path router)
:result
:interceptors
(map :name))))))))))
(deftest chain-test
(testing "chain can produce interceptor chain of any IntoInterceptor"
@ -204,12 +204,12 @@
i5 {:compile (fn [{:keys [mount?]} _]
(when mount?
(interceptor ::i5)))}
chain1 (interceptor/chain [i1 i2 i3 i4 i5] handler {:mount? true})
chain2 (interceptor/chain [i1 i2 i3 i4 i5] handler {:mount? false})
chain3 (interceptor/chain [i1 i2 i3 i4 i5] nil {:mount? false})]
(is (= [::i1 ::i3 ::i4 ::i5 :ok ::i5 ::i4 ::i3 ::i1] (execute chain1 [])))
(is (= [::i1 ::i3 ::i4 :ok ::i4 ::i3 ::i1] (execute chain2 [])))
(is (= [::i1 ::i3 ::i4 ::i4 ::i3 ::i1] (execute chain3 []))))))
chain1 (interceptor/chain [i1 i2 i3 i4 i5 handler] {:mount? true})
chain2 (interceptor/chain [i1 i2 i3 i4 i5 handler] {:mount? false})
chain3 (interceptor/chain [i1 i2 i3 i4 i5] {:mount? false})]
(is (= [::enter_i1 ::enter_i3 ::enter_i4 ::enter_i5 :ok ::leave_i5 ::leave_i4 ::leave_i3 ::leave_i1] (execute chain1 ctx)))
(is (= [::enter_i1 ::enter_i3 ::enter_i4 :ok ::leave_i4 ::leave_i3 ::leave_i1] (execute chain2 ctx)))
(is (= [::leave_i4 ::leave_i3 ::leave_i1] (execute chain3 ctx))))))
(deftest interceptor-transform-test
(let [debug-i (enter ::debug)
@ -227,7 +227,7 @@
(is (= [::olipa ::kerran ::avaruus :ok] (app "/ping")))))
(testing "interceptors can be re-ordered"
(let [app (create {::interceptor/transform (partial sort-by :name)})]
(let [app (create {::interceptor/transform (partial sort-by (juxt :handler? :name))})]
(is (= [::avaruus ::kerran ::olipa :ok] (app "/ping")))))
(testing "adding debug interceptor between interceptors"