diff --git a/CHANGELOG.md b/CHANGELOG.md index 25dc33a6..7bad6e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,27 @@ 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-http/src/reitit/http/coercion.cljc b/modules/reitit-http/src/reitit/http/coercion.cljc index cf3dba03..30af60db 100644 --- a/modules/reitit-http/src/reitit/http/coercion.cljc +++ b/modules/reitit-http/src/reitit/http/coercion.cljc @@ -18,12 +18,13 @@ (not parameters) {} ;; mount :else - (let [coercers (coercion/request-coercers coercion parameters opts)] + (if-let [coercers (coercion/request-coercers coercion parameters opts)] {:enter (fn [ctx] (let [request (:request ctx) coerced (coercion/coerce-request coercers request) request (impl/fast-assoc request :parameters coerced)] - (assoc ctx :request request)))})))}) + (assoc ctx :request request)))} + {})))}) (defn coerce-response-interceptor "Interceptor for pluggable response coercion. @@ -40,12 +41,13 @@ (not responses) {} ;; mount :else - (let [coercers (coercion/response-coercers coercion responses opts)] + (if-let [coercers (coercion/response-coercers coercion responses opts)] {:leave (fn [ctx] (let [request (:request ctx) response (:response ctx) response (coercion/coerce-response coercers request response)] - (assoc ctx :response response)))})))}) + (assoc ctx :response response)))} + {})))}) (defn coerce-exceptions-interceptor "Interceptor for handling coercion exceptions. diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 3e57ef28..9300f5ad 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])) @@ -30,43 +34,43 @@ (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] (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 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)) + (-validate [_ value] (validator value)) + (-explain [_ value] (explainer value))))) {:keys [formats default]} (transformers type) default-coercer (->coercer default) encode (or encoder (fn [value _format] value)) 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 -> 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))))))) @@ -111,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 @@ -165,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/project.clj b/project.clj index 0382ff16..3e0d7a9e 100644 --- a/project.clj +++ b/project.clj @@ -33,7 +33,7 @@ [metosin/muuntaja "0.6.7"] [metosin/jsonista "0.2.6"] [metosin/sieppari "0.0.0-alpha13"] - [metosin/malli "0.0.1-20200404.091302-14"] + [metosin/malli "0.0.1-20200525.162645-15"] ;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111 [com.fasterxml.jackson.core/jackson-core "2.11.0"] diff --git a/test/clj/reitit/http_coercion_test.clj b/test/clj/reitit/http_coercion_test.clj index 7e4e4059..5c5c8352 100644 --- a/test/clj/reitit/http_coercion_test.clj +++ b/test/clj/reitit/http_coercion_test.clj @@ -7,10 +7,21 @@ [reitit.coercion.schema :as schema] [muuntaja.interceptor] [jsonista.core :as j] - [reitit.interceptor.sieppari :as sieppari]) + [reitit.interceptor.sieppari :as sieppari] + [reitit.coercion.malli :as malli] + [reitit.core :as r]) (:import (clojure.lang ExceptionInfo) (java.io ByteArrayInputStream))) +(defn mounted-interceptor [app path method] + (->> 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 a754ef53..e6294eb9 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -8,11 +8,19 @@ [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 mounted-middleware [app path method] + (->> app + (ring/get-router) + (r/compiled-routes) + (filter (comp (partial = path) first)) + (first) (last) method :middleware (filter :wrap) (mapv :name))) + (defn handler [{{{:keys [a]} :query {:keys [b]} :body {:keys [c]} :form @@ -204,6 +212,38 @@ (ring/ring-handler (ring/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}]]] @@ -254,6 +294,41 @@ rrc/coerce-request-middleware rrc/coerce-response-middleware])] + (testing "just validation" + (is (= 400 (:status (app {:uri "/api/validate" + :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/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 + :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}}