Merge remote-tracking branch 'origin/master' into fix/top-level-mw-registry

This commit is contained in:
Joel Kaasinen 2025-10-13 08:50:38 +03:00
commit 4e572e86d6
63 changed files with 304 additions and 187 deletions

View file

@ -12,12 +12,25 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## UNRELEASED
## 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)
## 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)
## 0.9.0 (2025-05-23)
* Improvements to mime type handling in `create-file-handler` and `create-resource-handler` [#733](https://github.com/metosin/reitit/pull/733)
* New `:mime-types` option to configure a map from file extension to mime type
* Don't set Content-Type header at all if mime type is not known
* Fix location of openapi deprecated metadata [#714](https://github.com/metosin/reitit/pull/714)
* Fix location of OpenAPI deprecated metadata [#714](https://github.com/metosin/reitit/pull/714)
* **BREAKING** Fix & clarify `:responses :default` and `:content :default` handling. See [docs](./doc/ring/coercion.md). [#735](https://github.com/metosin/reitit/pull/735)
* Summary: If `:responses <status>` is present, `:responses :default` is not used, even if `:responses <status>` defines no schema.
* Should not break normal use, but might cause surprises related to defaults applying/not applying
* **NOTE** This release depends on malli 0.18.0, which changes the format of OpenAPI & Swagger named schemas from `foo.bar/quux` to `foo.bar.quux`
## 0.8.0 (2025-03-28)

View file

@ -1,4 +1,4 @@
# reitit
# reitit
[![Build Status](https://github.com/metosin/reitit/actions/workflows/testsuite.yml/badge.svg)](https://github.com/metosin/reitit/actions)
[![cljdoc badge](https://cljdoc.org/badge/metosin/reitit)](https://cljdoc.org/d/metosin/reitit/)
@ -54,7 +54,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
* `metosin/reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari)
* `metosin/reitit-dev` - development utilities
... * This is not a typo; the new `reitit-openapi` was released under the new, verified `fi.metosin` group. Existing
... * This is not a typo; the new `reitit-openapi` was released under the new, verified `fi.metosin` group. Existing
modules will continue to be released under `metosin` for compatibility purposes.
## Extra modules
@ -66,7 +66,7 @@ modules will continue to be released under `metosin` for compatibility purposes.
All main modules bundled:
```clj
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
```
Optionally, the parts can be required separately.
@ -109,6 +109,7 @@ A Ring routing app with input & output coercion using [data-specs](https://githu
(require '[reitit.ring :as ring])
(require '[reitit.coercion.spec])
(require '[reitit.ring.coercion :as rrc])
(require '[reitit.ring.middleware.exception :as exception])
(require '[reitit.ring.middleware.muuntaja :as muuntaja])
(require '[reitit.ring.middleware.parameters :as parameters])
@ -124,39 +125,45 @@ A Ring routing app with input & output coercion using [data-specs](https://githu
;; router data affecting all routes
{:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware [parameters/parameters-middleware
:middleware [parameters/parameters-middleware ; decoding query & form params
muuntaja/format-middleware ; content negotiation
exception/exception-middleware ; converting exceptions to HTTP responses
rrc/coerce-request-middleware
muuntaja/format-response-middleware
rrc/coerce-response-middleware]}})))
```
Valid request:
```clj
(app {:request-method :get
:uri "/api/math"
:query-params {:x "1", :y "2"}})
(-> (app {:request-method :get
:uri "/api/math"
:query-params {:x "1", :y "2"}})
(update :body slurp))
; {:status 200
; :body {:total 3}}
; :body "{\"total\":3}"
; :headers {"Content-Type" "application/json; charset=utf-8"}}
```
Invalid request:
```clj
(app {:request-method :get
:uri "/api/math"
:query-params {:x "1", :y "a"}})
;{:status 400,
; :body {:type :reitit.coercion/request-coercion,
; :coercion :spec,
; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec20745/x :$spec20745/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})",
; :problems [{:path [:y],
; :pred "clojure.core/int?",
; :val "a",
; :via [:$spec20745/y],
; :in [:y]}],
; :value {:x "1", :y "a"},
; :in [:request :query-params]}}
(-> (app {:request-method :get
:uri "/api/math"
:query-params {:x "1", :y "a"}})
(update :body jsonista.core/read-value))
; {:status 400
; :headers {"Content-Type" "application/json; charset=utf-8"}
; :body {"spec" "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:spec$8974/x :spec$8974/y]), :type :map, :leaf? false})"
; "value" {"x" "1"
; "y" "a"}
; "problems" [{"via" ["spec$8974/y"]
; "path" ["y"]
; "pred" "clojure.core/int?"
; "in" ["y"]
; "val" "a"}]
; "type" "reitit.coercion/request-coercion"
; "coercion" "spec"
; "in" ["request" "query-params"]}}
```
## More examples

View file

@ -41,7 +41,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
All bundled:
```clj
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
```
Optionally, the parts can be required separately.

View file

@ -22,7 +22,7 @@ The default exception formatting uses `reitit.exception/exception`. It produces
## Pretty Errors
```clj
[metosin/reitit-dev "0.8.0"]
[metosin/reitit-dev "0.9.1"]
```
For human-readable and developer-friendly exception messages, there is `reitit.dev.pretty/exception` (in the `reitit-dev` module). It is inspired by the lovely errors messages of [ELM](https://elm-lang.org/blog/compiler-errors-for-humans) and [ETA](https://twitter.com/jyothsnasrin/status/1037703436043603968) and uses [fipp](https://github.com/brandonbloom/fipp), [expound](https://github.com/bhb/expound) and [spell-spec](https://github.com/bhauman/spell-spec) for most of heavy lifting.

View file

@ -32,10 +32,6 @@ We use [Break Versioning][breakver]. Remember our promise: patch-level bumps nev
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
```bash
# Check that you're using Java 8! Making the release with a newer Java version
# means that it is broken when used with Java 8.
java -version
# new version
./scripts/set-version "1.0.0"

View file

@ -1,7 +1,7 @@
# Default Interceptors
```clj
[metosin/reitit-interceptors "0.8.0"]
[metosin/reitit-interceptors "0.9.1"]
```
Just like the [ring default middleware](../ring/default_middleware.md), but for interceptors.

View file

@ -5,7 +5,7 @@ Reitit has also support for [interceptors](http://pedestal.io/reference/intercep
## Reitit-http
```clj
[metosin/reitit-http "0.8.0"]
[metosin/reitit-http "0.9.1"]
```
A module for http-routing using interceptors instead of middleware. Builds on top of the [`reitit-ring`](../ring/ring.md) module having all the same features.

View file

@ -3,7 +3,7 @@
[Pedestal](http://pedestal.io/) is a backend web framework for Clojure. `reitit-pedestal` provides an alternative routing engine for Pedestal.
```clj
[metosin/reitit-pedestal "0.8.0"]
[metosin/reitit-pedestal "0.9.1"]
```
Why should one use reitit instead of the Pedestal [default routing](http://pedestal.io/reference/routing-quick-reference)?
@ -26,8 +26,8 @@ A minimalistic example on how to to swap the default-router with a reitit router
```clj
; [io.pedestal/pedestal.service "0.5.5"]
; [io.pedestal/pedestal.jetty "0.5.5"]
; [metosin/reitit-pedestal "0.8.0"]
; [metosin/reitit "0.8.0"]
; [metosin/reitit-pedestal "0.9.1"]
; [metosin/reitit "0.9.1"]
(require '[io.pedestal.http :as server])
(require '[reitit.pedestal :as pedestal])

View file

@ -1,7 +1,7 @@
# Sieppari
```clj
[metosin/reitit-sieppari "0.8.0"]
[metosin/reitit-sieppari "0.9.1"]
```
[Sieppari](https://github.com/metosin/sieppari) is a new and fast interceptor implementation for Clojure, with pluggable async supporting [core.async](https://github.com/clojure/core.async), [Manifold](https://github.com/ztellman/manifold) and [Promesa](http://funcool.github.io/promesa/latest).

View file

@ -65,7 +65,7 @@ There is an extra option in http-router (actually, in the underlying interceptor
### Printing Context Diffs
```clj
[metosin/reitit-interceptors "0.8.0"]
[metosin/reitit-interceptors "0.9.1"]
```
Using `reitit.http.interceptors.dev/print-context-diffs` transformation, the context diffs between each interceptor are printed out to the console. To use it, add the following router option:

View file

@ -2,6 +2,8 @@
Ring [defines middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) as a function of type `handler & args => request => response`. It is relatively easy to understand and allows for good performance. A downside is that the middleware chain is just a opaque function, making things like debugging and composition hard. It is too easy to apply the middlewares in wrong order.
For the basics of reitit middleware, [read this first](ring.md#middleware).
Reitit defines middleware as data:
1. A middleware can be defined as first-class data entries

View file

@ -1,7 +1,7 @@
# Default Middleware
```clj
[metosin/reitit-middleware "0.8.0"]
[metosin/reitit-middleware "0.9.1"]
```
Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware.
@ -17,8 +17,6 @@ Any Ring middleware can be used with `reitit-ring`, but using data-driven middle
`reitit.ring.middleware.parameters/parameters-middleware` to capture query- and form-params. Wraps
`ring.middleware.params/wrap-params`.
**NOTE**: This middleware will be factored into two parts: a query-parameters middleware and a Muuntaja format responsible for the the `application/x-www-form-urlencoded` body format. cf. https://github.com/metosin/reitit/issues/134
## Exception Handling
See [Exception Handling with Ring](exceptions.md).

View file

@ -1,7 +1,7 @@
# Exception Handling with Ring
```clj
[metosin/reitit-middleware "0.8.0"]
[metosin/reitit-middleware "0.9.1"]
```
Exceptions thrown in router creation can be [handled with custom exception handler](../basics/error_messages.md). By default, exceptions thrown at runtime from a handler or a middleware are not caught by the `reitit.ring/ring-handler`. A good practice is to have a top-level exception handler to log and format errors for clients.

View file

@ -5,7 +5,7 @@
Read more about the [Ring Concepts](https://github.com/ring-clojure/ring/wiki/Concepts).
```clj
[metosin/reitit-ring "0.8.0"]
[metosin/reitit-ring "0.9.1"]
```
## `reitit.ring/router`
@ -141,7 +141,7 @@ Name-based reverse routing:
# Middleware
Middleware can be mounted using a `:middleware` key - either to top-level or under request method submap. Its value should be a vector of `reitit.middleware/IntoMiddleware` values. These include:
Middleware can be mounted using a `:middleware` key in [Route Data](../basics/route_data.md) - either to top-level or under request method submap. Its value should be a vector of `reitit.middleware/IntoMiddleware` values. These include:
1. normal ring middleware function `handler -> request -> response`
2. vector of middleware function `[handler args*] -> request -> response` and it's arguments
@ -194,11 +194,56 @@ Top-level middleware, applied before any routing is done:
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [[mw :api]]}
["/api" {:middleware [[wrap :api]]}
["/get" {:get handler}]])
nil
{:middleware [[mw :top]]}))
{:middleware [[wrap :top]]}))
(app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:top :api :ok]}
```
Same middleware for all routes, using [top-level route data](route_data.md#top-level-route-data):
```clj
(def app
(ring/ring-handler
(ring/router
["/api"
["/get" {:get handler
:middleware [[wrap :specific]]}]]
{:data {:middleware [[wrap :generic]]}})))
(app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:generic :specific :handler]}
```
## Execution order
Here's a full example that shows the execution order of the middleware
using all of the above techniques:
```clj
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [[wrap :3-parent]]}
["/get" {:get handler
:middleware [[wrap :4-route]]}]]
{:data {:middleware [[wrap :2-top-level-route-data]]}})
nil
{:middleware [[wrap :1-top]]}))
(app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:1-top :2-top-level-route-data :3-parent :4-route :handler]}
```
## Which method should I use for defining middleware?
- If you have middleware that you want to apply to the default handler (second argument of `ring/ring-handler`), use _top-level middleware_
- If you have a generic middleware, that doesn't depend on the route, use _top-level middleware_ or _top-level route data_
- If you are using top-level route data anyway for some other reasons, it might be clearest to have all the middleware there. This is what most of the reitit examples do.
- If you want to apply a middleware to only a couple of routes, use _nested middleware_ (ie. _route data_)
- If you want a middleware to apply to all routes, but use route-specific data, you need _top-level route data_ combined with [Compiling Middleware](compiling_middleware.md)
- This is what many reitit features like [Ring Coercion](coercion.md) do. Check the examples & docs for the reitit features you want to use!

View file

@ -1,7 +1,7 @@
# Swagger Support
```
[metosin/reitit-swagger "0.8.0"]
[metosin/reitit-swagger "0.9.1"]
```
Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys.
@ -47,7 +47,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
```
[metosin/reitit-swagger-ui "0.8.0"]
[metosin/reitit-swagger-ui "0.9.1"]
```
`reitit.swagger-ui/create-swagger-ui-handler` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options:

View file

@ -59,7 +59,7 @@ There is an extra option in the Ring router (actually, in the underlying middlew
### Printing Request Diffs
```clj
[metosin/reitit-middleware "0.8.0"]
[metosin/reitit-middleware "0.9.1"]
```
Using `reitit.ring.middleware.dev/print-request-diffs` transformation, the request diffs between each middleware are printed out to the console. To use it, add the following router option:

View file

@ -2,6 +2,6 @@
:description "Reitit Buddy Auth App"
:dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
[buddy "2.0.0"]]
:repl-options {:init-ns example.server})

View file

@ -10,9 +10,9 @@
[ring "1.12.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.8.0"]
[metosin/reitit-schema "0.8.0"]
[metosin/reitit-frontend "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/reitit-schema "0.9.1"]
[metosin/reitit-frontend "0.9.1"]
[cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.8.0"]
[metosin/reitit-schema "0.8.0"]
[metosin/reitit-frontend "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/reitit-schema "0.9.1"]
[metosin/reitit-frontend "0.9.1"]
[cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.10.520"]
[metosin/reitit "0.8.0"]
[metosin/reitit-spec "0.8.0"]
[metosin/reitit-frontend "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/reitit-spec "0.9.1"]
[metosin/reitit-frontend "0.9.1"]
[cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.8.0"]
[metosin/reitit-malli "0.8.0"]
[metosin/reitit-frontend "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/reitit-malli "0.9.1"]
[metosin/reitit-frontend "0.9.1"]
[cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match

View file

@ -10,9 +10,9 @@
[ring "1.12.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.8.0"]
[metosin/reitit-spec "0.8.0"]
[metosin/reitit-frontend "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/reitit-spec "0.9.1"]
[metosin/reitit-frontend "0.9.1"]
[cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match

View file

@ -1,7 +1,7 @@
(defproject frontend-re-frame "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.11.2"]
[org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
[reagent "1.2.0"]
[re-frame "0.10.6"]
[cljsjs/react "17.0.2-0"]

View file

@ -10,9 +10,9 @@
[ring "1.12.1"]
[hiccup "1.0.5"]
[org.clojure/clojurescript "1.11.132"]
[metosin/reitit "0.8.0"]
[metosin/reitit-spec "0.8.0"]
[metosin/reitit-frontend "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/reitit-spec "0.9.1"]
[metosin/reitit-frontend "0.9.1"]
[cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"]
;; Just for pretty printting the match

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"]
[aleph "0.7.1"]
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server})

View file

@ -5,5 +5,5 @@
[funcool/promesa "11.0.678"]
[manifold "0.4.2"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]]
[metosin/reitit "0.9.1"]]
:repl-options {:init-ns example.server})

View file

@ -2,4 +2,4 @@
:description "Reitit coercion with vanilla ring"
:dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]])
[metosin/reitit "0.9.1"]])

View file

@ -3,7 +3,7 @@
:dependencies [[org.clojure/clojure "1.11.2"]
[metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -137,6 +137,32 @@
{:from "0003"
:amount -6.5}]}})}}]
["/complex"
{:post {:summary "Complex schema with :multi, :enum, :tuple etc."
:request {:content
{:default
{:schema [:map
[:vector-of-tuples [:vector [:tuple :string :int]]]
[:regex [:re "[0-9]+"]]
[:enum [:enum 1 3 5 42]]
[:multi [:multi {:dispatch :type}
[:literal [:map
[:type [:= :literal]]
[:value [:or :int :string]]]]
[:reference [:map
[:type [:= :reference]]
[:description :string]
[:ref :uuid]]]]]]
:example {:vector-of-tuples [["a" 1] ["b" 2]]
:regex "01234"
:enum 5
:multi {:type :literal
:value "x"}}}}}
:responses {200 {:content {:default {:schema [:map]}}}}
:handler (fn [request]
{:status 200
:body (:body request)})}}]
["/secure"
{:tags #{"secure"}
:openapi {:security [{"auth" []}]}}

View file

@ -3,7 +3,7 @@
:dependencies [[org.clojure/clojure "1.11.2"]
[io.pedestal/pedestal.service "0.6.3"]
[io.pedestal/pedestal.jetty "0.6.3"]
[metosin/reitit-malli "0.8.0"]
[metosin/reitit-pedestal "0.8.0"]
[metosin/reitit "0.8.0"]]
[metosin/reitit-malli "0.9.1"]
[metosin/reitit-pedestal "0.9.1"]
[metosin/reitit "0.9.1"]]
:repl-options {:init-ns server})

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"]
[io.pedestal/pedestal.service "0.6.3"]
[io.pedestal/pedestal.jetty "0.6.3"]
[metosin/reitit-pedestal "0.8.0"]
[metosin/reitit "0.8.0"]]
[metosin/reitit-pedestal "0.9.1"]
[metosin/reitit "0.9.1"]]
:repl-options {:init-ns example.server})

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"]
[io.pedestal/pedestal.service "0.6.3"]
[io.pedestal/pedestal.jetty "0.6.3"]
[metosin/reitit-pedestal "0.8.0"]
[metosin/reitit "0.8.0"]]
[metosin/reitit-pedestal "0.9.1"]
[metosin/reitit "0.9.1"]]
:repl-options {:init-ns example.server})

View file

@ -2,5 +2,5 @@
:description "Reitit Ring App"
:dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]]
[metosin/reitit "0.9.1"]]
:repl-options {:init-ns example.server})

View file

@ -2,7 +2,7 @@
:description "Reitit Ring App with Integrant"
:dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
[integrant "0.8.1"]]
:main example.server
:repl-options {:init-ns user}

View file

@ -3,6 +3,6 @@
:dependencies [[org.clojure/clojure "1.11.2"]
[metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]]
[metosin/reitit "0.9.1"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -3,7 +3,7 @@
:dependencies [[org.clojure/clojure "1.11.2"]
[metosin/jsonista "0.3.8"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -2,7 +2,7 @@
:description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.11.2"]
[ring/ring-jetty-adapter "1.12.1"]
[metosin/reitit "0.8.0"]
[metosin/reitit "0.9.1"]
[metosin/ring-swagger-ui "5.9.0"]]
:repl-options {:init-ns example.server}
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}})

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-core "0.8.0"
(defproject metosin/reitit-core "0.9.1"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -178,8 +178,9 @@
(let [format->coercer (or (status->format->coercer (:status response))
(status->format->coercer :default))
format (extract-response-format request response)
coercer (or (format->coercer format)
(format->coercer :default))]
coercer (when format->coercer
(or (format->coercer format)
(format->coercer :default)))]
(if-not coercer
response
(let [value (:body response)

View file

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

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-dev "0.8.0"
(defproject metosin/reitit-dev "0.9.1"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-frontend "0.8.0"
(defproject metosin/reitit-frontend "0.9.1"
:description "Reitit: Clojurescript frontend routing core"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-http "0.8.0"
(defproject metosin/reitit-http "0.9.1"
:description "Reitit: HTTP routing with interceptors"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-interceptors "0.8.0"
(defproject metosin/reitit-interceptors "0.9.1"
:description "Reitit, common interceptors bundled"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-malli "0.8.0"
(defproject metosin/reitit-malli "0.9.1"
:description "Reitit: Malli coercion"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-middleware "0.8.0"
(defproject metosin/reitit-middleware "0.9.1"
:description "Reitit, common middleware bundled"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject fi.metosin/reitit-openapi "0.8.0"
(defproject fi.metosin/reitit-openapi "0.9.1"
:description "Reitit: OpenAPI-support"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

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

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-pedestal "0.8.0"
(defproject metosin/reitit-pedestal "0.9.1"
:description "Reitit + Pedestal Integration"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-ring "0.8.0"
(defproject metosin/reitit-ring "0.9.1"
:description "Reitit: Ring routing"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-schema "0.8.0"
(defproject metosin/reitit-schema "0.9.1"
:description "Reitit: Plumatic Schema coercion"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-sieppari "0.8.0"
(defproject metosin/reitit-sieppari "0.9.1"
:description "Reitit: Sieppari Interceptors"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-spec "0.8.0"
(defproject metosin/reitit-spec "0.9.1"
:description "Reitit: clojure.spec coercion"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-swagger-ui "0.8.0"
(defproject metosin/reitit-swagger-ui "0.9.1"
:description "Reitit: Swagger-ui support"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-swagger "0.8.0"
(defproject metosin/reitit-swagger "0.9.1"
:description "Reitit: Swagger-support"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit "0.8.0"
(defproject metosin/reitit "0.9.1"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"

9
package-lock.json generated
View file

@ -9,7 +9,7 @@
"shadow-cljs": "^2.28.22"
},
"devDependencies": {
"@seriousme/openapi-schema-validator": "^2.3.1",
"@seriousme/openapi-schema-validator": "^2.4.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-cli": "^2.0.0",
@ -27,11 +27,10 @@
}
},
"node_modules/@seriousme/openapi-schema-validator": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@seriousme/openapi-schema-validator/-/openapi-schema-validator-2.3.1.tgz",
"integrity": "sha512-szUXBZJUhq+Yw+vUro2QeltSIoZvMDQi3MLqJhIKcRcRYyFt9B6dyjMD1RVf3nFvNAHkWqa48NJA46ti2P8smA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@seriousme/openapi-schema-validator/-/openapi-schema-validator-2.4.0.tgz",
"integrity": "sha512-2PWq2QbDMu+CANpBLZ2Uch9PgTIiftLpiLH4lcaykjV463f4Vt9eD61EeaVI++D0HII4JKnptX46391pII1XZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.17.1",
"ajv-draft-04": "^1.0.0",

View file

@ -2,7 +2,7 @@
"name": "reitit",
"private": true,
"devDependencies": {
"@seriousme/openapi-schema-validator": "^2.3.1",
"@seriousme/openapi-schema-validator": "^2.4.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-cli": "^2.0.0",

View file

@ -1,4 +1,4 @@
(defproject metosin/reitit-parent "0.8.0"
(defproject metosin/reitit-parent "0.9.1"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
@ -18,29 +18,29 @@
;; TODO: need to verify that the code actually worked with Java1.8, see #242
;; Ring 1.13.1 drops support for Java 1.8 so lets target 11
:javac-options ["-Xlint:unchecked" "-target" "11" "-source" "11"]
:managed-dependencies [[metosin/reitit "0.8.0"]
[metosin/reitit-core "0.8.0"]
[metosin/reitit-dev "0.8.0"]
[metosin/reitit-spec "0.8.0"]
[metosin/reitit-malli "0.8.0"]
[metosin/reitit-schema "0.8.0"]
[metosin/reitit-ring "0.8.0"]
[metosin/reitit-middleware "0.8.0"]
[metosin/reitit-http "0.8.0"]
[metosin/reitit-interceptors "0.8.0"]
[metosin/reitit-swagger "0.8.0"]
[fi.metosin/reitit-openapi "0.8.0"]
[metosin/reitit-swagger-ui "0.8.0"]
[metosin/reitit-frontend "0.8.0"]
[metosin/reitit-sieppari "0.8.0"]
[metosin/reitit-pedestal "0.8.0"]
:managed-dependencies [[metosin/reitit "0.9.1"]
[metosin/reitit-core "0.9.1"]
[metosin/reitit-dev "0.9.1"]
[metosin/reitit-spec "0.9.1"]
[metosin/reitit-malli "0.9.1"]
[metosin/reitit-schema "0.9.1"]
[metosin/reitit-ring "0.9.1"]
[metosin/reitit-middleware "0.9.1"]
[metosin/reitit-http "0.9.1"]
[metosin/reitit-interceptors "0.9.1"]
[metosin/reitit-swagger "0.9.1"]
[fi.metosin/reitit-openapi "0.9.1"]
[metosin/reitit-swagger-ui "0.9.1"]
[metosin/reitit-frontend "0.9.1"]
[metosin/reitit-sieppari "0.9.1"]
[metosin/reitit-pedestal "0.9.1"]
[metosin/ring-swagger-ui "5.20.0"]
[metosin/spec-tools "0.10.7"]
[metosin/schema-tools "0.13.1"]
[metosin/muuntaja "0.6.11"]
[metosin/jsonista "0.3.13"]
[metosin/sieppari "0.0.0-alpha13"]
[metosin/malli "0.17.0"]
[metosin/malli "0.18.0"]
;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111
[com.fasterxml.jackson.core/jackson-core "2.18.2"]
@ -99,7 +99,7 @@
[metosin/muuntaja "0.6.11"]
[metosin/sieppari "0.0.0-alpha13"]
[metosin/jsonista "0.3.13"]
[metosin/malli "0.17.0"]
[metosin/malli "0.18.0"]
[lambdaisland/deep-diff "0.0-47"]
[meta-merge "1.0.0"]
[com.bhauman/spell-spec "0.1.2"]

View file

@ -1009,7 +1009,7 @@
{:content
{"application/json"
{:schema
{:$ref "#/components/schemas/reitit.openapi-test~1Plus"}}}}}}
{:$ref "#/components/schemas/reitit.openapi-test.Plus"}}}}}}
"/get"
{:get
{:parameters
@ -1019,19 +1019,15 @@
{:in "query"
:name :y
:required true
:schema {:$ref "#/components/schemas/reitit.openapi-test~1Y"}}]}}}
:schema {:$ref "#/components/schemas/reitit.openapi-test.Y"}}]}}}
:components
{:schemas
{"reitit.openapi-test/Plus"
{"reitit.openapi-test.Plus"
{:type "object"
:properties
{:x {:type "integer"}
:y {:$ref "#/components/schemas/reitit.openapi-test~1Y"}}
:y {:$ref "#/components/schemas/reitit.openapi-test.Y"}}
:required [:x :y]}
"reitit.openapi-test/Y" {:type "integer"}}}}
"reitit.openapi-test.Y" {:type "integer"}}}}
spec))
;; TODO: the OAS 3.1 json schema disallows "/" in :components :schemas keys,
;; even though the text of the spec allows it. See:
;; https://github.com/seriousme/openapi-schema-validator/blob/772375bf4895f0e641d103c27140cdd1d2afc34e/schemas/v3.1/schema.json#L282
#_
(is (nil? (validate spec))))))

View file

@ -695,15 +695,22 @@
(testing (str coercion)
(let [app (ring/ring-handler
(ring/router
["/foo" {:post {:responses {200 {:content {:default {:schema schema-200}}}
201 {:content {"application/edn" {:schema schema-200}}}
202 {:description "status code and content-type explicitly mentioned, but no :schema"
:content {"application/edn" {}
"application/json" {}}}
:default {:content {"application/json" {:schema schema-default}}}}
:handler (fn [req]
{:status (-> req :body-params :status)
:body (-> req :body-params :response)})}}]
[["/foo" {:post {:responses {200 {:content {:default {:schema schema-200}}}
201 {:content {"application/edn" {:schema schema-200}}}
202 {:description "status code and content-type explicitly mentioned, but no :schema"
:content {"application/edn" {}
"application/json" {}}}
:default {:content {"application/json" {:schema schema-default}}}}
:handler (fn [req]
{:status (-> req :body-params :status)
:body (-> req :body-params :response)})}}]
["/bar" {:post {:responses {200 {:content {:default {:schema schema-200}}}}
:handler (fn [req]
{:status (-> req :body-params :status)
:body (-> req :body-params :response)})}}]
["/quux" {:post {:handler (fn [req]
{:status (-> req :body-params :status)
:body (-> req :body-params :response)})}}]]
{:validate reitit.ring.spec/validate
:data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware]
@ -713,40 +720,52 @@
(app request)
(catch ExceptionInfo e
(select-keys (ex-data e) [:type :in]))))
request (fn [body]
request (fn [uri body]
{:request-method :post
:uri "/foo"
:uri uri
:muuntaja/request {:format "application/json"}
:muuntaja/response {:format (:format body "application/json")}
:body-params body})]
(testing "explicit response schema"
(is (= {:status 200 :body {:a 1}}
(call (request {:status 200 :response {:a 1}})))
(call (request "/foo" {:status 200 :response {:a 1}})))
"valid response")
(is (= {:type :reitit.coercion/response-coercion, :in [:response :body]}
(call (request {:status 200 :response {:b 1}})))
(call (request "/foo" {:status 200 :response {:b 1}})))
"invalid response")
(is (= {:type :reitit.coercion/response-coercion, :in [:response :body]}
(call (request {:status 200 :response {:b 1} :format "application/edn"})))
(call (request "/foo" {:status 200 :response {:b 1} :format "application/edn"})))
"invalid response, different content-type"))
(testing "explicit response schema, but for the wrong content-type"
(is (= {:status 201 :body "anything goes!"}
(call (request {:status 201 :response "anything goes!"})))
(call (request "/foo" {:status 201 :response "anything goes!"})))
"no coercion applied"))
(testing "response config without :schema"
(is (= {:status 202 :body "anything goes!"}
(call (request {:status 202 :response "anything goes!"})))
(call (request "/foo" {:status 202 :response "anything goes!"})))
"no coercion applied"))
(testing "default response schema"
(is (= {:status 300 :body {:b 2}}
(call (request {:status 300 :response {:b 2}})))
(call (request "/foo" {:status 300 :response {:b 2}})))
"valid response")
(is (= {:type :reitit.coercion/response-coercion, :in [:response :body]}
(call (request {:status 300 :response {:a 2}})))
(call (request "/foo" {:status 300 :response {:a 2}})))
"invalid response")
(is (= {:status 300 :body "anything goes!"}
(call (request {:status 300 :response "anything goes!" :format "application/edn"})))
"no coercion applied due to content-type")))))))
(call (request "/foo" {:status 300 :response "anything goes!" :format "application/edn"})))
"no coercion applied due to content-type"))
(testing "no default"
(is (= {:status 200 :body {:a 1}}
(call (request "/bar" {:status 200 :response {:a 1}})))
"valid response")
(testing "unlisted response code"
(is (= {:status 202 :body "anything goes!"}
(call (request "/bar" {:status 202 :response "anything goes!"})))
"no coercion applied")))
(testing "no response coercion"
(is (= {:status 200 :body "anything goes!"}
(call (request "/quux" {:status 200 :response "anything goes!"})))
"no coercion applied")))))))
#?(:clj
(deftest muuntaja-test

View file

@ -12,6 +12,9 @@
(s/def ::role #{:admin :user})
(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
(testing "validation is turned off by default"
(is (r/router?
@ -85,6 +88,12 @@
(ring/router
["/api" {:handler identity
:middleware '()}]
{:validate rrs/validate}))))
(testing "handler can be a multimethod"
(is (r/router?
(ring/router
["/api" {:get {:handler my-multi}}]
{:validate rrs/validate})))))
(deftest coercion-spec-test

View file

@ -161,14 +161,14 @@
expected {:x-id #{::math}
:swagger "2.0"
:info {:title "my-api"}
:definitions {"reitit.swagger-test/req-key" {:type "string"
:definitions {"reitit.swagger-test.req-key" {:type "string"
:x-anyOf [{:type "string"}
{:type "string"}]}
"reitit.swagger-test/req-val" {:type "object"
"reitit.swagger-test.req-val" {:type "object"
:x-anyOf [{:type "object"}
{:type "string"}]}
"reitit.swagger-test/resp-map" {:type "object"},
"reitit.swagger-test/resp-string" {:type "string"
"reitit.swagger-test.resp-map" {:type "object"},
"reitit.swagger-test.resp-string" {:type "string"
:minLength 1}}
:paths {"/api/spec/plus/{z}" {:patch {:parameters []
:summary "patch"
@ -287,12 +287,12 @@
:schema
{:type "object"
:additionalProperties
{:$ref "#/definitions/reitit.swagger-test~1req-val"}}}]
{:$ref "#/definitions/reitit.swagger-test.req-val"}}}]
:responses {200
{:schema
{:$ref "#/definitions/reitit.swagger-test~1resp-map"
:x-anyOf [{:$ref "#/definitions/reitit.swagger-test~1resp-map"}
{:$ref "#/definitions/reitit.swagger-test~1resp-string"}]}
{:$ref "#/definitions/reitit.swagger-test.resp-map"
:x-anyOf [{:$ref "#/definitions/reitit.swagger-test.resp-map"}
{:$ref "#/definitions/reitit.swagger-test.resp-string"}]}
:description ""}
500 {:description "fail"}}
:summary "plus put with definitions"}}
@ -532,24 +532,24 @@
{:get {:no-doc true
:handler (swagger/create-swagger-handler)}}]]))
spec (:body (app {:request-method :get, :uri "/swagger.json"}))]
(is (= {:definitions {"reitit.swagger-test/Plus" {:properties {:x {:$ref "#/definitions/reitit.swagger-test~1X"},
:y {:$ref "#/definitions/reitit.swagger-test~1Y"}},
(is (= {:definitions {"reitit.swagger-test.Plus" {:properties {:x {:$ref "#/definitions/reitit.swagger-test.X"},
:y {:$ref "#/definitions/reitit.swagger-test.Y"}},
:required [:x :y],
:type "object"},
"reitit.swagger-test/X" {:format "int64",
"reitit.swagger-test.X" {:format "int64",
:type "integer"},
"reitit.swagger-test/Y" {:format "int64",
"reitit.swagger-test.Y" {:format "int64",
:type "integer"},
"reitit.swagger-test/Result" {:type "object",
"reitit.swagger-test.Result" {:type "object",
:properties {:result {:type "integer", :format "int64"}},
:required [:result]}},
:paths {"/post" {:post {:parameters [{:description "",
:in "body",
:name "body",
:required true,
:schema {:$ref "#/definitions/reitit.swagger-test~1Plus"}}]
:schema {:$ref "#/definitions/reitit.swagger-test.Plus"}}]
:responses {200 {:description ""
:schema {:$ref "#/definitions/reitit.swagger-test~1Result"}}}}}
:schema {:$ref "#/definitions/reitit.swagger-test.Result"}}}}}
"/get" {:get {:parameters [{:in "query"
:name :x
:description ""
@ -563,7 +563,7 @@
:required true
:format "int64"}]
:responses {200 {:description ""
:schema {:$ref "#/definitions/reitit.swagger-test~1Result"}}}}}}
:schema {:$ref "#/definitions/reitit.swagger-test.Result"}}}}}}
:swagger "2.0",
:x-id #{:reitit.swagger/default}}
spec))))