Add :on-invalid for custom handling of invalid values in coercer

This commit is contained in:
Joshua Davey 2025-06-18 11:53:45 -04:00
parent 7520d20f12
commit 3b5100d8a9
3 changed files with 75 additions and 11 deletions

View file

@ -20,7 +20,8 @@
(-decode [this value])
(-encode [this value])
(-validate [this value])
(-explain [this value]))
(-explain [this value])
(-on-invalid [this value explained]))
(defprotocol TransformationProvider
(-transformer [this options]))
@ -37,18 +38,24 @@
(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- -return-coercion-error
[value error]
(coercion/map->CoercionError (assoc error :transformed value)))
(defn- -coercer [schema type transformers f {:keys [validate enabled options on-invalid]}]
(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))
explainer (m/explainer schema options)]
explainer (m/explainer schema options)
report (or on-invalid -return-coercion-error)]
(reify Coercer
(-decode [_ value] (decoder value))
(-encode [_ value] (encoder value))
(-validate [_ value] (validator value))
(-explain [_ value] (explainer value)))))
(-explain [_ value] (explainer value))
(-on-invalid [_ value explained] (report value explained)))))
{:keys [formats default]} (transformers type)
default-coercer (->coercer default)
format-coercers (some->> (for [[f t] formats] [f (->coercer t)]) (filter second) (seq) (into {}))
@ -63,8 +70,7 @@
(if (-validate coercer transformed)
transformed
(let [error (-explain coercer transformed)]
(coercion/map->CoercionError
(assoc error :transformed transformed)))))
(-on-invalid coercer transformed error))))
value))
;; encode: decode -> validate -> encode
(fn [value format]
@ -73,8 +79,7 @@
(if (-validate coercer transformed)
(-encode coercer transformed)
(let [error (-explain coercer transformed)]
(coercion/map->CoercionError
(assoc error :transformed transformed))))
(-on-invalid coercer transformed error)))
value))))))))
(defn- -query-string-coercer
@ -120,6 +125,8 @@
:default-values true
;; encode-error
:encode-error nil
;; custom handler for validation errors (vs returning them)
:on-invalid nil
;; malli options
:options nil})

View file

@ -192,7 +192,8 @@
(is (= 500 status))))))))))
(deftest malli-coercion-test
(let [create (fn [interceptors]
(let [most-recent (atom nil)
create (fn [interceptors]
(http/ring-handler
(http/router
["/api"
@ -213,6 +214,16 @@
{:status 200
:body (-> req :parameters :body)})}}]
["/warn" {:summary "log and return original"
:coercion (reitit.coercion.malli/create {:transformers {},
:on-invalid (fn [value error]
(reset! most-recent error)
value)})
: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?]]}
@ -290,6 +301,19 @@
:reitit.interceptor/handler]
(mounted-interceptor app "/api/validate" :post))))
(testing "validation, log on invalid"
(is (= 123 (:body (app {:uri "/api/warn"
: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/warn" :post)))
(let [received @most-recent]
(is (= 123 (-> @most-recent :errors first :value)))))
(testing "no tranformation & validation"
(is (= 123 (:body (app {:uri "/api/no-op"
:request-method :post

View file

@ -245,7 +245,8 @@
(reduce custom-meta-merge-checking-parameters left (cons right more))))
(deftest malli-coercion-test
(let [create (fn [middleware routes]
(let [most-recent (atom nil)
create (fn [middleware routes]
(ring/ring-handler
(ring/router
routes
@ -270,6 +271,17 @@
{:status 200
:body (-> req :parameters :body)})}}]
["/warn" {:summary "log and return original"
:coercion (reitit.coercion.malli/create {:transformers {},
:on-invalid (fn [value error]
(reset! most-recent error)
value)})
: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?]]}
@ -311,7 +323,16 @@
:handler (fn [req]
{:status 200
:body (-> req :parameters :body)})}}]
["/warn" {:summary "log and return original"
:coercion (reitit.coercion.malli/create {:transformers {}
:on-invalid (fn [value error]
(reset! most-recent error)
value)})
:post {:parameters {:body {:x int?}}
:responses {200 {:body {:x int?}}}
:handler (fn [req]
{:status 200
:body (-> req :parameters :body)})}}]
["/skip" {:summary "skip"
:coercion (reitit.coercion.malli/create {:enabled false})
:post {:parameters {:body {:x int?}}
@ -397,6 +418,18 @@
:reitit.ring.coercion/coerce-response]
(mounted-middleware app "/api/no-op" :post))))
(testing "validate and log when invalid"
(reset! most-recent nil)
(is (= 123 (:body (app {:uri "/api/warn"
:request-method :post
:muuntaja/request {:format "application/edn"}
:body-params 123}))))
(is (= 123 (-> @most-recent :errors first :value)))
(is (= [:reitit.ring.coercion/coerce-exceptions
:reitit.ring.coercion/coerce-request
:reitit.ring.coercion/coerce-response]
(mounted-middleware app "/api/warn" :post))))
(testing "skipping coercion"
(is (= nil (:body (app {:uri "/api/skip"
:request-method :post