mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
305 lines
12 KiB
Markdown
305 lines
12 KiB
Markdown
# Swagger Support
|
|
|
|
```
|
|
[metosin/reitit-swagger "0.9.2-rc1"]
|
|
```
|
|
|
|
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.
|
|
|
|
See also: [OpenAPI support](openapi.md).
|
|
|
|
To enable swagger-documentation for a Ring router:
|
|
|
|
1. annotate your routes with swagger-data
|
|
2. mount a swagger-handler to serve the swagger-spec
|
|
3. optionally mount a swagger-ui to visualize the swagger-spec
|
|
|
|
## Swagger data
|
|
|
|
The following route data keys contribute to the generated swagger specification:
|
|
|
|
| key | description |
|
|
| --------------|-------------|
|
|
| :swagger | map of any swagger-data. Can have `:id` (keyword or sequence of keywords) to identify the api
|
|
| :no-doc | optional boolean to exclude endpoint from api docs
|
|
| :tags | optional set of string or keyword tags for an endpoint api docs
|
|
| :summary | optional short string summary of an endpoint
|
|
| :description | optional long description of an endpoint. Supports http://spec.commonmark.org/
|
|
| :operationId | optional string specifying the unique ID of an Operation
|
|
|
|
Coercion keys also contribute to the docs:
|
|
|
|
| key | description |
|
|
| --------------|-------------|
|
|
| :parameters | optional input parameters for a route, in a format defined by the coercion
|
|
| :responses | optional descriptions of responses, in a format defined by coercion
|
|
|
|
There is a `reitit.swagger.swagger-feature`, which acts as both a `Middleware` and an `Interceptor` that is not participating in any request processing - it just defines the route data specs for the routes it's mounted to. It is only needed if the [route data validation](route_data_validation.md) is turned on.
|
|
|
|
## Swagger spec
|
|
|
|
To serve the actual [Swagger Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md), there is `reitit.swagger/create-swagger-handler`. It takes no arguments and returns a ring-handler which collects at request-time data from all routes for the same swagger api and returns a formatted Swagger specification as Clojure data, to be encoded by a response formatter.
|
|
|
|
If you need to post-process the generated spec, just wrap the handler with a custom `Middleware` or an `Interceptor`.
|
|
|
|
## Swagger-ui
|
|
|
|
[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.9.2-rc1"]
|
|
```
|
|
|
|
`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:
|
|
|
|
| key | description |
|
|
| -----------------|-------------|
|
|
| :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:`
|
|
| :root | optional resource root, defaults to `"swagger-ui"`
|
|
| :url | path to swagger endpoint, defaults to `/swagger.json`
|
|
| :path | optional path to mount the handler to. Works only if mounted outside of a router.
|
|
| :config | parameters passed to swagger-ui as-is. See [the docs](https://github.com/swagger-api/swagger-ui/tree/2.x#parameters)
|
|
|
|
We use swagger-ui from [ring-swagger-ui](https://github.com/metosin/ring-swagger-ui), which can be easily configured from routing application. It stores files `swagger-ui` in the resource classpath.
|
|
|
|
Webjars also hosts a [version](https://github.com/webjars/swagger-ui) of the swagger-ui.
|
|
|
|
**NOTE**: Currently, swagger-ui module is just for Clojure. ClojureScript-support welcome as a PR!
|
|
|
|
**NOTE:** If you want to use swagger-ui 2.x you can do so by explicitly downgrading `metosin/ring-swagger-ui` to `2.2.10`.
|
|
|
|
**NOTE:** If you use swagger-ui 3.x, you need to include `:responses` for Swagger-UI
|
|
to display the response when trying out endpoints. You can define `:responses {200 {:schema s/Any}}`
|
|
at the top-level to show responses for all endpoints.
|
|
|
|
## Examples
|
|
|
|
### Simple example
|
|
|
|
* two routes
|
|
* swagger-spec served from `"/swagger.json"`
|
|
* swagger-ui mounted to `"/api-docs"`
|
|
* note that for real-world use, you need a [content-negotiation middleware][muuntaja] -
|
|
see the next example
|
|
|
|
[muuntaja]: ../ring/default_middleware.md#content-negotiation
|
|
|
|
```clj
|
|
(require '[reitit.ring :as ring])
|
|
(require '[reitit.swagger :as swagger])
|
|
(require '[reitit.swagger-ui :as swagger-ui])
|
|
|
|
(def app
|
|
(ring/ring-handler
|
|
(ring/router
|
|
[["/api"
|
|
["/ping" {:get (constantly {:status 200, :body "ping"})}]
|
|
["/pong" {:post (constantly {:status 200, :body "pong"})}]]
|
|
["" {:no-doc true}
|
|
["/swagger.json" {:get (swagger/create-swagger-handler)}]
|
|
["/api-docs/*" {:get (swagger-ui/create-swagger-ui-handler)}]]])))
|
|
```
|
|
|
|
The generated swagger spec:
|
|
|
|
```clj
|
|
(app {:request-method :get :uri "/swagger.json"})
|
|
;{:status 200
|
|
; :body {:swagger "2.0"
|
|
; :x-id #{:reitit.swagger/default}
|
|
; :paths {"/api/ping" {:get {}}
|
|
; "/api/pong" {:post {}}}}}
|
|
```
|
|
|
|
Swagger-ui:
|
|
|
|
```clj
|
|
(app {:request-method :get, :uri "/api-docs/index.html"})
|
|
; ... the swagger-ui index-page, configured correctly
|
|
```
|
|
|
|
You might be interested in adding a [trailing slash handler](slash_handler.md) to the app to serve the swagger-ui from `/api-docs` (without the trailing slash) too.
|
|
|
|
Another way to serve the swagger-ui is using the [default handler](default_handler.md):
|
|
|
|
```clj
|
|
(def app
|
|
(ring/ring-handler
|
|
(ring/router
|
|
[["/api"
|
|
["/ping" {:get (constantly {:status 200, :body "ping"})}]
|
|
["/pong" {:post (constantly {:status 200, :body "pong"})}]]
|
|
["/swagger.json"
|
|
{:get {:no-doc true
|
|
:handler (swagger/create-swagger-handler)}}]])
|
|
(swagger-ui/create-swagger-ui-handler {:path "/api-docs"})))
|
|
```
|
|
|
|
### More complete example
|
|
|
|
* `clojure.spec` coercion
|
|
* swagger data (`:tags`, `:produces`, `:summary`, `:basePath`)
|
|
* swagger-spec served from `"/swagger.json"`
|
|
* swagger-ui mounted to `"/"`
|
|
* set of middleware for content negotiation, exceptions, multipart etc.
|
|
* missed routes are handled by `create-default-handler`
|
|
* served via [ring-jetty](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter)
|
|
|
|
Whole example project is in [`/examples/ring-spec-swagger`](https://github.com/metosin/reitit/tree/master/examples/ring-spec-swagger).
|
|
|
|
```clj
|
|
(ns example.server
|
|
(:require [reitit.ring :as ring]
|
|
[reitit.swagger :as swagger]
|
|
[reitit.swagger-ui :as swagger-ui]
|
|
[reitit.ring.coercion :as coercion]
|
|
[reitit.coercion.spec]
|
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
|
[reitit.ring.middleware.exception :as exception]
|
|
[reitit.ring.middleware.multipart :as multipart]
|
|
[reitit.ring.middleware.parameters :as parameters]
|
|
[ring.middleware.params :as params]
|
|
[ring.adapter.jetty :as jetty]
|
|
[muuntaja.core :as m]
|
|
[clojure.java.io :as io]))
|
|
|
|
(def app
|
|
(ring/ring-handler
|
|
(ring/router
|
|
[["/swagger.json"
|
|
{:get {:no-doc true
|
|
:swagger {:info {:title "my-api"}
|
|
:basePath "/"} ;; prefix for all paths
|
|
:handler (swagger/create-swagger-handler)}}]
|
|
|
|
["/files"
|
|
{:swagger {:tags ["files"]}}
|
|
|
|
["/upload"
|
|
{:post {:summary "upload a file"
|
|
:parameters {:multipart {:file multipart/temp-file-part}}
|
|
:responses {200 {:body {:file multipart/temp-file-part}}}
|
|
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
|
|
{:status 200
|
|
:body {:file file}})}}]
|
|
|
|
["/download"
|
|
{:get {:summary "downloads a file"
|
|
:swagger {:produces ["image/png"]}
|
|
:handler (fn [_]
|
|
{:status 200
|
|
:headers {"Content-Type" "image/png"}
|
|
:body (io/input-stream (io/resource "reitit.png"))})}}]]
|
|
|
|
["/math"
|
|
{:swagger {:tags ["math"]}}
|
|
|
|
["/plus"
|
|
{:get {:summary "plus with spec query parameters"
|
|
:parameters {:query {:x int?, :y int?}}
|
|
:responses {200 {:body {:total int?}}}
|
|
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
|
{:status 200
|
|
:body {:total (+ x y)}})}
|
|
:post {:summary "plus with spec body parameters"
|
|
:parameters {:body {:x int?, :y int?}}
|
|
:responses {200 {:body {:total int?}}}
|
|
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
|
{:status 200
|
|
:body {:total (+ x y)}})}}]]]
|
|
|
|
{:data {:coercion reitit.coercion.spec/coercion
|
|
:muuntaja m/instance
|
|
:middleware [;; query-params & form-params
|
|
parameters/parameters-middleware
|
|
;; content-negotiation
|
|
muuntaja/format-negotiate-middleware
|
|
;; encoding response body
|
|
muuntaja/format-response-middleware
|
|
;; exception handling
|
|
exception/exception-middleware
|
|
;; decoding request body
|
|
muuntaja/format-request-middleware
|
|
;; coercing response bodys
|
|
coercion/coerce-response-middleware
|
|
;; coercing request parameters
|
|
coercion/coerce-request-middleware
|
|
;; multipart
|
|
multipart/multipart-middleware]}})
|
|
(ring/routes
|
|
(swagger-ui/create-swagger-ui-handler {:path "/"})
|
|
(ring/create-default-handler))))
|
|
|
|
(defn start []
|
|
(jetty/run-jetty #'app {:port 3000, :join? false})
|
|
(println "server running in port 3000"))
|
|
```
|
|
|
|
http://localhost:3000 should render now the swagger-ui:
|
|
|
|

|
|
|
|
## Multiple swagger apis
|
|
|
|
There can be multiple swagger apis within a router. Each route can be part of 0..n swagger apis. Swagger apis are identified by value in route data under key path `[:swagger :id]`. It can be either a keyword or a sequence of keywords. Normal route data [scoping rules](../basics/route_data.md#nested-route-data) rules apply.
|
|
|
|
Example with:
|
|
|
|
* 4 routes
|
|
* 2 swagger apis `::one` and `::two`
|
|
* 3 swagger specs
|
|
|
|
```clj
|
|
(require '[reitit.ring :as ring])
|
|
(require '[reitit.swagger :as swagger])
|
|
|
|
(def ping-route
|
|
["/ping" {:get (constantly {:status 200, :body "ping"})}])
|
|
|
|
(def spec-route
|
|
["/swagger.json"
|
|
{:get {:no-doc true
|
|
:handler (swagger/create-swagger-handler)}}])
|
|
|
|
(def app
|
|
(ring/ring-handler
|
|
(ring/router
|
|
[["/common" {:swagger {:id #{::one ::two}}} ping-route]
|
|
["/one" {:swagger {:id ::one}} ping-route spec-route]
|
|
["/two" {:swagger {:id ::two}} ping-route spec-route
|
|
["/deep" {:swagger {:id ::one}} ping-route]]
|
|
["/one-two" {:swagger {:id #{::one ::two}}} spec-route]])))
|
|
```
|
|
|
|
```clj
|
|
(-> {:request-method :get, :uri "/one/swagger.json"} app :body :paths keys)
|
|
; ("/common/ping" "/one/ping" "/two/deep/ping")
|
|
```
|
|
|
|
```clj
|
|
(-> {:request-method :get, :uri "/two/swagger.json"} app :body :paths keys)
|
|
; ("/common/ping" "/two/ping")
|
|
```
|
|
|
|
```clj
|
|
(-> {:request-method :get, :uri "/one-two/swagger.json"} app :body :paths keys)
|
|
; ("/common/ping" "/one/ping" "/two/ping" "/two/deep/ping")
|
|
```
|
|
|
|
## Reusable schema definitions
|
|
|
|
Swagger supports having reusable schema definitions under the
|
|
`"definitions"` key. These can be reused in different parts of
|
|
swagger.json using the `"$ref": "#/definitions/Foo"` syntax. These
|
|
definitions are also rendered in their own section in Swagger UI.
|
|
|
|
Reusable schema objects are generated for Malli `:ref`s and vars.
|
|
Currently (as of 0.7.2), reusable schema objects are **not** generated
|
|
for Plumatic Schema or Spec.
|
|
|
|
## TODO
|
|
|
|
* ClojureScript
|
|
* example for [Macchiato](https://github.com/macchiato-framework)
|
|
* body formatting
|
|
* resource handling
|