Merge pull request #702 from metosin/openapi-example-docs

clarify openapi docs plus minor fix
This commit is contained in:
Joel Kaasinen 2024-09-16 15:51:39 +03:00 committed by GitHub
commit 86871a6a55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 128 additions and 81 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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"

View file

@ -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])