diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d68907a..83e348f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,22 @@ * new optional module for [Pedestal](http://pedestal.io/) integration. See [the docs](https://metosin.github.io/reitit/http/pedestal.html). +## `reitit-middleware` + +* `reitit.ring.middleware.dev/print-request-diffs` middleware transformation function to print out request diffs between middleware to the console + * read the [docs](https://metosin.github.io/reitit/ring/transforming_middleware_chain.html#printing-request-diffs) + * see [example app](https://github.com/metosin/reitit/tree/master/examples/ring-swagger) + + + +## `reitit-interceptors` + +* `reitit.http.interceptors.dev/print-context-diffs` interceptor transformation function to print out context diffs between interceptor steps to the console: + * read the [docs](https://metosin.github.io/reitit/http/transforming_interceptor_chain.html#printing-context-diffs) + * see [example app](https://github.com/metosin/reitit/tree/master/examples/http-swagger) + + + ### dependencies * updated: diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 71bd208c..849725c6 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -44,6 +44,7 @@ * [Pedestal](http/pedestal.md) * [Sieppari](http/sieppari.md) * [Default Interceptors](http/default_interceptors.md) +* [Transforming Interceptor Chain](http/transforming_interceptor_chain.md) ## Frontend diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index 6e4014dd..7ba4c771 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -40,7 +40,8 @@ ["Interceptors" {:file "doc/http/interceptors.md"}] ["Pedestal" {:file "doc/http/pedestal.md"}] ["Sieppari" {:file "doc/http/sieppari.md"}] - ["Default Interceptors" {:file "doc/http/default_interceptors.md"}]] + ["Default Interceptors" {:file "doc/http/default_interceptors.md"}] + ["Transforming Interceptor Chain" {:file "doc/http/transforming_interceptor_chain.md"}]] ["Frontend" {} ["Basics" {:file "doc/frontend/basics.md"}] ["Browser integration" {:file "doc/frontend/browser.md"}] diff --git a/doc/http/interceptors.md b/doc/http/interceptors.md index 8e74d968..bcd1a0c4 100644 --- a/doc/http/interceptors.md +++ b/doc/http/interceptors.md @@ -12,9 +12,46 @@ An module for http-routing using interceptors instead of middleware. Builds on t The differences: -* instead of `:middleware`, uses `:interceptors`. -* `reitit.http/http-router` requires an extra option `:executor` of type `reitit.interceptor/Executor`. -* instead of creating a ring-handler, apps can be wrapped into a routing interceptor that enqueues the matched interceptors into the context. For this, there is `reitit.http/routing-interceptor`. +* `:interceptors` key in used in route data instead of `:middleware` +* `reitit.http/http-router` requires an extra option `:executor` of type `reitit.interceptor/Executor` to execute the interceptor chain + * optionally, a routing interceptor can be used - it enqueues the matched interceptors into the context. See `reitit.http/routing-interceptor` for details. + +## Simple example + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.http :as http]) +(require '[reitit.interceptor.sieppari :as sieppari]) + +(defn interceptor [number] + {:enter (fn [ctx] (update-in ctx [:request :number] (fnil + 0) number))}) + +(def app + (http/ring-handler + (http/router + ["/api" + {:interceptors [(interceptor 1)]} + + ["/number" + {:interceptors [(interceptor 10)] + :get {:interceptors [(interceptor 100)] + :handler (fn [req] + {:status 200 + :body (select-keys req [:number])})}}]]) + + ;; the default handler + (ring/create-default-handler) + + ;; executor + {:executor sieppari/executor})) + + +(app {:request-method :get, :uri "/"}) +; {:status 404, :body "", :headers {}} + +(app {:request-method :get, :uri "/api/number"}) +; {:status 200, :body {:number 111}} +``` ## Why interceptors? diff --git a/doc/http/pedestal.md b/doc/http/pedestal.md index e0b441ce..be1efdc4 100644 --- a/doc/http/pedestal.md +++ b/doc/http/pedestal.md @@ -29,14 +29,24 @@ A minimalistic example on how to to swap the default-router with a reitit router ; [metosin/reitit-pedestal "0.2.10-alpha1"] ; [metosin/reitit "0.2.10-alpha1"] -(ns example.server - (:require [io.pedestal.http :as server] - [reitit.pedestal :as pedestal] - [reitit.http :as http] - [reitit.ring :as ring])) +(require '[io.pedestal.http :as server]) +(require '[reitit.pedestal :as pedestal]) +(require '[reitit.http :as http]) +(require '[reitit.ring :as ring]) + +(defn interceptor [number] + {:enter (fn [ctx] (update-in ctx [:request :number] (fnil + 0) number))}) (def routes - ["/ping" {:get (fn [_] {:status 200, :body "pong"})}]) + ["/api" + {:interceptors [(interceptor 1)]} + + ["/number" + {:interceptors [(interceptor 10)] + :get {:interceptors [(interceptor 100)] + :handler (fn [req] + {:status 200 + :body (select-keys req [:number])})}}]]) (-> {::server/type :jetty ::server/port 3000 diff --git a/doc/http/transforming_interceptor_chain.md b/doc/http/transforming_interceptor_chain.md new file mode 100644 index 00000000..ca380b72 --- /dev/null +++ b/doc/http/transforming_interceptor_chain.md @@ -0,0 +1,79 @@ +# Transforming the Interceptor Chain + +There is an extra option in http-router (actually, in the underlying interceptor-router): `:reitit.interceptor/transform` to transform the interceptor chain per endpoint. Value should be a function or a vector of functions that get a vector of compiled interceptors and should return a new vector of interceptors. + +**Note:** the last interceptor in the chain is usually the handler, compiled into an Interceptor. Applying a tranfromation `clojure.core/reverse` would put this interceptor into first in the chain, making the rest of the interceptors effectively unreachable. There is a helper `reitit.interceptor/transform-butlast` to transform all but the last interceptor. + +## Example Application + +```clj +(require '[reitit.http :as http]) +(require '[reitit.interceptor.sieppari :as sieppari]) + +(defn interceptor [message] + {:enter (fn [ctx] (update-in ctx [:request :message] (fnil conj []) message))}) + +(defn handler [req] + {:status 200 + :body (select-keys req [:message])}) + +(def app + (http/ring-handler + (http/router + ["/api" {:interceptors [(interceptor 1) (interceptor 2)]} + ["/ping" {:get {:interceptors [(interceptor 3)] + :handler handler}}]]) + {:executor sieppari/executor})) + +(app {:request-method :get, :uri "/api/ping"}) +; {:status 200, :body {:message [1 2 3]}} + +``` + +### Reversing the Interceptor Chain + +```clj +(def app + (http/ring-handler + (http/router + ["/api" {:interceptors [(interceptor 1) (interceptor 2)]} + ["/ping" {:get {:interceptors [(interceptor 3)] + :handler handler}}]] + {::interceptor/transform (interceptor/transform-butlast reverse)}) + {:executor sieppari/executor})) + +(app {:request-method :get, :uri "/api/ping"}) +; {:status 200, :body {:message [3 2 1]}} +``` + +### Interleaving Interceptors + +```clj +(def app + (http/ring-handler + (http/router + ["/api" {:interceptors [(interceptor 1) (interceptor 2)]} + ["/ping" {:get {:interceptors [(interceptor 3)] + :handler handler}}]] + {::interceptor/transform #(interleave % (repeat (interceptor :debug)))}) + {:executor sieppari/executor})) + +(app {:request-method :get, :uri "/api/ping"}) +; {:status 200, :body {:message [1 :debug 2 :debug 3 :debug]}} +``` + +### Printing Context Diffs + +```clj +[metosin/reitit-interceptors "0.2.9"] +``` + +Using `reitit.http.interceptors.dev/print-context-diffs` transformation, the context diffs between each interceptor are printed out to the console. To use it, add the following router option: + +```clj +:reitit.interceptor/transform reitit.http.interceptor.dev/print-context-diffs +``` + +Sample output: + +![Http Context Diff](../images/http-context-diff.png) diff --git a/doc/images/http-context-diff.png b/doc/images/http-context-diff.png new file mode 100644 index 00000000..88c07ece Binary files /dev/null and b/doc/images/http-context-diff.png differ diff --git a/doc/images/ring-request-diff.png b/doc/images/ring-request-diff.png new file mode 100644 index 00000000..a7973e60 Binary files /dev/null and b/doc/images/ring-request-diff.png differ diff --git a/doc/ring/default_middleware.md b/doc/ring/default_middleware.md index 018739b9..dbdb80a6 100644 --- a/doc/ring/default_middleware.md +++ b/doc/ring/default_middleware.md @@ -6,19 +6,20 @@ Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases, yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware. -* [Parameter handling](#parameters-handling) -* [Exception handling](#exception-handling) -* [Content negotiation](#content-negotiation) -* [Multipart request handling](#multipart-request-handling) +* [Parameter Handling](#parameters-handling) +* [Exception Handling](#exception-handling) +* [Content Negotiation](#content-negotiation) +* [Multipart Request Handling](#multipart-request-handling) +* [Inspecting Requests](#inspecting-requests) -## Parameters handling +## Parameters Handling `reitit.ring.middleware.parameters/parameters-middleware` to capture query- and form-params. Wraps `ring.middleware.params/wrap-params`. **NOTE**: will be factored into two parts: a query-parameters middleware and a Muuntaja format responsible for the the `application/x-www-form-urlencoded` body format. -## Exception handling +## Exception Handling A polished version of [compojure-api](https://github.com/metosin/compojure-api) exception handling. Catches all exceptions and invokes configured exception handler. @@ -207,7 +208,7 @@ Server: Jetty(9.2.21.v20170120) kukka ``` -## Multipart request handling +## Multipart Request Handling Wrapper for [Ring Multipart Middleware](https://github.com/ring-clojure/ring/blob/master/ring-core/src/ring/middleware/multipart_params.clj). Emits swagger `:consumes` definitions automatically. @@ -225,6 +226,18 @@ Expected route data: * `multipart/multipart-middleware` a preconfigured middleware for multipart handling * `multipart/create-multipart-middleware` to generate with custom configuration +## Inspecting Requests + +`reitit.ring.middleware.dev/print-request-diffs` is a [middleware chain transforming function](transforming_middleware_chain.md). It prints a request diff between each middleware. To use it, add the following router option: + +```clj +:reitit.middleware/transform reitit.ring.middleware.dev/print-request-diffs +``` + +Partial sample output: + +![Opensensors perf test](../images/ring-request-diff.png) + ## Example app See an example app with the default middleware in action: https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj. diff --git a/doc/ring/transforming_middleware_chain.md b/doc/ring/transforming_middleware_chain.md index 55d60d08..9dcff0e1 100644 --- a/doc/ring/transforming_middleware_chain.md +++ b/doc/ring/transforming_middleware_chain.md @@ -1,37 +1,74 @@ # Transforming the Middleware Chain -There is an extra option in ring-router (actually, in the underlying middleware-router): `:reitit.middleware/transform` to transform the middleware chain per endpoint. It gets the vector of compiled middleware and should return a new vector of middleware. +There is an extra option in ring-router (actually, in the underlying middleware-router): `:reitit.middleware/transform` to transform the middleware chain per endpoint. Value should be a function or a vector of functions that get a vector of compiled middleware and should return a new vector of middleware. -## Adding debug middleware between all other middleware +## Example Application + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.middleware :as middleware]) + +(defn wrap [handler id] + (fn [request] + (handler (update request ::acc (fnil conj []) id)))) + +(defn handler [{:keys [::acc]}] + {:status 200, :body (conj acc :handler)}) + +(def app + (ring/ring-handler + (ring/router + ["/api" {:middleware [[wrap 1] [wrap 2]]} + ["/ping" {:get {:middleware [[wrap 3]] + :handler handler}}]]))) + +(app {:request-method :get, :uri "/api/ping"}) +; {:status 200, :body [1 2 3 :handler]} +``` + +### Reversing the Middleware Chain ```clj (def app (ring/ring-handler (ring/router - ["/api" {:middleware [[wrap 1] [wrap2 2]]} - ["/ping" {:get {:middleware [[wrap3 3]] + ["/api" {:middleware [[wrap 1] [wrap 2]]} + ["/ping" {:get {:middleware [[wrap 3]] :handler handler}}]] - {::middleware/transform #(interleave % (repeat [wrap :debug]))}))) + {::middleware/transform reverse}))) + +(app {:request-method :get, :uri "/api/ping"}) +; {:status 200, :body [3 2 1 :handler]} ``` +## Interleaving Middleware + ```clj +(def app + (ring/ring-handler + (ring/router + ["/api" {:middleware [[wrap 1] [wrap 2]]} + ["/ping" {:get {:middleware [[wrap 3]] + :handler handler}}]] + {::middleware/transform #(interleave % (repeat [wrap :debug]))}))) + (app {:request-method :get, :uri "/api/ping"}) ; {:status 200, :body [1 :debug 2 :debug 3 :debug :handler]} ``` -## Reversing the middleware chain +### Printing Request Diffs ```clj -(def app - (ring/ring-handler - (ring/router - ["/api" {:middleware [[wrap 1] [wrap2 2]]} - ["/ping" {:get {:middleware [[wrap3 3]] - :handler handler}}]] - {::middleware/transform reverse)}))) +[metosin/reitit-middleware "0.2.9"] ``` +Using `reitit.ring.middleware.dev/print-request-diffs` transformation, the request diffs between each middleware are printed out to the console. To use it, add the following router option: + ```clj -(app {:request-method :get, :uri "/api/ping"}) -; {:status 200, :body [3 2 1 :handler]} +:reitit.middleware/transform reitit.ring.middleware.dev/print-request-diffs ``` + +Sample output: + +![Ring Request Diff](../images/ring-request-diff.png) + diff --git a/examples/http-swagger/src/example/server.clj b/examples/http-swagger/src/example/server.clj index eb84bd73..81b8ede4 100644 --- a/examples/http-swagger/src/example/server.clj +++ b/examples/http-swagger/src/example/server.clj @@ -109,7 +109,7 @@ {:status 200 :body {:total (- x y)}})}}]]] - {;:reitit.interceptor/transform dev/print-request-diffs + {;;:reitit.interceptor/transform dev/print-context-diffs :data {:coercion spec-coercion/coercion :muuntaja m/instance :interceptors [;; query-params & form-params diff --git a/examples/pedestal-swagger/src/example/server.clj b/examples/pedestal-swagger/src/example/server.clj index af99e8d3..51e16059 100644 --- a/examples/pedestal-swagger/src/example/server.clj +++ b/examples/pedestal-swagger/src/example/server.clj @@ -10,6 +10,7 @@ [reitit.http.interceptors.parameters :as parameters] [reitit.http.interceptors.muuntaja :as muuntaja] [reitit.http.interceptors.multipart :as multipart] + [reitit.http.interceptors.dev :as dev] [clojure.core.async :as a] [clojure.java.io :as io] [muuntaja.core :as m])) @@ -75,7 +76,8 @@ {:status 200 :body {:total (+ x y)}})}}]]] - {:data {:coercion spec-coercion/coercion + {;;:reitit.interceptor/transform dev/print-context-diffs + :data {:coercion spec-coercion/coercion :muuntaja m/instance :interceptors [;; query-params & form-params (parameters/parameters-interceptor) diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc index 1a2ff1fa..e3d4f3c9 100644 --- a/modules/reitit-core/src/reitit/interceptor.cljc +++ b/modules/reitit-core/src/reitit/interceptor.cljc @@ -127,6 +127,15 @@ :queue ((or queue identity) chain) :data data})))) +(defn transform-butlast + "Returns a function to that takes a interceptor transformation function and + transforms all but last of the interceptors (e.g. the handler)" + [f] + (fn [interceptors] + (concat + (f (butlast interceptors)) + [(last interceptors)]))) + (defn router "Creates a [[reitit.core/Router]] from raw route data and optionally an options map with support for Interceptors. See documentation on [[reitit.core/router]] for available options. diff --git a/modules/reitit-interceptors/src/reitit/http/interceptors/dev.clj b/modules/reitit-interceptors/src/reitit/http/interceptors/dev.clj index 096f9c08..bd652885 100644 --- a/modules/reitit-interceptors/src/reitit/http/interceptors/dev.clj +++ b/modules/reitit-interceptors/src/reitit/http/interceptors/dev.clj @@ -33,18 +33,19 @@ (assoc ::previous ctx))))) (defn diff-interceptor - [stages {:keys [enter leave error name]}] - (cond-> {:name ::diff} - (and enter (stages :enter)) (assoc :enter (handle name :enter)) - (and leave (stages :leave)) (assoc :leave (handle name :leave)) - (and error (stages :error)) (assoc :error (handle name :error)))) + [stages {:keys [enter leave error name] :as interceptor}] + (if (->> (select-keys interceptor stages) (vals) (keep identity) (seq)) + (cond-> {:name ::diff} + (and enter (stages :enter)) (assoc :enter (handle name :enter)) + (and leave (stages :leave)) (assoc :leave (handle name :leave)) + (and error (stages :error)) (assoc :error (handle name :error))))) (defn print-context-diffs "A interceptor chain transformer that adds a context diff printer between all interceptors" - [chain] + [interceptors] (reduce (fn [chain interceptor] - (into chain [(diff-interceptor #{:leave :error} interceptor) - interceptor - (diff-interceptor #{:enter} interceptor)])) - [(diff-interceptor #{:enter :leave :error} {:enter identity})] chain)) + (into chain (keep identity [(diff-interceptor #{:leave :error} interceptor) + interceptor + (diff-interceptor #{:enter} interceptor)]))) + [(diff-interceptor #{:enter :leave :error} {:enter identity})] interceptors)) diff --git a/test/cljc/reitit/interceptor_test.cljc b/test/cljc/reitit/interceptor_test.cljc index 3eed9ae7..8071e48f 100644 --- a/test/cljc/reitit/interceptor_test.cljc +++ b/test/cljc/reitit/interceptor_test.cljc @@ -223,14 +223,8 @@ (enter ::avaruus)] :handler handler}] options))) - inject-debug (fn [interceptors] - (concat - (interleave (butlast interceptors) (repeat debug-i)) - [(last interceptors)])) - sort-interceptors (fn [interceptors] - (concat - (sort-by :name (butlast interceptors)) - [(last interceptors)]))] + inject-debug (interceptor/transform-butlast #(interleave % (repeat debug-i))) + sort-interceptors (interceptor/transform-butlast (partial sort-by :name))] (testing "by default, all interceptors are applied in order" (let [app (create nil)]