mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11:11 +00:00
Cleanup Coercion
This commit is contained in:
parent
5a74a4269f
commit
b9f032a5ce
20 changed files with 444 additions and 277 deletions
|
|
@ -1,5 +1,5 @@
|
|||
(ns example.dspec
|
||||
(:require [reitit.ring.coercion :as coercion]
|
||||
(:require [reitit.ring.coercion-middleware :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]
|
||||
[example.server :as server]))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
(ns example.schema
|
||||
(:require [reitit.ring.coercion :as coercion]
|
||||
(:require [reitit.ring.coercion-middleware :as coercion]
|
||||
[reitit.ring.coercion.schema :as schema-coercion]
|
||||
[example.server :as server]))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
(ns example.server
|
||||
(:require [ring.adapter.jetty :as jetty]
|
||||
[reitit.middleware :as middleware]
|
||||
[reitit.ring.coercion :as coercion]))
|
||||
[reitit.ring.coercion-middleware :as coercion]))
|
||||
|
||||
(defonce ^:private server (atom nil))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
(ns example.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[spec-tools.spec :as spec]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion-middleware :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]
|
||||
[example.server :as server]))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
(ns example.dspec
|
||||
(:require [reitit.ring.coercion :as coercion]
|
||||
(:require [reitit.ring.coercion-middleware :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]))
|
||||
|
||||
(def routes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
(ns example.schema
|
||||
(:require [schema.core :as s]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion-middleware :as coercion]
|
||||
[reitit.ring.coercion.schema :as schema-coercion]))
|
||||
|
||||
(def routes
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
[ring.middleware.params]
|
||||
[muuntaja.middleware]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion-middleware :as coercion]
|
||||
[example.dspec]
|
||||
[example.schema]
|
||||
[example.spec]))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
(ns example.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[spec-tools.spec :as spec]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion-middleware :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]))
|
||||
|
||||
;; wrap into Spec Records to enable runtime conforming
|
||||
|
|
|
|||
134
modules/reitit-core/src/reitit/coercion.cljc
Normal file
134
modules/reitit-core/src/reitit/coercion.cljc
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
(ns reitit.coercion
|
||||
(:require [clojure.walk :as walk]
|
||||
[spec-tools.core :as st]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
;;
|
||||
;; Protocol
|
||||
;;
|
||||
|
||||
(defprotocol Coercion
|
||||
"Pluggable coercion protocol"
|
||||
(-get-name [this] "Keyword name for the coercion")
|
||||
(-get-apidocs [this model data] "???")
|
||||
(-compile-model [this model name] "Compiles a model")
|
||||
(-open-model [this model] "Returns a new model which allows extra keys in maps")
|
||||
(-encode-error [this error] "Converts error in to a serializable format")
|
||||
(-request-coercer [this type model] "Returns a `value format => value` request coercion function")
|
||||
(-response-coercer [this model] "Returns a `value format => value` response coercion function"))
|
||||
|
||||
(defrecord CoercionError [])
|
||||
|
||||
(defn error? [x]
|
||||
(instance? CoercionError x))
|
||||
|
||||
;;
|
||||
;; api-docs
|
||||
;;
|
||||
|
||||
#_(defn get-apidocs [coercion spec info]
|
||||
(protocol/get-apidocs coercion spec info))
|
||||
|
||||
;;
|
||||
;; coercer
|
||||
;;
|
||||
|
||||
(defrecord ParameterCoercion [in style keywordize? open?])
|
||||
|
||||
(def ^:no-doc ring-parameter-coercion
|
||||
{:query (->ParameterCoercion :query-params :string true true)
|
||||
:body (->ParameterCoercion :body-params :body false false)
|
||||
:form (->ParameterCoercion :form-params :string true true)
|
||||
:header (->ParameterCoercion :header-params :string true true)
|
||||
:path (->ParameterCoercion :path-params :string true true)})
|
||||
|
||||
(defn ^:no-doc request-coercion-failed! [result coercion value in request]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Request coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::request-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:request in]
|
||||
:request request}))))
|
||||
|
||||
(defn ^:no-doc response-coercion-failed! [result coercion value request response]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Response coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::response-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:response :body]
|
||||
:request request
|
||||
:response response}))))
|
||||
|
||||
;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
|
||||
(defn request-coercer [coercion type model {:keys [extract-request-format]
|
||||
:or {extract-request-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type)
|
||||
transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||
model (if open? (-open-model coercion model) model)
|
||||
coercer (-request-coercer coercion style model)]
|
||||
(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)
|
||||
result))))))
|
||||
|
||||
(defn response-coercer [coercion model {:keys [extract-response-format]
|
||||
:or {extract-response-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [coercer (-response-coercer coercion model)]
|
||||
(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)
|
||||
result))))))
|
||||
|
||||
(defn encode-error [data]
|
||||
(-> data
|
||||
(dissoc :request :response)
|
||||
(update :coercion -get-name)
|
||||
(->> (-encode-error (:coercion data)))))
|
||||
|
||||
(defn coerce-request [coercers request]
|
||||
(reduce-kv
|
||||
(fn [acc k coercer]
|
||||
(impl/fast-assoc acc k (coercer request)))
|
||||
{}
|
||||
coercers))
|
||||
|
||||
(defn coerce-response [coercers request response]
|
||||
(if response
|
||||
(if-let [coercer (or (coercers (:status response)) (coercers :default))]
|
||||
(impl/fast-assoc response :body (coercer request response)))))
|
||||
|
||||
(defn request-coercers [coercion parameters opts]
|
||||
(->> (for [[k v] parameters
|
||||
:when v]
|
||||
[k (request-coercer coercion k v opts)])
|
||||
(into {})))
|
||||
|
||||
(defn response-coercers [coercion responses opts]
|
||||
(->> (for [[status {:keys [schema]}] responses :when schema]
|
||||
[status (response-coercer coercion schema opts)])
|
||||
(into {})))
|
||||
|
||||
;;
|
||||
;; integration
|
||||
;;
|
||||
|
||||
(defn compile-request-coercers [[p {:keys [parameters coercion]}] opts]
|
||||
(if (and parameters coercion)
|
||||
(request-coercers coercion parameters opts)))
|
||||
136
modules/reitit-core/src/reitit/interceptor.cljc
Normal file
136
modules/reitit-core/src/reitit/interceptor.cljc
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
(ns reitit.interceptor
|
||||
(:require [meta-merge.core :refer [meta-merge]]
|
||||
[reitit.core :as r]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
(defprotocol IntoInterceptor
|
||||
(into-interceptor [this data opts]))
|
||||
|
||||
(defrecord Interceptor [name enter leave error])
|
||||
(defrecord Endpoint [data interceptors])
|
||||
|
||||
(defn create [{:keys [name wrap compile] :as m}]
|
||||
(when (and wrap compile)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Interceptor can't have both :wrap and :compile defined " m) m)))
|
||||
(map->Interceptor m))
|
||||
|
||||
(def ^:dynamic *max-compile-depth* 10)
|
||||
|
||||
(extend-protocol IntoInterceptor
|
||||
|
||||
#?(:clj clojure.lang.APersistentVector
|
||||
:cljs cljs.core.PersistentVector)
|
||||
(into-interceptor [[f & args] data opts]
|
||||
(if-let [{:keys [wrap] :as mw} (into-interceptor f data opts)]
|
||||
(assoc mw :wrap #(apply wrap % args))))
|
||||
|
||||
#?(:clj clojure.lang.Fn
|
||||
:cljs function)
|
||||
(into-interceptor [this _ _]
|
||||
(map->Interceptor
|
||||
{:enter this}))
|
||||
|
||||
#?(:clj clojure.lang.PersistentArrayMap
|
||||
:cljs cljs.core.PersistentArrayMap)
|
||||
(into-interceptor [this data opts]
|
||||
(into-interceptor (create this) data opts))
|
||||
|
||||
#?(:clj clojure.lang.PersistentHashMap
|
||||
:cljs cljs.core.PersistentHashMap)
|
||||
(into-interceptor [this data opts]
|
||||
(into-interceptor (create this) data opts))
|
||||
|
||||
Interceptor
|
||||
(into-interceptor [{:keys [compile] :as this} data opts]
|
||||
(if-not compile
|
||||
this
|
||||
(let [compiled (::compiled opts 0)
|
||||
opts (assoc opts ::compiled (inc compiled))]
|
||||
(when (>= compiled *max-compile-depth*)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Too deep Interceptor compilation - " compiled)
|
||||
{:this this, :data data, :opts opts})))
|
||||
(if-let [interceptor (into-interceptor (compile data opts) data opts)]
|
||||
(map->Interceptor
|
||||
(merge
|
||||
(dissoc this :create)
|
||||
(impl/strip-nils interceptor)))))))
|
||||
|
||||
nil
|
||||
(into-interceptor [_ _ _]))
|
||||
|
||||
(defn- ensure-handler! [path data scope]
|
||||
(when-not (:handler data)
|
||||
(throw (ex-info
|
||||
(str "path \"" path "\" doesn't have a :handler defined"
|
||||
(if scope (str " for " scope)))
|
||||
(merge {:path path, :data data}
|
||||
(if scope {:scope scope}))))))
|
||||
|
||||
(defn expand [interceptors data opts]
|
||||
(->> interceptors
|
||||
(keep #(into-interceptor % data opts))
|
||||
(into [])))
|
||||
|
||||
(defn interceptor-chain [interceptors handler data opts]
|
||||
(expand (conj interceptors handler) data opts))
|
||||
|
||||
(defn compile-result
|
||||
([route opts]
|
||||
(compile-result route opts nil))
|
||||
([[path {:keys [interceptors handler] :as data}]
|
||||
{:keys [::transform] :or {transform identity} :as opts} scope]
|
||||
(ensure-handler! path data scope)
|
||||
(let [interceptors (expand (transform (expand interceptors data opts)) data opts)]
|
||||
(map->Endpoint
|
||||
{:interceptors (interceptor-chain interceptors handler data opts)
|
||||
:data data}))))
|
||||
|
||||
(defn router
|
||||
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
|
||||
support for Interceptors. See [docs](https://metosin.github.io/reitit/) for details.
|
||||
|
||||
Example:
|
||||
|
||||
(router
|
||||
[\"/api\" {:interceptors [i/format i/oauth2]}
|
||||
[\"/users\" {:interceptors [i/delete]
|
||||
:handler get-user}]])
|
||||
|
||||
See router options from [[reitit.core/router]]."
|
||||
([data]
|
||||
(router data nil))
|
||||
([data opts]
|
||||
(let [opts (meta-merge {:compile compile-result} opts)]
|
||||
(r/router data opts))))
|
||||
|
||||
(defn interceptor-handler [router]
|
||||
(with-meta
|
||||
(fn [path]
|
||||
(some->> path
|
||||
(r/match-by-path router)
|
||||
:result
|
||||
:interceptors))
|
||||
{::router router}))
|
||||
|
||||
(defn execute [r {{:keys [uri]} :request :as ctx}]
|
||||
(if-let [interceptors (-> (r/match-by-path r uri)
|
||||
:result
|
||||
:interceptors)]
|
||||
(as-> ctx $
|
||||
(reduce #(%2 %1) $ (keep :enter interceptors))
|
||||
(reduce #(%2 %1) $ (keep :leave interceptors)))))
|
||||
|
||||
(def r
|
||||
(router
|
||||
["/api" {:interceptors [{:name ::add
|
||||
:enter (fn [ctx]
|
||||
(assoc ctx :enter true))
|
||||
:leave (fn [ctx]
|
||||
(assoc ctx :leave true))}]}
|
||||
["/ping" (fn [ctx] (assoc ctx :response "ok"))]]))
|
||||
|
||||
(execute r {:request {:uri "/api/ping"}})
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
(when (>= compiled *max-compile-depth*)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Too deep middleware compilation - " compiled)
|
||||
(str "Too deep Middleware compilation - " compiled)
|
||||
{:this this, :data data, :opts opts})))
|
||||
(if-let [middeware (into-middleware (compile data opts) data opts)]
|
||||
(map->Middleware
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
(ns reitit.ring.coercion
|
||||
(:require [clojure.walk :as walk]
|
||||
[spec-tools.core :as st]
|
||||
[reitit.middleware :as middleware]
|
||||
[reitit.ring.coercion.protocol :as protocol]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
#_(defn get-apidocs [coercion spec info]
|
||||
(protocol/get-apidocs coercion spec info))
|
||||
|
||||
;;
|
||||
;; coercer
|
||||
;;
|
||||
|
||||
(defrecord ParameterCoercion [in style keywordize? open?])
|
||||
|
||||
(def ^:no-doc ring-parameter-coercion
|
||||
{:query (->ParameterCoercion :query-params :string true true)
|
||||
:body (->ParameterCoercion :body-params :body false false)
|
||||
:form (->ParameterCoercion :form-params :string true true)
|
||||
:header (->ParameterCoercion :header-params :string true true)
|
||||
:path (->ParameterCoercion :path-params :string true true)})
|
||||
|
||||
(defn ^:no-doc request-coercion-failed! [result coercion value in request]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Request coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::request-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:request in]
|
||||
:request request}))))
|
||||
|
||||
(defn ^:no-doc response-coercion-failed! [result coercion value request response]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Response coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::response-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:response :body]
|
||||
:request request
|
||||
:response response}))))
|
||||
|
||||
;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
|
||||
(defn ^:no-doc request-coercer [coercion type model {:keys [extract-request-format]
|
||||
:or {extract-request-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type)
|
||||
transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||
model (if open? (protocol/open-model coercion model) model)
|
||||
coercer (protocol/request-coercer coercion style model)]
|
||||
(fn [request]
|
||||
(let [value (transform request)
|
||||
format (extract-request-format request)
|
||||
result (coercer value format)]
|
||||
(if (protocol/error? result)
|
||||
(request-coercion-failed! result coercion value in request)
|
||||
result))))))
|
||||
|
||||
(defn ^:no-doc response-coercer [coercion model {:keys [extract-response-format]
|
||||
:or {extract-response-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [coercer (protocol/response-coercer coercion model)]
|
||||
(fn [request response]
|
||||
(let [format (extract-response-format request response)
|
||||
value (:body response)
|
||||
result (coercer value format)]
|
||||
(if (protocol/error? result)
|
||||
(response-coercion-failed! result coercion value request response)
|
||||
result))))))
|
||||
|
||||
(defn ^:no-doc encode-error [data]
|
||||
(-> data
|
||||
(dissoc :request :response)
|
||||
(update :coercion protocol/get-name)
|
||||
(->> (protocol/encode-error (:coercion data)))))
|
||||
|
||||
(defn ^:no-doc coerce-request [coercers request]
|
||||
(reduce-kv
|
||||
(fn [acc k coercer]
|
||||
(impl/fast-assoc acc k (coercer request)))
|
||||
{}
|
||||
coercers))
|
||||
|
||||
(defn ^:no-doc coerce-response [coercers request response]
|
||||
(if response
|
||||
(if-let [coercer (or (coercers (:status response)) (coercers :default))]
|
||||
(impl/fast-assoc response :body (coercer request response)))))
|
||||
|
||||
(defn ^:no-doc request-coercers [coercion parameters opts]
|
||||
(->> (for [[k v] parameters
|
||||
:when v]
|
||||
[k (request-coercer coercion k v opts)])
|
||||
(into {})))
|
||||
|
||||
(defn ^:no-doc response-coercers [coercion responses opts]
|
||||
(->> (for [[status {:keys [schema]}] responses :when schema]
|
||||
[status (response-coercer coercion schema opts)])
|
||||
(into {})))
|
||||
|
||||
(defn ^:no-doc handle-coercion-exception [e respond raise]
|
||||
(let [data (ex-data e)]
|
||||
(if-let [status (condp = (:type data)
|
||||
::request-coercion 400
|
||||
::response-coercion 500
|
||||
nil)]
|
||||
(respond
|
||||
{:status status
|
||||
:body (encode-error data)})
|
||||
(raise e))))
|
||||
|
||||
;;
|
||||
;; middleware
|
||||
;;
|
||||
|
||||
(def coerce-request-middleware
|
||||
"Middleware for pluggable request coercion.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :parameters from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-parameters
|
||||
:compile (fn [{:keys [coercion parameters]} opts]
|
||||
(if (and coercion parameters)
|
||||
(let [coercers (request-coercers coercion parameters opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(let [coerced (coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced))))
|
||||
([request respond raise]
|
||||
(let [coerced (coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
|
||||
|
||||
(def coerce-response-middleware
|
||||
"Middleware for pluggable response coercion.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :responses from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-response
|
||||
:compile (fn [{:keys [coercion responses]} opts]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(coerce-response coercers request (handler request)))
|
||||
([request respond raise]
|
||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||
|
||||
(def coerce-exceptions-middleware
|
||||
"Middleware for handling coercion exceptions.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :parameters or :responses from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-exceptions
|
||||
:compile (fn [{:keys [coercion parameters responses]} _]
|
||||
(if (and coercion (or parameters responses))
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(try
|
||||
(handler request)
|
||||
(catch #?(:clj Exception :cljs js/Error) e
|
||||
(handle-coercion-exception e identity #(throw %)))))
|
||||
([request respond raise]
|
||||
(try
|
||||
(handler request respond #(handle-coercion-exception % respond raise))
|
||||
(catch #?(:clj Exception :cljs js/Error) e
|
||||
(handle-coercion-exception e respond raise))))))))}))
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
(ns reitit.ring.coercion.protocol)
|
||||
|
||||
(defprotocol Coercion
|
||||
"Pluggable coercion protocol"
|
||||
(get-name [this] "Keyword name for the coercion")
|
||||
(get-apidocs [this model data] "???")
|
||||
(compile-model [this model name] "Compiles a model")
|
||||
(open-model [this model] "Returns a new model which allows extra keys in maps")
|
||||
(encode-error [this error] "Converts error in to a serializable format")
|
||||
(request-coercer [this type model] "Returns a `value format => value` request coercion function")
|
||||
(response-coercer [this model] "Returns a `value format => value` response coercion function"))
|
||||
|
||||
(defrecord CoercionError [])
|
||||
|
||||
(defn error? [x]
|
||||
(instance? CoercionError x))
|
||||
74
modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc
Normal file
74
modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
(ns reitit.ring.coercion-middleware
|
||||
(:require [reitit.middleware :as middleware]
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
(defn handle-coercion-exception [e respond raise]
|
||||
(let [data (ex-data e)]
|
||||
(if-let [status (condp = (:type data)
|
||||
::coercion/request-coercion 400
|
||||
::coercion/response-coercion 500
|
||||
nil)]
|
||||
(respond
|
||||
{:status status
|
||||
:body (coercion/encode-error data)})
|
||||
(raise e))))
|
||||
|
||||
;;
|
||||
;; middleware
|
||||
;;
|
||||
|
||||
(def coerce-request-middleware
|
||||
"Middleware for pluggable request coercion.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-parameters
|
||||
:compile (fn [{:keys [coercion parameters]} opts]
|
||||
(if (and coercion parameters)
|
||||
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(let [coerced (coercion/coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced))))
|
||||
([request respond raise]
|
||||
(let [coerced (coercion/coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
|
||||
|
||||
(def coerce-response-middleware
|
||||
"Middleware for pluggable response coercion.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :responses from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-response
|
||||
:compile (fn [{:keys [coercion responses]} opts]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (coercion/response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(coercion/coerce-response coercers request (handler request)))
|
||||
([request respond raise]
|
||||
(handler request #(respond (coercion/coerce-response coercers request %)) raise)))))))}))
|
||||
|
||||
(def coerce-exceptions-middleware
|
||||
"Middleware for handling coercion exceptions.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters or :responses from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-exceptions
|
||||
:compile (fn [{:keys [coercion parameters responses]} _]
|
||||
(if (and coercion (or parameters responses))
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(try
|
||||
(handler request)
|
||||
(catch #?(:clj Exception :cljs js/Error) e
|
||||
(handle-coercion-exception e identity #(throw %)))))
|
||||
([request respond raise]
|
||||
(try
|
||||
(handler request respond #(handle-coercion-exception % respond raise))
|
||||
(catch #?(:clj Exception :cljs js/Error) e
|
||||
(handle-coercion-exception e respond raise))))))))}))
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
(ns reitit.ring.coercion.schema
|
||||
(:require [schema.core :as s]
|
||||
(ns reitit.coercion.schema
|
||||
(:require [clojure.walk :as walk]
|
||||
[schema.core :as s]
|
||||
[schema-tools.core :as st]
|
||||
[schema.coerce :as sc]
|
||||
[schema.utils :as su]
|
||||
[schema-tools.coerce :as stc]
|
||||
[spec-tools.swagger.core :as swagger]
|
||||
[clojure.walk :as walk]
|
||||
[reitit.ring.coercion.protocol :as protocol]))
|
||||
[reitit.coercion :as coercion]))
|
||||
|
||||
(def string-coercion-matcher
|
||||
stc/string-coercion-matcher)
|
||||
|
|
@ -35,24 +35,24 @@
|
|||
|
||||
(defrecord SchemaCoercion [name matchers coerce-response?]
|
||||
|
||||
protocol/Coercion
|
||||
(get-name [_] name)
|
||||
coercion/Coercion
|
||||
(-get-name [_] name)
|
||||
|
||||
(get-apidocs [_ _ {:keys [parameters responses] :as info}]
|
||||
(-get-apidocs [_ _ {:keys [parameters responses] :as info}]
|
||||
(cond-> (dissoc info :parameters :responses)
|
||||
parameters (assoc ::swagger/parameters parameters)
|
||||
responses (assoc ::swagger/responses responses)))
|
||||
|
||||
(compile-model [_ model _] model)
|
||||
(-compile-model [_ model _] model)
|
||||
|
||||
(open-model [_ schema] (st/open-schema schema))
|
||||
(-open-model [_ schema] (st/open-schema schema))
|
||||
|
||||
(encode-error [_ error]
|
||||
(-encode-error [_ error]
|
||||
(-> error
|
||||
(update :schema stringify)
|
||||
(update :errors stringify)))
|
||||
|
||||
(request-coercer [_ type schema]
|
||||
(-request-coercer [_ type schema]
|
||||
(let [{:keys [formats default]} (matchers type)
|
||||
coercers (->> (for [m (conj (vals formats) default)]
|
||||
[m (sc/coercer schema m)])
|
||||
|
|
@ -62,15 +62,15 @@
|
|||
(let [coercer (coercers matcher)
|
||||
coerced (coercer value)]
|
||||
(if-let [error (su/error-val coerced)]
|
||||
(protocol/map->CoercionError
|
||||
(coercion/map->CoercionError
|
||||
{:schema schema
|
||||
:errors error})
|
||||
coerced))
|
||||
value))))
|
||||
|
||||
(response-coercer [this schema]
|
||||
(-response-coercer [this schema]
|
||||
(if (coerce-response? schema)
|
||||
(protocol/request-coercer this :response schema))))
|
||||
(coercion/-request-coercer this :response schema))))
|
||||
|
||||
(def default-options
|
||||
{:coerce-response? coerce-response?
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
(ns reitit.ring.coercion.spec
|
||||
(ns reitit.coercion.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[spec-tools.core :as st #?@(:cljs [:refer [Spec]])]
|
||||
[spec-tools.data-spec :as ds]
|
||||
[spec-tools.conform :as conform]
|
||||
[spec-tools.swagger.core :as swagger]
|
||||
[reitit.ring.coercion.protocol :as protocol])
|
||||
[reitit.coercion :as coercion])
|
||||
#?(:clj
|
||||
(:import (spec_tools.core Spec))))
|
||||
|
||||
|
|
@ -54,51 +54,51 @@
|
|||
|
||||
(defrecord SpecCoercion [name conforming coerce-response?]
|
||||
|
||||
protocol/Coercion
|
||||
(get-name [_] name)
|
||||
coercion/Coercion
|
||||
(-get-name [_] name)
|
||||
|
||||
(get-apidocs [this _ {:keys [parameters responses] :as info}]
|
||||
(-get-apidocs [this _ {:keys [parameters responses] :as info}]
|
||||
(cond-> (dissoc info :parameters :responses)
|
||||
parameters (assoc
|
||||
::swagger/parameters
|
||||
(into
|
||||
(empty parameters)
|
||||
(for [[k v] parameters]
|
||||
[k (protocol/compile-model this v nil)])))
|
||||
[k (coercion/-compile-model this v nil)])))
|
||||
responses (assoc
|
||||
::swagger/responses
|
||||
(into
|
||||
(empty responses)
|
||||
(for [[k response] responses]
|
||||
[k (update response :schema #(protocol/compile-model this % nil))])))))
|
||||
[k (update response :schema #(coercion/-compile-model this % nil))])))))
|
||||
|
||||
(compile-model [_ model _]
|
||||
(-compile-model [_ model _]
|
||||
(into-spec model (or name (gensym "spec"))))
|
||||
|
||||
(open-model [_ spec] spec)
|
||||
(-open-model [_ spec] spec)
|
||||
|
||||
(encode-error [_ error]
|
||||
(-encode-error [_ error]
|
||||
(-> error
|
||||
(update :spec (comp str s/form))
|
||||
(update :problems (partial mapv #(update % :pred stringify-pred)))))
|
||||
|
||||
(request-coercer [this type spec]
|
||||
(let [spec (protocol/compile-model this spec nil)
|
||||
(-request-coercer [this type spec]
|
||||
(let [spec (coercion/-compile-model this spec nil)
|
||||
{:keys [formats default]} (conforming type)]
|
||||
(fn [value format]
|
||||
(if-let [conforming (or (get formats format) default)]
|
||||
(let [conformed (st/conform spec value conforming)]
|
||||
(if (s/invalid? conformed)
|
||||
(let [problems (st/explain-data spec value conforming)]
|
||||
(protocol/map->CoercionError
|
||||
(coercion/map->CoercionError
|
||||
{:spec spec
|
||||
:problems (::s/problems problems)}))
|
||||
(s/unform spec conformed)))
|
||||
value))))
|
||||
|
||||
(response-coercer [this spec]
|
||||
(-response-coercer [this spec]
|
||||
(if (coerce-response? spec)
|
||||
(protocol/request-coercer this :response spec))))
|
||||
(coercion/-request-coercer this :response spec))))
|
||||
|
||||
(def default-options
|
||||
{:coerce-response? coerce-response?
|
||||
|
|
@ -4,18 +4,16 @@
|
|||
[reitit.perf-utils :refer :all]
|
||||
[clojure.spec.alpha :as s]
|
||||
[spec-tools.core :as st]
|
||||
|
||||
[reitit.core :as reitit]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec]
|
||||
[reitit.ring.coercion.schema :as schema]
|
||||
[reitit.ring.coercion.protocol :as protocol]
|
||||
[spec-tools.data-spec :as ds]
|
||||
[muuntaja.middleware :as mm]
|
||||
[muuntaja.core :as m]
|
||||
[muuntaja.format.jsonista :as jsonista-format]
|
||||
[jsonista.core :as j]
|
||||
[reitit.coercion-middleware :as coercion-middleware]
|
||||
[reitit.coercion.spec :as spec]
|
||||
[reitit.coercion.schema :as schema]
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.core :as r])
|
||||
(:import (java.io ByteArrayInputStream)))
|
||||
|
||||
|
|
@ -41,14 +39,14 @@
|
|||
(s/def ::k (s/keys :req-un [::x ::y]))
|
||||
|
||||
(let [spec (spec/into-spec {:x int?, :y int?} ::jeah)
|
||||
coercers (#'coercion/request-coercers spec/coercion {:body spec})
|
||||
coercers (#'coercion-middleware/request-coercers spec/coercion {:body spec})
|
||||
params {:x "1", :y "2"}
|
||||
request {:body-params {:x "1", :y "2"}}]
|
||||
|
||||
;; 4600ns
|
||||
(bench!
|
||||
"coerce-parameters"
|
||||
(#'coercion/coerce-parameters coercers request))
|
||||
(#'coercion-middleware/coerce-parameters coercers request))
|
||||
|
||||
;; 2700ns
|
||||
(bench!
|
||||
|
|
@ -85,14 +83,14 @@
|
|||
params))))))
|
||||
|
||||
(defrecord NoOpCoercion []
|
||||
protocol/Coercion
|
||||
(get-name [_] :no-op)
|
||||
(get-apidocs [_ _ {:keys [parameters responses] :as info}])
|
||||
(compile-model [_ model _] model)
|
||||
(open-model [_ spec] spec)
|
||||
(encode-error [_ error] error)
|
||||
(request-coercer [_ type spec] (fn [value format] value))
|
||||
(response-coercer [this spec] (protocol/request-coercer this :response spec)))
|
||||
coercion/Coercion
|
||||
(-get-name [_] :no-op)
|
||||
(-get-apidocs [_ _ {:keys [parameters responses] :as info}])
|
||||
(-compile-model [_ model _] model)
|
||||
(-open-model [_ spec] spec)
|
||||
(-encode-error [_ error] error)
|
||||
(-request-coercer [_ type spec] (fn [value format] value))
|
||||
(-response-coercer [this spec] (protocol/request-coercer this :response spec)))
|
||||
|
||||
(comment
|
||||
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]]
|
||||
|
|
@ -107,24 +105,24 @@
|
|||
app (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware]
|
||||
:coercion coercion}}))
|
||||
app2 (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware]
|
||||
:coercion coercion}}))
|
||||
app3 (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/wrap-coerce-response]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/wrap-coerce-response]
|
||||
:coercion coercion}}))
|
||||
app4 (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion coercion}}))
|
||||
req {:request-method :get
|
||||
:uri "/api/ping"
|
||||
|
|
@ -161,8 +159,8 @@
|
|||
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}})))
|
||||
|
||||
(app
|
||||
|
|
@ -205,8 +203,8 @@
|
|||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [[mm/wrap-format m]
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion schema/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
@ -228,8 +226,8 @@
|
|||
:handler (fn [request]
|
||||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion schema/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
@ -251,8 +249,8 @@
|
|||
:handler (fn [request]
|
||||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
@ -279,8 +277,8 @@
|
|||
:handler (fn [request]
|
||||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@
|
|||
"httpRouter"
|
||||
|
||||
;; 77ns
|
||||
;; 730ns
|
||||
;; 960ns
|
||||
;; 700ns
|
||||
;; 890ns
|
||||
(title "reitit-ring")
|
||||
(let [r1 (map->Request {:request-method :get, :uri "/1/users"})
|
||||
r2 (map->Request {:request-method :get, :uri "/1/classes/go"})
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[schema.core :as s]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec]
|
||||
[reitit.ring.coercion.schema :as schema])
|
||||
[reitit.ring.coercion-middleware :as coercion-middleware]
|
||||
[reitit.coercion.spec :as spec]
|
||||
[reitit.coercion.schema :as schema])
|
||||
#?(:clj
|
||||
(:import (clojure.lang ExceptionInfo))))
|
||||
|
||||
|
|
@ -53,8 +53,8 @@
|
|||
:coercion spec/coercion}})))]
|
||||
|
||||
(testing "withut exception handling"
|
||||
(let [app (create [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
(let [app (create [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
|
|
@ -74,9 +74,9 @@
|
|||
(app invalid-request2))))))
|
||||
|
||||
(testing "with exception handling"
|
||||
(let [app (create [coercion/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
(let [app (create [coercion-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
|
|
@ -108,8 +108,8 @@
|
|||
:coercion schema/coercion}})))]
|
||||
|
||||
(testing "withut exception handling"
|
||||
(let [app (create [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
(let [app (create [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
|
|
@ -129,9 +129,9 @@
|
|||
(app invalid-request2))))
|
||||
|
||||
(testing "with exception handling"
|
||||
(let [app (create [coercion/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
(let [app (create [coercion-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
|
|
|
|||
|
|
@ -234,3 +234,19 @@
|
|||
[["/a"] ["/a"]]))))
|
||||
(testing "can be configured to ignore"
|
||||
(is (not (nil? (r/router [["/a"] ["/a"]] {:conflicts (constantly nil)})))))))
|
||||
|
||||
(require '[reitit.coercion :as coercion])
|
||||
(require '[reitit.coercion.spec :as spec])
|
||||
|
||||
(def r
|
||||
(r/router
|
||||
["/user/:user-id" {:name ::user
|
||||
:parameters {:path {:user-id int?}}}]
|
||||
{:compile coercion/compile-request-coercers
|
||||
:data {:coercion spec/coercion}}))
|
||||
|
||||
(def m
|
||||
(r/match-by-path r "/user/123"))
|
||||
|
||||
(let [m (r/match-by-path r "/user/123")]
|
||||
(coercion/coerce-request (:result m) {:path-params (:params m)}))
|
||||
|
|
|
|||
Loading…
Reference in a new issue