feat: openapi default request/response schemas

use a default schema from :request/:response :body for all specified
:content-types
This commit is contained in:
Joel Kaasinen 2023-03-06 09:12:56 +02:00
parent c3a3ca9f95
commit 4c990fb44f
4 changed files with 94 additions and 35 deletions

View file

@ -141,7 +141,6 @@
(let [current-opts (merge options opts)] (let [current-opts (merge options opts)]
(json-schema/transform (coercion/-compile-model coercion schema current-opts) (json-schema/transform (coercion/-compile-model coercion schema current-opts)
current-opts)))] current-opts)))]
(merge (merge
(when (seq parameters) (when (seq parameters)
{:parameters {:parameters
@ -165,17 +164,26 @@
:content-type content-type})] :content-type content-type})]
[content-type {:schema schema}]))) [content-type {:schema schema}])))
content-types)}}) content-types)}})
(when request (when request
;; request allow to different :requestBody per content-type ;; request allow to different :requestBody per content-type
{:requestBody {:requestBody
{:content {:content (merge
(when (:body request)
(into {}
(map (fn [content-type]
(let [schema (->schema-object (:body request) {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types))
(into {} (into {}
(map (fn [[content-type requestBody]] (map (fn [[content-type requestBody]]
(let [schema (->schema-object requestBody {:in :requestBody (let [schema (->schema-object requestBody {:in :requestBody
:type :schema :type :schema
:content-type content-type})] :content-type content-type})]
[content-type {:schema schema}]))) [content-type {:schema schema}])))
(:content request))}}) (:content request)))}})
(when responses (when responses
{:responses {:responses
(into {} (into {}

View file

@ -47,7 +47,8 @@
(reify coercion/Coercion (reify coercion/Coercion
(-get-name [_] :schema) (-get-name [_] :schema)
(-get-options [_] opts) (-get-options [_] opts)
(-get-apidocs [this specification {:keys [parameters responses]}] (-get-apidocs [this specification {:keys [parameters responses content-types]
:or {content-types ["application/json"]}}]
;; TODO: this looks identical to spec, refactor when schema is done. ;; TODO: this looks identical to spec, refactor when schema is done.
(case specification (case specification
:swagger (swagger/swagger-spec :swagger (swagger/swagger-spec
@ -77,23 +78,27 @@
[k (coercion/-compile-model this v nil)]))})) [k (coercion/-compile-model this v nil)]))}))
(when (:body parameters) (when (:body parameters)
{:requestBody (openapi/openapi-spec {:requestBody (openapi/openapi-spec
{::openapi/content {"application/json" (:body parameters)}})}) {::openapi/content (zipmap content-types (repeat (:body parameters)))})})
(when (:request parameters) (when (:request parameters)
{:requestBody (openapi/openapi-spec {:requestBody (openapi/openapi-spec
{::openapi/content (:content (:request parameters))})}) {::openapi/content (merge
(when-let [default (get-in parameters [:request :body])]
(zipmap content-types (repeat default)))
(:content (:request parameters)))})})
(when responses (when responses
{:responses {:responses
(into (into
(empty responses) (empty responses)
(for [[k response] responses] (for [[k {:keys [body content] :as response}] responses]
[k (merge [k (merge
(select-keys response [:description]) (select-keys response [:description])
(when (:body response) (when (or body content)
(openapi/openapi-spec (openapi/openapi-spec
{::openapi/content {"application/json" (coercion/-compile-model this (:body response) nil)}})) {::openapi/content (merge
(when (:content response) (when body
(openapi/openapi-spec (zipmap content-types (repeat (coercion/-compile-model this body nil))))
{::openapi/content (:content response)})))]))})) (when response
(:content response)))})))]))}))
(throw (throw
(ex-info (ex-info

View file

@ -86,7 +86,8 @@
(reify coercion/Coercion (reify coercion/Coercion
(-get-name [_] :spec) (-get-name [_] :spec)
(-get-options [_] opts) (-get-options [_] opts)
(-get-apidocs [this specification {:keys [parameters responses]}] (-get-apidocs [this specification {:keys [parameters responses content-types]
:or {content-types ["application/json"]}}]
(case specification (case specification
:swagger (swagger/swagger-spec :swagger (swagger/swagger-spec
(merge (merge
@ -115,27 +116,31 @@
[k (coercion/-compile-model this v nil)]))}) [k (coercion/-compile-model this v nil)]))})
(when (:body parameters) (when (:body parameters)
{:requestBody (openapi/openapi-spec {:requestBody (openapi/openapi-spec
{::openapi/content {"application/json" (coercion/-compile-model this (:body parameters) nil)}})}) {::openapi/content (zipmap content-types (repeat (coercion/-compile-model this (:body parameters) nil)))})})
(when (:request parameters) (when (:request parameters)
{:requestBody (openapi/openapi-spec {:requestBody (openapi/openapi-spec
{::openapi/content (into {} {::openapi/content (merge
(when-let [default (get-in parameters [:request :body])]
(zipmap content-types (repeat (coercion/-compile-model this default nil))))
(into {}
(for [[format model] (:content (:request parameters))] (for [[format model] (:content (:request parameters))]
[format (coercion/-compile-model this model nil)]))})}) [format (coercion/-compile-model this model nil)])))})})
(when responses (when responses
{:responses {:responses
(into (into
(empty responses) (empty responses)
(for [[k response] responses] (for [[k {:keys [body content] :as response}] responses]
[k (merge [k (merge
(select-keys response [:description]) (select-keys response [:description])
(when (:body response) (when (or body content)
(openapi/openapi-spec (openapi/openapi-spec
{::openapi/content {"application/json" (coercion/-compile-model this (:body response) nil)}})) {::openapi/content (merge
(when (:content response) (when body
(openapi/openapi-spec (zipmap content-types (repeat (coercion/-compile-model this (:body response) nil))))
{::openapi/content (into {} (when response
(into {}
(for [[format model] (:content response)] (for [[format model] (:content response)]
[format (coercion/-compile-model this model nil)]))})))]))}))) [format (coercion/-compile-model this model nil)]))))})))]))})))
(throw (throw
(ex-info (ex-info
(str "Can't produce Spec apidocs for " specification) (str "Can't produce Spec apidocs for " specification)

View file

@ -2,6 +2,7 @@
(:require [clojure.test :refer [deftest is testing]] (:require [clojure.test :refer [deftest is testing]]
[jsonista.core :as j] [jsonista.core :as j]
[matcher-combinators.test :refer [match?]] [matcher-combinators.test :refer [match?]]
[matcher-combinators.matchers :as matchers]
[muuntaja.core :as m] [muuntaja.core :as m]
[reitit.coercion.malli :as malli] [reitit.coercion.malli :as malli]
[reitit.coercion.schema :as schema] [reitit.coercion.schema :as schema]
@ -457,3 +458,43 @@
(app (assoc query :body-params {:z 1})) (app (assoc query :body-params {:z 1}))
(catch clojure.lang.ExceptionInfo e (catch clojure.lang.ExceptionInfo e
(select-keys (ex-data e) [:type :in])))))))))))) (select-keys (ex-data e) [:type :in]))))))))))))
(deftest default-content-type-test
(doseq [[coercion ->schema]
[[#'malli/coercion (fn [nom] [:map [nom :string]])]
[#'schema/coercion (fn [nom] {nom s/Str})]
[#'spec/coercion (fn [nom] {nom string?})]]]
(testing coercion
(doseq [content-type ["application/json" "application/edn"]]
(testing (str "default content type " content-type)
(let [app (ring/ring-handler
(ring/router
[["/parameters"
{:post {:coercion @coercion
:content-types [content-type] ;; TODO should this be under :openapi ?
:parameters {:request {:content {"application/transit" (->schema :transit)}
:body (->schema :default)}}
:responses {200 {:content {"application/transit" (->schema :transit)}
:body (->schema :default)}}
:handler (fn [req]
{:status 200
:body (-> req :parameters :request)})}}]
["/openapi.json"
{:get {:handler (openapi/create-openapi-handler)
:no-doc true}}]]
{:data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware]}}))
spec (-> {:request-method :get
:uri "/openapi.json"}
app
:body)]
(testing "body parameter"
(is (match? (matchers/in-any-order [content-type "application/transit"])
(-> spec
(get-in [:paths "/parameters" :post :requestBody :content])
keys))))
(testing "body response"
(is (match? (matchers/in-any-order [content-type "application/transit"])
(-> spec
(get-in [:paths "/parameters" :post :responses 200 :content])
keys))))))))))