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
(-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)

View file

@ -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