mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 17:01:11 +00:00
New options for malli coercion
This commit is contained in:
parent
f41006c8bb
commit
e649ed22b9
6 changed files with 138 additions and 47 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue