Response coercion is decode + validate + encode

This commit is contained in:
Tommi Reiman 2020-01-07 07:03:57 +02:00
parent 9200afe814
commit 9b024db897
3 changed files with 118 additions and 46 deletions

View file

@ -3,9 +3,11 @@
[malli.transform :as mt] [malli.transform :as mt]
[malli.edn :as edn] [malli.edn :as edn]
[malli.error :as me] [malli.error :as me]
[malli.util :as mu]
[malli.swagger :as swagger] [malli.swagger :as swagger]
[malli.core :as m] [malli.core :as m]
[clojure.set :as set])) [clojure.set :as set]
[clojure.walk :as walk]))
;; ;;
;; coercion ;; coercion
@ -13,28 +15,22 @@
(defrecord Coercer [decoder encoder validator explainer]) (defrecord Coercer [decoder encoder validator explainer])
(def string-transformer (defprotocol TransformationProvider
(mt/transformer (-transformer [this options]))
mt/strip-extra-keys-transformer
mt/string-transformer
mt/default-value-transformer))
(def json-transformer (defn- -provider [transformer]
(mt/transformer (reify TransformationProvider
mt/strip-extra-keys-transformer (-transformer [_ {:keys [strip-extra-keys default-values]}]
mt/json-transformer (mt/transformer
mt/default-value-transformer)) (if strip-extra-keys (mt/strip-extra-keys-transformer))
transformer
(if default-values (mt/default-value-transformer))))))
(def default-transformer (def string-transformer-provider (-provider (mt/string-transformer)))
(mt/transformer (def json-transformer-provider (-provider (mt/json-transformer)))
mt/strip-extra-keys-transformer (def default-transformer-provider (-provider nil))
mt/default-value-transformer))
;; TODO: are these needed? (defn- -coercer [schema type transformers f encoder opts]
(defmulti coerce-response? identity :default ::default)
(defmethod coerce-response? ::default [_] true)
(defn- -coercer [schema type transformers f opts]
(if schema (if schema
(let [->coercer (fn [t] (if t (->Coercer (m/decoder schema opts t) (let [->coercer (fn [t] (if t (->Coercer (m/decoder schema opts t)
(m/encoder schema opts t) (m/encoder schema opts t)
@ -42,17 +38,18 @@
(m/explainer schema opts)))) (m/explainer schema opts))))
{:keys [formats default]} (transformers type) {:keys [formats default]} (transformers type)
default-coercer (->coercer default) default-coercer (->coercer default)
encode (or encoder (fn [value _format] value))
format-coercers (some->> (for [[f t] formats] [f (->coercer t)]) (filter second) (seq) (into {})) format-coercers (some->> (for [[f t] formats] [f (->coercer t)]) (filter second) (seq) (into {}))
get-coercer (cond format-coercers (fn [format] (or (get format-coercers format) default-coercer)) get-coercer (cond format-coercers (fn [format] (or (get format-coercers format) default-coercer))
default-coercer (constantly default-coercer))] default-coercer (constantly default-coercer))]
(if get-coercer (if get-coercer
(if (= f :decode) (if (= f :decode)
;; transform -> validate ;; decode -> validate
(fn [value format] (fn [value format]
(if-let [coercer (get-coercer format)] (if-let [coercer (get-coercer format)]
(let [transform (:decoder coercer) (let [decoder (:decoder coercer)
validator (:validator coercer) validator (:validator coercer)
transformed (transform value)] transformed (decoder value)]
(if (validator transformed) (if (validator transformed)
transformed transformed
(let [explainer (:explainer coercer) (let [explainer (:explainer coercer)
@ -60,16 +57,18 @@
(coercion/map->CoercionError (coercion/map->CoercionError
(assoc error :transformed transformed))))) (assoc error :transformed transformed)))))
value)) value))
;; validate -> transform ;; decode -> validate -> encode
(fn [value format] (fn [value format]
(if-let [coercer (get-coercer format)] (if-let [coercer (get-coercer format)]
(let [transform (:encoder coercer) (let [decoder (:decoder coercer)
validator (:validator coercer) validator (:validator coercer)
explainer (:explainer coercer)] transformed (decoder value)]
(if (validator value) (if (validator transformed)
(transform value) (encode transformed format)
(coercion/map->CoercionError (let [explainer (:explainer coercer)
(explainer value)))) error (explainer transformed)]
(coercion/map->CoercionError
(assoc error :transformed transformed)))))
value))))))) value)))))))
;; ;;
@ -104,14 +103,18 @@
;; ;;
(def default-options (def default-options
{:coerce-response? coerce-response? {:transformers {:body {:default default-transformer-provider
:transformers {:body {:default default-transformer :formats {"application/json" json-transformer-provider}}
:formats {"application/json" json-transformer}} :string {:default string-transformer-provider}
:string {:default string-transformer} :response {:default default-transformer-provider}}
:response {:default default-transformer
:formats {"application/json" json-transformer}}}
;; set of keys to include in error messages ;; set of keys to include in error messages
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} :error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed}
;; schema identity function
:compile mu/closed-schema
;; strip-extra-keys (effects only default transformers!)
:strip-extra-keys true
;; add default values
:default-values true
;; malli options ;; malli options
:options nil}) :options nil})
@ -119,8 +122,9 @@
([] ([]
(create nil)) (create nil))
([opts] ([opts]
(let [{:keys [transformers coerce-response? options error-keys] :as opts} (merge default-options opts) (let [{:keys [transformers compile options error-keys] :as opts} (merge default-options opts)
show? (fn [key] (contains? error-keys key))] show? (fn [key] (contains? error-keys key))
transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers)]
^{:type ::coercion/coercion} ^{:type ::coercion/coercion}
(reify coercion/Coercion (reify coercion/Coercion
(-get-name [_] :malli) (-get-name [_] :malli)
@ -131,7 +135,7 @@
(if parameters (if parameters
{:parameters {:parameters
(->> (for [[in schema] parameters (->> (for [[in schema] parameters
parameter (extract-parameter in schema)] parameter (extract-parameter in (compile schema))]
parameter) parameter)
(into []))}) (into []))})
(if responses (if responses
@ -143,13 +147,15 @@
(set/rename-keys $ {:body :schema}) (set/rename-keys $ {:body :schema})
(update $ :description (fnil identity "")) (update $ :description (fnil identity ""))
(if (:schema $) (if (:schema $)
(update $ :schema swagger/transform {:type :schema}) (-> $
(update :schema compile)
(update :schema swagger/transform {:type :schema}))
$))]))})) $))]))}))
(throw (throw
(ex-info (ex-info
(str "Can't produce Schema apidocs for " specification) (str "Can't produce Schema apidocs for " specification)
{:type specification, :coercion :schema})))) {:type specification, :coercion :schema}))))
(-compile-model [_ model _] (m/schema model)) (-compile-model [_ model _] (compile model))
(-open-model [_ schema] schema) (-open-model [_ schema] schema)
(-encode-error [_ error] (-encode-error [_ error]
(cond-> error (cond-> error
@ -159,9 +165,10 @@
(update :errors (partial map #(update % :schema edn/write-string opts)))) (update :errors (partial map #(update % :schema edn/write-string opts))))
(seq error-keys) (select-keys error-keys))) (seq error-keys) (select-keys error-keys)))
(-request-coercer [_ type schema] (-request-coercer [_ type schema]
(-coercer schema type transformers :decode options)) (-coercer (compile schema) type transformers :decode nil options))
(-response-coercer [_ schema] (-response-coercer [_ schema]
(if (coerce-response? schema) (let [schema (compile schema)
(-coercer schema :response transformers :encode options))))))) encoder (-coercer schema :body transformers :encode nil options)]
(-coercer schema :response transformers :encode encoder options)))))))
(def coercion (create default-options)) (def coercion (create default-options))

View file

@ -33,7 +33,7 @@
[metosin/muuntaja "0.6.5"] [metosin/muuntaja "0.6.5"]
[metosin/jsonista "0.2.5"] [metosin/jsonista "0.2.5"]
[metosin/sieppari "0.0.0-alpha7"] [metosin/sieppari "0.0.0-alpha7"]
[metosin/malli "0.0.1-20191228.073043-6"] [metosin/malli "0.0.1-20200106.232607-10"]
[meta-merge "1.0.0"] [meta-merge "1.0.0"]
[fipp "0.6.21" :exclusions [org.clojure/core.rrb-vector]] [fipp "0.6.21" :exclusions [org.clojure/core.rrb-vector]]

View file

@ -265,7 +265,72 @@
(testing "invalid response" (testing "invalid response"
(let [{:keys [status]} (app invalid-request2)] (let [{:keys [status]} (app invalid-request2)]
(is (= 500 status)))))))) (is (= 500 status))))))
(testing "open & closed schemas"
(let [endpoint (fn [schema]
{:get {:parameters {:body schema}
:responses {200 {:body schema}}
:handler (fn [{{:keys [body]} :parameters}]
{:status 200, :body (assoc body :response true)})}})
->app (fn [options]
(ring/ring-handler
(ring/router
["/api"
["/default" (endpoint [:map [:x int?]])]
["/closed" (endpoint [:map {:closed true} [:x int?]])]
["/open" (endpoint [:map {:closed false} [:x int?]])]]
{:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:coercion (malli/create options)}})))
->request (fn [uri] {:uri (str "/api/" uri)
:request-method :get
:muuntaja/request {:format "application/json"}
:body-params {:x 1, :request true}})]
(testing "with defaults"
(let [app (->app nil)]
(testing "default: keys are stripped"
(is (= {:status 200, :body {:x 1}}
(app (->request "default")))))
(testing "closed: keys are stripped"
(is (= {:status 200, :body {:x 1}}
(app (->request "closed")))))
(testing "open: keys are NOT stripped"
(is (= {:status 200, :body {:x 1, :request true, :response true}}
(app (->request "open")))))))
(testing "when schemas are not closed"
(let [app (->app {:compile identity})]
(testing "default: keys are stripped"
(is (= {:status 200, :body {:x 1}}
(app (->request "default")))))
(testing "closed: keys are stripped"
(is (= {:status 200, :body {:x 1}}
(app (->request "closed")))))
(testing "open: keys are NOT stripped"
(is (= {:status 200, :body {:x 1, :request true, :response true}}
(app (->request "open")))))))
(testing "when schemas are not closed and extra keys are not stripped"
(let [app (->app {:compile identity, :strip-extra-keys false})]
(testing "default: keys are NOT stripped"
(is (= {:status 200, :body {:x 1, :request true, :response true}}
(app (->request "default")))))
(testing "closed: FAILS for extra keys"
(is (= 400 (:status (app (->request "closed"))))))
(testing "open: keys are NOT stripped"
(is (= {:status 200, :body {:x 1, :request true, :response true}}
(app (->request "open")))))))))))
#?(:clj #?(:clj
(deftest muuntaja-test (deftest muuntaja-test