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.
This commit is contained in:
Stig Brautaset 2023-10-13 20:28:40 +01:00
parent 620d0c2711
commit f262daa454
No known key found for this signature in database
GPG key ID: 68D77873AEB4B2B9
2 changed files with 54 additions and 6 deletions

View file

@ -18,7 +18,8 @@
(defprotocol Coercer (defprotocol Coercer
(-decode [this value]) (-decode [this value])
(-encode [this value]) (-encode [this value])
(-validate [this value]) (-validate-request [this value])
(-validate-response [this value])
(-explain [this value])) (-explain [this value]))
(defprotocol TransformationProvider (defprotocol TransformationProvider
@ -36,17 +37,20 @@
(def json-transformer-provider (-provider (mt/json-transformer))) (def json-transformer-provider (-provider (mt/json-transformer)))
(def default-transformer-provider (-provider nil)) (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 (if schema
(let [->coercer (fn [t] (let [->coercer (fn [t]
(let [decoder (if t (m/decoder schema options t) identity) (let [decoder (if t (m/decoder schema options t) identity)
encoder (if t (m/encoder 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)] explainer (m/explainer schema options)]
(reify Coercer (reify Coercer
(-decode [_ value] (decoder value)) (-decode [_ value] (decoder value))
(-encode [_ value] (encoder 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))))) (-explain [_ value] (explainer value)))))
{:keys [formats default]} (transformers type) {:keys [formats default]} (transformers type)
default-coercer (->coercer default) default-coercer (->coercer default)
@ -59,7 +63,7 @@
(fn [value format] (fn [value format]
(if-let [coercer (get-coercer format)] (if-let [coercer (get-coercer format)]
(let [transformed (-decode coercer value)] (let [transformed (-decode coercer value)]
(if (-validate coercer transformed) (if (-validate-request coercer transformed)
transformed transformed
(let [error (-explain coercer transformed)] (let [error (-explain coercer transformed)]
(coercion/map->CoercionError (coercion/map->CoercionError
@ -69,7 +73,7 @@
(fn [value format] (fn [value format]
(let [transformed (-decode default-coercer value)] (let [transformed (-decode default-coercer value)]
(if-let [coercer (get-coercer format)] (if-let [coercer (get-coercer format)]
(if (-validate coercer transformed) (if (-validate-response coercer transformed)
(-encode coercer transformed) (-encode coercer transformed)
(let [error (-explain coercer transformed)] (let [error (-explain coercer transformed)]
(coercion/map->CoercionError (coercion/map->CoercionError
@ -95,6 +99,8 @@
:compile mu/closed-schema :compile mu/closed-schema
;; validate request & response ;; validate request & response
:validate true :validate true
:validate-request true
:validate-response true
;; top-level short-circuit to disable request & response coercion ;; top-level short-circuit to disable request & response coercion
:enabled true :enabled true
;; strip-extra-keys (affects only predefined transformers) ;; strip-extra-keys (affects only predefined transformers)

View file

@ -205,6 +205,22 @@
{:status 200 {:status 200
:body (-> req :parameters :body)})}}] :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" ["/no-op" {:summary "no-operation"
:coercion (reitit.coercion.malli/create {:transformers {}, :validate false}) :coercion (reitit.coercion.malli/create {:transformers {}, :validate false})
:post {:parameters {:body [:map [:x int?]]} :post {:parameters {:body [:map [:x int?]]}
@ -290,11 +306,37 @@
:reitit.interceptor/handler] :reitit.interceptor/handler]
(mounted-interceptor app "/api/validate" :post)))) (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" (testing "no tranformation & validation"
(is (= 123 (:body (app {:uri "/api/no-op" (is (= 123 (:body (app {:uri "/api/no-op"
:request-method :post :request-method :post
:muuntaja/request {:format "application/edn"} :muuntaja/request {:format "application/edn"}
:body-params 123})))) :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 (is (= [:reitit.http.coercion/coerce-exceptions
:reitit.http.coercion/coerce-request :reitit.http.coercion/coerce-request
:reitit.http.coercion/coerce-response :reitit.http.coercion/coerce-response