diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index 8ca8fb68..5080449d 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" {: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]}}))) ``` ## Pretty printing spec errors diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 19106ac0..75167027 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,45 @@ 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 {:schema (:body model)}]) - [[:default {:schema model}]]) - format->coercer (some->> (for [[format {:keys [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 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 +128,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 {:keys [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 +163,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 request opts] + (let [crc (when request (some->> (content-request-coercer coercion request opts) (array-map :request))) + rcs (request-coercers coercion parameters (cond-> opts 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 +185,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 +212,6 @@ (into {})))) (-get-apidocs coercion specification)))))) - ;; ;; integration ;; diff --git a/modules/reitit-http/src/reitit/http/coercion.cljc b/modules/reitit-http/src/reitit/http/coercion.cljc index 8e63db28..4807f3a3 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 36daefa4..30290fba 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -134,8 +134,8 @@ :options nil}) (defn -get-apidocs-openapi - [_ {: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)] diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index eee8452c..07332ec1 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -32,17 +32,23 @@ (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? :schema] f] - [[http-method? :parameters :request :content any? :schema] f] - [[:parameters :request :body] f] - [[http-method? :parameters :request :body] 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]])) diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc index efbe83f7..8d7cbe0f 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 b746c516..a6beeae7 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -47,7 +47,7 @@ (reify coercion/Coercion (-get-name [_] :schema) (-get-options [_] opts) - (-get-apidocs [_ specification {:keys [parameters responses content-types] + (-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 @@ -67,12 +67,12 @@ (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 (:body request)] (zipmap content-types (repeat default))) - (->> (for [[content-type {:keys [schema]}] (:content (:request parameters))] + (->> (for [[content-type {:keys [schema]}] (:content request)] [content-type schema]) (into {})))})}) (when (:multipart parameters) diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index 848fc3a4..ca7b724d 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -88,7 +88,7 @@ (reify coercion/Coercion (-get-name [_] :spec) (-get-options [_] opts) - (-get-apidocs [this specification {:keys [parameters responses content-types] + (-get-apidocs [this specification {:keys [request parameters responses content-types] :or {content-types ["application/json"]}}] (case specification :swagger (swagger/swagger-spec @@ -108,12 +108,12 @@ (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 (:body request)] (zipmap content-types (repeat default))) - (->> (for [[content-type {:keys [schema]}] (:content (:request parameters))] + (->> (for [[content-type {:keys [schema]}] (:content request)] [content-type schema]) (into {})))})}) (when (:multipart parameters) diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index c4b9b6d4..31af695a 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -457,8 +457,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 @@ -573,8 +573,8 @@ [["/parameters" {:post {:description "parameters" :coercion coercion - :parameters {:request {:content {"application/json" {:schema (->schema :b)} - "application/edn" {:schema (->schema :c)}}}} + :request {:content {"application/json" {:schema (->schema :b)} + "application/edn" {:schema (->schema :c)}}} :responses {200 {:description "success" :content {"application/json" {:schema (->schema :ok)} "application/edn" {:schema (->schema :edn)}}}} @@ -664,8 +664,8 @@ {:post {:description "parameters" :coercion coercion :content-types [content-type] ;; TODO should this be under :openapi ? - :parameters {:request {:content {"application/transit" {:schema (->schema :transit)}} - :body (->schema :default)}} + :request {:content {"application/transit" {:schema (->schema :transit)}} + :body (->schema :default)} :responses {200 {:description "success" :content {"application/transit" {:schema (->schema :transit)}} :body (->schema :default)}} @@ -705,16 +705,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)})}}] diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index 5827bedd..2c919f9c 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -606,53 +606,70 @@ {: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" {: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}})) - 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 [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}})) + (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}}))]] + (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 c00f9cd7..2862edf1 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