mirror of
https://github.com/metosin/reitit.git
synced 2026-02-24 18:32:22 +00:00
Compare commits
13 commits
ef7ae39b93
...
a2c113fc29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2c113fc29 | ||
|
|
ef9dd495be | ||
|
|
9509e40dae | ||
|
|
67918a3f9c | ||
|
|
45951aa82e | ||
|
|
1cdca2e3d5 | ||
|
|
2f22838820 | ||
|
|
d809291553 | ||
|
|
4e572e86d6 | ||
|
|
f26dc1ab19 | ||
|
|
019e7e3d98 | ||
|
|
3b5100d8a9 | ||
|
|
0454e8914f |
9 changed files with 157 additions and 21 deletions
|
|
@ -16,6 +16,8 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
||||||
|
|
||||||
* Allow multimethods as handlers when validating [#755](https://github.com/metosin/reitit/pull/755)
|
* Allow multimethods as handlers when validating [#755](https://github.com/metosin/reitit/pull/755)
|
||||||
* Improve error reporting when generating OpenAPI fails [#754](https://github.com/metosin/reitit/pull/754)
|
* Improve error reporting when generating OpenAPI fails [#754](https://github.com/metosin/reitit/pull/754)
|
||||||
|
* Allow middleware registry to be used when defining middleware in `ring-handler`. See [docs](./doc/ring/middleware_registry.md). [#739](https://github.com/metosin/reitit/pull/739)
|
||||||
|
* Allow passing options (eg. `:malli.transform/add-optional-keys`) to malli's `default-value-transformer`. See [docs](./doc/coercion/malli_coercion.md). [#756](https://github.com/metosin/reitit/pull/756)
|
||||||
|
|
||||||
## 0.9.1 (2025-05-27)
|
## 0.9.1 (2025-05-27)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,10 @@ Using `create` with options to create the coercion instead of `coercion`:
|
||||||
{:transformers {:body {:default reitit.coercion.malli/default-transformer-provider
|
{:transformers {:body {:default reitit.coercion.malli/default-transformer-provider
|
||||||
:formats {"application/json" reitit.coercion.malli/json-transformer-provider}}
|
:formats {"application/json" reitit.coercion.malli/json-transformer-provider}}
|
||||||
:string {:default reitit.coercion.malli/string-transformer-provider}
|
:string {:default reitit.coercion.malli/string-transformer-provider}
|
||||||
:response {:default reitit.coercion.malli/default-transformer-provider}}
|
:response {:default reitit.coercion.malli/default-transformer-provider
|
||||||
|
:formats {"application/json" reitit.coercion.malli/json-transformer-provider}}}
|
||||||
;; set of keys to include in error messages
|
;; set of keys to include in error messages
|
||||||
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed}
|
:error-keys #{:type :coercion :in #_:schema :value #_:errors :humanized #_:transformed}
|
||||||
;; support lite syntax?
|
;; support lite syntax?
|
||||||
:lite true
|
:lite true
|
||||||
;; schema identity function (default: close all map schemas)
|
;; schema identity function (default: close all map schemas)
|
||||||
|
|
@ -87,7 +88,11 @@ Using `create` with options to create the coercion instead of `coercion`:
|
||||||
;; strip-extra-keys (affects only predefined transformers)
|
;; strip-extra-keys (affects only predefined transformers)
|
||||||
:strip-extra-keys true
|
:strip-extra-keys true
|
||||||
;; add/set default values
|
;; add/set default values
|
||||||
|
;; Can be false, true or a map of options to pass to malli.transform/default-value-transformer,
|
||||||
|
;; for example {:malli.transform/add-optional-keys true}
|
||||||
:default-values true
|
:default-values true
|
||||||
|
;; encode-error
|
||||||
|
:encode-error nil
|
||||||
;; malli options
|
;; malli options
|
||||||
:options nil})
|
:options nil})
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,20 @@ Router creation fails fast if the registry doesn't contain the middleware:
|
||||||
;| :bonus | reitit.ring_test$wrap_bonus@59fddabb |
|
;| :bonus | reitit.ring_test$wrap_bonus@59fddabb |
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Middleware defined in the registry can also be used on the `ring-handler` level:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api"
|
||||||
|
["/bonus" {:get (fn [{:keys [bonus]}]
|
||||||
|
{:status 200, :body {:bonus bonus}})}]]
|
||||||
|
{::middleware/registry {:bonus wrap-bonus}})
|
||||||
|
nil
|
||||||
|
{:middleware [[:bonus 15]]}))
|
||||||
|
```
|
||||||
|
|
||||||
## When to use the registry?
|
## When to use the registry?
|
||||||
|
|
||||||
Middleware as Keywords helps to keep the routes (all but handlers) as literal data (i.e. data that evaluates to itself), enabling the routes to be persisted in external formats like EDN-files and databases. Duct is a good example, where the [middleware can be referenced from EDN-files](https://github.com/duct-framework/duct/wiki/Configuration). It should be easy to make Duct configuration a Middleware Registry in `reitit-ring`.
|
Middleware as Keywords helps to keep the routes (all but handlers) as literal data (i.e. data that evaluates to itself), enabling the routes to be persisted in external formats like EDN-files and databases. Duct is a good example, where the [middleware can be referenced from EDN-files](https://github.com/duct-framework/duct/wiki/Configuration). It should be easy to make Duct configuration a Middleware Registry in `reitit-ring`.
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@
|
||||||
[malli.swagger :as swagger]
|
[malli.swagger :as swagger]
|
||||||
[malli.transform :as mt]
|
[malli.transform :as mt]
|
||||||
[malli.util :as mu]
|
[malli.util :as mu]
|
||||||
[reitit.coercion :as coercion]
|
[reitit.coercion :as coercion]))
|
||||||
[clojure.string :as string]))
|
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; coercion
|
;; coercion
|
||||||
|
|
@ -20,7 +19,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]))
|
||||||
|
|
@ -31,24 +31,30 @@
|
||||||
(mt/transformer
|
(mt/transformer
|
||||||
(if strip-extra-keys (mt/strip-extra-keys-transformer))
|
(if strip-extra-keys (mt/strip-extra-keys-transformer))
|
||||||
transformer
|
transformer
|
||||||
(if default-values (mt/default-value-transformer))))))
|
(if default-values (mt/default-value-transformer (if (map? default-values) default-values {})))))))
|
||||||
|
|
||||||
(def string-transformer-provider (-provider (mt/string-transformer)))
|
(def string-transformer-provider (-provider (mt/string-transformer)))
|
||||||
(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 +69,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]
|
||||||
|
|
@ -72,9 +77,9 @@
|
||||||
(if-let [coercer (get-coercer format)]
|
(if-let [coercer (get-coercer format)]
|
||||||
(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
|
encoded (-encode coercer transformed)]
|
||||||
(assoc error :transformed transformed))))
|
(-on-invalid coercer encoded error)))
|
||||||
value))))))))
|
value))))))))
|
||||||
|
|
||||||
(defn- -query-string-coercer
|
(defn- -query-string-coercer
|
||||||
|
|
@ -116,10 +121,14 @@
|
||||||
:enabled true
|
:enabled true
|
||||||
;; strip-extra-keys (affects only predefined transformers)
|
;; strip-extra-keys (affects only predefined transformers)
|
||||||
:strip-extra-keys true
|
:strip-extra-keys true
|
||||||
;; add/set default values
|
;; add/set default values.
|
||||||
|
;; Can be false, true or a map of options to pass to malli.transform/default-value-transformer,
|
||||||
|
;; for example {:malli.transform/add-optional-keys true}
|
||||||
: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})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@
|
||||||
([router default-handler {:keys [middleware inject-match? inject-router?]
|
([router default-handler {:keys [middleware inject-match? inject-router?]
|
||||||
:or {inject-match? true, inject-router? true}}]
|
:or {inject-match? true, inject-router? true}}]
|
||||||
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
||||||
wrap (if middleware (partial middleware/chain middleware) identity)
|
wrap (if middleware #(middleware/chain middleware % nil (r/options router)) identity)
|
||||||
enrich-request (create-enrich-request inject-match? inject-router?)
|
enrich-request (create-enrich-request inject-match? inject-router?)
|
||||||
enrich-default-request (create-enrich-default-request inject-router?)]
|
enrich-default-request (create-enrich-default-request inject-router?)]
|
||||||
(with-meta
|
(with-meta
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,36 @@
|
||||||
(let [m (r/match-by-path r "/none/kikka/abba")]
|
(let [m (r/match-by-path r "/none/kikka/abba")]
|
||||||
(is (= nil (coercion/coerce! m))))))))
|
(is (= nil (coercion/coerce! m))))))))
|
||||||
|
|
||||||
|
(deftest malli-query-parameter-coercion-test
|
||||||
|
(let [router (fn [coercion]
|
||||||
|
(r/router ["/test"
|
||||||
|
{:coercion coercion
|
||||||
|
:parameters {:query [:map
|
||||||
|
[:a [:string {:default "a"}]]
|
||||||
|
[:x {:optional true} [:keyword {:default :a}]]]}}]
|
||||||
|
{:compile coercion/compile-request-coercers}))]
|
||||||
|
(testing "default values for :optional query keys do not get added"
|
||||||
|
(is (= {:query {:a "a"}}
|
||||||
|
(-> (r/match-by-path (router reitit.coercion.malli/coercion) "/test")
|
||||||
|
(assoc :query-params {})
|
||||||
|
(coercion/coerce!)))))
|
||||||
|
(testing "default values for :optional query keys get added when :malli.transform/add-optional-keys is set"
|
||||||
|
(is (= {:query {:a "a" :x :a}}
|
||||||
|
(-> (r/match-by-path (router (reitit.coercion.malli/create
|
||||||
|
(assoc reitit.coercion.malli/default-options
|
||||||
|
:default-values {:malli.transform/add-optional-keys true}))) "/test")
|
||||||
|
(assoc :query-params {})
|
||||||
|
(coercion/coerce!)))))
|
||||||
|
(testing "default values can be disabled"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Request coercion failed"
|
||||||
|
(-> (r/match-by-path (router (reitit.coercion.malli/create
|
||||||
|
(assoc reitit.coercion.malli/default-options
|
||||||
|
:default-values false))) "/test")
|
||||||
|
(assoc :query-params {})
|
||||||
|
(coercion/coerce!)))))))
|
||||||
|
|
||||||
(defn match-by-path-and-coerce! [router path]
|
(defn match-by-path-and-coerce! [router path]
|
||||||
(if-let [match (r/match-by-path router path)]
|
(if-let [match (r/match-by-path router path)]
|
||||||
(assoc match :parameters (coercion/coerce! match))))
|
(assoc match :parameters (coercion/coerce! match))))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,25 @@
|
||||||
(is (= {:status 200, :body [:top :api :ok]}
|
(is (= {:status 200, :body [:top :api :ok]}
|
||||||
(app {:uri "/api/get" :request-method :get}))))))
|
(app {:uri "/api/get" :request-method :get}))))))
|
||||||
|
|
||||||
|
(testing "middleware from registry"
|
||||||
|
(let [router (ring/router
|
||||||
|
["/api" {:middleware [:mw-foo]}
|
||||||
|
["/get" {:middleware [[:mw :inner]]
|
||||||
|
:get handler}]]
|
||||||
|
{::middleware/registry {:mw mw
|
||||||
|
:mw-foo #(mw % :foo)}})
|
||||||
|
app (ring/ring-handler router nil {:middleware [[:mw :top]]})]
|
||||||
|
|
||||||
|
(testing "router can be extracted"
|
||||||
|
(is (= router (ring/get-router app))))
|
||||||
|
|
||||||
|
(testing "not found"
|
||||||
|
(is (= nil (app {:uri "/favicon.ico"}))))
|
||||||
|
|
||||||
|
(testing "on match"
|
||||||
|
(is (= {:status 200, :body [:top :foo :inner :ok]}
|
||||||
|
(app {:uri "/api/get" :request-method :get}))))))
|
||||||
|
|
||||||
(testing "named routes"
|
(testing "named routes"
|
||||||
(let [router (ring/router
|
(let [router (ring/router
|
||||||
[["/api"
|
[["/api"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue