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

View file

@ -192,7 +192,8 @@
(is (= 500 status)))))))))) (is (= 500 status))))))))))
(deftest malli-coercion-test (deftest malli-coercion-test
(let [create (fn [interceptors] (let [most-recent (atom nil)
create (fn [interceptors]
(http/ring-handler (http/ring-handler
(http/router (http/router
["/api" ["/api"
@ -213,6 +214,16 @@
{:status 200 {:status 200
:body (-> req :parameters :body)})}}] :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" ["/skip" {:summary "skip"
:coercion (reitit.coercion.malli/create {:enabled false}) :coercion (reitit.coercion.malli/create {:enabled false})
:post {:parameters {:body [:map [:x int?]]} :post {:parameters {:body [:map [:x int?]]}
@ -290,6 +301,19 @@
:reitit.interceptor/handler] :reitit.interceptor/handler]
(mounted-interceptor app "/api/validate" :post)))) (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" (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

View file

@ -245,7 +245,8 @@
(reduce custom-meta-merge-checking-parameters left (cons right more)))) (reduce custom-meta-merge-checking-parameters left (cons right more))))
(deftest malli-coercion-test (deftest malli-coercion-test
(let [create (fn [middleware routes] (let [most-recent (atom nil)
create (fn [middleware routes]
(ring/ring-handler (ring/ring-handler
(ring/router (ring/router
routes routes
@ -270,6 +271,17 @@
{:status 200 {:status 200
:body (-> req :parameters :body)})}}] :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" ["/skip" {:summary "skip"
:coercion (reitit.coercion.malli/create {:enabled false}) :coercion (reitit.coercion.malli/create {:enabled false})
:post {:parameters {:body [:map [:x int?]]} :post {:parameters {:body [:map [:x int?]]}
@ -311,7 +323,16 @@
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :body)})}}] :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" ["/skip" {:summary "skip"
:coercion (reitit.coercion.malli/create {:enabled false}) :coercion (reitit.coercion.malli/create {:enabled false})
:post {:parameters {:body {:x int?}} :post {:parameters {:body {:x int?}}
@ -397,6 +418,18 @@
:reitit.ring.coercion/coerce-response] :reitit.ring.coercion/coerce-response]
(mounted-middleware app "/api/no-op" :post)))) (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" (testing "skipping coercion"
(is (= nil (:body (app {:uri "/api/skip" (is (= nil (:body (app {:uri "/api/skip"
:request-method :post :request-method :post