Merge pull request #628 from metosin/openapi-parameters

Openapi parameters
This commit is contained in:
Tommi Reiman 2023-08-24 09:25:46 +03:00 committed by GitHub
commit b0c810a981
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 429 additions and 284 deletions

View file

@ -157,21 +157,21 @@ You can also specify request and response body schemas per content-type. The syn
```clj ```clj
(def app (def app
(ring/ring-handler (ring/ring-handler
(ring/router (ring/router
["/api" ["/api"
["/example" {:post {:coercion reitit.coercion.schema/coercion ["/example" {:post {:coercion reitit.coercion.schema/coercion
:parameters {:request {:content {"application/json" {:y s/Int} :request {:content {"application/json" {:schema {:y s/Int}}
"application/edn" {:z s/Int}} "application/edn" {:schema {:z s/Int}}}
;; default if no content-type matches: ;; default if no content-type matches:
:body {:yy s/Int}}} :body {:yy s/Int}}
:responses {200 {:content {"application/json" {:w s/Int} :responses {200 {:content {"application/json" {:schema {:w s/Int}}
"application/edn" {:x s/Int}} "application/edn" {:schema {:x s/Int}}}
;; default if no content-type matches: ;; default if no content-type matches:
:body {:ww s/Int}} :body {:ww s/Int}}}
:handler ...}}]] :handler ...}}]]
{:data {:middleware [rrc/coerce-exceptions-middleware {:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware rrc/coerce-request-middleware
rrc/coerce-response-middleware]}}))) rrc/coerce-response-middleware]}})))
``` ```
## Pretty printing spec errors ## Pretty printing spec errors

View file

@ -37,7 +37,6 @@
(def ^:no-doc default-parameter-coercion (def ^:no-doc default-parameter-coercion
{:query (->ParameterCoercion :query-params :string true true) {:query (->ParameterCoercion :query-params :string true true)
:body (->ParameterCoercion :body-params :body false false) :body (->ParameterCoercion :body-params :body false false)
:request (->ParameterCoercion :body-params :request false false)
:form (->ParameterCoercion :form-params :string true true) :form (->ParameterCoercion :form-params :string true true)
:header (->ParameterCoercion :headers :string true true) :header (->ParameterCoercion :headers :string true true)
:path (->ParameterCoercion :path-params :string true true) :path (->ParameterCoercion :path-params :string true true)
@ -83,34 +82,53 @@
value) value)
;; TODO: support faster key walking, walk/keywordize-keys is quite slow... ;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
(defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result] (defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result skip]
:or {extract-request-format extract-request-format-default :or {extract-request-format extract-request-format-default
parameter-coercion default-parameter-coercion}}] parameter-coercion default-parameter-coercion
skip #{}}}]
(if coercion (if coercion
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] (when-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in) (when-not (skip style)
->open (if open? #(-open-model coercion %) identity) (let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
format-schema-pairs (if (= :request style) ->open (if open? #(-open-model coercion %) identity)
(conj (:content model) [:default (:body model)]) coercer (-request-coercer coercion style (->open model))]
[[:default model]]) (when coercer
format->coercer (some->> (for [[format schema] format-schema-pairs (fn [request]
:when schema (let [value (transform request)
:let [type (case style :request :body style)]] format (extract-request-format request)
[format (-request-coercer coercion type (->open schema))]) result (coercer value format)]
(filter second) (if (error? result)
(seq) (request-coercion-failed! result coercion value in request serialize-failed-result)
(into {}))] result)))))))))
(when format->coercer
(fn [request] (defn get-default-schema [request-or-response]
(let [value (transform request) (or (-> request-or-response :content :default :schema)
format (extract-request-format request) (:body request-or-response)))
coercer (or (format->coercer format)
(format->coercer :default) (defn get-default [request-or-response]
-identity-coercer) (or (-> request-or-response :content :default)
result (coercer value format)] (some->> request-or-response :body (assoc {} :schema))))
(if (error? result)
(request-coercion-failed! result coercion value in request serialize-failed-result) (defn content-request-coercer [coercion {:keys [content body]} {::keys [extract-request-format serialize-failed-result]
result)))))))) :or {extract-request-format extract-request-format-default}}]
(when coercion
(let [in :body-params
format->coercer (some->> (concat (when body
[[:default (-request-coercer coercion :body body)]])
(for [[format {:keys [schema]}] content, :when schema]
[format (-request-coercer coercion :body schema)]))
(filter second) (seq) (into (array-map)))]
(when format->coercer
(fn [request]
(let [value (in request)
format (extract-request-format request)
coercer (or (format->coercer format)
(format->coercer :default)
-identity-coercer)
result (coercer value format)]
(if (error? result)
(request-coercion-failed! result coercion value in request serialize-failed-result)
result)))))))
(defn extract-response-format-default [request _] (defn extract-response-format-default [request _]
(-> request :muuntaja/response :format)) (-> request :muuntaja/response :format))
@ -118,18 +136,18 @@
(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 [format->coercer (some->> (concat (when body
:when schema] [[:default (-response-coercer coercion body)]])
[format (-response-coercer coercion schema)]) (for [[format {:keys [schema]}] content, :when schema]
(filter second) [format (-response-coercer coercion schema)]))
(seq) (filter second) (seq) (into (array-map)))]
(into {})) (when format->coercer
default (when body (-response-coercer coercion body))]
(when (or per-format-coercers default)
(fn [request response] (fn [request response]
(let [format (extract-response-format request response) (let [format (extract-response-format request response)
value (:body response) value (:body response)
coercer (get per-format-coercers format (or default -identity-coercer)) coercer (or (format->coercer format)
(format->coercer :default)
-identity-coercer)
result (coercer value format)] result (coercer value format)]
(if (error? result) (if (error? result)
(response-coercion-failed! result coercion value request response serialize-failed-result) (response-coercion-failed! result coercion value request response serialize-failed-result)
@ -153,10 +171,15 @@
(impl/fast-assoc response :body (coercer request response)) (impl/fast-assoc response :body (coercer request response))
response))) response)))
(defn request-coercers [coercion parameters opts] (defn request-coercers
(some->> (for [[k v] parameters, :when v] ([coercion parameters opts]
[k (request-coercer coercion k v opts)]) (some->> (for [[k v] parameters, :when v]
(filter second) (seq) (into {}))) [k (request-coercer coercion k v opts)])
(filter second) (seq) (into {})))
([coercion parameters route-request opts]
(let [crc (when route-request (some->> (content-request-coercer coercion route-request opts) (array-map :request)))
rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))]
(if (and crc rcs) (into crc (vec rcs)) (or crc rcs)))))
(defn response-coercers [coercion responses opts] (defn response-coercers [coercion responses opts]
(some->> (for [[status model] responses] (some->> (for [[status model] responses]
@ -170,8 +193,8 @@
;; api-docs ;; api-docs
;; ;;
(defn -warn-unsupported-coercions [{:keys [parameters responses] :as _data}] (defn -warn-unsupported-coercions [{:keys [request responses] :as _data}]
(when (:request parameters) (when request
(println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion")) (println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion"))
(when (some :content (vals responses)) (when (some :content (vals responses))
(println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion"))) (println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion")))
@ -197,7 +220,6 @@
(into {})))) (into {}))))
(-get-apidocs coercion specification)))))) (-get-apidocs coercion specification))))))
;; ;;
;; integration ;; integration
;; ;;

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 (s/or :string string?, :default #{:default}) :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

@ -10,15 +10,15 @@
[] []
{:name ::coerce-request {:name ::coerce-request
:spec ::rs/parameters :spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts] :compile (fn [{:keys [coercion parameters request]} opts]
(cond (cond
;; no coercion, skip ;; no coercion, skip
(not coercion) nil (not coercion) nil
;; just coercion, don't mount ;; just coercion, don't mount
(not parameters) {} (not (or parameters request)) {}
;; mount ;; mount
:else :else
(if-let [coercers (coercion/request-coercers coercion parameters opts)] (if-let [coercers (coercion/request-coercers coercion parameters request opts)]
{:enter (fn [ctx] {:enter (fn [ctx]
(let [request (:request ctx) (let [request (:request ctx)
coerced (coercion/coerce-request coercers request) coerced (coercion/coerce-request coercers request)

View file

@ -133,13 +133,22 @@
;; malli options ;; malli options
:options nil}) :options nil})
;; TODO: this is now seems like a generic transforming function that could be used in all of malli, spec, schema
;; ... just tranform the schemas in place
;; also, this has internally massive amount of duplicate code, could be simplified
;; ... tests too
(defn -get-apidocs-openapi (defn -get-apidocs-openapi
[coercion {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options] [_ {:keys [request parameters responses content-types] :or {content-types ["application/json"]}} options]
(let [{:keys [body request multipart]} parameters (let [{:keys [body multipart]} parameters
parameters (dissoc parameters :request :body :multipart) parameters (dissoc parameters :request :body :multipart)
->schema-object (fn [schema opts] ->schema-object (fn [schema opts]
(let [current-opts (merge options opts)] (let [current-opts (merge options opts)]
(json-schema/transform schema current-opts)))] (json-schema/transform schema current-opts)))
->content (fn [data schema]
(merge
{:schema schema}
(select-keys data [:description :examples])
(:openapi data)))]
(merge (merge
(when (seq parameters) (when (seq parameters)
{:parameters {:parameters
@ -168,20 +177,21 @@
;; request allow to different :requestBody per content-type ;; request allow to different :requestBody per content-type
{:requestBody {:requestBody
{:content (merge {:content (merge
(when (:body request) (select-keys request [:description])
(when-let [{:keys [schema] :as data} (coercion/get-default request)]
(into {} (into {}
(map (fn [content-type] (map (fn [content-type]
(let [schema (->schema-object (:body request) {: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 (->content data schema)])))
content-types)) content-types))
(into {} (into {}
(map (fn [[content-type requestBody]] (map (fn [[content-type {:keys [schema] :as data}]]
(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 (->content data schema)])))
(:content request)))}}) (:content request)))}})
(when multipart (when multipart
{:requestBody {:requestBody
@ -194,29 +204,30 @@
(when responses (when responses
{:responses {:responses
(into {} (into {}
(map (fn [[status {:keys [body content] (map (fn [[status {:keys [content], :as response}]]
:as response}]] (let [default (coercion/get-default-schema response)
(let [content (merge content (-> (merge
(when body (when default
(into {} (into {}
(map (fn [content-type] (map (fn [content-type]
(let [schema (->schema-object body {:in :responses (let [schema (->schema-object default {:in :responses
:type :schema :type :schema
:content-type content-type})] :content-type content-type})]
[content-type {:schema schema}]))) [content-type (->content nil schema)])))
content-types)) content-types))
(when content (when content
(into {} (into {}
(map (fn [[content-type schema]] (map (fn [[content-type {:keys [schema] :as data}]]
(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})]
[content-type {:schema schema}]))) [content-type (->content data schema)])))
content)))] content)))
(dissoc :default))]
[status (merge (select-keys response [:description]) [status (merge (select-keys response [:description])
(when content (when content
{:content content}))]))) {:content content}))]))
responses)})))) responses))}))))
(defn create (defn create
([] ([]

View file

@ -32,19 +32,25 @@
(defn -update-paths [f] (defn -update-paths [f]
(let [not-request? #(not= :request %) (let [not-request? #(not= :request %)
http-method? #(contains? http-methods %)] http-method? #(contains? http-methods %)]
[;; default parameters and responses [;; default parameters
[[:parameters not-request?] f] [[:parameters not-request?] f]
[[http-method? :parameters not-request?] f] [[http-method? :parameters not-request?] f]
;; default responses
[[:responses any? :body] f] [[:responses any? :body] f]
[[http-method? :responses any? :body] f] [[http-method? :responses any? :body] f]
;; openapi3 parameters and responses ;; openapi3 request
[[:parameters :request :content any?] f] [[:request :content any? :schema] f]
[[http-method? :parameters :request :content any?] f] [[http-method? :request :content any? :schema] f]
[[:parameters :request :body] f]
[[http-method? :parameters :request :body] f] ;; openapi3 LEGACY body
[[:responses any? :content any?] f] [[:request :body] f]
[[http-method? :responses any? :content any?] f]])) [[http-method? :request :body] f]
;; openapi3 responses
[[:responses any? :content any? :schema] 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

@ -24,15 +24,15 @@
and :parameters from route data, otherwise does not mount." and :parameters from route data, otherwise does not mount."
{:name ::coerce-request {:name ::coerce-request
:spec ::rs/parameters :spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts] :compile (fn [{:keys [coercion parameters request]} opts]
(cond (cond
;; no coercion, skip ;; no coercion, skip
(not coercion) nil (not coercion) nil
;; just coercion, don't mount ;; just coercion, don't mount
(not parameters) {} (not (or parameters request)) {}
;; mount ;; mount
:else :else
(if-let [coercers (coercion/request-coercers coercion parameters opts)] (if-let [coercers (coercion/request-coercers coercion parameters request opts)]
(fn [handler] (fn [handler]
(fn (fn
([request] ([request]

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 [request 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,40 @@
(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
{: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 (coercion/get-default-schema request)]
(zipmap content-types (repeat default))) (zipmap content-types (repeat default)))
(:content (:request parameters)))})}) (->> (for [[content-type {:keys [schema]}] (:content request)]
(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 [content] :as response}] responses
(when (or body content) :let [default (coercion/get-default-schema response)]]
(openapi/openapi-spec [k (merge
{::openapi/content (merge (select-keys response [:description])
(when body (when (or content default)
(zipmap content-types (repeat body))) (openapi/openapi-spec
(when response {::openapi/content (-> (merge
(:content response)))})))]))})) (when default
(zipmap content-types (repeat default)))
(->> (for [[content-type {:keys [schema]}] content]
[content-type schema])
(into {})))
(dissoc :default))})))]))}))
(throw (throw
(ex-info (ex-info

View file

@ -88,8 +88,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 content-types] (-get-apidocs [_ specification {:keys [request parameters responses content-types]
:or {content-types ["application/json"]}}] :or {content-types ["application/json"]}}]
(case specification (case specification
:swagger (swagger/swagger-spec :swagger (swagger/swagger-spec
(merge (merge
@ -108,12 +108,14 @@
(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
{: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 (coercion/get-default-schema request)]
(zipmap content-types (repeat default))) (zipmap content-types (repeat default)))
(:content (:request parameters)))})}) (->> (for [[content-type {:keys [schema]}] (:content request)]
[content-type schema])
(into {})))})})
(when (:multipart parameters) (when (:multipart parameters)
{:requestBody {:requestBody
(openapi/openapi-spec (openapi/openapi-spec
@ -122,16 +124,20 @@
{:responses {:responses
(into (into
(empty responses) (empty responses)
(for [[k {:keys [body content] :as response}] responses] (for [[k {:keys [content] :as response}] responses
:let [default (coercion/get-default-schema response)
content-types (remove #{:default} content-types)]]
[k (merge [k (merge
(select-keys response [:description]) (select-keys response [:description])
(when (or body content) (when (or content default)
(openapi/openapi-spec (openapi/openapi-spec
{::openapi/content (merge {::openapi/content (-> (merge
(when body (when default
(zipmap content-types (repeat (:body response)))) (zipmap content-types (repeat default)))
(when response (->> (for [[content-type {:keys [schema]}] content]
(:content response)))})))]))})) [content-type schema])
(into {})))
(dissoc :default))})))]))}))
(throw (throw
(ex-info (ex-info
(str "Can't produce Spec apidocs for " specification) (str "Can't produce Spec apidocs for " specification)

View file

@ -108,6 +108,7 @@
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
[metosin/ring-http-response "0.9.3"] [metosin/ring-http-response "0.9.3"]
[metosin/ring-swagger-ui "4.18.1"] [metosin/ring-swagger-ui "4.18.1"]
[org.clojure/tools.analyzer "1.1.1"]
[criterium "0.4.6"] [criterium "0.4.6"]
[org.clojure/test.check "1.1.1"] [org.clojure/test.check "1.1.1"]

View file

@ -65,7 +65,10 @@
:description "kosh"}}} :description "kosh"}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:body {:total int?}} :body {:total int?}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:handler (fn [{{{:keys [z]} :path :handler (fn [{{{:keys [z]} :path
xs :body} :parameters}] xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
@ -91,7 +94,10 @@
:content {"application/json" {:schema {:type "string"}}}}}} :content {"application/json" {:schema {:type "string"}}}}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:body [:map [:total int?]]} :body [:map [:total int?]]}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:handler (fn [{{{:keys [z]} :path :handler (fn [{{{:keys [z]} :path
xs :body} :parameters}] xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
@ -101,7 +107,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"
@ -117,7 +123,10 @@
:description "kosh"}}} :description "kosh"}}}
:responses {200 {:description "success" :responses {200 {:description "success"
:body {:total s/Int}} :body {:total s/Int}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error s/Str}}}
:body {:masked s/Str}}}
:handler (fn [{{{:keys [z]} :path :handler (fn [{{{:keys [z]} :path
xs :body} :parameters}] xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]]
@ -193,7 +202,11 @@
:type "object"}}}} :type "object"}}}}
400 {:content {"application/json" {:schema {:type "string"}}} 400 {:content {"application/json" {:schema {:type "string"}}}
:description "kosh"} :description "kosh"}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:summary "plus with body"}} :summary "plus with body"}}
"/api/malli/plus/{z}" {:get {:parameters [{:in "query" "/api/malli/plus/{z}" {:get {:parameters [{:in "query"
:name :x :name :x
@ -231,7 +244,12 @@
:type "object"}}}} :type "object"}}}}
400 {:description "kosh" 400 {:description "kosh"
:content {"application/json" {:schema {:type "string"}}}} :content {"application/json" {:schema {:type "string"}}}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {:error {:type "string"}}
:required [:error]
:type "object"}}}}}
:summary "plus with body"}} :summary "plus with body"}}
"/api/schema/plus/{z}" {:get {:parameters [{:description "" "/api/schema/plus/{z}" {:get {:parameters [{:description ""
:in "query" :in "query"
@ -280,10 +298,15 @@
:type "object"}}}} :type "object"}}}}
400 {:description "kosh" 400 {:description "kosh"
:content {"application/json" {:schema {:type "string"}}}} :content {"application/json" {:schema {:type "string"}}}}
500 {:description "fail"}} 500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:summary "plus with body"}}}}] :summary "plus with body"}}}}]
(is (= expected spec)) (is (= expected spec))
(is (nil? (validate spec)))))) (is (= nil (validate spec))))))
(defn spec-paths [app uri] (defn spec-paths [app uri]
(-> {:request-method :get, :uri uri} app :body :paths keys)) (-> {:request-method :get, :uri uri} app :body :paths keys))
@ -457,8 +480,8 @@
[["/examples" [["/examples"
{:post {:decription "examples" {:post {:decription "examples"
:coercion @coercion :coercion @coercion
:parameters {:query (->schema :q) :request {:body (->schema :b)}
:request {:body (->schema :b)}} :parameters {:query (->schema :q)}
:responses {200 {:description "success" :responses {200 {:description "success"
:body (->schema :ok)}} :body (->schema :ok)}}
:openapi {:requestBody :openapi {:requestBody
@ -517,20 +540,19 @@
(is (nil? (validate spec)))))))) (is (nil? (validate spec))))))))
(deftest multipart-test (deftest multipart-test
(doseq [[coercion file-schema string-schema] (doseq [[coercion file-schema string-schema] [[#'malli/coercion
[[#'malli/coercion reitit.ring.malli/bytes-part
reitit.ring.malli/bytes-part :string]
:string] [#'schema/coercion
[#'schema/coercion (schema-tools.core/schema {:filename s/Str
(schema-tools.core/schema {:filename s/Str :content-type s/Str
:content-type s/Str :bytes s/Num}
:bytes s/Num} {:openapi {:type "string"
{:openapi {:type "string" :format "binary"}})
:format "binary"}}) s/Str]
s/Str] [#'spec/coercion
[#'spec/coercion reitit.http.interceptors.multipart/bytes-part
reitit.http.interceptors.multipart/bytes-part string?]]]
string?]]]
(testing (str coercion) (testing (str coercion)
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router
@ -565,21 +587,20 @@
(is (nil? (validate spec)))))))) (is (nil? (validate spec))))))))
(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) :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 +615,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"
@ -653,23 +675,22 @@
(is (nil? (validate spec)))))))) (is (nil? (validate spec))))))))
(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 (str coercion)
(testing 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)} :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
@ -707,16 +728,15 @@
[["/parameters" [["/parameters"
{:post {:description "parameters" {:post {:description "parameters"
:coercion malli/coercion :coercion malli/coercion
:parameters {:request :request {:body
{:body [:schema
[:schema {:registry {"friend" [:map
{:registry {"friend" [:map [:age int?]
[:age int?] [:pet [:ref "pet"]]]
[:pet [:ref "pet"]]] "pet" [:map
"pet" [:map [:name :string]
[:name :string] [:friends [:vector [:ref "friend"]]]]}}
[:friends [:vector [:ref "friend"]]]]}} "friend"]}
"friend"]}}
:handler (fn [req] :handler (fn [req]
{:status 200 {:status 200
:body (-> req :parameters :request)})}}] :body (-> req :parameters :request)})}}]
@ -754,3 +774,53 @@
spec)) spec))
(testing "spec is valid" (testing "spec is valid"
(is (nil? (validate spec)))))) (is (nil? (validate spec))))))
(deftest openapi-malli-tests
(let [app (ring/ring-handler
(ring/router
[["/openapi.json"
{:get {:no-doc true
:handler (openapi/create-openapi-handler)}}]
["/malli" {:coercion malli/coercion}
["/plus" {:post {:summary "plus with body"
:request {:description "body description"
:content {"application/json" {:schema {:x int?, :y int?}
:examples {"1+1" {:x 1, :y 1}
"1+2" {:x 1, :y 2}}
:openapi {:example {:x 2, :y 2}}}}}
:responses {200 {:description "success"
:content {"application/json" {:schema {:total int?}
:examples {"2" {:total 2}
"3" {:total 3}}
:openapi {:example {:total 4}}}}}}
:handler (fn [request]
(let [{:keys [x y]} (-> request :parameters :body)]
{:status 200, :body {:total (+ x y)}}))}}]]]
{:validate reitit.ring.spec/validate
:data {:middleware [openapi/openapi-feature
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}}))]
(is (= {"/malli/plus" {:post {:requestBody {:content {:description "body description",
"application/json" {:schema {:type "object",
:properties {:x {:type "integer"},
:y {:type "integer"}},
:required [:x :y],
:additionalProperties false},
:examples {"1+1" {:x 1, :y 1}, "1+2" {:x 1, :y 2}},
:example {:x 2, :y 2}}}},
:responses {200 {:description "success",
:content {"application/json" {:schema {:type "object",
:properties {:total {:type "integer"}},
:required [:total],
:additionalProperties false},
:examples {"2" {:total 2}, "3" {:total 3}},
:example {:total 4}}}}},
:summary "plus with body"}}})
(-> {:request-method :get
:uri "/openapi.json"}
(app)
:body
:paths))))

View file

@ -606,53 +606,74 @@
{:request any? :response (clojure.spec.alpha/spec #{:end})} {:request any? :response (clojure.spec.alpha/spec #{:end})}
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]] {:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
(testing (str coercion) (testing (str coercion)
(let [app (ring/ring-handler (doseq [{:keys [name app]}
(ring/router [{:name "using top-level :body"
["/foo" {:post {:parameters {:request {:content {"application/json" json-request :app (ring/ring-handler
"application/edn" edn-request} (ring/router
:body default-request}} ["/foo" {:post {:request {:content {"application/json" {:schema json-request}
:responses {200 {:content {"application/json" json-response "application/edn" {:schema edn-request}}
"application/edn" edn-response} :body default-request}
:body default-response}} :responses {200 {:content {"application/json" {:schema json-response}
:handler (fn [req] "application/edn" {:schema edn-response}}
{:status 200 :body default-response}}
:body (-> req :parameters :request)})}}] :handler (fn [req]
{#_#_:validate reitit.ring.spec/validate {:status 200
:data {:middleware [rrc/coerce-request-middleware :body (-> req :parameters :request)})}}]
rrc/coerce-response-middleware] {:validate reitit.ring.spec/validate
:coercion coercion}})) :data {:middleware [rrc/coerce-request-middleware
call (fn [request] rrc/coerce-response-middleware]
(try :coercion coercion}}))}
(app request) {:name "using :default content"
(catch ExceptionInfo e :app (ring/ring-handler
(select-keys (ex-data e) [:type :in])))) (ring/router
request (fn [request-format response-format body] ["/foo" {:post {:request {:content {"application/json" {:schema json-request}
{:request-method :post "application/edn" {:schema edn-request}
:uri "/foo" :default {:schema default-request}}
:muuntaja/request {:format request-format} :body json-request} ;; not applied as :default exists
:muuntaja/response {:format response-format} :responses {200 {:content {"application/json" {:schema json-response}
:body-params body})] "application/edn" {:schema edn-response}
(testing "succesful call" :default {:schema default-response}}
(is (= {:status 200 :body {:request :json, :response :json}} :body json-response}} ;; not applied as :default exists
(call (request "application/json" "application/json" {:request :json :response :json})))) :handler (fn [req]
(is (= {:status 200 :body {:request :edn, :response :json}} {:status 200
(call (request "application/edn" "application/json" {:request :edn :response :json})))) :body (-> req :parameters :request)})}}]
(is (= {:status 200 :body {:request :default, :response :default}} {:validate reitit.ring.spec/validate
(call (request "application/transit" "application/transit" {:request :default :response :default}))))) :data {:middleware [rrc/coerce-request-middleware
(testing "request validation fails" rrc/coerce-response-middleware]
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} :coercion coercion}}))}]]
(call (request "application/edn" "application/json" {:request :json :response :json})))) (testing name
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} (let [call (fn [request]
(call (request "application/json" "application/json" {:request :edn :response :json})))) (try
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]} (app request)
(call (request "application/transit" "application/json" {:request :edn :response :json}))))) (catch ExceptionInfo e
(testing "response validation fails" (select-keys (ex-data e) [:type :in]))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]} request (fn [request-format response-format body]
(call (request "application/json" "application/json" {:request :json :response :edn})))) {:request-method :post
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]} :uri "/foo"
(call (request "application/json" "application/edn" {:request :json :response :json})))) :muuntaja/request {:format request-format}
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]} :muuntaja/response {:format response-format}
(call (request "application/json" "application/transit" {:request :json :response :json}))))))))) :body-params body})]
(testing "succesful call"
(is (= {:status 200 :body {:request :json, :response :json}}
(call (request "application/json" "application/json" {:request :json :response :json}))))
(is (= {:status 200 :body {:request :edn, :response :json}}
(call (request "application/edn" "application/json" {:request :edn :response :json}))))
(is (= {:status 200 :body {:request :default, :response :default}}
(call (request "application/transit" "application/transit" {:request :default :response :default})))))
(testing "request validation fails"
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/edn" "application/json" {:request :json :response :json}))))
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/json" "application/json" {:request :edn :response :json}))))
(is (= {:type :reitit.coercion/request-coercion :in [:request :body-params]}
(call (request "application/transit" "application/json" {:request :edn :response :json})))))
(testing "response validation fails"
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/json" {:request :json :response :edn}))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/edn" {:request :json :response :json}))))
(is (= {:type :reitit.coercion/response-coercion :in [:response :body]}
(call (request "application/json" "application/transit" {:request :json :response :json})))))))))))
#?(:clj #?(:clj

View file

@ -401,7 +401,7 @@
(ring/router (ring/router
[["/parameters" [["/parameters"
{:post {:coercion spec/coercion {:post {:coercion spec/coercion
:parameters {:request {:content {"application/json" {:x string?}}}} :request {:content {"application/json" {:x string?}}}
:handler identity}}] :handler identity}}]
["/swagger.json" ["/swagger.json"
{:get {:no-doc true {:get {:no-doc true