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 | 167 ++++++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 62 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,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 @@ -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" @@ -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" @@ -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 @@ -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]]}} +``` 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 `.