From 610586f0d3ca88c1628b4aed1fef9e4f096868d5 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Mon, 16 Sep 2024 12:45:50 +0300 Subject: [PATCH 1/3] fix: OpenAPI :description belongs at Response level, not Media Type also, support singular :example in addition to :examples --- examples/openapi/src/example/server.clj | 22 +++++++++---------- modules/reitit-openapi/src/reitit/openapi.clj | 6 ++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/openapi/src/example/server.clj b/examples/openapi/src/example/server.clj index 77639e41..83e3a6b0 100644 --- a/examples/openapi/src/example/server.clj +++ b/examples/openapi/src/example/server.clj @@ -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" @@ -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" @@ -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 @@ -114,9 +112,11 @@ :email "heidi@alps.ch"}]})}}] ["/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" diff --git a/modules/reitit-openapi/src/reitit/openapi.clj b/modules/reitit-openapi/src/reitit/openapi.clj index aac112a2..df7c01a4 100644 --- a/modules/reitit-openapi/src/reitit/openapi.clj +++ b/modules/reitit-openapi/src/reitit/openapi.clj @@ -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 @@ -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] @@ -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]) From afc8945d78135e45480137f00396729b8112a19e Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Mon, 16 Sep 2024 12:47:33 +0300 Subject: [PATCH 2/3] doc: improve openapi docs --- doc/ring/openapi.md | 177 +++++++++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 67 deletions(-) diff --git a/doc/ring/openapi.md b/doc/ring/openapi.md index 620ec1b4..ec3d2264 100644 --- a/doc/ring/openapi.md +++ b/doc/ring/openapi.md @@ -34,12 +34,100 @@ 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 + +## 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 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 @@ -81,73 +169,28 @@ Spec: :y int?}}}}}] ``` -## Per-content-type coercions +### Adding examples -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). +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. + +**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 -["/pizza" - {:get {:summary "Fetch a pizza | Multiple content-types, multiple examples" - :responses {200 {:content {"application/json" {:description "Fetch a pizza as 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" {: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})}}}}}} +;; Wrong! +{:parameters {:query [:map + {:json-schema/example {:a 1}} + [:a :int]]}} +;; Right! +{:parameters {:query [:map + [:a {:json-schema/example 1} :int]]}} ``` - -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. From 923bafdc9b05b66da852bad9e340673b040e9456 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Mon, 16 Sep 2024 12:49:30 +0300 Subject: [PATCH 3/3] doc: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcae664d..2c6a455c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ We use [Break Versioning][breakver]. The version numbers follow a `.