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