wrap :content schemas in :schema

This commit is contained in:
Tommi Reiman 2023-05-27 19:01:17 +03:00
parent 499f84be21
commit d17c97780e
8 changed files with 114 additions and 104 deletions

View file

@ -91,9 +91,9 @@
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in) (let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
->open (if open? #(-open-model coercion %) identity) ->open (if open? #(-open-model coercion %) identity)
format-schema-pairs (if (= :request style) format-schema-pairs (if (= :request style)
(conj (:content model) [:default (:body model)]) (conj (:content model) [:default {:schema (:body model)}])
[[:default model]]) [[:default {:schema model}]])
format->coercer (some->> (for [[format schema] format-schema-pairs format->coercer (some->> (for [[format {:keys [schema]}] format-schema-pairs
:when schema :when schema
:let [type (case style :request :body style)]] :let [type (case style :request :body style)]]
[format (-request-coercer coercion type (->open schema))]) [format (-request-coercer coercion type (->open schema))])
@ -118,7 +118,7 @@
(defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result] (defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result]
:or {extract-response-format extract-response-format-default}}] :or {extract-response-format extract-response-format-default}}]
(if coercion (if coercion
(let [per-format-coercers (some->> (for [[format schema] content (let [per-format-coercers (some->> (for [[format {:keys [schema]}] content
:when schema] :when schema]
[format (-response-coercer coercion schema)]) [format (-response-coercer coercion schema)])
(filter second) (filter second)

View file

@ -82,8 +82,11 @@
(s/def :reitit.core.coercion/model any?) (s/def :reitit.core.coercion/model any?)
(s/def :reitit.core.coercion/schema any?)
(s/def :reitit.core.coercion/map-model (s/keys :opt-un [:reitit.core.coercion/schema]))
(s/def :reitit.core.coercion/content (s/def :reitit.core.coercion/content
(s/map-of string? :reitit.core.coercion/model)) (s/map-of string? :reitit.core.coercion/map-model))
(s/def :reitit.core.coercion/query :reitit.core.coercion/model) (s/def :reitit.core.coercion/query :reitit.core.coercion/model)
(s/def :reitit.core.coercion/body :reitit.core.coercion/model) (s/def :reitit.core.coercion/body :reitit.core.coercion/model)

View file

@ -134,7 +134,7 @@
:options nil}) :options nil})
(defn -get-apidocs-openapi (defn -get-apidocs-openapi
[coercion {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options] [_ {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options]
(let [{:keys [body request multipart]} parameters (let [{:keys [body request multipart]} parameters
parameters (dissoc parameters :request :body :multipart) parameters (dissoc parameters :request :body :multipart)
->schema-object (fn [schema opts] ->schema-object (fn [schema opts]
@ -177,10 +177,10 @@
[content-type {:schema schema}]))) [content-type {:schema schema}])))
content-types)) content-types))
(into {} (into {}
(map (fn [[content-type requestBody]] (map (fn [[content-type {:keys [schema]}]]
(let [schema (->schema-object requestBody {:in :requestBody (let [schema (->schema-object schema {: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 multipart (when multipart
@ -207,7 +207,7 @@
content-types)) content-types))
(when content (when content
(into {} (into {}
(map (fn [[content-type schema]] (map (fn [[content-type {:keys [schema]}]]
(let [schema (->schema-object schema {:in :responses (let [schema (->schema-object schema {:in :responses
:type :schema :type :schema
:content-type content-type})] :content-type content-type})]

View file

@ -39,12 +39,12 @@
[[http-method? :responses any? :body] f] [[http-method? :responses any? :body] f]
;; openapi3 parameters and responses ;; openapi3 parameters and responses
[[:parameters :request :content any?] f] [[:parameters :request :content any? :schema] f]
[[http-method? :parameters :request :content any?] f] [[http-method? :parameters :request :content any? :schema] f]
[[:parameters :request :body] f] [[:parameters :request :body] f]
[[http-method? :parameters :request :body] f] [[http-method? :parameters :request :body] f]
[[:responses any? :content any?] f] [[:responses any? :content any? :schema] f]
[[http-method? :responses any? :content any?] f]])) [[http-method? :responses any? :content any? :schema] f]]))
(defn -compile-coercion [{:keys [coercion] :as data}] (defn -compile-coercion [{:keys [coercion] :as data}]
(cond-> data coercion (impl/path-update (-update-paths #(coercion/-compile-model coercion % nil))))) (cond-> data coercion (impl/path-update (-update-paths #(coercion/-compile-model coercion % nil)))))

View file

@ -47,9 +47,9 @@
(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 content-types] (-get-apidocs [_ specification {:keys [parameters responses content-types]
:or {content-types ["application/json"]}}] :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
(merge (merge
@ -62,35 +62,38 @@
(for [[k response] responses] (for [[k response] responses]
[k (set/rename-keys response {:body :schema})]))}))) [k (set/rename-keys response {:body :schema})]))})))
:openapi (merge :openapi (merge
(when (seq (dissoc parameters :body :request :multipart)) (when (seq (dissoc parameters :body :request :multipart))
(openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)})) (openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)}))
(when (:body parameters) (when (:body parameters)
{:requestBody (openapi/openapi-spec {:requestBody (openapi/openapi-spec
{::openapi/content (zipmap content-types (repeat (: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 (merge {::openapi/content (merge
(when-let [default (get-in parameters [:request :body])] (when-let [default (get-in parameters [:request :body])]
(zipmap content-types (repeat default))) (zipmap content-types (repeat default)))
(:content (:request parameters)))})}) (->> (for [[content-type {:keys [schema]}] (:content (:request parameters))]
(when (:multipart parameters) [content-type schema])
{:requestBody (into {})))})})
(openapi/openapi-spec (when (:multipart parameters)
{::openapi/content {"multipart/form-data" (:multipart parameters)}})}) {:requestBody
(when responses (openapi/openapi-spec
{:responses {::openapi/content {"multipart/form-data" (:multipart parameters)}})})
(into (when responses
(empty responses) {:responses
(for [[k {:keys [body content] :as response}] responses] (into
[k (merge (empty responses)
(select-keys response [:description]) (for [[k {:keys [body content] :as response}] responses]
(when (or body content) [k (merge
(openapi/openapi-spec (select-keys response [:description])
{::openapi/content (merge (when (or body content)
(when body (openapi/openapi-spec
(zipmap content-types (repeat body))) {::openapi/content (merge
(when response (when body
(:content response)))})))]))})) (zipmap content-types (repeat body)))
(->> (for [[content-type {:keys [schema]}] (:content response)]
[content-type schema])
(into {})))})))]))}))
(throw (throw
(ex-info (ex-info

View file

@ -113,7 +113,9 @@
{::openapi/content (merge {::openapi/content (merge
(when-let [default (get-in parameters [:request :body])] (when-let [default (get-in parameters [:request :body])]
(zipmap content-types (repeat default))) (zipmap content-types (repeat default)))
(:content (:request parameters)))})}) (->> (for [[content-type {:keys [schema]}] (:content (:request parameters))]
[content-type schema])
(into {})))})})
(when (:multipart parameters) (when (:multipart parameters)
{:requestBody {:requestBody
(openapi/openapi-spec (openapi/openapi-spec
@ -130,8 +132,9 @@
{::openapi/content (merge {::openapi/content (merge
(when body (when body
(zipmap content-types (repeat (:body response)))) (zipmap content-types (repeat (:body response))))
(when response (->> (for [[content-type {:keys [schema]}] (:content response)]
(:content response)))})))]))})) [content-type schema])
(into {})))})))]))}))
(throw (throw
(ex-info (ex-info
(str "Can't produce Spec apidocs for " specification) (str "Can't produce Spec apidocs for " specification)

View file

@ -101,7 +101,7 @@
{:get {:summary "plus" {:get {:summary "plus"
:tags [:plus :schema] :tags [:plus :schema]
:parameters {:query {:x s/Int, :y s/Int} :parameters {:query {:x s/Int, :y s/Int}
:path {:z s/Int}} :path {:z s/Int}}
:openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}}
:description "kosh"}}} :description "kosh"}}}
:responses {200 {:description "success" :responses {200 {:description "success"
@ -566,20 +566,20 @@
(deftest per-content-type-test (deftest per-content-type-test
(doseq [[coercion ->schema] (doseq [[coercion ->schema]
[[#'malli/coercion (fn [nom] [:map [nom :string]])] [[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)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
[["/parameters" [["/parameters"
{:post {:description "parameters" {:post {:description "parameters"
:coercion @coercion :coercion coercion
:parameters {:request {:content {"application/json" (->schema :b) :parameters {:request {:content {"application/json" {:schema (->schema :b)}
"application/edn" (->schema :c)}}} "application/edn" {:schema (->schema :c)}}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:content {"application/json" (->schema :ok) :content {"application/json" {:schema (->schema :ok)}
"application/edn" (->schema :edn)}}} "application/edn" {:schema (->schema :edn)}}}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :request)})}}] :body (-> req :parameters :request)})}}]
@ -594,41 +594,42 @@
spec (-> {:request-method :get spec (-> {:request-method :get
:uri "/openapi.json"} :uri "/openapi.json"}
app app
:body)] :body)
spec-coercion (= coercion spec/coercion)]
(testing "body parameter" (testing "body parameter"
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:b {:type "string"}} :properties {:b {:type "string"}}
:required ["b"]} :required ["b"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema])
normalize))) normalize)))
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:c {:type "string"}} :properties {:c {:type "string"}}
:required ["c"]} :required ["c"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema]) (get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema])
normalize)))) normalize))))
(testing "body response" (testing "body response"
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:ok {:type "string"}} :properties {:ok {:type "string"}}
:required ["ok"]} :required ["ok"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema])
normalize))) normalize)))
(is (match? (merge {:type "object" (is (= (merge {:type "object"
:properties {:edn {:type "string"}} :properties {:edn {:type "string"}}
:required ["edn"]} :required ["edn"]}
(when-not (#{#'spec/coercion} coercion) (when-not spec-coercion
{:additionalProperties false})) {:additionalProperties false}))
(-> spec (-> spec
(get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema]) (get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema])
normalize)))) normalize))))
(testing "validation" (testing "validation"
(let [query {:request-method :post (let [query {:request-method :post
:uri "/parameters" :uri "/parameters"
@ -654,22 +655,22 @@
(deftest default-content-type-test (deftest default-content-type-test
(doseq [[coercion ->schema] (doseq [[coercion ->schema]
[[#'malli/coercion (fn [nom] [:map [nom :string]])] [[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 coercion (testing (str coercion)
(doseq [content-type ["application/json" "application/edn"]] (doseq [content-type ["application/json" "application/edn"]]
(testing (str "default content type " content-type) (testing (str "default content type " content-type)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
[["/parameters" [["/parameters"
{:post {:description "parameters" {:post {:description "parameters"
:coercion @coercion :coercion coercion
:content-types [content-type] ;; TODO should this be under :openapi ? :content-types [content-type] ;; TODO should this be under :openapi ?
:parameters {:request {:content {"application/transit" (->schema :transit)} :parameters {:request {:content {"application/transit" {:schema (->schema :transit)}}
:body (->schema :default)}} :body (->schema :default)}}
:responses {200 {:description "success" :responses {200 {:description "success"
:content {"application/transit" (->schema :transit)} :content {"application/transit" {:schema (->schema :transit)}}
:body (->schema :default)}} :body (->schema :default)}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200

View file

@ -608,16 +608,16 @@
(testing (str coercion) (testing (str coercion)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
["/foo" {:post {:parameters {:request {:content {"application/json" json-request ["/foo" {:post {:parameters {:request {:content {"application/json" {:schema json-request}
"application/edn" edn-request} "application/edn" {:schema edn-request}}
:body default-request}} :body default-request}}
:responses {200 {:content {"application/json" json-response :responses {200 {:content {"application/json" {:schema json-response}
"application/edn" edn-response} "application/edn" {:schema edn-response}}
:body default-response}} :body default-response}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :request)})}}] :body (-> req :parameters :request)})}}]
{#_#_:validate reitit.ring.spec/validate {:validate reitit.ring.spec/validate
:data {:middleware [rrc/coerce-request-middleware :data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware] rrc/coerce-response-middleware]
:coercion coercion}})) :coercion coercion}}))