Compare commits

..

1 commit

Author SHA1 Message Date
Daniel Compton
8c4cff30a0
Merge 7a707f042b into 7520d20f12 2025-05-27 09:28:04 -04:00
11 changed files with 25 additions and 122 deletions

View file

@ -12,13 +12,6 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md [breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## Unreleased
* 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)
* 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)
* **FIX**: response coercion threw an exception for unlisted HTTP status codes if there was no `:default`. Broken in 0.9.0. [#742](https://github.com/metosin/reitit/issues/742) * **FIX**: response coercion threw an exception for unlisted HTTP status codes if there was no `:default`. Broken in 0.9.0. [#742](https://github.com/metosin/reitit/issues/742)

View file

@ -73,10 +73,9 @@ 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)
@ -88,11 +87,7 @@ 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})
``` ```

View file

@ -2,7 +2,7 @@
The `:middleware` syntax in `reitit-ring` also supports Keywords. Keywords are looked up from the Middleware Registry, which is a map of `keyword => IntoMiddleware`. Middleware registry should be stored under key `:reitit.middleware/registry` in the router options. If a middleware keyword isn't found in the registry, router creation fails fast with a descriptive error message. The `:middleware` syntax in `reitit-ring` also supports Keywords. Keywords are looked up from the Middleware Registry, which is a map of `keyword => IntoMiddleware`. Middleware registry should be stored under key `:reitit.middleware/registry` in the router options. If a middleware keyword isn't found in the registry, router creation fails fast with a descriptive error message.
## Examples ## Examples
Application using middleware defined in the Middleware Registry: Application using middleware defined in the Middleware Registry:
@ -52,20 +52,6 @@ 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`.

View file

@ -37,11 +37,8 @@
;; Default data ;; Default data
;; ;;
(defn -multi? [x]
(instance? #?(:clj clojure.lang.MultiFn :cljs cljs.core.MultiFn) x))
(s/def ::name keyword?) (s/def ::name keyword?)
(s/def ::handler (s/or :fn fn? :var var? :multi -multi?)) (s/def ::handler (s/or :fn fn? :var var?))
(s/def ::no-doc boolean?) (s/def ::no-doc boolean?)
(s/def ::conflicting boolean?) (s/def ::conflicting boolean?)
(s/def ::default-data (s/def ::default-data

View file

@ -9,7 +9,8 @@
[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
@ -30,7 +31,7 @@
(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 (map? default-values) default-values {}))))))) (if default-values (mt/default-value-transformer))))))
(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)))
@ -115,9 +116,7 @@
: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

View file

@ -58,10 +58,7 @@
"Creates a Middleware to handle the multipart params, based on "Creates a Middleware to handle the multipart params, based on
ring.middleware.multipart-params, taking same options. Mounts only ring.middleware.multipart-params, taking same options. Mounts only
if endpoint has `[:parameters :multipart]` defined. Publishes coerced if endpoint has `[:parameters :multipart]` defined. Publishes coerced
parameters into `[:parameters :multipart]` under request. parameters into `[:parameters :multipart]` under request."
Note! You want to have multipart-middleware after coerce-request-middleware,
because coerce-request-middleware overwrites `:parameters`."
([] ([]
(create-multipart-middleware nil)) (create-multipart-middleware nil))
([options] ([options]
@ -72,8 +69,5 @@
"Middleware to handle the multipart params, based on "Middleware to handle the multipart params, based on
ring.middleware.multipart-params, taking same options. Mounts only ring.middleware.multipart-params, taking same options. Mounts only
if endpoint has `[:parameters :multipart]` defined. Publishes coerced if endpoint has `[:parameters :multipart]` defined. Publishes coerced
parameters into `[:parameters :multipart]` under request. parameters into `[:parameters :multipart]` under request."
Note! You want to have multipart-middleware after coerce-request-middleware,
because coerce-request-middleware overwrites `:parameters`."
(create-multipart-middleware)) (create-multipart-middleware))

View file

@ -206,23 +206,20 @@
accept-route (fn [route] accept-route (fn [route]
(-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq)) (-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq))
definitions (volatile! {}) definitions (volatile! {})
transform-endpoint (fn [path [method {{:keys [coercion no-doc openapi] :as data} :data transform-endpoint (fn [[method {{:keys [coercion no-doc openapi] :as data} :data
middleware :middleware middleware :middleware
interceptors :interceptors}]] interceptors :interceptors}]]
(try (if (and data (not no-doc))
(if (and data (not no-doc)) [method
[method (meta-merge
(meta-merge (apply meta-merge (keep (comp :openapi :data) middleware))
(apply meta-merge (keep (comp :openapi :data) middleware)) (apply meta-merge (keep (comp :openapi :data) interceptors))
(apply meta-merge (keep (comp :openapi :data) interceptors)) (if coercion
(if coercion (-get-apidocs-openapi coercion data definitions))
(-get-apidocs-openapi coercion data definitions)) (select-keys data [:tags :summary :description])
(select-keys data [:tags :summary :description]) (strip-top-level-keys openapi))]))
(strip-top-level-keys openapi))])
(catch Throwable t
(throw (ex-info "While building openapi docs" {:path path :method method} t)))))
transform-path (fn [[p _ c]] transform-path (fn [[p _ c]]
(if-let [endpoint (some->> c (keep (partial transform-endpoint p)) (seq) (into {}))] (if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
[(openapi-path p (r/options router)) endpoint])) [(openapi-path p (r/options router)) endpoint]))
map-in-order #(->> % (apply concat) (apply array-map)) map-in-order #(->> % (apply concat) (apply array-map))
paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)] paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)]

View file

@ -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 #(middleware/chain middleware % nil (r/options router)) identity) wrap (if middleware (partial middleware/chain middleware) 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

View file

@ -140,36 +140,6 @@
(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))))

View file

@ -12,9 +12,6 @@
(s/def ::role #{:admin :user}) (s/def ::role #{:admin :user})
(s/def ::roles (s/and (s/coll-of ::role :into #{}) set?)) (s/def ::roles (s/and (s/coll-of ::role :into #{}) set?))
(defmulti my-multi (constantly :default))
(defmethod my-multi :default [x] x)
(deftest route-data-validation-test (deftest route-data-validation-test
(testing "validation is turned off by default" (testing "validation is turned off by default"
(is (r/router? (is (r/router?
@ -88,12 +85,6 @@
(ring/router (ring/router
["/api" {:handler identity ["/api" {:handler identity
:middleware '()}] :middleware '()}]
{:validate rrs/validate}))))
(testing "handler can be a multimethod"
(is (r/router?
(ring/router
["/api" {:get {:handler my-multi}}]
{:validate rrs/validate}))))) {:validate rrs/validate})))))
(deftest coercion-spec-test (deftest coercion-spec-test

View file

@ -114,25 +114,6 @@
(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"
@ -762,7 +743,7 @@
(is (= (redirect "/docs/index.html") response))) (is (= (redirect "/docs/index.html") response)))
(let [response (app (request "/foobar"))] (let [response (app (request "/foobar"))]
(is (= 404 (:status response))))))) (is (= 404 (:status response)))))))
(testing "with additional mime types" (testing "with additional mime types"
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router