From c8d679c6b3d41ad160754188731ae4505c3f6b97 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Thu, 2 Mar 2023 14:16:56 +0200 Subject: [PATCH] feat: per-content-type request/response coercions implemented on the reitit-core level so individual coercions don't need changes syntax: {:parameters {:request {:content {"application/edn" [:map ...]}}} :responses {200 {:content {"application/edn" [:map ...]}}}} --- modules/reitit-core/src/reitit/coercion.cljc | 47 +++++++++---- test/cljc/reitit/ring_coercion_test.cljc | 73 ++++++++++++++++++++ 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 442409da..6e55b209 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -37,6 +37,7 @@ (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)}) @@ -84,11 +85,22 @@ (if coercion (if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)] (let [transform (comp (if keywordize? walk/keywordize-keys identity) in) - model (if open? (-open-model coercion model) model)] - (if-let [coercer (-request-coercer coercion style model)] + ->open (if open? #(-open-model coercion %) identity) + format-coercer-pairs (if (= :request style) + (for [[format schema] (:content model)] + [format (-request-coercer coercion :body (->open schema))]) + [[:default (-request-coercer coercion style (->open model))]]) + format->coercer (some->> format-coercer-pairs + (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) + (fn [value _format] value)) result (coercer value format)] (if (error? result) (request-coercion-failed! result coercion value in request serialize-failed-result) @@ -97,17 +109,24 @@ (defn extract-response-format-default [request _] (-> request :muuntaja/response :format)) -(defn response-coercer [coercion body {:keys [extract-response-format serialize-failed-result] - :or {extract-response-format extract-response-format-default}}] +(defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result] + :or {extract-response-format extract-response-format-default}}] (if coercion - (if-let [coercer (-response-coercer coercion body)] - (fn [request response] - (let [format (extract-response-format request response) - value (:body response) - result (coercer value format)] - (if (error? result) - (response-coercion-failed! result coercion value request response serialize-failed-result) - result)))))) + (let [per-format-coercers (some->> (for [[format schema] content] + [format (-response-coercer coercion schema)]) + (filter second) + (seq) + (into {})) + default (when body (-response-coercer coercion body))] + (when (or per-format-coercers default) + (fn [request response] + (let [format (extract-response-format request response) + value (:body response) + coercer (get per-format-coercers format (or default (fn [value _format] value))) + result (coercer value format)] + (if (error? result) + (response-coercion-failed! result coercion value request response serialize-failed-result) + result))))))) (defn encode-error [data] (-> data @@ -136,8 +155,8 @@ (into {}))) (defn response-coercers [coercion responses opts] - (some->> (for [[status {:keys [body]}] responses :when body] - [status (response-coercer coercion body opts)]) + (some->> (for [[status model] responses] + [status (response-coercer coercion model opts)]) (filter second) (seq) (into {}))) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index e8bee456..c8287c9f 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -13,6 +13,7 @@ [reitit.ring :as ring] [reitit.ring.coercion :as rrc] [schema.core :as s] + [clojure.spec.alpha] [spec-tools.data-spec :as ds]) #?(:clj (:import (clojure.lang ExceptionInfo) @@ -582,6 +583,78 @@ (is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call m/schema custom-meta-merge-checking-schema))) (is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call identity custom-meta-merge-checking-parameters))))))) +(deftest per-content-type-test + (doseq [[coercion json-request edn-request default-request json-response edn-response default-response] + [[#'malli/coercion + [:map [:request [:enum :json]] [:response any?]] + [:map [:request [:enum :edn]] [:response any?]] + [:map [:request [:enum :default]] [:response any?]] + [:map [:request any?] [:response [:enum :json]]] + [:map [:request any?] [:response [:enum :edn]]] + [:map [:request any?] [:response [:enum :default]]]] + [#'schema/coercion + {:request (s/eq :json) :response s/Any} + {:request (s/eq :edn) :response s/Any} + {:request (s/eq :default) :response s/Any} + {:request s/Any :response (s/eq :json)} + {:request s/Any :response (s/eq :edn)} + {:request s/Any :response (s/eq :default)}] + [#'spec/coercion + {:request (clojure.spec.alpha/spec #{:json}) :response any?} + {:request (clojure.spec.alpha/spec #{:edn}) :response any?} + {:request (clojure.spec.alpha/spec #{:default}) :response any?} + {:request any? :response (clojure.spec.alpha/spec #{:json})} + {:request any? :response (clojure.spec.alpha/spec #{:end})} + {:request any? :response (clojure.spec.alpha/spec #{:default})}]]] + (testing coercion + (let [app (ring/ring-handler + (ring/router + [["/foo" {:post {:parameters {:request {:content {"application/json" json-request + "application/edn" edn-request + :default 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)})}}]] + {: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}))))))))) + + #?(:clj (deftest muuntaja-test (let [app (ring/ring-handler