diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index 8ca8fb681..32f51e683 100644 --- a/doc/ring/coercion.md +++ b/doc/ring/coercion.md @@ -157,21 +157,21 @@ You can also specify request and response body schemas per content-type. The syn ```clj (def app (ring/ring-handler - (ring/router - ["/api" - ["/example" {:post {:coercion reitit.coercion.schema/coercion - :parameters {:request {:content {"application/json" {:y s/Int} - "application/edn" {:z s/Int}} - ;; default if no content-type matches: - :body {:yy s/Int}}} - :responses {200 {:content {"application/json" {:w s/Int} - "application/edn" {:x s/Int}} - ;; default if no content-type matches: - :body {:ww s/Int}} - :handler ...}}]] - {:data {:middleware [rrc/coerce-exceptions-middleware - rrc/coerce-request-middleware - rrc/coerce-response-middleware]}}))) + (ring/router + ["/api" + ["/example" {:post {:coercion reitit.coercion.schema/coercion + :request {:content {"application/json" {:schema {:y s/Int}} + "application/edn" {:schema {:z s/Int}}} + ;; default if no content-type matches: + :body {:yy s/Int}} + :responses {200 {:content {"application/json" {:schema {:w s/Int}} + "application/edn" {:schema {:x s/Int}}} + ;; default if no content-type matches: + :body {:ww s/Int}}} + :handler ...}}]] + {:data {:middleware [rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware]}}))) ``` ## Pretty printing spec errors diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 0952bfab3..75b78d79b 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -37,7 +37,6 @@ (def ^:no-doc default-parameter-coercion {:query (->ParameterCoercion :query-params :string true true) :body (->ParameterCoercion :body-params :body false false) - :request (->ParameterCoercion :body-params :request false false) :form (->ParameterCoercion :form-params :string true true) :header (->ParameterCoercion :headers :string true true) :path (->ParameterCoercion :path-params :string true true) @@ -83,34 +82,53 @@ value) ;; 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 - parameter-coercion default-parameter-coercion}}] + parameter-coercion default-parameter-coercion + skip #{}}}] (if coercion - (if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] - (let [transform (comp (if keywordize? walk/keywordize-keys identity) in) - ->open (if open? #(-open-model coercion %) identity) - format-schema-pairs (if (= :request style) - (conj (:content model) [:default (:body model)]) - [[:default model]]) - format->coercer (some->> (for [[format schema] format-schema-pairs - :when schema - :let [type (case style :request :body style)]] - [format (-request-coercer coercion type (->open schema))]) - (filter second) - (seq) - (into {}))] - (when format->coercer - (fn [request] - (let [value (transform 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)))))))) + (when-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] + (when-not (skip style) + (let [transform (comp (if keywordize? walk/keywordize-keys identity) in) + ->open (if open? #(-open-model coercion %) identity) + coercer (-request-coercer coercion style (->open model))] + (when coercer + (fn [request] + (let [value (transform request) + format (extract-request-format request) + result (coercer value format)] + (if (error? result) + (request-coercion-failed! result coercion value in request serialize-failed-result) + result))))))))) + +(defn get-default-schema [request-or-response] + (or (-> request-or-response :content :default :schema) + (:body request-or-response))) + +(defn get-default [request-or-response] + (or (-> request-or-response :content :default) + (some->> request-or-response :body (assoc {} :schema)))) + +(defn content-request-coercer [coercion {:keys [content body]} {::keys [extract-request-format serialize-failed-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 _] (-> request :muuntaja/response :format)) @@ -118,18 +136,18 @@ (defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result] :or {extract-response-format extract-response-format-default}}] (if coercion - (let [per-format-coercers (some->> (for [[format schema] content - :when schema] - [format (-response-coercer coercion schema)]) - (filter second) - (seq) - (into {})) - default (when body (-response-coercer coercion body))] - (when (or per-format-coercers default) + (let [format->coercer (some->> (concat (when body + [[:default (-response-coercer coercion body)]]) + (for [[format {:keys [schema]}] content, :when schema] + [format (-response-coercer coercion schema)])) + (filter second) (seq) (into (array-map)))] + (when format->coercer (fn [request response] (let [format (extract-response-format request 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)] (if (error? result) (response-coercion-failed! result coercion value request response serialize-failed-result) @@ -153,10 +171,15 @@ (impl/fast-assoc response :body (coercer request response)) response))) -(defn request-coercers [coercion parameters opts] - (some->> (for [[k v] parameters, :when v] - [k (request-coercer coercion k v opts)]) - (filter second) (seq) (into {}))) +(defn request-coercers + ([coercion parameters opts] + (some->> (for [[k v] parameters, :when v] + [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] (some->> (for [[status model] responses] @@ -170,8 +193,8 @@ ;; api-docs ;; -(defn -warn-unsupported-coercions [{:keys [parameters responses] :as _data}] - (when (:request parameters) +(defn -warn-unsupported-coercions [{:keys [request responses] :as _data}] + (when request (println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion")) (when (some :content (vals responses)) (println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion"))) @@ -197,7 +220,6 @@ (into {})))) (-get-apidocs coercion specification)))))) - ;; ;; integration ;; diff --git a/modules/reitit-core/src/reitit/spec.cljc b/modules/reitit-core/src/reitit/spec.cljc index d5ddaf841..3da7d9eab 100644 --- a/modules/reitit-core/src/reitit/spec.cljc +++ b/modules/reitit-core/src/reitit/spec.cljc @@ -82,8 +82,11 @@ (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/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/body :reitit.core.coercion/model) diff --git a/modules/reitit-http/src/reitit/http/coercion.cljc b/modules/reitit-http/src/reitit/http/coercion.cljc index 8e63db281..4807f3a35 100644 --- a/modules/reitit-http/src/reitit/http/coercion.cljc +++ b/modules/reitit-http/src/reitit/http/coercion.cljc @@ -10,15 +10,15 @@ [] {:name ::coerce-request :spec ::rs/parameters - :compile (fn [{:keys [coercion parameters]} opts] + :compile (fn [{:keys [coercion parameters request]} opts] (cond ;; no coercion, skip (not coercion) nil ;; just coercion, don't mount - (not parameters) {} + (not (or parameters request)) {} ;; mount :else - (if-let [coercers (coercion/request-coercers coercion parameters opts)] + (if-let [coercers (coercion/request-coercers coercion parameters request opts)] {:enter (fn [ctx] (let [request (:request ctx) coerced (coercion/coerce-request coercers request) diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 6f4dff5b5..e4f3ab2d3 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -133,13 +133,22 @@ ;; malli options :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 - [coercion {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options] - (let [{:keys [body request multipart]} parameters + [_ {:keys [request parameters responses content-types] :or {content-types ["application/json"]}} options] + (let [{:keys [body multipart]} parameters parameters (dissoc parameters :request :body :multipart) ->schema-object (fn [schema 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 (when (seq parameters) {:parameters @@ -168,20 +177,21 @@ ;; request allow to different :requestBody per content-type {:requestBody {:content (merge - (when (:body request) + (select-keys request [:description]) + (when-let [{:keys [schema] :as data} (coercion/get-default request)] (into {} (map (fn [content-type] - (let [schema (->schema-object (:body request) {:in :requestBody - :type :schema - :content-type content-type})] - [content-type {:schema schema}]))) + (let [schema (->schema-object schema {:in :requestBody + :type :schema + :content-type content-type})] + [content-type (->content data schema)]))) content-types)) (into {} - (map (fn [[content-type requestBody]] - (let [schema (->schema-object requestBody {:in :requestBody - :type :schema - :content-type content-type})] - [content-type {:schema schema}]))) + (map (fn [[content-type {:keys [schema] :as data}]] + (let [schema (->schema-object schema {:in :requestBody + :type :schema + :content-type content-type})] + [content-type (->content data schema)]))) (:content request)))}}) (when multipart {:requestBody @@ -194,29 +204,30 @@ (when responses {:responses (into {} - (map (fn [[status {:keys [body content] - :as response}]] - (let [content (merge - (when body - (into {} - (map (fn [content-type] - (let [schema (->schema-object body {:in :responses - :type :schema - :content-type content-type})] - [content-type {:schema schema}]))) - content-types)) - (when content - (into {} - (map (fn [[content-type schema]] - (let [schema (->schema-object schema {:in :responses - :type :schema - :content-type content-type})] - [content-type {:schema schema}]))) - content)))] + (map (fn [[status {:keys [content], :as response}]] + (let [default (coercion/get-default-schema response) + content (-> (merge + (when default + (into {} + (map (fn [content-type] + (let [schema (->schema-object default {:in :responses + :type :schema + :content-type content-type})] + [content-type (->content nil schema)]))) + content-types)) + (when content + (into {} + (map (fn [[content-type {:keys [schema] :as data}]] + (let [schema (->schema-object schema {:in :responses + :type :schema + :content-type content-type})] + [content-type (->content data schema)]))) + content))) + (dissoc :default))] [status (merge (select-keys response [:description]) (when content - {:content content}))]))) - responses)})))) + {:content content}))])) + responses))})))) (defn create ([] diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 7a4cde7f8..07332ec14 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -32,19 +32,25 @@ (defn -update-paths [f] (let [not-request? #(not= :request %) http-method? #(contains? http-methods %)] - [;; default parameters and responses + [;; default parameters [[:parameters not-request?] f] [[http-method? :parameters not-request?] f] + + ;; default responses [[:responses any? :body] f] [[http-method? :responses any? :body] f] - ;; openapi3 parameters and responses - [[:parameters :request :content any?] f] - [[http-method? :parameters :request :content any?] f] - [[:parameters :request :body] f] - [[http-method? :parameters :request :body] f] - [[:responses any? :content any?] f] - [[http-method? :responses any? :content any?] f]])) + ;; openapi3 request + [[:request :content any? :schema] f] + [[http-method? :request :content any? :schema] f] + + ;; openapi3 LEGACY body + [[:request :body] 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}] (cond-> data coercion (impl/path-update (-update-paths #(coercion/-compile-model coercion % nil))))) diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc index efbe83f7d..8d7cbe0fc 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion.cljc @@ -24,15 +24,15 @@ and :parameters from route data, otherwise does not mount." {:name ::coerce-request :spec ::rs/parameters - :compile (fn [{:keys [coercion parameters]} opts] + :compile (fn [{:keys [coercion parameters request]} opts] (cond ;; no coercion, skip (not coercion) nil ;; just coercion, don't mount - (not parameters) {} + (not (or parameters request)) {} ;; mount :else - (if-let [coercers (coercion/request-coercers coercion parameters opts)] + (if-let [coercers (coercion/request-coercers coercion parameters request opts)] (fn [handler] (fn ([request] diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index 3dc50bb1f..e0822ad95 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -47,9 +47,9 @@ (reify coercion/Coercion (-get-name [_] :schema) (-get-options [_] opts) - (-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. + (-get-apidocs [_ specification {:keys [request parameters responses content-types] + :or {content-types ["application/json"]}}] + ;; TODO: this looks identical to spec, refactor when schema is done. (case specification :swagger (swagger/swagger-spec (merge @@ -62,35 +62,40 @@ (for [[k response] responses] [k (set/rename-keys response {:body :schema})]))}))) :openapi (merge - (when (seq (dissoc parameters :body :request :multipart)) - (openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)})) - (when (:body parameters) - {:requestBody (openapi/openapi-spec - {::openapi/content (zipmap content-types (repeat (:body parameters)))})}) - (when (:request parameters) - {:requestBody (openapi/openapi-spec - {::openapi/content (merge - (when-let [default (get-in parameters [:request :body])] - (zipmap content-types (repeat default))) - (:content (:request parameters)))})}) - (when (:multipart parameters) - {:requestBody - (openapi/openapi-spec - {::openapi/content {"multipart/form-data" (:multipart parameters)}})}) - (when responses - {:responses - (into - (empty responses) - (for [[k {:keys [body content] :as response}] responses] - [k (merge - (select-keys response [:description]) - (when (or body content) - (openapi/openapi-spec - {::openapi/content (merge - (when body - (zipmap content-types (repeat body))) - (when response - (:content response)))})))]))})) + (when (seq (dissoc parameters :body :request :multipart)) + (openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)})) + (when (:body parameters) + {:requestBody (openapi/openapi-spec + {::openapi/content (zipmap content-types (repeat (:body parameters)))})}) + (when request + {:requestBody (openapi/openapi-spec + {::openapi/content (merge + (when-let [default (coercion/get-default-schema request)] + (zipmap content-types (repeat default))) + (->> (for [[content-type {:keys [schema]}] (:content request)] + [content-type schema]) + (into {})))})}) + (when (:multipart parameters) + {:requestBody + (openapi/openapi-spec + {::openapi/content {"multipart/form-data" (:multipart parameters)}})}) + (when responses + {:responses + (into + (empty responses) + (for [[k {:keys [content] :as response}] responses + :let [default (coercion/get-default-schema response)]] + [k (merge + (select-keys response [:description]) + (when (or content default) + (openapi/openapi-spec + {::openapi/content (-> (merge + (when default + (zipmap content-types (repeat default))) + (->> (for [[content-type {:keys [schema]}] content] + [content-type schema]) + (into {}))) + (dissoc :default))})))]))})) (throw (ex-info diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index 40110843c..98ff9e375 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -88,8 +88,8 @@ (reify coercion/Coercion (-get-name [_] :spec) (-get-options [_] opts) - (-get-apidocs [this specification {:keys [parameters responses content-types] - :or {content-types ["application/json"]}}] + (-get-apidocs [_ specification {:keys [request parameters responses content-types] + :or {content-types ["application/json"]}}] (case specification :swagger (swagger/swagger-spec (merge @@ -108,12 +108,14 @@ (when (:body parameters) {:requestBody (openapi/openapi-spec {::openapi/content (zipmap content-types (repeat (:body parameters)))})}) - (when (:request parameters) + (when request {:requestBody (openapi/openapi-spec {::openapi/content (merge - (when-let [default (get-in parameters [:request :body])] + (when-let [default (coercion/get-default-schema request)] (zipmap content-types (repeat default))) - (:content (:request parameters)))})}) + (->> (for [[content-type {:keys [schema]}] (:content request)] + [content-type schema]) + (into {})))})}) (when (:multipart parameters) {:requestBody (openapi/openapi-spec @@ -122,16 +124,20 @@ {:responses (into (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 (select-keys response [:description]) - (when (or body content) + (when (or content default) (openapi/openapi-spec - {::openapi/content (merge - (when body - (zipmap content-types (repeat (:body response)))) - (when response - (:content response)))})))]))})) + {::openapi/content (-> (merge + (when default + (zipmap content-types (repeat default))) + (->> (for [[content-type {:keys [schema]}] content] + [content-type schema]) + (into {}))) + (dissoc :default))})))]))})) (throw (ex-info (str "Can't produce Spec apidocs for " specification) diff --git a/project.clj b/project.clj index 24b83c43b..4387218e5 100644 --- a/project.clj +++ b/project.clj @@ -108,6 +108,7 @@ [ikitommi/immutant-web "3.0.0-alpha1"] [metosin/ring-http-response "0.9.3"] [metosin/ring-swagger-ui "4.18.1"] + [org.clojure/tools.analyzer "1.1.1"] [criterium "0.4.6"] [org.clojure/test.check "1.1.1"] diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index 605f5e049..f147c4072 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -65,7 +65,10 @@ :description "kosh"}}} :responses {200 {:description "success" :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 xs :body} :parameters}] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] @@ -91,7 +94,10 @@ :content {"application/json" {:schema {:type "string"}}}}}} :responses {200 {:description "success" :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 xs :body} :parameters}] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] @@ -101,7 +107,7 @@ {:get {:summary "plus" :tags [:plus :schema] :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"}}} :description "kosh"}}} :responses {200 {:description "success" @@ -117,7 +123,10 @@ :description "kosh"}}} :responses {200 {:description "success" :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 xs :body} :parameters}] {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]] @@ -193,7 +202,11 @@ :type "object"}}}} 400 {:content {"application/json" {:schema {:type "string"}}} :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"}} "/api/malli/plus/{z}" {:get {:parameters [{:in "query" :name :x @@ -231,7 +244,12 @@ :type "object"}}}} 400 {:description "kosh" :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"}} "/api/schema/plus/{z}" {:get {:parameters [{:description "" :in "query" @@ -280,10 +298,15 @@ :type "object"}}}} 400 {:description "kosh" :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"}}}}] (is (= expected spec)) - (is (nil? (validate spec)))))) + (is (= nil (validate spec)))))) (defn spec-paths [app uri] (-> {:request-method :get, :uri uri} app :body :paths keys)) @@ -457,8 +480,8 @@ [["/examples" {:post {:decription "examples" :coercion @coercion - :parameters {:query (->schema :q) - :request {:body (->schema :b)}} + :request {:body (->schema :b)} + :parameters {:query (->schema :q)} :responses {200 {:description "success" :body (->schema :ok)}} :openapi {:requestBody @@ -517,20 +540,19 @@ (is (nil? (validate spec)))))))) (deftest multipart-test - (doseq [[coercion file-schema string-schema] - [[#'malli/coercion - reitit.ring.malli/bytes-part - :string] - [#'schema/coercion - (schema-tools.core/schema {:filename s/Str - :content-type s/Str - :bytes s/Num} - {:openapi {:type "string" - :format "binary"}}) - s/Str] - [#'spec/coercion - reitit.http.interceptors.multipart/bytes-part - string?]]] + (doseq [[coercion file-schema string-schema] [[#'malli/coercion + reitit.ring.malli/bytes-part + :string] + [#'schema/coercion + (schema-tools.core/schema {:filename s/Str + :content-type s/Str + :bytes s/Num} + {:openapi {:type "string" + :format "binary"}}) + s/Str] + [#'spec/coercion + reitit.http.interceptors.multipart/bytes-part + string?]]] (testing (str coercion) (let [app (ring/ring-handler (ring/router @@ -565,21 +587,20 @@ (is (nil? (validate spec)))))))) (deftest per-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?})]]] + (doseq [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])] + [schema/coercion (fn [nom] {nom s/Str})] + [spec/coercion (fn [nom] {nom string?})]]] (testing (str coercion) (let [app (ring/ring-handler (ring/router [["/parameters" {:post {:description "parameters" - :coercion @coercion - :parameters {:request {:content {"application/json" (->schema :b) - "application/edn" (->schema :c)}}} + :coercion coercion + :request {:content {"application/json" {:schema (->schema :b)} + "application/edn" {:schema (->schema :c)}}} :responses {200 {:description "success" - :content {"application/json" (->schema :ok) - "application/edn" (->schema :edn)}}} + :content {"application/json" {:schema (->schema :ok)} + "application/edn" {:schema (->schema :edn)}}}} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] @@ -594,41 +615,42 @@ spec (-> {:request-method :get :uri "/openapi.json"} app - :body)] + :body) + spec-coercion (= coercion spec/coercion)] (testing "body parameter" - (is (match? (merge {:type "object" - :properties {:b {:type "string"}} - :required ["b"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) - normalize))) - (is (match? (merge {:type "object" - :properties {:c {:type "string"}} - :required ["c"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema]) - normalize)))) + (is (= (merge {:type "object" + :properties {:b {:type "string"}} + :required ["b"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema]) + normalize))) + (is (= (merge {:type "object" + :properties {:c {:type "string"}} + :required ["c"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema]) + normalize)))) (testing "body response" - (is (match? (merge {:type "object" - :properties {:ok {:type "string"}} - :required ["ok"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) - normalize))) - (is (match? (merge {:type "object" - :properties {:edn {:type "string"}} - :required ["edn"]} - (when-not (#{#'spec/coercion} coercion) - {:additionalProperties false})) - (-> spec - (get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema]) - normalize)))) + (is (= (merge {:type "object" + :properties {:ok {:type "string"}} + :required ["ok"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema]) + normalize))) + (is (= (merge {:type "object" + :properties {:edn {:type "string"}} + :required ["edn"]} + (when-not spec-coercion + {:additionalProperties false})) + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema]) + normalize)))) (testing "validation" (let [query {:request-method :post :uri "/parameters" @@ -653,23 +675,22 @@ (is (nil? (validate spec)))))))) (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 [[coercion ->schema] [[malli/coercion (fn [nom] [:map [nom :string]])] + [schema/coercion (fn [nom] {nom s/Str})] + [spec/coercion (fn [nom] {nom string?})]]] + (testing (str coercion) (doseq [content-type ["application/json" "application/edn"]] (testing (str "default content type " content-type) (let [app (ring/ring-handler (ring/router [["/parameters" {:post {:description "parameters" - :coercion @coercion + :coercion coercion :content-types [content-type] ;; TODO should this be under :openapi ? - :parameters {:request {:content {"application/transit" (->schema :transit)} - :body (->schema :default)}} + :request {:content {"application/transit" {:schema (->schema :transit)}} + :body (->schema :default)} :responses {200 {:description "success" - :content {"application/transit" (->schema :transit)} + :content {"application/transit" {:schema (->schema :transit)}} :body (->schema :default)}} :handler (fn [req] {:status 200 @@ -707,16 +728,15 @@ [["/parameters" {:post {:description "parameters" :coercion malli/coercion - :parameters {:request - {:body - [:schema - {:registry {"friend" [:map - [:age int?] - [:pet [:ref "pet"]]] - "pet" [:map - [:name :string] - [:friends [:vector [:ref "friend"]]]]}} - "friend"]}} + :request {:body + [:schema + {:registry {"friend" [:map + [:age int?] + [:pet [:ref "pet"]]] + "pet" [:map + [:name :string] + [:friends [:vector [:ref "friend"]]]]}} + "friend"]} :handler (fn [req] {:status 200 :body (-> req :parameters :request)})}}] @@ -754,3 +774,53 @@ spec)) (testing "spec is valid" (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)))) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index 10aa84098..ec2bd1fe4 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -606,53 +606,74 @@ {:request any? :response (clojure.spec.alpha/spec #{:end})} {:request any? :response (clojure.spec.alpha/spec #{:default})}]]] (testing (str coercion) - (let [app (ring/ring-handler - (ring/router - ["/foo" {:post {:parameters {:request {:content {"application/json" json-request - "application/edn" edn-request} - :body default-request}} - :responses {200 {:content {"application/json" json-response - "application/edn" edn-response} - :body default-response}} - :handler (fn [req] - {:status 200 - :body (-> req :parameters :request)})}}] - {#_#_:validate reitit.ring.spec/validate - :data {:middleware [rrc/coerce-request-middleware - rrc/coerce-response-middleware] - :coercion coercion}})) - call (fn [request] - (try - (app request) - (catch ExceptionInfo e - (select-keys (ex-data e) [:type :in])))) - request (fn [request-format response-format body] - {:request-method :post - :uri "/foo" - :muuntaja/request {:format request-format} - :muuntaja/response {:format response-format} - :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}))))))))) + (doseq [{:keys [name app]} + [{:name "using top-level :body" + :app (ring/ring-handler + (ring/router + ["/foo" {:post {:request {:content {"application/json" {:schema json-request} + "application/edn" {:schema edn-request}} + :body default-request} + :responses {200 {:content {"application/json" {:schema json-response} + "application/edn" {:schema edn-response}} + :body default-response}} + :handler (fn [req] + {:status 200 + :body (-> req :parameters :request)})}}] + {:validate reitit.ring.spec/validate + :data {:middleware [rrc/coerce-request-middleware + rrc/coerce-response-middleware] + :coercion coercion}}))} + {:name "using :default content" + :app (ring/ring-handler + (ring/router + ["/foo" {:post {:request {:content {"application/json" {:schema json-request} + "application/edn" {:schema edn-request} + :default {:schema default-request}} + :body json-request} ;; not applied as :default exists + :responses {200 {:content {"application/json" {:schema json-response} + "application/edn" {:schema edn-response} + :default {:schema default-response}} + :body json-response}} ;; not applied as :default exists + :handler (fn [req] + {:status 200 + :body (-> req :parameters :request)})}}] + {:validate reitit.ring.spec/validate + :data {:middleware [rrc/coerce-request-middleware + rrc/coerce-response-middleware] + :coercion coercion}}))}]] + (testing name + (let [call (fn [request] + (try + (app request) + (catch ExceptionInfo e + (select-keys (ex-data e) [:type :in])))) + request (fn [request-format response-format body] + {:request-method :post + :uri "/foo" + :muuntaja/request {:format request-format} + :muuntaja/response {:format response-format} + :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 diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj index c00f9cd75..2862edf12 100644 --- a/test/cljc/reitit/swagger_test.clj +++ b/test/cljc/reitit/swagger_test.clj @@ -401,7 +401,7 @@ (ring/router [["/parameters" {:post {:coercion spec/coercion - :parameters {:request {:content {"application/json" {:x string?}}}} + :request {:content {"application/json" {:x string?}}} :handler identity}}] ["/swagger.json" {:get {:no-doc true