mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 08:21:11 +00:00
Merge pull request #702 from metosin/openapi-example-docs
clarify openapi docs plus minor fix
This commit is contained in:
commit
86871a6a55
4 changed files with 128 additions and 81 deletions
|
|
@ -12,6 +12,10 @@ 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
|
||||||
|
|
||||||
|
* Improve OpenAPI docs, plus don't emit `:description` in the wrong place [#702](https://github.com/metosin/reitit/pull/702)
|
||||||
|
|
||||||
## 0.7.2 (2024-09-02)
|
## 0.7.2 (2024-09-02)
|
||||||
|
|
||||||
* Speed up routes and inline it in code ring handler [#693](https://github.com/metosin/reitit/pull/693) [#693](https://github.com/metosin/reitit/pull/696)
|
* Speed up routes and inline it in code ring handler [#693](https://github.com/metosin/reitit/pull/693) [#693](https://github.com/metosin/reitit/pull/696)
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,100 @@ Coercion keys also contribute to the docs:
|
||||||
| :request | optional description of body parameters, possibly per content-type
|
| :request | optional description of body parameters, possibly per content-type
|
||||||
| :responses | optional descriptions of responses, in a format defined by coercion
|
| :responses | optional descriptions of responses, in a format defined by coercion
|
||||||
|
|
||||||
## Annotating schemas
|
|
||||||
|
## Per-content-type coercions
|
||||||
|
|
||||||
|
Use `:request` coercion (instead of `:body`) to unlock
|
||||||
|
per-content-type coercions. This also lets you specify multiple named
|
||||||
|
examples. See [Coercion](coercion.md) for more info. See also [the
|
||||||
|
openapi example](../../examples/openapi).
|
||||||
|
|
||||||
|
```clj
|
||||||
|
["/pizza"
|
||||||
|
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
|
||||||
|
:responses {200 {:description "Fetch a pizza as json or EDN"
|
||||||
|
:content {"application/json" {:schema [:map
|
||||||
|
[:color :keyword]
|
||||||
|
[:pineapple :boolean]]
|
||||||
|
:examples {:white {:description "White pizza with pineapple"
|
||||||
|
:value {:color :white
|
||||||
|
:pineapple true}}
|
||||||
|
:red {:description "Red pizza"
|
||||||
|
:value {:color :red
|
||||||
|
:pineapple false}}}}
|
||||||
|
"application/edn" {:schema [:map
|
||||||
|
[:color :keyword]
|
||||||
|
[:pineapple :boolean]]
|
||||||
|
:examples {:red {:description "Red pizza with pineapple"
|
||||||
|
:value (pr-str {:color :red :pineapple true})}}}}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The special `:default` content types map to the content types supported by the Muuntaja
|
||||||
|
instance. You can override these by using the `:openapi/request-content-types`
|
||||||
|
and `:openapi/response-content-types` keys, which must contain vector of
|
||||||
|
supported content types. If there is no Muuntaja instance, and these keys are
|
||||||
|
not defined, the content types will default to `["application/json"]`.
|
||||||
|
|
||||||
|
## OpenAPI spec
|
||||||
|
|
||||||
|
Serving the OpenAPI specification is handled by
|
||||||
|
`reitit.openapi/create-openapi-handler`. It takes no arguments and returns a
|
||||||
|
ring handler which collects at request-time data from all routes and returns an
|
||||||
|
OpenAPI specification as Clojure data, to be encoded by a response formatter.
|
||||||
|
|
||||||
|
You can use the `:openapi` route data key of the `create-openapi-handler` route
|
||||||
|
to populate the top level of the OpenAPI spec.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
["/openapi.json"
|
||||||
|
{:get {:handler (openapi/create-openapi-handler)
|
||||||
|
:openapi {:info {:title "my nice api" :version "0.0.1"}}
|
||||||
|
:no-doc true}}]
|
||||||
|
```
|
||||||
|
|
||||||
|
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. See `reitit.swagger-ui/create-swagger-ui-handle`
|
||||||
|
|
||||||
|
## Finetuning the OpenAPI output
|
||||||
|
|
||||||
|
There are a number of ways you can specify extra data that gets
|
||||||
|
included in the OpenAPI spec.
|
||||||
|
|
||||||
|
### Custom OpenAPI data
|
||||||
|
|
||||||
|
The `:openapi` route data key can be used to add top-level or
|
||||||
|
route-level information to the generated OpenAPI spec.
|
||||||
|
|
||||||
|
A straightforward use case is adding `"externalDocs"`:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
["/account"
|
||||||
|
{:get {:summary "Fetch an account | Recursive schemas using malli registry, link to external docs"
|
||||||
|
:openapi {:externalDocs {:description "The reitit repository"
|
||||||
|
:url "https://github.com/metosin/reitit"}}
|
||||||
|
...}}]
|
||||||
|
```
|
||||||
|
|
||||||
|
In a more complex use case is providing `"securitySchemes"`. See
|
||||||
|
[the openapi example](../../examples/openapi) for a working example of
|
||||||
|
`"securitySchemes"`. See also the
|
||||||
|
[OpenAPI docs](https://spec.openapis.org/oas/v3.1.0.html#security-scheme-object)
|
||||||
|
|
||||||
|
### Annotating schemas
|
||||||
|
|
||||||
You can use malli properties, schema-tools data or spec-tools data to
|
You can use malli properties, schema-tools data or spec-tools data to
|
||||||
annotate your models with examples, descriptions and defaults that
|
annotate your models with examples, descriptions and defaults that
|
||||||
show up in the OpenAPI spec.
|
show up in the OpenAPI spec.
|
||||||
|
|
||||||
|
This approach lets you add additional keys to the
|
||||||
|
[OpenAPI Schema Objects](https://spec.openapis.org/oas/v3.1.0.html#schema-object).
|
||||||
|
The most common ones are default and example values for parameters.
|
||||||
|
|
||||||
Malli:
|
Malli:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
|
|
@ -81,73 +169,28 @@ Spec:
|
||||||
:y int?}}}}}]
|
:y int?}}}}}]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Per-content-type coercions
|
### Adding examples
|
||||||
|
|
||||||
Use `:request` coercion (instead of `:body`) to unlock
|
Adding request/response examples have been mentioned above a couple of times
|
||||||
per-content-type coercions. This also lets you specify multiple named
|
above. Here's a summary of the different ways to do it:
|
||||||
examples. See [Coercion](coercion.md) for more info. See also [the
|
|
||||||
openapi example](../../examples/openapi).
|
1. Add an example to the schema object using a `:openapi/example`
|
||||||
|
(schema, spec) or `:json-schema/example` (malli) key in your
|
||||||
|
schema/spec/malli model metadata. See the examples above.
|
||||||
|
2. Use `:example` (a single example) or `:examples` (named examples)
|
||||||
|
with per-content-type coercion.
|
||||||
|
|
||||||
|
**Caveat!** When adding examples for query parameters (or headers),
|
||||||
|
you must add the examples to the individual parameters, not the map
|
||||||
|
schema surrounding them. This is due to limitations in how OpenAPI
|
||||||
|
represents query parameters.
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
["/pizza"
|
;; Wrong!
|
||||||
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
|
{:parameters {:query [:map
|
||||||
:responses {200 {:content {"application/json" {:description "Fetch a pizza as json"
|
{:json-schema/example {:a 1}}
|
||||||
:schema [:map
|
[:a :int]]}}
|
||||||
[:color :keyword]
|
;; Right!
|
||||||
[:pineapple :boolean]]
|
{:parameters {:query [:map
|
||||||
:examples {:white {:description "White pizza with pineapple"
|
[:a {:json-schema/example 1} :int]]}}
|
||||||
:value {:color :white
|
|
||||||
:pineapple true}}
|
|
||||||
:red {:description "Red pizza"
|
|
||||||
:value {:color :red
|
|
||||||
:pineapple false}}}}
|
|
||||||
"application/edn" {:description "Fetch a pizza as edn"
|
|
||||||
:schema [:map
|
|
||||||
[:color :keyword]
|
|
||||||
[:pineapple :boolean]]
|
|
||||||
:examples {:red {:description "Red pizza with pineapple"
|
|
||||||
:value (pr-str {:color :red :pineapple true})}}}}}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The special `:default` content types map to the content types supported by the Muuntaja
|
|
||||||
instance. You can override these by using the `:openapi/request-content-types`
|
|
||||||
and `:openapi/response-content-types` keys, which must contain vector of
|
|
||||||
supported content types. If there is no Muuntaja instance, and these keys are
|
|
||||||
not defined, the content types will default to `["application/json"]`.
|
|
||||||
|
|
||||||
## Custom OpenAPI data
|
|
||||||
|
|
||||||
The `:openapi` route data key can be used to add top-level or
|
|
||||||
route-level information to the generated OpenAPI spec. This is useful
|
|
||||||
for providing `"securitySchemes"` or other OpenAPI keys that are not
|
|
||||||
generated automatically by reitit.
|
|
||||||
|
|
||||||
See [the openapi example](../../examples/openapi) for a working
|
|
||||||
example of `"securitySchemes"`.
|
|
||||||
|
|
||||||
## OpenAPI spec
|
|
||||||
|
|
||||||
Serving the OpenAPI specification is handled by
|
|
||||||
`reitit.openapi/create-openapi-handler`. It takes no arguments and returns a
|
|
||||||
ring handler which collects at request-time data from all routes and returns an
|
|
||||||
OpenAPI specification as Clojure data, to be encoded by a response formatter.
|
|
||||||
|
|
||||||
You can use the `:openapi` route data key of the `create-openapi-handler` route
|
|
||||||
to populate the top level of the OpenAPI spec.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
["/openapi.json"
|
|
||||||
{:get {:handler (openapi/create-openapi-handler)
|
|
||||||
:openapi {:info {:title "my nice api" :version "0.0.1"}}
|
|
||||||
:no-doc true}}]
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Note: you need Swagger-UI 5 for OpenAPI 3.1 support. As of 2023-03-10, a v5.0.0-alpha.0 is out.
|
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@
|
||||||
|
|
||||||
["/pizza"
|
["/pizza"
|
||||||
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
|
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
|
||||||
:responses {200 {:content {"application/json" {:description "Fetch a pizza as json"
|
:responses {200 {:description "Fetch a pizza as json or EDN"
|
||||||
:schema [:map
|
:content {"application/json" {:schema [:map
|
||||||
[:color :keyword]
|
[:color :keyword]
|
||||||
[:pineapple :boolean]]
|
[:pineapple :boolean]]
|
||||||
:examples {:white {:description "White pizza with pineapple"
|
:examples {:white {:description "White pizza with pineapple"
|
||||||
|
|
@ -60,8 +60,7 @@
|
||||||
:red {:description "Red pizza"
|
:red {:description "Red pizza"
|
||||||
:value {:color :red
|
:value {:color :red
|
||||||
:pineapple false}}}}
|
:pineapple false}}}}
|
||||||
"application/edn" {:description "Fetch a pizza as edn"
|
"application/edn" {:schema [:map
|
||||||
:schema [:map
|
|
||||||
[:color :keyword]
|
[:color :keyword]
|
||||||
[:pineapple :boolean]]
|
[:pineapple :boolean]]
|
||||||
:examples {:red {:description "Red pizza with pineapple"
|
:examples {:red {:description "Red pizza with pineapple"
|
||||||
|
|
@ -71,20 +70,19 @@
|
||||||
:body {:color :red
|
:body {:color :red
|
||||||
:pineapple true}})}
|
:pineapple true}})}
|
||||||
:post {:summary "Create a pizza | Multiple content-types, multiple examples"
|
:post {:summary "Create a pizza | Multiple content-types, multiple examples"
|
||||||
:request {:content {"application/json" {:description "Create a pizza using json"
|
:request {:description "Create a pizza using json or EDN"
|
||||||
:schema [:map
|
:content {"application/json" {:schema [:map
|
||||||
[:color :keyword]
|
[:color :keyword]
|
||||||
[:pineapple :boolean]]
|
[:pineapple :boolean]]
|
||||||
:examples {:purple {:value {:color :purple
|
:examples {:purple {:value {:color :purple
|
||||||
:pineapple false}}}}
|
:pineapple false}}}}
|
||||||
"application/edn" {:description "Create a pizza using EDN"
|
"application/edn" {:schema [:map
|
||||||
:schema [:map
|
|
||||||
[:color :keyword]
|
[:color :keyword]
|
||||||
[:pineapple :boolean]]
|
[:pineapple :boolean]]
|
||||||
:examples {:purple {:value (pr-str {:color :purple
|
:examples {:purple {:value (pr-str {:color :purple
|
||||||
:pineapple false})}}}}}
|
:pineapple false})}}}}}
|
||||||
:responses {200 {:content {:default {:description "Success"
|
:responses {200 {:description "Success"
|
||||||
:schema [:map [:success :boolean]]
|
:content {:default {:schema [:map [:success :boolean]]
|
||||||
:example {:success true}}}}}
|
:example {:success true}}}}}
|
||||||
:handler (fn [_request]
|
:handler (fn [_request]
|
||||||
{:status 200
|
{:status 200
|
||||||
|
|
@ -114,9 +112,11 @@
|
||||||
:email "heidi@alps.ch"}]})}}]
|
:email "heidi@alps.ch"}]})}}]
|
||||||
|
|
||||||
["/account"
|
["/account"
|
||||||
{:get {:summary "Fetch an account | Recursive schemas using malli registry"
|
{:get {:summary "Fetch an account | Recursive schemas using malli registry, link to external docs"
|
||||||
:parameters {:query #'AccountId}
|
:parameters {:query #'AccountId}
|
||||||
:responses {200 {:content {:default {:schema #'Account}}}}
|
:responses {200 {:content {:default {:schema #'Account}}}}
|
||||||
|
:openapi {:externalDocs {:description "The reitit repository"
|
||||||
|
:url "https://github.com/metosin/reitit"}}
|
||||||
:handler (fn [_request]
|
:handler (fn [_request]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:bank "MiniBank"
|
:body {:bank "MiniBank"
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
->content (fn [data schema]
|
->content (fn [data schema]
|
||||||
(merge
|
(merge
|
||||||
{:schema schema}
|
{:schema schema}
|
||||||
(select-keys data [:description :examples])
|
(select-keys data [:example :examples])
|
||||||
(:openapi data)))
|
(:openapi data)))
|
||||||
->schema-object (fn [model opts]
|
->schema-object (fn [model opts]
|
||||||
(let [result (coercion/-get-model-apidocs
|
(let [result (coercion/-get-model-apidocs
|
||||||
|
|
@ -112,7 +112,7 @@
|
||||||
(select-keys schema [:description])))
|
(select-keys schema [:description])))
|
||||||
(into []))})
|
(into []))})
|
||||||
(when body
|
(when body
|
||||||
;; body uses a single schema to describe every :requestBody
|
;; :body uses a single schema to describe every :requestBody
|
||||||
;; the schema-object transformer should be able to transform into distinct content-types
|
;; the schema-object transformer should be able to transform into distinct content-types
|
||||||
{:requestBody {:content (into {}
|
{:requestBody {:content (into {}
|
||||||
(map (fn [content-type]
|
(map (fn [content-type]
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
request-content-types)}})
|
request-content-types)}})
|
||||||
|
|
||||||
(when request
|
(when request
|
||||||
;; request allow to different :requestBody per content-type
|
;; :request allows different :requestBody per content-type
|
||||||
{:requestBody
|
{:requestBody
|
||||||
(merge
|
(merge
|
||||||
(select-keys request [:description])
|
(select-keys request [:description])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue