mirror of
https://github.com/metosin/reitit.git
synced 2026-01-29 09:20:33 +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
|
(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)})
|
||||||
|
|
@ -84,11 +85,22 @@
|
||||||
(if coercion
|
(if coercion
|
||||||
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
|
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
|
||||||
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||||
model (if open? (-open-model coercion model) model)]
|
->open (if open? #(-open-model coercion %) identity)
|
||||||
(if-let [coercer (-request-coercer coercion style model)]
|
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]
|
(fn [request]
|
||||||
(let [value (transform request)
|
(let [value (transform request)
|
||||||
format (extract-request-format request)
|
format (extract-request-format request)
|
||||||
|
coercer (or (format->coercer format)
|
||||||
|
(format->coercer :default)
|
||||||
|
(fn [value _format] value))
|
||||||
result (coercer value format)]
|
result (coercer value format)]
|
||||||
(if (error? result)
|
(if (error? result)
|
||||||
(request-coercion-failed! result coercion value in request serialize-failed-result)
|
(request-coercion-failed! result coercion value in request serialize-failed-result)
|
||||||
|
|
@ -97,17 +109,24 @@
|
||||||
(defn extract-response-format-default [request _]
|
(defn extract-response-format-default [request _]
|
||||||
(-> request :muuntaja/response :format))
|
(-> request :muuntaja/response :format))
|
||||||
|
|
||||||
(defn response-coercer [coercion 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
|
||||||
(if-let [coercer (-response-coercer coercion body)]
|
(let [per-format-coercers (some->> (for [[format schema] content]
|
||||||
(fn [request response]
|
[format (-response-coercer coercion schema)])
|
||||||
(let [format (extract-response-format request response)
|
(filter second)
|
||||||
value (:body response)
|
(seq)
|
||||||
result (coercer value format)]
|
(into {}))
|
||||||
(if (error? result)
|
default (when body (-response-coercer coercion body))]
|
||||||
(response-coercion-failed! result coercion value request response serialize-failed-result)
|
(when (or per-format-coercers default)
|
||||||
result))))))
|
(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]
|
(defn encode-error [data]
|
||||||
(-> data
|
(-> data
|
||||||
|
|
@ -136,8 +155,8 @@
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
(defn response-coercers [coercion responses opts]
|
(defn response-coercers [coercion responses opts]
|
||||||
(some->> (for [[status {:keys [body]}] responses :when body]
|
(some->> (for [[status model] responses]
|
||||||
[status (response-coercer coercion body opts)])
|
[status (response-coercer coercion model opts)])
|
||||||
(filter second)
|
(filter second)
|
||||||
(seq)
|
(seq)
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as rrc]
|
||||||
[schema.core :as s]
|
[schema.core :as s]
|
||||||
|
[clojure.spec.alpha]
|
||||||
[spec-tools.data-spec :as ds])
|
[spec-tools.data-spec :as ds])
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import (clojure.lang ExceptionInfo)
|
(: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 m/schema custom-meta-merge-checking-schema)))
|
||||||
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call identity custom-meta-merge-checking-parameters)))))))
|
(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
|
#?(:clj
|
||||||
(deftest muuntaja-test
|
(deftest muuntaja-test
|
||||||
(let [app (ring/ring-handler
|
(let [app (ring/ring-handler
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue