From b9f032a5ce48e64ecff7290700c6f6c409c5dca3 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Dec 2017 22:49:32 +0200 Subject: [PATCH] Cleanup Coercion --- .../src/example/dspec.clj | 2 +- .../src/example/schema.clj | 2 +- .../src/example/server.clj | 2 +- .../src/example/spec.clj | 2 +- examples/ring-example/src/example/dspec.clj | 2 +- examples/ring-example/src/example/schema.clj | 2 +- examples/ring-example/src/example/server.clj | 2 +- examples/ring-example/src/example/spec.clj | 2 +- modules/reitit-core/src/reitit/coercion.cljc | 134 ++++++++++++++ .../reitit-core/src/reitit/interceptor.cljc | 136 ++++++++++++++ .../reitit-core/src/reitit/middleware.cljc | 2 +- .../reitit-ring/src/reitit/ring/coercion.cljc | 175 ------------------ .../src/reitit/ring/coercion/protocol.cljc | 16 -- .../src/reitit/ring/coercion_middleware.cljc | 74 ++++++++ .../reitit/{ring => }/coercion/schema.cljc | 28 +-- .../src/reitit/{ring => }/coercion/spec.cljc | 30 +-- perf-test/clj/reitit/coercion_perf_test.clj | 64 ++++--- perf-test/clj/reitit/go_perf_test.clj | 4 +- test/cljc/reitit/coercion_test.cljc | 26 +-- test/cljc/reitit/core_test.cljc | 16 ++ 20 files changed, 444 insertions(+), 277 deletions(-) create mode 100644 modules/reitit-core/src/reitit/coercion.cljc create mode 100644 modules/reitit-core/src/reitit/interceptor.cljc delete mode 100644 modules/reitit-ring/src/reitit/ring/coercion.cljc delete mode 100644 modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc create mode 100644 modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc rename modules/reitit-schema/src/reitit/{ring => }/coercion/schema.cljc (81%) rename modules/reitit-spec/src/reitit/{ring => }/coercion/spec.cljc (81%) diff --git a/examples/just-coercion-with-ring/src/example/dspec.clj b/examples/just-coercion-with-ring/src/example/dspec.clj index 0a52b145..42cc6503 100644 --- a/examples/just-coercion-with-ring/src/example/dspec.clj +++ b/examples/just-coercion-with-ring/src/example/dspec.clj @@ -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])) diff --git a/examples/just-coercion-with-ring/src/example/schema.clj b/examples/just-coercion-with-ring/src/example/schema.clj index 198ecb87..137a2ae0 100644 --- a/examples/just-coercion-with-ring/src/example/schema.clj +++ b/examples/just-coercion-with-ring/src/example/schema.clj @@ -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])) diff --git a/examples/just-coercion-with-ring/src/example/server.clj b/examples/just-coercion-with-ring/src/example/server.clj index 776a5127..90fbf11c 100644 --- a/examples/just-coercion-with-ring/src/example/server.clj +++ b/examples/just-coercion-with-ring/src/example/server.clj @@ -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)) diff --git a/examples/just-coercion-with-ring/src/example/spec.clj b/examples/just-coercion-with-ring/src/example/spec.clj index 90f5a40c..dbd04cd1 100644 --- a/examples/just-coercion-with-ring/src/example/spec.clj +++ b/examples/just-coercion-with-ring/src/example/spec.clj @@ -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])) diff --git a/examples/ring-example/src/example/dspec.clj b/examples/ring-example/src/example/dspec.clj index 7c336214..25813527 100644 --- a/examples/ring-example/src/example/dspec.clj +++ b/examples/ring-example/src/example/dspec.clj @@ -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 diff --git a/examples/ring-example/src/example/schema.clj b/examples/ring-example/src/example/schema.clj index 55c3179a..93129d9e 100644 --- a/examples/ring-example/src/example/schema.clj +++ b/examples/ring-example/src/example/schema.clj @@ -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 diff --git a/examples/ring-example/src/example/server.clj b/examples/ring-example/src/example/server.clj index 0181c606..db933348 100644 --- a/examples/ring-example/src/example/server.clj +++ b/examples/ring-example/src/example/server.clj @@ -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])) diff --git a/examples/ring-example/src/example/spec.clj b/examples/ring-example/src/example/spec.clj index e6acc570..8e8fbe3c 100644 --- a/examples/ring-example/src/example/spec.clj +++ b/examples/ring-example/src/example/spec.clj @@ -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 diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc new file mode 100644 index 00000000..af9e2ba2 --- /dev/null +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -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))) diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc new file mode 100644 index 00000000..847500cb --- /dev/null +++ b/modules/reitit-core/src/reitit/interceptor.cljc @@ -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"}}) diff --git a/modules/reitit-core/src/reitit/middleware.cljc b/modules/reitit-core/src/reitit/middleware.cljc index 4d9e6de6..2ca2d276 100644 --- a/modules/reitit-core/src/reitit/middleware.cljc +++ b/modules/reitit-core/src/reitit/middleware.cljc @@ -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 diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc deleted file mode 100644 index 77d1f1b4..00000000 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ /dev/null @@ -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))))))))})) diff --git a/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc b/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc deleted file mode 100644 index 12838b55..00000000 --- a/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc +++ /dev/null @@ -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)) diff --git a/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc b/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc new file mode 100644 index 00000000..e2b2cf27 --- /dev/null +++ b/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc @@ -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))))))))})) diff --git a/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc similarity index 81% rename from modules/reitit-schema/src/reitit/ring/coercion/schema.cljc rename to modules/reitit-schema/src/reitit/coercion/schema.cljc index f514fc47..fa130571 100644 --- a/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -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? diff --git a/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc similarity index 81% rename from modules/reitit-spec/src/reitit/ring/coercion/spec.cljc rename to modules/reitit-spec/src/reitit/coercion/spec.cljc index 90dd3f2d..f316dd85 100644 --- a/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -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? diff --git a/perf-test/clj/reitit/coercion_perf_test.clj b/perf-test/clj/reitit/coercion_perf_test.clj index e664f1f0..d3641446 100644 --- a/perf-test/clj/reitit/coercion_perf_test.clj +++ b/perf-test/clj/reitit/coercion_perf_test.clj @@ -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" diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 4a7c2790..676c99a2 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -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"}) diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index fcd7c8ae..9442505c 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -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 diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index cbc1e266..01aa2770 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -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)}))