mirror of
https://github.com/metosin/reitit.git
synced 2026-01-07 07:59:50 +00:00
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 ...]}}}}
This commit is contained in:
parent
8f48cdc96c
commit
c8d679c6b3
2 changed files with 106 additions and 14 deletions
|
|
@ -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 {})))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue