Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clarify openapi docs plus minor fix #702

Merged
merged 3 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

* 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)
Expand Down
167 changes: 105 additions & 62 deletions doc/ring/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,52 +34,6 @@ Coercion keys also contribute to the docs:
| :request | optional description of body parameters, possibly per content-type
| :responses | optional descriptions of responses, in a format defined by coercion

## Annotating schemas

You can use malli properties, schema-tools data or spec-tools data to
annotate your models with examples, descriptions and defaults that
show up in the OpenAPI spec.

Malli:

```clj
["/plus"
{:post
{:parameters
{:body [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}}}]
```

Schema:

```clj
["/plus"
{:post
{:parameters
{:body {:x (schema-tools.core/schema s/Num {:description "Description for X parameter"
:openapi/example 13
:openapi/default 42})
:y int?}}}}]
```

Spec:

```clj
["/plus"
{:post
{:parameters
{:body (spec-tools.data-spec/spec ::foo
{:x (schema-tools.core/spec {:spec int?
:description "Description for X parameter"
:openapi/example 13
:openapi/default 42})
:y int?}}}}}]
```

## Per-content-type coercions

Expand All @@ -91,8 +45,8 @@ openapi example](../../examples/openapi).
```clj
["/pizza"
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
:responses {200 {:content {"application/json" {:description "Fetch a pizza as json"
:schema [:map
: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"
Expand All @@ -101,8 +55,7 @@ openapi example](../../examples/openapi).
:red {:description "Red pizza"
:value {:color :red
:pineapple false}}}}
"application/edn" {:description "Fetch a pizza as edn"
:schema [:map
"application/edn" {:schema [:map
[:color :keyword]
[:pineapple :boolean]]
:examples {:red {:description "Red pizza with pineapple"
Expand All @@ -115,16 +68,6 @@ 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
Expand All @@ -148,6 +91,106 @@ If you need to post-process the generated spec, just wrap the handler with a cus

## 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.
[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
annotate your models with examples, descriptions and defaults that
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:

```clj
["/plus"
{:post
{:parameters
{:body [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}}}]
```

Schema:

```clj
["/plus"
{:post
{:parameters
{:body {:x (schema-tools.core/schema s/Num {:description "Description for X parameter"
:openapi/example 13
:openapi/default 42})
:y int?}}}}]
```

Spec:

```clj
["/plus"
{:post
{:parameters
{:body (spec-tools.data-spec/spec ::foo
{:x (schema-tools.core/spec {:spec int?
:description "Description for X parameter"
:openapi/example 13
:openapi/default 42})
:y int?}}}}}]
```

### Adding examples

Adding request/response examples have been mentioned above a couple of times
above. Here's a summary of the different ways to do it:

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.

Note: you need Swagger-UI 5 for OpenAPI 3.1 support. As of 2023-03-10, a v5.0.0-alpha.0 is out.
**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
;; Wrong!
{:parameters {:query [:map
{:json-schema/example {:a 1}}
[:a :int]]}}
;; Right!
{:parameters {:query [:map
[:a {:json-schema/example 1} :int]]}}
```
22 changes: 11 additions & 11 deletions examples/openapi/src/example/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@

["/pizza"
{:get {:summary "Fetch a pizza | Multiple content-types, multiple examples"
:responses {200 {:content {"application/json" {:description "Fetch a pizza as json"
:schema [:map
: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"
Expand All @@ -60,8 +60,7 @@
:red {:description "Red pizza"
:value {:color :red
:pineapple false}}}}
"application/edn" {:description "Fetch a pizza as edn"
:schema [:map
"application/edn" {:schema [:map
[:color :keyword]
[:pineapple :boolean]]
:examples {:red {:description "Red pizza with pineapple"
Expand All @@ -71,20 +70,19 @@
:body {:color :red
:pineapple true}})}
:post {:summary "Create a pizza | Multiple content-types, multiple examples"
:request {:content {"application/json" {:description "Create a pizza using json"
:schema [:map
:request {:description "Create a pizza using json or EDN"
:content {"application/json" {:schema [:map
[:color :keyword]
[:pineapple :boolean]]
:examples {:purple {:value {:color :purple
:pineapple false}}}}
"application/edn" {:description "Create a pizza using EDN"
:schema [:map
"application/edn" {:schema [:map
[:color :keyword]
[:pineapple :boolean]]
:examples {:purple {:value (pr-str {:color :purple
:pineapple false})}}}}}
:responses {200 {:content {:default {:description "Success"
:schema [:map [:success :boolean]]
:responses {200 {:description "Success"
:content {:default {:schema [:map [:success :boolean]]
:example {:success true}}}}}
:handler (fn [_request]
{:status 200
Expand Down Expand Up @@ -114,9 +112,11 @@
:email "[email protected]"}]})}}]

["/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}
:responses {200 {:content {:default {:schema #'Account}}}}
:openapi {:externalDocs {:description "The reitit repository"
:url "https://github.com/metosin/reitit"}}
:handler (fn [_request]
{:status 200
:body {:bank "MiniBank"
Expand Down
6 changes: 3 additions & 3 deletions modules/reitit-openapi/src/reitit/openapi.clj
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
->content (fn [data schema]
(merge
{:schema schema}
(select-keys data [:description :examples])
(select-keys data [:example :examples])
(:openapi data)))
->schema-object (fn [model opts]
(let [result (coercion/-get-model-apidocs
Expand Down Expand Up @@ -112,7 +112,7 @@
(select-keys schema [:description])))
(into []))})
(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
{:requestBody {:content (into {}
(map (fn [content-type]
Expand All @@ -123,7 +123,7 @@
request-content-types)}})

(when request
;; request allow to different :requestBody per content-type
;; :request allows different :requestBody per content-type
{:requestBody
(merge
(select-keys request [:description])
Expand Down
Loading