From 28962e75df009332cc1a8ac0a38e3d5d3b45374d Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 25 May 2020 21:53:40 +0300 Subject: [PATCH 1/6] Update malli --- CHANGELOG.md | 6 ++++++ project.clj | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25dc33a6..2a9e3909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ We use [Break Versioning][breakver]. The version numbers follow a `. Date: Mon, 25 May 2020 21:54:27 +0300 Subject: [PATCH 2/6] Fix #407 --- test/cljc/reitit/ring_coercion_test.cljc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index a754ef53..ba71934d 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -199,11 +199,22 @@ (let [{:keys [status]} (app invalid-request2)] (is (= 500 status)))))))) +(def or-maps-schema + [:or [:map [:x int?]] [:map [:y int?]]]) + (deftest malli-coercion-test (let [create (fn [middleware] (ring/ring-handler (ring/router ["/api" + + ["/or" {:post {:summary "accepts either of two map schemas" + :parameters {:body or-maps-schema} + :responses {200 {:body [:map [:msg string?]]}} + :handler (fn [{{{:keys [x]} :body} :parameters}] + {:status 200 + :body {:msg (if x "you sent x" "you sent y")}})}}] + ["/plus/:e" {:get {:parameters {:query [:map [:a {:optional true} int?]] :body [:map [:b int?]] :form [:map [:c [int? {:default 3}]]] @@ -254,6 +265,13 @@ rrc/coerce-request-middleware rrc/coerce-response-middleware])] + (testing "or #407" + (is (= {:status 200 + :body {:msg "you sent x"}} + (app {:uri "/api/or" + :request-method :post + :body-params {:x 1}})))) + (testing "all good" (is (= {:status 200 :body {:total 15}} From ea5ec93793c4f19067b7cf47a51b7e099155f7be Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 25 May 2020 21:54:55 +0300 Subject: [PATCH 3/6] faster malli coercion --- .../src/reitit/coercion/malli.cljc | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 3e57ef28..9eea3632 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -13,7 +13,11 @@ ;; coercion ;; -(defrecord Coercer [decoder encoder validator explainer]) +(defprotocol Coercer + (-decode [this value]) + (-encode [this value]) + (-validate [this value]) + (-explain [this value])) (defprotocol TransformationProvider (-transformer [this options])) @@ -32,10 +36,16 @@ (defn- -coercer [schema type transformers f encoder opts] (if schema - (let [->coercer (fn [t] (if t (->Coercer (m/decoder schema opts t) - (m/encoder schema opts t) - (m/validator schema opts) - (m/explainer schema opts)))) + (let [->coercer (fn [t] + (let [decoder (if t (m/decoder schema opts t) (constantly true)) + encoder (if t (m/encoder schema opts t) (constantly true)) + validator (m/validator schema opts) + explainer (m/explainer schema opts)] + (reify Coercer + (-decode [_ value] (decoder value)) + (-encode [_ value] (encoder value)) + (-validate [_ value] (validator value)) + (-explain [_ value] (explainer value))))) {:keys [formats default]} (transformers type) default-coercer (->coercer default) encode (or encoder (fn [value _format] value)) @@ -44,29 +54,23 @@ default-coercer (constantly default-coercer))] (if get-coercer (if (= f :decode) - ;; decode -> validate + ;; decode: decode -> validate (fn [value format] (if-let [coercer (get-coercer format)] - (let [decoder (:decoder coercer) - validator (:validator coercer) - transformed (decoder value)] - (if (validator transformed) + (let [transformed (-decode coercer value)] + (if (-validate coercer transformed) transformed - (let [explainer (:explainer coercer) - error (explainer transformed)] + (let [error (-explain coercer transformed)] (coercion/map->CoercionError (assoc error :transformed transformed))))) value)) - ;; decode -> validate -> encode + ;; encode: decode -> validate -> encode (fn [value format] (if-let [coercer (get-coercer format)] - (let [decoder (:decoder coercer) - validator (:validator coercer) - transformed (decoder value)] - (if (validator transformed) + (let [transformed (-decode coercer value)] + (if (-validate coercer transformed) (encode transformed format) - (let [explainer (:explainer coercer) - error (explainer transformed)] + (let [error (-explain coercer transformed)] (coercion/map->CoercionError (assoc error :transformed transformed))))) value))))))) From f41006c8bb2ce0f9f9c3fd37585e4440a925fbac Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 25 May 2020 23:50:27 +0300 Subject: [PATCH 4/6] just validation --- test/cljc/reitit/ring_coercion_test.cljc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index ba71934d..7c483992 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -208,6 +208,13 @@ (ring/router ["/api" + ["/custom" {:summary "just validation" + :coercion (reitit.coercion.malli/create {:transformers {}}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] ["/or" {:post {:summary "accepts either of two map schemas" :parameters {:body or-maps-schema} :responses {200 {:body [:map [:msg string?]]}} @@ -265,6 +272,12 @@ rrc/coerce-request-middleware rrc/coerce-response-middleware])] + (testing "just validation" + (is (= 400 (:status (app {:uri "/api/custom" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123}))))) + (testing "or #407" (is (= {:status 200 :body {:msg "you sent x"}} From e649ed22b9400cb4ca8748db4802c20eed8fd970 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 May 2020 08:09:35 +0300 Subject: [PATCH 5/6] New options for malli coercion --- CHANGELOG.md | 11 +++ doc/coercion/malli_coercion.md | 26 +++++++ modules/reitit-core/src/reitit/coercion.cljc | 41 +++++----- .../src/reitit/coercion/malli.cljc | 22 +++--- .../reitit-ring/src/reitit/ring/coercion.cljc | 10 ++- test/cljc/reitit/ring_coercion_test.cljc | 75 +++++++++++++++---- 6 files changed, 138 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a9e3909..d9fcab14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,17 @@ We use [Break Versioning][breakver]. The version numbers follow a `. ExceptionInfo Request coercion failed... ``` + +## Configuring coercion + +Using `create` with options to create the coercion instead of `coercion`: + +```clj +(reitit.coercion.malli/create + {:transformers {:body {:default default-transformer-provider + :formats {"application/json" json-transformer-provider}} + :string {:default string-transformer-provider} + :response {:default default-transformer-provider}} + ;; set of keys to include in error messages + :error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} + ;; schema identity function (default: close all map schemas) + :compile mu/closed-schema + ;; validate request & response + :validate true + ;; top-level short-circuit to disable request & response coercion + :enabled true + ;; strip-extra-keys (effects only predefined transformers) + :strip-extra-keys true + ;; add/set default values + :default-values true + ;; malli options + :options nil}) +``` diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index bcb7dfc9..57d55628 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -76,15 +76,15 @@ (if coercion (if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] (let [transform (comp (if keywordize? walk/keywordize-keys identity) in) - model (if open? (-open-model coercion model) model) - coercer (-request-coercer coercion style model)] - (fn [request] - (let [value (transform request) - format (extract-request-format request) - result (coercer value format)] - (if (error? result) - (request-coercion-failed! result coercion value in request) - result))))))) + model (if open? (-open-model coercion model) model)] + (if-let [coercer (-request-coercer coercion style model)] + (fn [request] + (let [value (transform request) + format (extract-request-format request) + result (coercer value format)] + (if (error? result) + (request-coercion-failed! result coercion value in request) + result)))))))) (defn extract-response-format-default [request _] (-> request :muuntaja/response :format)) @@ -111,8 +111,7 @@ (reduce-kv (fn [acc k coercer] (impl/fast-assoc acc k (coercer request))) - {} - coercers)) + {} coercers)) (defn coerce-response [coercers request response] (if response @@ -121,17 +120,19 @@ response))) (defn request-coercers [coercion parameters opts] - (->> (for [[k v] parameters - :when v] - [k (request-coercer coercion k v opts)]) - (filter second) - (into {}))) + (some->> (for [[k v] parameters + :when v] + [k (request-coercer coercion k v opts)]) + (filter second) + (seq) + (into {}))) (defn response-coercers [coercion responses opts] - (->> (for [[status {:keys [body]}] responses :when body] - [status (response-coercer coercion body opts)]) - (filter second) - (into {}))) + (some->> (for [[status {:keys [body]}] responses :when body] + [status (response-coercer coercion body opts)]) + (filter second) + (seq) + (into {}))) ;; ;; api-docs diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 9eea3632..9300f5ad 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -34,13 +34,13 @@ (def json-transformer-provider (-provider (mt/json-transformer))) (def default-transformer-provider (-provider nil)) -(defn- -coercer [schema type transformers f encoder opts] +(defn- -coercer [schema type transformers f encoder {:keys [validate enabled options]}] (if schema (let [->coercer (fn [t] - (let [decoder (if t (m/decoder schema opts t) (constantly true)) - encoder (if t (m/encoder schema opts t) (constantly true)) - validator (m/validator schema opts) - explainer (m/explainer schema opts)] + (let [decoder (if t (m/decoder schema options t) identity) + encoder (if t (m/encoder schema options t) identity) + validator (if validate (m/validator schema options) (constantly true)) + explainer (m/explainer schema options)] (reify Coercer (-decode [_ value] (decoder value)) (-encode [_ value] (encoder value)) @@ -52,7 +52,7 @@ format-coercers (some->> (for [[f t] formats] [f (->coercer t)]) (filter second) (seq) (into {})) get-coercer (cond format-coercers (fn [format] (or (get format-coercers format) default-coercer)) default-coercer (constantly default-coercer))] - (if get-coercer + (if (and enabled get-coercer) (if (= f :decode) ;; decode: decode -> validate (fn [value format] @@ -115,6 +115,10 @@ :error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} ;; schema identity function (default: close all map schemas) :compile mu/closed-schema + ;; validate request & response + :validate true + ;; top-level short-circuit to disable request & response coercion + :enabled true ;; strip-extra-keys (effects only predefined transformers) :strip-extra-keys true ;; add/set default values @@ -169,10 +173,10 @@ (update :errors (partial map #(update % :schema edn/write-string opts)))) (seq error-keys) (select-keys error-keys))) (-request-coercer [_ type schema] - (-coercer (compile schema options) type transformers :decode nil options)) + (-coercer (compile schema options) type transformers :decode nil opts)) (-response-coercer [_ schema] (let [schema (compile schema options) - encoder (-coercer schema :body transformers :encode nil options)] - (-coercer schema :response transformers :encode encoder options))))))) + encoder (-coercer schema :body transformers :encode nil opts)] + (-coercer schema :response transformers :encode encoder opts))))))) (def coercion (create default-options)) diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc index 3ee5676f..3f1c280d 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion.cljc @@ -32,7 +32,7 @@ (not parameters) {} ;; mount :else - (let [coercers (coercion/request-coercers coercion parameters opts)] + (if-let [coercers (coercion/request-coercers coercion parameters opts)] (fn [handler] (fn ([request] @@ -40,7 +40,8 @@ (handler (impl/fast-assoc request :parameters coerced)))) ([request respond raise] (let [coerced (coercion/coerce-request coercers request)] - (handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}) + (handler (impl/fast-assoc request :parameters coerced) respond raise))))) + {})))}) (def coerce-response-middleware "Middleware for pluggable response coercion. @@ -56,13 +57,14 @@ (not responses) {} ;; mount :else - (let [coercers (coercion/response-coercers coercion responses opts)] + (if-let [coercers (coercion/response-coercers coercion responses opts)] (fn [handler] (fn ([request] (coercion/coerce-response coercers request (handler request))) ([request respond raise] - (handler request #(respond (coercion/coerce-response coercers request %)) raise)))))))}) + (handler request #(respond (coercion/coerce-response coercers request %)) raise)))) + {})))}) (def coerce-exceptions-middleware "Middleware for handling coercion exceptions. diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index 7c483992..dde6181b 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -8,11 +8,22 @@ [reitit.coercion.malli :as malli] [reitit.coercion.schema :as schema] #?@(:clj [[muuntaja.middleware] - [jsonista.core :as j]])) + [jsonista.core :as j]]) + [reitit.core :as r]) #?(:clj (:import (clojure.lang ExceptionInfo) (java.io ByteArrayInputStream)))) +(defn middleware-name [{:keys [wrap name]}] + (or name (-> wrap str symbol))) + +(defn mounted-middleware [app path method] + (->> app + (ring/get-router) + (r/compiled-routes) + (filter (comp (partial = path) first)) + (first) (last) method :middleware (filter :wrap) (mapv middleware-name))) + (defn handler [{{{:keys [a]} :query {:keys [b]} :body {:keys [c]} :form @@ -199,24 +210,38 @@ (let [{:keys [status]} (app invalid-request2)] (is (= 500 status)))))))) -(def or-maps-schema - [:or [:map [:x int?]] [:map [:y int?]]]) - (deftest malli-coercion-test (let [create (fn [middleware] (ring/ring-handler (ring/router ["/api" - ["/custom" {:summary "just validation" - :coercion (reitit.coercion.malli/create {:transformers {}}) - :post {:parameters {:body [:map [:x int?]]} - :responses {200 {:body [:map [:x int?]]}} - :handler (fn [req] - {:status 200 - :body (-> req :parameters :body)})}}] + ["/validate" {:summary "just validation" + :coercion (reitit.coercion.malli/create {:transformers {}}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] + + ["/no-op" {:summary "no-operation" + :coercion (reitit.coercion.malli/create {:transformers {}, :validate false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] + + ["/skip" {:summary "skip" + :coercion (reitit.coercion.malli/create {:enabled false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] + ["/or" {:post {:summary "accepts either of two map schemas" - :parameters {:body or-maps-schema} + :parameters {:body [:or [:map [:x int?]] [:map [:y int?]]]} :responses {200 {:body [:map [:msg string?]]}} :handler (fn [{{{:keys [x]} :body} :parameters}] {:status 200 @@ -273,10 +298,32 @@ rrc/coerce-response-middleware])] (testing "just validation" - (is (= 400 (:status (app {:uri "/api/custom" + (is (= 400 (:status (app {:uri "/api/validate" :request-method :post :muuntaja/request {:format "application/edn"} - :body-params 123}))))) + :body-params 123})))) + (is (= [:reitit.ring.coercion/coerce-exceptions + :reitit.ring.coercion/coerce-request + :reitit.ring.coercion/coerce-response] + (mounted-middleware app "/api/validate" :post)))) + + (testing "no tranformation & validation" + (is (= 123 (:body (app {:uri "/api/no-op" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + (is (= [:reitit.ring.coercion/coerce-exceptions + :reitit.ring.coercion/coerce-request + :reitit.ring.coercion/coerce-response] + (mounted-middleware app "/api/no-op" :post)))) + + (testing "skipping coercion" + (is (= nil (:body (app {:uri "/api/skip" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + (is (= [:reitit.ring.coercion/coerce-exceptions] + (mounted-middleware app "/api/skip" :post)))) (testing "or #407" (is (= {:status 200 From 57da6fa5adca56c7356b736d3492a8926aa0927e Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 26 May 2020 21:32:26 +0300 Subject: [PATCH 6/6] optimized http-coercion --- CHANGELOG.md | 6 +- .../reitit-http/src/reitit/http/coercion.cljc | 10 +- test/clj/reitit/http_coercion_test.clj | 301 +++++++++++++++++- test/cljc/reitit/ring_coercion_test.cljc | 5 +- 4 files changed, 300 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9fcab14..7bad6e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,14 @@ We use [Break Versioning][breakver]. The version numbers follow a `.> app + (http/get-router) + (r/compiled-routes) + (filter (comp (partial = path) first)) + (first) (last) method :interceptors + (filter #(->> (select-keys % [:enter :leave :error]) (vals) (keep identity) (seq))) + (mapv :name))) + (defn handler [{{{:keys [a]} :query {:keys [b]} :body {:keys [c]} :form @@ -22,7 +33,7 @@ {:status 200 :body {:total (+ a b c d e)}})) -(def valid-request +(def valid-request1 {:uri "/api/plus/5" :request-method :get :query-params {"a" "1"} @@ -30,7 +41,25 @@ :form-params {:c 3} :headers {"d" "4"}}) -(def invalid-request +(def valid-request2 + {:uri "/api/plus/5" + :request-method :get + :muuntaja/request {:format "application/json"} + :query-params {} + :body-params {:b 2} + :form-params {:c 3} + :headers {"d" "4"}}) + +(def valid-request3 + {:uri "/api/plus/5" + :request-method :get + :muuntaja/request {:format "application/edn"} + :query-params {"a" "1", "EXTRA" "VALUE"} + :body-params {:b 2, :EXTRA "VALUE"} + :form-params {:c 3, :EXTRA "VALUE"} + :headers {"d" "4", "EXTRA" "VALUE"}}) + +(def invalid-request1 {:uri "/api/plus/5" :request-method :get}) @@ -67,16 +96,16 @@ (testing "all good" (is (= {:status 200 :body {:total 15}} - (app valid-request))) + (app valid-request1))) (is (= {:status 500 :body {:evil true}} - (app (assoc-in valid-request [:query-params "a"] "666"))))) + (app (assoc-in valid-request1 [:query-params "a"] "666"))))) (testing "invalid request" (is (thrown-with-msg? ExceptionInfo #"Request coercion failed" - (app invalid-request)))) + (app invalid-request1)))) (testing "invalid response" (is (thrown-with-msg? @@ -92,10 +121,10 @@ (testing "all good" (is (= {:status 200 :body {:total 15}} - (app valid-request)))) + (app valid-request1)))) (testing "invalid request" - (let [{:keys [status]} (app invalid-request)] + (let [{:keys [status]} (app invalid-request1)] (is (= 400 status)))) (testing "invalid response" @@ -127,16 +156,16 @@ (testing "all good" (is (= {:status 200 :body {:total 15}} - (app valid-request))) + (app valid-request1))) (is (= {:status 500 :body {:evil true}} - (app (assoc-in valid-request [:query-params "a"] "666"))))) + (app (assoc-in valid-request1 [:query-params "a"] "666"))))) (testing "invalid request" (is (thrown-with-msg? ExceptionInfo #"Request coercion failed" - (app invalid-request)))) + (app invalid-request1)))) (testing "invalid response" (is (thrown-with-msg? @@ -152,16 +181,262 @@ (testing "all good" (is (= {:status 200 :body {:total 15}} - (app valid-request)))) + (app valid-request1)))) (testing "invalid request" - (let [{:keys [status]} (app invalid-request)] + (let [{:keys [status]} (app invalid-request1)] (is (= 400 status)))) (testing "invalid response" (let [{:keys [status]} (app invalid-request2)] (is (= 500 status)))))))))) +(deftest malli-coercion-test + (let [create (fn [interceptors] + (http/ring-handler + (http/router + ["/api" + + ["/validate" {:summary "just validation" + :coercion (reitit.coercion.malli/create {:transformers {}}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] + + ["/no-op" {:summary "no-operation" + :coercion (reitit.coercion.malli/create {:transformers {}, :validate false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] + + ["/skip" {:summary "skip" + :coercion (reitit.coercion.malli/create {:enabled false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :body)})}}] + + ["/or" {:post {:summary "accepts either of two map schemas" + :parameters {:body [:or [:map [:x int?]] [:map [:y int?]]]} + :responses {200 {:body [:map [:msg string?]]}} + :handler (fn [{{{:keys [x]} :body} :parameters}] + {:status 200 + :body {:msg (if x "you sent x" "you sent y")}})}}] + + ["/plus/:e" {:get {:parameters {:query [:map [:a {:optional true} int?]] + :body [:map [:b int?]] + :form [:map [:c [int? {:default 3}]]] + :header [:map [:d int?]] + :path [:map [:e int?]]} + :responses {200 {:body [:map [:total pos-int?]]} + 500 {:description "fail"}} + :handler handler}}]] + {:data {:interceptors interceptors + :coercion malli/coercion}}) + {:executor sieppari/executor}))] + + (testing "withut exception handling" + (let [app (create [(rrc/coerce-request-interceptor) + (rrc/coerce-response-interceptor)])] + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request1))) + #_(is (= {:status 200 + :body {:total 115}} + (app valid-request2))) + (is (= {:status 200 + :body {:total 15}} + (app valid-request3))) + (testing "default values work" + (is (= {:status 200 + :body {:total 15}} + (app (update valid-request3 :form-params dissoc :c))))) + (is (= {:status 500 + :body {:evil true}} + (app (assoc-in valid-request1 [:query-params "a"] "666"))))) + + (testing "invalid request" + (is (thrown-with-msg? + ExceptionInfo + #"Request coercion failed" + (app invalid-request1)))) + + (testing "invalid response" + (is (thrown-with-msg? + ExceptionInfo + #"Response coercion failed" + (app invalid-request2)))))) + + (testing "with exception handling" + (let [app (create [(rrc/coerce-exceptions-interceptor) + (rrc/coerce-request-interceptor) + (rrc/coerce-response-interceptor)])] + + (testing "just validation" + (is (= 400 (:status (app {:uri "/api/validate" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + (is (= [:reitit.http.coercion/coerce-exceptions + :reitit.http.coercion/coerce-request + :reitit.http.coercion/coerce-response + :reitit.interceptor/handler] + (mounted-interceptor app "/api/validate" :post)))) + + (testing "no tranformation & validation" + (is (= 123 (:body (app {:uri "/api/no-op" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + (is (= [:reitit.http.coercion/coerce-exceptions + :reitit.http.coercion/coerce-request + :reitit.http.coercion/coerce-response + :reitit.interceptor/handler] + (mounted-interceptor app "/api/no-op" :post)))) + + (testing "skipping coercion" + (is (= nil (:body (app {:uri "/api/skip" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params 123})))) + + (is (= [:reitit.http.coercion/coerce-exceptions + :reitit.interceptor/handler] + (mounted-interceptor app "/api/skip" :post)))) + + (testing "or #407" + (is (= {:status 200 + :body {:msg "you sent x"}} + (app {:uri "/api/or" + :request-method :post + :body-params {:x 1}})))) + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request1)))) + + (testing "invalid request" + (let [{:keys [status]} (app invalid-request1)] + (is (= 400 status)))) + + (testing "invalid response" + (let [{:keys [status]} (app invalid-request2)] + (is (= 500 status)))))) + + (testing "open & closed schemas" + (let [endpoint (fn [schema] + {:get {:parameters {:body schema} + :responses {200 {:body schema}} + :handler (fn [{{:keys [body]} :parameters}] + {:status 200, :body (assoc body :response true)})}}) + ->app (fn [options] + (http/ring-handler + (http/router + ["/api" + ["/default" (endpoint [:map [:x int?]])] + ["/closed" (endpoint [:map {:closed true} [:x int?]])] + ["/open" (endpoint [:map {:closed false} [:x int?]])]] + {:data {:interceptors [(rrc/coerce-exceptions-interceptor) + (rrc/coerce-request-interceptor) + (rrc/coerce-response-interceptor)] + :coercion (malli/create options)}}) + {:executor sieppari/executor})) + ->request (fn [uri] {:uri (str "/api/" uri) + :request-method :get + :muuntaja/request {:format "application/json"} + :body-params {:x 1, :request true}})] + + (testing "with defaults" + (let [app (->app nil)] + + (testing "default: keys are stripped" + (is (= {:status 200, :body {:x 1}} + (app (->request "default"))))) + + (testing "closed: keys are stripped" + (is (= {:status 200, :body {:x 1}} + (app (->request "closed"))))) + + (testing "open: keys are NOT stripped" + (is (= {:status 200, :body {:x 1, :request true, :response true}} + (app (->request "open"))))))) + + (testing "when schemas are not closed" + (let [app (->app {:compile (fn [v _] v)})] + + (testing "default: keys are stripped" + (is (= {:status 200, :body {:x 1}} + (app (->request "default"))))) + + (testing "closed: keys are stripped" + (is (= {:status 200, :body {:x 1}} + (app (->request "closed"))))) + + (testing "open: keys are NOT stripped" + (is (= {:status 200, :body {:x 1, :request true, :response true}} + (app (->request "open"))))))) + + (testing "when schemas are not closed and extra keys are not stripped" + (let [app (->app {:compile (fn [v _] v) :strip-extra-keys false})] + (testing "default: keys are NOT stripped" + (is (= {:status 200, :body {:x 1, :request true, :response true}} + (app (->request "default"))))) + + (testing "closed: FAILS for extra keys" + (is (= 400 (:status (app (->request "closed")))))) + + (testing "open: keys are NOT stripped" + (is (= {:status 200, :body {:x 1, :request true, :response true}} + (app (->request "open"))))))))) + + (testing "sequence schemas" + (let [app (http/ring-handler + (http/router + ["/ping" {:get {:parameters {:body [:vector [:map [:message string?]]]} + :responses {200 {:body [:vector [:map [:pong string?]]]} + 501 {:body [:vector [:map [:error string?]]]}} + :handler (fn [{{[{:keys [message]}] :body} :parameters :as req}] + (condp = message + "ping" {:status 200 + :body [{:pong message}]} + "fail" {:status 501 + :body [{:error "fail"}]} + {:status 200 + :body {:invalid "response"}}))}}] + {:data {:interceptors [(rrc/coerce-exceptions-interceptor) + (rrc/coerce-request-interceptor) + (rrc/coerce-response-interceptor)] + :coercion malli/coercion}}) + {:executor sieppari/executor}) + ->request (fn [body] + {:uri "/ping" + :request-method :get + :muuntaja/request {:format "application/json"} + :body-params body})] + + (testing "succesfull request" + (let [{:keys [status body]} (app (->request [{:message "ping"}]))] + (is (= 200 status)) + (is (= [{:pong "ping"}] body))) + + (testing "succesfull failure" + (let [{:keys [status body]} (app (->request [{:message "fail"}]))] + (is (= 501 status)) + (is (= [{:error "fail"}] body)))) + + (testing "failed response" + (let [{:keys [status body]} (app (->request [{:message "kosh"}]))] + (is (= 500 status)) + (is (= :reitit.coercion/response-coercion (:type body)))))))))) + (deftest muuntaja-test (let [app (http/ring-handler (http/router diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index dde6181b..e6294eb9 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -14,15 +14,12 @@ (:import (clojure.lang ExceptionInfo) (java.io ByteArrayInputStream)))) -(defn middleware-name [{:keys [wrap name]}] - (or name (-> wrap str symbol))) - (defn mounted-middleware [app path method] (->> app (ring/get-router) (r/compiled-routes) (filter (comp (partial = path) first)) - (first) (last) method :middleware (filter :wrap) (mapv middleware-name))) + (first) (last) method :middleware (filter :wrap) (mapv :name))) (defn handler [{{{:keys [a]} :query {:keys [b]} :body