From f262daa454e9e33ef3aa99fe3254a15c00794d8d Mon Sep 17 00:00:00 2001 From: Stig Brautaset Date: Fri, 13 Oct 2023 20:28:40 +0100 Subject: [PATCH] Add separate validation options to Malli coercer The motivation for this is adding a new API to a legacy service whose data model has evolved over the years. Maybe we cannot guarantee the presence of all fields that are considered mandatory now. Rather than blowing up when attempting to return legacy entries we may want to disable response validation. However, we do want to ensure *new* entries added to the system are valid, so we do not want to disable request validation. Retain the ability to turn off both request and response validation with a single setting for backwards compatibility. --- .../src/reitit/coercion/malli.cljc | 18 +++++--- test/clj/reitit/http_coercion_test.clj | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 583a8db3..b54ceddf 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -18,7 +18,8 @@ (defprotocol Coercer (-decode [this value]) (-encode [this value]) - (-validate [this value]) + (-validate-request [this value]) + (-validate-response [this value]) (-explain [this value])) (defprotocol TransformationProvider @@ -36,17 +37,20 @@ (def json-transformer-provider (-provider (mt/json-transformer))) (def default-transformer-provider (-provider nil)) -(defn- -coercer [schema type transformers f {:keys [validate enabled options]}] +(defn- -coercer [schema type transformers f {:keys [validate-request validate-response validate enabled options]}] (if schema (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)) + validator (m/validator schema options) + request-validator (if (and validate validate-request) validator (constantly true)) + response-validator (if (and validate validate-response) validator (constantly true)) explainer (m/explainer schema options)] (reify Coercer (-decode [_ value] (decoder value)) (-encode [_ value] (encoder value)) - (-validate [_ value] (validator value)) + (-validate-request [_ value] (request-validator value)) + (-validate-response [_ value] (response-validator value)) (-explain [_ value] (explainer value))))) {:keys [formats default]} (transformers type) default-coercer (->coercer default) @@ -59,7 +63,7 @@ (fn [value format] (if-let [coercer (get-coercer format)] (let [transformed (-decode coercer value)] - (if (-validate coercer transformed) + (if (-validate-request coercer transformed) transformed (let [error (-explain coercer transformed)] (coercion/map->CoercionError @@ -69,7 +73,7 @@ (fn [value format] (let [transformed (-decode default-coercer value)] (if-let [coercer (get-coercer format)] - (if (-validate coercer transformed) + (if (-validate-response coercer transformed) (-encode coercer transformed) (let [error (-explain coercer transformed)] (coercion/map->CoercionError @@ -95,6 +99,8 @@ :compile mu/closed-schema ;; validate request & response :validate true + :validate-request true + :validate-response true ;; top-level short-circuit to disable request & response coercion :enabled true ;; strip-extra-keys (affects only predefined transformers) diff --git a/test/clj/reitit/http_coercion_test.clj b/test/clj/reitit/http_coercion_test.clj index 53c749a7..9228cd7f 100644 --- a/test/clj/reitit/http_coercion_test.clj +++ b/test/clj/reitit/http_coercion_test.clj @@ -205,6 +205,22 @@ {:status 200 :body (-> req :parameters :body)})}}] + ["/validate-request" {:summary "just request validation" + :coercion (reitit.coercion.malli/create {:transformers {} :validate-response false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body "I am do not validate"})}}] + + ["/validate-response" {:summary "just response validation" + :coercion (reitit.coercion.malli/create {:transformers {} :validate-request false}) + :post {:parameters {:body [:map [:x int?]]} + :responses {200 {:body [:map [:x int?]]}} + :handler (fn [req] + {:status 200 + :body "I do not validate"})}}] + ["/no-op" {:summary "no-operation" :coercion (reitit.coercion.malli/create {:transformers {}, :validate false}) :post {:parameters {:body [:map [:x int?]]} @@ -290,11 +306,37 @@ :reitit.interceptor/handler] (mounted-interceptor app "/api/validate" :post)))) + (testing "just request validation" + (is (= 400 (:status (app {:uri "/api/validate-request" + :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-request" :post)))) + + (testing "just response validation" + (is (= 500 (:status (app {:uri "/api/validate-response" + :request-method :post + :muuntaja/request {:format "application/edn"} + :body-params "not an integer"})))) + (is (= [:reitit.http.coercion/coerce-exceptions + :reitit.http.coercion/coerce-request + :reitit.http.coercion/coerce-response + :reitit.interceptor/handler] + (mounted-interceptor app "/api/validate-response" :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 (= "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