mirror of
https://github.com/metosin/reitit.git
synced 2025-12-26 19:48:24 +00:00
commit
f2e0470ecd
9 changed files with 477 additions and 67 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
|
@ -12,6 +12,27 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
|||
|
||||
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
|
||||
|
||||
## UNRELEASED
|
||||
|
||||
```clj
|
||||
[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-ring`
|
||||
|
||||
* Coercion middleware will not to mount if the selected `:coercion` is not enabled for the given `:parameters`, e.g. "just api-docs"
|
||||
|
||||
### `reitit-http`
|
||||
|
||||
* Coercion interceptor 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)
|
||||
|
||||
```clj
|
||||
|
|
|
|||
|
|
@ -43,3 +43,29 @@ Failing coercion:
|
|||
(match-by-path-and-coerce! "/metosin/users/ikitommi")
|
||||
; => 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-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
|
||||
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||
model (if open? (-open-model coercion model) model)
|
||||
coercer (-request-coercer coercion style model)]
|
||||
(fn [request]
|
||||
(let [value (transform request)
|
||||
format (extract-request-format request)
|
||||
result (coercer value format)]
|
||||
(if (error? result)
|
||||
(request-coercion-failed! result coercion value in request)
|
||||
result)))))))
|
||||
model (if open? (-open-model coercion model) model)]
|
||||
(if-let [coercer (-request-coercer coercion style model)]
|
||||
(fn [request]
|
||||
(let [value (transform request)
|
||||
format (extract-request-format request)
|
||||
result (coercer value format)]
|
||||
(if (error? result)
|
||||
(request-coercion-failed! result coercion value in request)
|
||||
result))))))))
|
||||
|
||||
(defn extract-response-format-default [request _]
|
||||
(-> request :muuntaja/response :format))
|
||||
|
|
@ -111,8 +111,7 @@
|
|||
(reduce-kv
|
||||
(fn [acc k coercer]
|
||||
(impl/fast-assoc acc k (coercer request)))
|
||||
{}
|
||||
coercers))
|
||||
{} coercers))
|
||||
|
||||
(defn coerce-response [coercers request response]
|
||||
(if response
|
||||
|
|
@ -121,17 +120,19 @@
|
|||
response)))
|
||||
|
||||
(defn request-coercers [coercion parameters opts]
|
||||
(->> (for [[k v] parameters
|
||||
:when v]
|
||||
[k (request-coercer coercion k v opts)])
|
||||
(filter second)
|
||||
(into {})))
|
||||
(some->> (for [[k v] parameters
|
||||
:when v]
|
||||
[k (request-coercer coercion k v opts)])
|
||||
(filter second)
|
||||
(seq)
|
||||
(into {})))
|
||||
|
||||
(defn response-coercers [coercion responses opts]
|
||||
(->> (for [[status {:keys [body]}] responses :when body]
|
||||
[status (response-coercer coercion body opts)])
|
||||
(filter second)
|
||||
(into {})))
|
||||
(some->> (for [[status {:keys [body]}] responses :when body]
|
||||
[status (response-coercer coercion body opts)])
|
||||
(filter second)
|
||||
(seq)
|
||||
(into {})))
|
||||
|
||||
;;
|
||||
;; api-docs
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@
|
|||
(not parameters) {}
|
||||
;; mount
|
||||
:else
|
||||
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||
(if-let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||
{:enter (fn [ctx]
|
||||
(let [request (:request ctx)
|
||||
coerced (coercion/coerce-request coercers request)
|
||||
request (impl/fast-assoc request :parameters coerced)]
|
||||
(assoc ctx :request request)))})))})
|
||||
(assoc ctx :request request)))}
|
||||
{})))})
|
||||
|
||||
(defn coerce-response-interceptor
|
||||
"Interceptor for pluggable response coercion.
|
||||
|
|
@ -40,12 +41,13 @@
|
|||
(not responses) {}
|
||||
;; mount
|
||||
:else
|
||||
(let [coercers (coercion/response-coercers coercion responses opts)]
|
||||
(if-let [coercers (coercion/response-coercers coercion responses opts)]
|
||||
{:leave (fn [ctx]
|
||||
(let [request (:request ctx)
|
||||
response (:response ctx)
|
||||
response (coercion/coerce-response coercers request response)]
|
||||
(assoc ctx :response response)))})))})
|
||||
(assoc ctx :response response)))}
|
||||
{})))})
|
||||
|
||||
(defn coerce-exceptions-interceptor
|
||||
"Interceptor for handling coercion exceptions.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@
|
|||
;; coercion
|
||||
;;
|
||||
|
||||
(defrecord Coercer [decoder encoder validator explainer])
|
||||
(defprotocol Coercer
|
||||
(-decode [this value])
|
||||
(-encode [this value])
|
||||
(-validate [this value])
|
||||
(-explain [this value]))
|
||||
|
||||
(defprotocol TransformationProvider
|
||||
(-transformer [this options]))
|
||||
|
|
@ -30,43 +34,43 @@
|
|||
(def json-transformer-provider (-provider (mt/json-transformer)))
|
||||
(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
|
||||
(let [->coercer (fn [t] (if t (->Coercer (m/decoder schema opts t)
|
||||
(m/encoder schema opts t)
|
||||
(m/validator schema opts)
|
||||
(m/explainer schema opts))))
|
||||
(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)]
|
||||
(reify Coercer
|
||||
(-decode [_ value] (decoder value))
|
||||
(-encode [_ value] (encoder value))
|
||||
(-validate [_ value] (validator value))
|
||||
(-explain [_ value] (explainer value)))))
|
||||
{:keys [formats default]} (transformers type)
|
||||
default-coercer (->coercer default)
|
||||
encode (or encoder (fn [value _format] value))
|
||||
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))
|
||||
default-coercer (constantly default-coercer))]
|
||||
(if get-coercer
|
||||
(if (and enabled get-coercer)
|
||||
(if (= f :decode)
|
||||
;; decode -> validate
|
||||
;; decode: decode -> validate
|
||||
(fn [value format]
|
||||
(if-let [coercer (get-coercer format)]
|
||||
(let [decoder (:decoder coercer)
|
||||
validator (:validator coercer)
|
||||
transformed (decoder value)]
|
||||
(if (validator transformed)
|
||||
(let [transformed (-decode coercer value)]
|
||||
(if (-validate coercer transformed)
|
||||
transformed
|
||||
(let [explainer (:explainer coercer)
|
||||
error (explainer transformed)]
|
||||
(let [error (-explain coercer transformed)]
|
||||
(coercion/map->CoercionError
|
||||
(assoc error :transformed transformed)))))
|
||||
value))
|
||||
;; decode -> validate -> encode
|
||||
;; encode: decode -> validate -> encode
|
||||
(fn [value format]
|
||||
(if-let [coercer (get-coercer format)]
|
||||
(let [decoder (:decoder coercer)
|
||||
validator (:validator coercer)
|
||||
transformed (decoder value)]
|
||||
(if (validator transformed)
|
||||
(let [transformed (-decode coercer value)]
|
||||
(if (-validate coercer transformed)
|
||||
(encode transformed format)
|
||||
(let [explainer (:explainer coercer)
|
||||
error (explainer transformed)]
|
||||
(let [error (-explain coercer transformed)]
|
||||
(coercion/map->CoercionError
|
||||
(assoc error :transformed transformed)))))
|
||||
value)))))))
|
||||
|
|
@ -111,6 +115,10 @@
|
|||
: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
|
||||
|
|
@ -165,10 +173,10 @@
|
|||
(update :errors (partial map #(update % :schema edn/write-string opts))))
|
||||
(seq error-keys) (select-keys error-keys)))
|
||||
(-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]
|
||||
(let [schema (compile schema options)
|
||||
encoder (-coercer schema :body transformers :encode nil options)]
|
||||
(-coercer schema :response transformers :encode encoder options)))))))
|
||||
encoder (-coercer schema :body transformers :encode nil opts)]
|
||||
(-coercer schema :response transformers :encode encoder opts)))))))
|
||||
|
||||
(def coercion (create default-options))
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
(not parameters) {}
|
||||
;; mount
|
||||
:else
|
||||
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||
(if-let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
|
|
@ -40,7 +40,8 @@
|
|||
(handler (impl/fast-assoc request :parameters coerced))))
|
||||
([request respond raise]
|
||||
(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
|
||||
"Middleware for pluggable response coercion.
|
||||
|
|
@ -56,13 +57,14 @@
|
|||
(not responses) {}
|
||||
;; mount
|
||||
:else
|
||||
(let [coercers (coercion/response-coercers coercion responses opts)]
|
||||
(if-let [coercers (coercion/response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(coercion/coerce-response coercers request (handler request)))
|
||||
([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
|
||||
"Middleware for handling coercion exceptions.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
[metosin/muuntaja "0.6.7"]
|
||||
[metosin/jsonista "0.2.6"]
|
||||
[metosin/sieppari "0.0.0-alpha13"]
|
||||
[metosin/malli "0.0.1-20200404.091302-14"]
|
||||
[metosin/malli "0.0.1-20200525.162645-15"]
|
||||
|
||||
;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111
|
||||
[com.fasterxml.jackson.core/jackson-core "2.11.0"]
|
||||
|
|
|
|||
|
|
@ -7,10 +7,21 @@
|
|||
[reitit.coercion.schema :as schema]
|
||||
[muuntaja.interceptor]
|
||||
[jsonista.core :as j]
|
||||
[reitit.interceptor.sieppari :as sieppari])
|
||||
[reitit.interceptor.sieppari :as sieppari]
|
||||
[reitit.coercion.malli :as malli]
|
||||
[reitit.core :as r])
|
||||
(:import (clojure.lang ExceptionInfo)
|
||||
(java.io ByteArrayInputStream)))
|
||||
|
||||
(defn mounted-interceptor [app path method]
|
||||
(->> app
|
||||
(http/get-router)
|
||||
(r/compiled-routes)
|
||||
(filter (comp (partial = path) first))
|
||||
(first) (last) method :interceptors
|
||||
(filter #(->> (select-keys % [:enter :leave :error]) (vals) (keep identity) (seq)))
|
||||
(mapv :name)))
|
||||
|
||||
(defn handler [{{{:keys [a]} :query
|
||||
{:keys [b]} :body
|
||||
{:keys [c]} :form
|
||||
|
|
@ -22,7 +33,7 @@
|
|||
{:status 200
|
||||
:body {:total (+ a b c d e)}}))
|
||||
|
||||
(def valid-request
|
||||
(def valid-request1
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get
|
||||
:query-params {"a" "1"}
|
||||
|
|
@ -30,7 +41,25 @@
|
|||
:form-params {:c 3}
|
||||
:headers {"d" "4"}})
|
||||
|
||||
(def invalid-request
|
||||
(def valid-request2
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get
|
||||
:muuntaja/request {:format "application/json"}
|
||||
:query-params {}
|
||||
:body-params {:b 2}
|
||||
:form-params {:c 3}
|
||||
:headers {"d" "4"}})
|
||||
|
||||
(def valid-request3
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get
|
||||
:muuntaja/request {:format "application/edn"}
|
||||
:query-params {"a" "1", "EXTRA" "VALUE"}
|
||||
:body-params {:b 2, :EXTRA "VALUE"}
|
||||
:form-params {:c 3, :EXTRA "VALUE"}
|
||||
:headers {"d" "4", "EXTRA" "VALUE"}})
|
||||
|
||||
(def invalid-request1
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get})
|
||||
|
||||
|
|
@ -67,16 +96,16 @@
|
|||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request)))
|
||||
(app valid-request1)))
|
||||
(is (= {:status 500
|
||||
:body {:evil true}}
|
||||
(app (assoc-in valid-request [:query-params "a"] "666")))))
|
||||
(app (assoc-in valid-request1 [:query-params "a"] "666")))))
|
||||
|
||||
(testing "invalid request"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Request coercion failed"
|
||||
(app invalid-request))))
|
||||
(app invalid-request1))))
|
||||
|
||||
(testing "invalid response"
|
||||
(is (thrown-with-msg?
|
||||
|
|
@ -92,10 +121,10 @@
|
|||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request))))
|
||||
(app valid-request1))))
|
||||
|
||||
(testing "invalid request"
|
||||
(let [{:keys [status]} (app invalid-request)]
|
||||
(let [{:keys [status]} (app invalid-request1)]
|
||||
(is (= 400 status))))
|
||||
|
||||
(testing "invalid response"
|
||||
|
|
@ -127,16 +156,16 @@
|
|||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request)))
|
||||
(app valid-request1)))
|
||||
(is (= {:status 500
|
||||
:body {:evil true}}
|
||||
(app (assoc-in valid-request [:query-params "a"] "666")))))
|
||||
(app (assoc-in valid-request1 [:query-params "a"] "666")))))
|
||||
|
||||
(testing "invalid request"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Request coercion failed"
|
||||
(app invalid-request))))
|
||||
(app invalid-request1))))
|
||||
|
||||
(testing "invalid response"
|
||||
(is (thrown-with-msg?
|
||||
|
|
@ -152,16 +181,262 @@
|
|||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request))))
|
||||
(app valid-request1))))
|
||||
|
||||
(testing "invalid request"
|
||||
(let [{:keys [status]} (app invalid-request)]
|
||||
(let [{:keys [status]} (app invalid-request1)]
|
||||
(is (= 400 status))))
|
||||
|
||||
(testing "invalid response"
|
||||
(let [{:keys [status]} (app invalid-request2)]
|
||||
(is (= 500 status))))))))))
|
||||
|
||||
(deftest malli-coercion-test
|
||||
(let [create (fn [interceptors]
|
||||
(http/ring-handler
|
||||
(http/router
|
||||
["/api"
|
||||
|
||||
["/validate" {:summary "just validation"
|
||||
:coercion (reitit.coercion.malli/create {:transformers {}})
|
||||
:post {:parameters {:body [:map [:x int?]]}
|
||||
:responses {200 {:body [:map [:x int?]]}}
|
||||
:handler (fn [req]
|
||||
{:status 200
|
||||
: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"
|
||||
:parameters {:body [:or [:map [:x int?]] [:map [:y int?]]]}
|
||||
:responses {200 {:body [:map [:msg string?]]}}
|
||||
:handler (fn [{{{:keys [x]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:msg (if x "you sent x" "you sent y")}})}}]
|
||||
|
||||
["/plus/:e" {:get {:parameters {:query [:map [:a {:optional true} int?]]
|
||||
:body [:map [:b int?]]
|
||||
:form [:map [:c [int? {:default 3}]]]
|
||||
:header [:map [:d int?]]
|
||||
:path [:map [:e int?]]}
|
||||
:responses {200 {:body [:map [:total pos-int?]]}
|
||||
500 {:description "fail"}}
|
||||
:handler handler}}]]
|
||||
{:data {:interceptors interceptors
|
||||
:coercion malli/coercion}})
|
||||
{:executor sieppari/executor}))]
|
||||
|
||||
(testing "withut exception handling"
|
||||
(let [app (create [(rrc/coerce-request-interceptor)
|
||||
(rrc/coerce-response-interceptor)])]
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request1)))
|
||||
#_(is (= {:status 200
|
||||
:body {:total 115}}
|
||||
(app valid-request2)))
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request3)))
|
||||
(testing "default values work"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app (update valid-request3 :form-params dissoc :c)))))
|
||||
(is (= {:status 500
|
||||
:body {:evil true}}
|
||||
(app (assoc-in valid-request1 [:query-params "a"] "666")))))
|
||||
|
||||
(testing "invalid request"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Request coercion failed"
|
||||
(app invalid-request1))))
|
||||
|
||||
(testing "invalid response"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Response coercion failed"
|
||||
(app invalid-request2))))))
|
||||
|
||||
(testing "with exception handling"
|
||||
(let [app (create [(rrc/coerce-exceptions-interceptor)
|
||||
(rrc/coerce-request-interceptor)
|
||||
(rrc/coerce-response-interceptor)])]
|
||||
|
||||
(testing "just validation"
|
||||
(is (= 400 (:status (app {:uri "/api/validate"
|
||||
: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" :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.http.coercion/coerce-exceptions
|
||||
:reitit.http.coercion/coerce-request
|
||||
:reitit.http.coercion/coerce-response
|
||||
:reitit.interceptor/handler]
|
||||
(mounted-interceptor 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.http.coercion/coerce-exceptions
|
||||
:reitit.interceptor/handler]
|
||||
(mounted-interceptor app "/api/skip" :post))))
|
||||
|
||||
(testing "or #407"
|
||||
(is (= {:status 200
|
||||
:body {:msg "you sent x"}}
|
||||
(app {:uri "/api/or"
|
||||
:request-method :post
|
||||
:body-params {:x 1}}))))
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request1))))
|
||||
|
||||
(testing "invalid request"
|
||||
(let [{:keys [status]} (app invalid-request1)]
|
||||
(is (= 400 status))))
|
||||
|
||||
(testing "invalid response"
|
||||
(let [{:keys [status]} (app invalid-request2)]
|
||||
(is (= 500 status))))))
|
||||
|
||||
(testing "open & closed schemas"
|
||||
(let [endpoint (fn [schema]
|
||||
{:get {:parameters {:body schema}
|
||||
:responses {200 {:body schema}}
|
||||
:handler (fn [{{:keys [body]} :parameters}]
|
||||
{:status 200, :body (assoc body :response true)})}})
|
||||
->app (fn [options]
|
||||
(http/ring-handler
|
||||
(http/router
|
||||
["/api"
|
||||
["/default" (endpoint [:map [:x int?]])]
|
||||
["/closed" (endpoint [:map {:closed true} [:x int?]])]
|
||||
["/open" (endpoint [:map {:closed false} [:x int?]])]]
|
||||
{:data {:interceptors [(rrc/coerce-exceptions-interceptor)
|
||||
(rrc/coerce-request-interceptor)
|
||||
(rrc/coerce-response-interceptor)]
|
||||
:coercion (malli/create options)}})
|
||||
{:executor sieppari/executor}))
|
||||
->request (fn [uri] {:uri (str "/api/" uri)
|
||||
:request-method :get
|
||||
:muuntaja/request {:format "application/json"}
|
||||
:body-params {:x 1, :request true}})]
|
||||
|
||||
(testing "with defaults"
|
||||
(let [app (->app nil)]
|
||||
|
||||
(testing "default: keys are stripped"
|
||||
(is (= {:status 200, :body {:x 1}}
|
||||
(app (->request "default")))))
|
||||
|
||||
(testing "closed: keys are stripped"
|
||||
(is (= {:status 200, :body {:x 1}}
|
||||
(app (->request "closed")))))
|
||||
|
||||
(testing "open: keys are NOT stripped"
|
||||
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||
(app (->request "open")))))))
|
||||
|
||||
(testing "when schemas are not closed"
|
||||
(let [app (->app {:compile (fn [v _] v)})]
|
||||
|
||||
(testing "default: keys are stripped"
|
||||
(is (= {:status 200, :body {:x 1}}
|
||||
(app (->request "default")))))
|
||||
|
||||
(testing "closed: keys are stripped"
|
||||
(is (= {:status 200, :body {:x 1}}
|
||||
(app (->request "closed")))))
|
||||
|
||||
(testing "open: keys are NOT stripped"
|
||||
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||
(app (->request "open")))))))
|
||||
|
||||
(testing "when schemas are not closed and extra keys are not stripped"
|
||||
(let [app (->app {:compile (fn [v _] v) :strip-extra-keys false})]
|
||||
(testing "default: keys are NOT stripped"
|
||||
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||
(app (->request "default")))))
|
||||
|
||||
(testing "closed: FAILS for extra keys"
|
||||
(is (= 400 (:status (app (->request "closed"))))))
|
||||
|
||||
(testing "open: keys are NOT stripped"
|
||||
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||
(app (->request "open")))))))))
|
||||
|
||||
(testing "sequence schemas"
|
||||
(let [app (http/ring-handler
|
||||
(http/router
|
||||
["/ping" {:get {:parameters {:body [:vector [:map [:message string?]]]}
|
||||
:responses {200 {:body [:vector [:map [:pong string?]]]}
|
||||
501 {:body [:vector [:map [:error string?]]]}}
|
||||
:handler (fn [{{[{:keys [message]}] :body} :parameters :as req}]
|
||||
(condp = message
|
||||
"ping" {:status 200
|
||||
:body [{:pong message}]}
|
||||
"fail" {:status 501
|
||||
:body [{:error "fail"}]}
|
||||
{:status 200
|
||||
:body {:invalid "response"}}))}}]
|
||||
{:data {:interceptors [(rrc/coerce-exceptions-interceptor)
|
||||
(rrc/coerce-request-interceptor)
|
||||
(rrc/coerce-response-interceptor)]
|
||||
:coercion malli/coercion}})
|
||||
{:executor sieppari/executor})
|
||||
->request (fn [body]
|
||||
{:uri "/ping"
|
||||
:request-method :get
|
||||
:muuntaja/request {:format "application/json"}
|
||||
:body-params body})]
|
||||
|
||||
(testing "succesfull request"
|
||||
(let [{:keys [status body]} (app (->request [{:message "ping"}]))]
|
||||
(is (= 200 status))
|
||||
(is (= [{:pong "ping"}] body)))
|
||||
|
||||
(testing "succesfull failure"
|
||||
(let [{:keys [status body]} (app (->request [{:message "fail"}]))]
|
||||
(is (= 501 status))
|
||||
(is (= [{:error "fail"}] body))))
|
||||
|
||||
(testing "failed response"
|
||||
(let [{:keys [status body]} (app (->request [{:message "kosh"}]))]
|
||||
(is (= 500 status))
|
||||
(is (= :reitit.coercion/response-coercion (:type body))))))))))
|
||||
|
||||
(deftest muuntaja-test
|
||||
(let [app (http/ring-handler
|
||||
(http/router
|
||||
|
|
|
|||
|
|
@ -8,11 +8,19 @@
|
|||
[reitit.coercion.malli :as malli]
|
||||
[reitit.coercion.schema :as schema]
|
||||
#?@(:clj [[muuntaja.middleware]
|
||||
[jsonista.core :as j]]))
|
||||
[jsonista.core :as j]])
|
||||
[reitit.core :as r])
|
||||
#?(:clj
|
||||
(:import (clojure.lang ExceptionInfo)
|
||||
(java.io ByteArrayInputStream))))
|
||||
|
||||
(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 :name)))
|
||||
|
||||
(defn handler [{{{:keys [a]} :query
|
||||
{:keys [b]} :body
|
||||
{:keys [c]} :form
|
||||
|
|
@ -204,6 +212,38 @@
|
|||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api"
|
||||
|
||||
["/validate" {:summary "just validation"
|
||||
:coercion (reitit.coercion.malli/create {:transformers {}})
|
||||
:post {:parameters {:body [:map [:x int?]]}
|
||||
:responses {200 {:body [:map [:x int?]]}}
|
||||
:handler (fn [req]
|
||||
{:status 200
|
||||
: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"
|
||||
:parameters {:body [:or [:map [:x int?]] [:map [:y int?]]]}
|
||||
:responses {200 {:body [:map [:msg string?]]}}
|
||||
:handler (fn [{{{:keys [x]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:msg (if x "you sent x" "you sent y")}})}}]
|
||||
|
||||
["/plus/:e" {:get {:parameters {:query [:map [:a {:optional true} int?]]
|
||||
:body [:map [:b int?]]
|
||||
:form [:map [:c [int? {:default 3}]]]
|
||||
|
|
@ -254,6 +294,41 @@
|
|||
rrc/coerce-request-middleware
|
||||
rrc/coerce-response-middleware])]
|
||||
|
||||
(testing "just validation"
|
||||
(is (= 400 (:status (app {:uri "/api/validate"
|
||||
: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/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"
|
||||
(is (= {:status 200
|
||||
:body {:msg "you sent x"}}
|
||||
(app {:uri "/api/or"
|
||||
:request-method :post
|
||||
:body-params {:x 1}}))))
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
|
|
|
|||
Loading…
Reference in a new issue