New options for malli coercion

This commit is contained in:
Tommi Reiman 2020-05-26 08:09:35 +03:00
parent f41006c8bb
commit e649ed22b9
6 changed files with 138 additions and 47 deletions

View file

@ -18,6 +18,17 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[metosin/malli "0.0.1-20200525.162645-15"] is available but we use "0.0.1-20200404.091302-14" [metosin/malli "0.0.1-20200525.162645-15"] is available but we use "0.0.1-20200404.091302-14"
``` ```
### `reitit-malli`
* Fixed coercion with `:and` and `:or`, fixes [#407](https://github.com/metosin/reitit/issues/407).
* New options to `reitit.coercion.malli/create`:
* `:validate` - boolean to indicate whether validation is enabled (true)
* `:enabled` - boolean to indicate whether coercion (and validation) is enabled (true)
### `reitit-middleware`
* Coercion middleware will not to mount if the selected `:coercion` is not enabled for the given `:parameters`, e.g. "just api-docs"
## 0.5.1 (2020-05-18) ## 0.5.1 (2020-05-18)
```clj ```clj

View file

@ -43,3 +43,29 @@ Failing coercion:
(match-by-path-and-coerce! "/metosin/users/ikitommi") (match-by-path-and-coerce! "/metosin/users/ikitommi")
; => ExceptionInfo Request coercion failed... ; => ExceptionInfo Request coercion failed...
``` ```
## Configuring coercion
Using `create` with options to create the coercion instead of `coercion`:
```clj
(reitit.coercion.malli/create
{:transformers {:body {:default default-transformer-provider
:formats {"application/json" json-transformer-provider}}
:string {:default string-transformer-provider}
:response {:default default-transformer-provider}}
;; set of keys to include in error messages
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed}
;; schema identity function (default: close all map schemas)
:compile mu/closed-schema
;; validate request & response
:validate true
;; top-level short-circuit to disable request & response coercion
:enabled true
;; strip-extra-keys (effects only predefined transformers)
:strip-extra-keys true
;; add/set default values
:default-values true
;; malli options
:options nil})
```

View file

@ -76,15 +76,15 @@
(if coercion (if coercion
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] (if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in) (let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
model (if open? (-open-model coercion model) model) model (if open? (-open-model coercion model) model)]
coercer (-request-coercer coercion style model)] (if-let [coercer (-request-coercer coercion style model)]
(fn [request] (fn [request]
(let [value (transform request) (let [value (transform request)
format (extract-request-format request) format (extract-request-format request)
result (coercer value format)] result (coercer value format)]
(if (error? result) (if (error? result)
(request-coercion-failed! result coercion value in request) (request-coercion-failed! result coercion value in request)
result))))))) result))))))))
(defn extract-response-format-default [request _] (defn extract-response-format-default [request _]
(-> request :muuntaja/response :format)) (-> request :muuntaja/response :format))
@ -111,8 +111,7 @@
(reduce-kv (reduce-kv
(fn [acc k coercer] (fn [acc k coercer]
(impl/fast-assoc acc k (coercer request))) (impl/fast-assoc acc k (coercer request)))
{} {} coercers))
coercers))
(defn coerce-response [coercers request response] (defn coerce-response [coercers request response]
(if response (if response
@ -121,17 +120,19 @@
response))) response)))
(defn request-coercers [coercion parameters opts] (defn request-coercers [coercion parameters opts]
(->> (for [[k v] parameters (some->> (for [[k v] parameters
:when v] :when v]
[k (request-coercer coercion k v opts)]) [k (request-coercer coercion k v opts)])
(filter second) (filter second)
(into {}))) (seq)
(into {})))
(defn response-coercers [coercion responses opts] (defn response-coercers [coercion responses opts]
(->> (for [[status {:keys [body]}] responses :when body] (some->> (for [[status {:keys [body]}] responses :when body]
[status (response-coercer coercion body opts)]) [status (response-coercer coercion body opts)])
(filter second) (filter second)
(into {}))) (seq)
(into {})))
;; ;;
;; api-docs ;; api-docs

View file

@ -34,13 +34,13 @@
(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 encoder opts] (defn- -coercer [schema type transformers f encoder {:keys [validate enabled options]}]
(if schema (if schema
(let [->coercer (fn [t] (let [->coercer (fn [t]
(let [decoder (if t (m/decoder schema opts t) (constantly true)) (let [decoder (if t (m/decoder schema options t) identity)
encoder (if t (m/encoder schema opts t) (constantly true)) encoder (if t (m/encoder schema options t) identity)
validator (m/validator schema opts) validator (if validate (m/validator schema options) (constantly true))
explainer (m/explainer schema opts)] 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))
@ -52,7 +52,7 @@
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 {}))
get-coercer (cond format-coercers (fn [format] (or (get format-coercers format) default-coercer)) get-coercer (cond format-coercers (fn [format] (or (get format-coercers format) default-coercer))
default-coercer (constantly default-coercer))] default-coercer (constantly default-coercer))]
(if get-coercer (if (and enabled get-coercer)
(if (= f :decode) (if (= f :decode)
;; decode: decode -> validate ;; decode: decode -> validate
(fn [value format] (fn [value format]
@ -115,6 +115,10 @@
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} :error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed}
;; schema identity function (default: close all map schemas) ;; schema identity function (default: close all map schemas)
:compile mu/closed-schema :compile mu/closed-schema
;; validate request & response
:validate true
;; top-level short-circuit to disable request & response coercion
:enabled true
;; strip-extra-keys (effects only predefined transformers) ;; strip-extra-keys (effects only predefined transformers)
:strip-extra-keys true :strip-extra-keys true
;; add/set default values ;; add/set default values
@ -169,10 +173,10 @@
(update :errors (partial map #(update % :schema edn/write-string opts)))) (update :errors (partial map #(update % :schema edn/write-string opts))))
(seq error-keys) (select-keys error-keys))) (seq error-keys) (select-keys error-keys)))
(-request-coercer [_ type schema] (-request-coercer [_ type schema]
(-coercer (compile schema options) type transformers :decode nil options)) (-coercer (compile schema options) type transformers :decode nil opts))
(-response-coercer [_ schema] (-response-coercer [_ schema]
(let [schema (compile schema options) (let [schema (compile schema options)
encoder (-coercer schema :body transformers :encode nil options)] encoder (-coercer schema :body transformers :encode nil opts)]
(-coercer schema :response transformers :encode encoder options))))))) (-coercer schema :response transformers :encode encoder opts)))))))
(def coercion (create default-options)) (def coercion (create default-options))

View file

@ -32,7 +32,7 @@
(not parameters) {} (not parameters) {}
;; mount ;; mount
:else :else
(let [coercers (coercion/request-coercers coercion parameters opts)] (if-let [coercers (coercion/request-coercers coercion parameters opts)]
(fn [handler] (fn [handler]
(fn (fn
([request] ([request]
@ -40,7 +40,8 @@
(handler (impl/fast-assoc request :parameters coerced)))) (handler (impl/fast-assoc request :parameters coerced))))
([request respond raise] ([request respond raise]
(let [coerced (coercion/coerce-request coercers request)] (let [coerced (coercion/coerce-request coercers request)]
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}) (handler (impl/fast-assoc request :parameters coerced) respond raise)))))
{})))})
(def coerce-response-middleware (def coerce-response-middleware
"Middleware for pluggable response coercion. "Middleware for pluggable response coercion.
@ -56,13 +57,14 @@
(not responses) {} (not responses) {}
;; mount ;; mount
:else :else
(let [coercers (coercion/response-coercers coercion responses opts)] (if-let [coercers (coercion/response-coercers coercion responses opts)]
(fn [handler] (fn [handler]
(fn (fn
([request] ([request]
(coercion/coerce-response coercers request (handler request))) (coercion/coerce-response coercers request (handler request)))
([request respond raise] ([request respond raise]
(handler request #(respond (coercion/coerce-response coercers request %)) raise)))))))}) (handler request #(respond (coercion/coerce-response coercers request %)) raise))))
{})))})
(def coerce-exceptions-middleware (def coerce-exceptions-middleware
"Middleware for handling coercion exceptions. "Middleware for handling coercion exceptions.

View file

@ -8,11 +8,22 @@
[reitit.coercion.malli :as malli] [reitit.coercion.malli :as malli]
[reitit.coercion.schema :as schema] [reitit.coercion.schema :as schema]
#?@(:clj [[muuntaja.middleware] #?@(:clj [[muuntaja.middleware]
[jsonista.core :as j]])) [jsonista.core :as j]])
[reitit.core :as r])
#?(:clj #?(:clj
(:import (clojure.lang ExceptionInfo) (:import (clojure.lang ExceptionInfo)
(java.io ByteArrayInputStream)))) (java.io ByteArrayInputStream))))
(defn middleware-name [{:keys [wrap name]}]
(or name (-> wrap str symbol)))
(defn mounted-middleware [app path method]
(->> app
(ring/get-router)
(r/compiled-routes)
(filter (comp (partial = path) first))
(first) (last) method :middleware (filter :wrap) (mapv middleware-name)))
(defn handler [{{{:keys [a]} :query (defn handler [{{{:keys [a]} :query
{:keys [b]} :body {:keys [b]} :body
{:keys [c]} :form {:keys [c]} :form
@ -199,24 +210,38 @@
(let [{:keys [status]} (app invalid-request2)] (let [{:keys [status]} (app invalid-request2)]
(is (= 500 status)))))))) (is (= 500 status))))))))
(def or-maps-schema
[:or [:map [:x int?]] [:map [:y int?]]])
(deftest malli-coercion-test (deftest malli-coercion-test
(let [create (fn [middleware] (let [create (fn [middleware]
(ring/ring-handler (ring/ring-handler
(ring/router (ring/router
["/api" ["/api"
["/custom" {:summary "just validation" ["/validate" {:summary "just validation"
:coercion (reitit.coercion.malli/create {:transformers {}}) :coercion (reitit.coercion.malli/create {:transformers {}})
:post {:parameters {:body [:map [:x int?]]} :post {:parameters {:body [:map [:x int?]]}
:responses {200 {:body [:map [:x int?]]}} :responses {200 {:body [:map [:x int?]]}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :body)})}}] :body (-> req :parameters :body)})}}]
["/no-op" {:summary "no-operation"
:coercion (reitit.coercion.malli/create {:transformers {}, :validate false})
: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?]]}
:responses {200 {:body [:map [:x int?]]}}
:handler (fn [req]
{:status 200
:body (-> req :parameters :body)})}}]
["/or" {:post {:summary "accepts either of two map schemas" ["/or" {:post {:summary "accepts either of two map schemas"
:parameters {:body or-maps-schema} :parameters {:body [:or [:map [:x int?]] [:map [:y int?]]]}
:responses {200 {:body [:map [:msg string?]]}} :responses {200 {:body [:map [:msg string?]]}}
:handler (fn [{{{:keys [x]} :body} :parameters}] :handler (fn [{{{:keys [x]} :body} :parameters}]
{:status 200 {:status 200
@ -273,10 +298,32 @@
rrc/coerce-response-middleware])] rrc/coerce-response-middleware])]
(testing "just validation" (testing "just validation"
(is (= 400 (:status (app {:uri "/api/custom" (is (= 400 (:status (app {:uri "/api/validate"
:request-method :post :request-method :post
:muuntaja/request {:format "application/edn"} :muuntaja/request {:format "application/edn"}
:body-params 123}))))) :body-params 123}))))
(is (= [:reitit.ring.coercion/coerce-exceptions
:reitit.ring.coercion/coerce-request
:reitit.ring.coercion/coerce-response]
(mounted-middleware app "/api/validate" :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 (= [:reitit.ring.coercion/coerce-exceptions
:reitit.ring.coercion/coerce-request
:reitit.ring.coercion/coerce-response]
(mounted-middleware app "/api/no-op" :post))))
(testing "skipping coercion"
(is (= nil (:body (app {:uri "/api/skip"
:request-method :post
:muuntaja/request {:format "application/edn"}
:body-params 123}))))
(is (= [:reitit.ring.coercion/coerce-exceptions]
(mounted-middleware app "/api/skip" :post))))
(testing "or #407" (testing "or #407"
(is (= {:status 200 (is (= {:status 200