mirror of
https://github.com/metosin/reitit.git
synced 2025-12-20 01:21:11 +00:00
Merge pull request #663 from metosin/openapi-exp
#636 Adds level-1 Muuntaja support for OpenAPI3
This commit is contained in:
commit
ca434f9c05
6 changed files with 140 additions and 55 deletions
|
|
@ -14,6 +14,8 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
||||||
|
|
||||||
## UNRELEASED
|
## UNRELEASED
|
||||||
|
|
||||||
|
* Fetch OpenAPI content types from Muuntaja [#636](https://github.com/metosin/reitit/issues/636)
|
||||||
|
* **BREAKING** OpenAPI support is now clj only
|
||||||
* Updated dependencies:
|
* Updated dependencies:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ The following route data keys contribute to the generated swagger specification:
|
||||||
| key | description |
|
| key | description |
|
||||||
| ---------------|-------------|
|
| ---------------|-------------|
|
||||||
| :openapi | map of any openapi data. Can contain keys like `:deprecated`.
|
| :openapi | map of any openapi data. Can contain keys like `:deprecated`.
|
||||||
| :openapi/request-content-types | vector of supported request content types. Defaults to `["application/json"]`. Only needed if you use the [:request :content :default] coercion.
|
|
||||||
| :openapi/response-content-types | vector of supported response content types. Defaults to `["application/json"]`. Only needed if you use the [:response nnn :content :default] coercion.
|
|
||||||
| :no-doc | optional boolean to exclude endpoint from api docs
|
| :no-doc | optional boolean to exclude endpoint from api docs
|
||||||
| :tags | optional set of string or keyword tags for an endpoint api docs
|
| :tags | optional set of string or keyword tags for an endpoint api docs
|
||||||
| :summary | optional short string summary of an endpoint
|
| :summary | optional short string summary of an endpoint
|
||||||
| :description | optional long description of an endpoint. Supports http://spec.commonmark.org/
|
| :description | optional long description of an endpoint. Supports http://spec.commonmark.org/
|
||||||
|
| :openapi/request-content-types | See the Per-content-type-coercions section below.
|
||||||
|
| :openapi/response-content-types |See the Per-content-type-coercions section below. vector of supported response content types. Defaults to `["application/json"]`. Only needed if you use the [:response nnn :content :default] coercion.
|
||||||
|
|
||||||
Coercion keys also contribute to the docs:
|
Coercion keys also contribute to the docs:
|
||||||
|
|
||||||
|
|
@ -109,7 +109,11 @@ openapi example](../../examples/openapi).
|
||||||
:value (pr-str {:color :red :pineapple true})}}}}}}
|
: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
|
## Custom OpenAPI data
|
||||||
|
|
||||||
|
|
@ -123,9 +127,13 @@ example of `"securitySchemes"`.
|
||||||
|
|
||||||
## OpenAPI spec
|
## 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.
|
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.
|
You can use the `:openapi` route data key of the `create-openapi-handler` route
|
||||||
|
to populate the top level of the OpenAPI spec.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,6 @@
|
||||||
[:pineapple :boolean]]
|
[:pineapple :boolean]]
|
||||||
:examples {:purple {:value (pr-str {:color :purple
|
:examples {:purple {:value (pr-str {:color :purple
|
||||||
:pineapple false})}}}}}
|
:pineapple false})}}}}}
|
||||||
;; Need to list content types explicitly because we use :default in :responses
|
|
||||||
:openapi/response-content-types ["application/json" "application/edn"]
|
|
||||||
:responses {200 {:content {:default {:description "Success"
|
:responses {200 {:content {:default {:description "Success"
|
||||||
:schema [:map [:success :boolean]]
|
:schema [:map [:success :boolean]]
|
||||||
:example {:success true}}}}}
|
:example {:success true}}}}}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@
|
||||||
:plugins [[lein-parent "0.3.9"]]
|
:plugins [[lein-parent "0.3.9"]]
|
||||||
:parent-project {:path "../../project.clj"
|
:parent-project {:path "../../project.clj"
|
||||||
:inherit [:deploy-repositories :managed-dependencies]}
|
:inherit [:deploy-repositories :managed-dependencies]}
|
||||||
:dependencies [[metosin/reitit-core]])
|
:dependencies [[metosin/reitit-core]
|
||||||
|
[metosin/muuntaja]])
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[meta-merge.core :refer [meta-merge]]
|
[meta-merge.core :refer [meta-merge]]
|
||||||
|
[muuntaja.core :as m]
|
||||||
[reitit.coercion :as coercion]
|
[reitit.coercion :as coercion]
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
[reitit.trie :as trie]))
|
[reitit.trie :as trie]))
|
||||||
|
|
@ -76,9 +77,7 @@
|
||||||
(-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
|
(-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
|
||||||
|
|
||||||
(defn -get-apidocs-openapi
|
(defn -get-apidocs-openapi
|
||||||
[coercion {:keys [request parameters responses openapi/request-content-types openapi/response-content-types]
|
[coercion {:keys [request muuntaja parameters responses openapi/request-content-types openapi/response-content-types]}]
|
||||||
:or {request-content-types ["application/json"]
|
|
||||||
response-content-types ["application/json"]}}]
|
|
||||||
(let [{:keys [body multipart]} parameters
|
(let [{:keys [body multipart]} parameters
|
||||||
parameters (dissoc parameters :request :body :multipart)
|
parameters (dissoc parameters :request :body :multipart)
|
||||||
->content (fn [data schema]
|
->content (fn [data schema]
|
||||||
|
|
@ -86,7 +85,13 @@
|
||||||
{:schema schema}
|
{:schema schema}
|
||||||
(select-keys data [:description :examples])
|
(select-keys data [:description :examples])
|
||||||
(:openapi data)))
|
(:openapi data)))
|
||||||
->schema-object #(coercion/-get-model-apidocs coercion :openapi %1 %2)]
|
->schema-object #(coercion/-get-model-apidocs coercion :openapi %1 %2)
|
||||||
|
request-content-types (or request-content-types
|
||||||
|
(when muuntaja (m/decodes muuntaja))
|
||||||
|
["application/json"])
|
||||||
|
response-content-types (or response-content-types
|
||||||
|
(when muuntaja (m/encodes muuntaja))
|
||||||
|
["application/json"])]
|
||||||
(merge
|
(merge
|
||||||
(when (seq parameters)
|
(when (seq parameters)
|
||||||
{:parameters
|
{:parameters
|
||||||
|
|
@ -130,7 +135,7 @@
|
||||||
:type :schema
|
:type :schema
|
||||||
:content-type content-type})]
|
:content-type content-type})]
|
||||||
[content-type (->content data schema)])))
|
[content-type (->content data schema)])))
|
||||||
(:content request)))}})
|
(dissoc (:content request) :default)))}})
|
||||||
(when multipart
|
(when multipart
|
||||||
{:requestBody
|
{:requestBody
|
||||||
{:content
|
{:content
|
||||||
|
|
@ -206,5 +211,5 @@
|
||||||
([req res raise]
|
([req res raise]
|
||||||
(try
|
(try
|
||||||
(res (create-openapi req))
|
(res (create-openapi req))
|
||||||
(catch #?(:clj Exception :cljs :default) e
|
(catch Exception e
|
||||||
(raise e))))))
|
(raise e))))))
|
||||||
|
|
@ -685,25 +685,56 @@
|
||||||
(testing "spec is valid"
|
(testing "spec is valid"
|
||||||
(is (nil? (validate spec))))))))
|
(is (nil? (validate spec))))))))
|
||||||
|
|
||||||
|
|
||||||
(deftest default-content-type-test
|
(deftest default-content-type-test
|
||||||
(doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])]
|
(doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])]
|
||||||
[schema/coercion (fn [nom] {nom s/Str})]
|
[schema/coercion (fn [nom] {nom s/Str})]
|
||||||
[spec/coercion (fn [nom] {nom string?})]]]
|
[spec/coercion (fn [nom] {nom string?})]]]
|
||||||
(testing (str coercion)
|
(testing (str coercion)
|
||||||
(doseq [content-type ["application/json" "application/edn"]]
|
|
||||||
(testing (str "default content type " content-type)
|
|
||||||
(let [app (ring/ring-handler
|
(let [app (ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
[["/parameters"
|
[["/explicit-content-type"
|
||||||
{:post {:description "parameters"
|
{:post {:description "parameters"
|
||||||
:coercion coercion
|
:coercion coercion
|
||||||
:openapi/request-content-types [content-type]
|
:request {:content {"application/json" {:schema (->schema :b)}
|
||||||
:openapi/response-content-types [content-type "application/response"]
|
"application/edn" {:schema (->schema :c)}}}
|
||||||
:request {:content {"application/transit" {:schema (->schema :transit)}}
|
|
||||||
:body (->schema :default)}
|
|
||||||
:responses {200 {:description "success"
|
:responses {200 {:description "success"
|
||||||
:content {"application/transit" {:schema (->schema :transit)}}
|
:content {"application/json" {:schema (->schema :ok)}
|
||||||
:body (->schema :default)}}
|
"application/edn" {:schema (->schema :edn)}}}}
|
||||||
|
:handler (fn [req]
|
||||||
|
{:status 200
|
||||||
|
:body (-> req :parameters :request)})}}]
|
||||||
|
["/muuntaja"
|
||||||
|
{:post {:description "default content types from muuntaja"
|
||||||
|
:coercion coercion
|
||||||
|
;;; TODO: test the :parameters syntax
|
||||||
|
:request {:content {:default {:schema (->schema :b)}
|
||||||
|
"application/reitit-request" {:schema (->schema :ok)}}}
|
||||||
|
:responses {200 {:description "success"
|
||||||
|
:content {:default {:schema (->schema :ok)}
|
||||||
|
"application/reitit-response" {:schema (->schema :ok)}}}}
|
||||||
|
:handler (fn [req]
|
||||||
|
{:status 200
|
||||||
|
:body (-> req :parameters :request)})}}]
|
||||||
|
["/override-default-content-type"
|
||||||
|
{:post {:description "override default content types from muuntaja"
|
||||||
|
:coercion coercion
|
||||||
|
:openapi/request-content-types ["application/request"]
|
||||||
|
:openapi/response-content-types ["application/response"]
|
||||||
|
;;; TODO: test the :parameters syntax
|
||||||
|
:request {:content {:default {:schema (->schema :b)}}}
|
||||||
|
:responses {200 {:description "success"
|
||||||
|
:content {:default {:schema (->schema :ok)}}}}
|
||||||
|
:handler (fn [req]
|
||||||
|
{:status 200
|
||||||
|
:body (-> req :parameters :request)})}}]
|
||||||
|
["/legacy"
|
||||||
|
{:post {:description "default content types from muuntaja, legacy syntax"
|
||||||
|
:coercion coercion
|
||||||
|
;;; TODO: test the :parameters syntax
|
||||||
|
:request {:body {:schema (->schema :b)}}
|
||||||
|
:responses {200 {:description "success"
|
||||||
|
:body {:schema (->schema :ok)}}}
|
||||||
:handler (fn [req]
|
:handler (fn [req]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body (-> req :parameters :request)})}}]
|
:body (-> req :parameters :request)})}}]
|
||||||
|
|
@ -712,25 +743,65 @@
|
||||||
:openapi {:info {:title "" :version "0.0.1"}}
|
:openapi {:info {:title "" :version "0.0.1"}}
|
||||||
:no-doc true}}]]
|
:no-doc true}}]]
|
||||||
{:validate reitit.ring.spec/validate
|
{:validate reitit.ring.spec/validate
|
||||||
:data {:middleware [openapi/openapi-feature
|
:data {:muuntaja (m/create (-> m/default-options
|
||||||
|
(update-in [:formats] select-keys ["application/transit+json"])
|
||||||
|
(assoc :default-format "application/transit+json")))
|
||||||
|
:middleware [openapi/openapi-feature
|
||||||
rrc/coerce-request-middleware
|
rrc/coerce-request-middleware
|
||||||
rrc/coerce-response-middleware]}}))
|
rrc/coerce-response-middleware]}}))
|
||||||
spec (-> {:request-method :get
|
spec (-> {:request-method :get
|
||||||
:uri "/openapi.json"}
|
:uri "/openapi.json"}
|
||||||
app
|
app
|
||||||
:body)]
|
:body)
|
||||||
|
spec-coercion (= coercion spec/coercion)]
|
||||||
|
(testing "explicit content types"
|
||||||
(testing "body parameter"
|
(testing "body parameter"
|
||||||
(is (match? (matchers/in-any-order [content-type "application/transit"])
|
(is (= ["application/edn" "application/json"]
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :requestBody :content])
|
(get-in [:paths "/explicit-content-type" :post :requestBody :content])
|
||||||
|
keys
|
||||||
|
sort))))
|
||||||
|
(testing "body response"
|
||||||
|
(is (= ["application/edn" "application/json"]
|
||||||
|
(-> spec
|
||||||
|
(get-in [:paths "/explicit-content-type" :post :responses 200 :content])
|
||||||
|
keys
|
||||||
|
sort)))))
|
||||||
|
(testing "muuntaja content types"
|
||||||
|
(testing "body parameter"
|
||||||
|
(is (= ["application/transit+json" "application/reitit-request"]
|
||||||
|
(-> spec
|
||||||
|
(get-in [:paths "/muuntaja" :post :requestBody :content])
|
||||||
keys))))
|
keys))))
|
||||||
(testing "body response"
|
(testing "body response"
|
||||||
(is (match? (matchers/in-any-order [content-type "application/transit" "application/response"])
|
(is (= ["application/transit+json" "application/reitit-response"]
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :responses 200 :content])
|
(get-in [:paths "/muuntaja" :post :responses 200 :content])
|
||||||
|
keys)))))
|
||||||
|
(testing "overridden muuntaja content types"
|
||||||
|
(testing "body parameter"
|
||||||
|
(is (= ["application/request"]
|
||||||
|
(-> spec
|
||||||
|
(get-in [:paths "/override-default-content-type" :post :requestBody :content])
|
||||||
keys))))
|
keys))))
|
||||||
|
(testing "body response"
|
||||||
|
(is (= ["application/response"]
|
||||||
|
(-> spec
|
||||||
|
(get-in [:paths "/override-default-content-type" :post :responses 200 :content])
|
||||||
|
keys)))))
|
||||||
|
(testing "legacy syntax muuntaja content types"
|
||||||
|
(testing "body parameter"
|
||||||
|
(is (= ["application/transit+json"]
|
||||||
|
(-> spec
|
||||||
|
(get-in [:paths "/legacy" :post :requestBody :content])
|
||||||
|
keys))))
|
||||||
|
(testing "body response"
|
||||||
|
(is (= ["application/transit+json"]
|
||||||
|
(-> spec
|
||||||
|
(get-in [:paths "/legacy" :post :responses 200 :content])
|
||||||
|
keys)))))
|
||||||
(testing "spec is valid"
|
(testing "spec is valid"
|
||||||
(is (nil? (validate spec))))))))))
|
(is (nil? (validate spec))))))))
|
||||||
|
|
||||||
(deftest recursive-test
|
(deftest recursive-test
|
||||||
;; Recursive schemas only properly supported for malli
|
;; Recursive schemas only properly supported for malli
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue