diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index 403f592f..4777b28a 100644 --- a/doc/ring/coercion.md +++ b/doc/ring/coercion.md @@ -16,12 +16,12 @@ To use `Coercion` with Ring, one needs to do the following: * `:responses` map, with response status codes as keys (or `:default` for "everything else") with maps with `:schema` and optionally `:description` as values. 2. Set a `Coercion` implementation to route data under `:coercion` 3. Mount request & response coercion middleware to the routes (can be done for all routes as the middleware are only mounted to routes which have the parameters &/ responses defined): - * `reitit.ring.coercion/gen-wrap-coerce-parameters` - * `reitit.ring.coercion/gen-wrap-coerce-response` + * `reitit.ring.coercion/coerce-request-middleware` + * `reitit.ring.coercion/coerce-response-middleware` If the request coercion succeeds, the coerced parameters are injected into request under `:parameters`. -If either request or response coercion fails, an descriptive error is thrown. To turn the exceptions into http responses, one can also mount the `reitit.ring.coercion/gen-wrap-coerce-exceptions` middleware +If either request or response coercion fails, an descriptive error is thrown. To turn the exceptions into http responses, one can also mount the `reitit.ring.coercion/coerce-exceptions-middleware` middleware ### Example with Schema @@ -40,9 +40,9 @@ If either request or response coercion fails, an descriptive error is thrown. To :handler (fn [{{{:keys [x y]} :body} :parameters}] {:status 200 :body {:total (+ x y)}})}}]] - {:data {:middleware [coercion/gen-wrap-coerce-exceptions - coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-exceptions-middleware + coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion schema/coercion}}))) ``` @@ -89,9 +89,9 @@ Invalid request: :handler (fn [{{{:keys [x y]} :body} :parameters}] {:status 200 :body {:total (+ x y)}})}}]] - {:data {:middleware [coercion/gen-wrap-coerce-exceptions - coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-exceptions-middleware + coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion spec/coercion}}))) ``` @@ -152,9 +152,9 @@ Currently, `clojure.spec` [doesn't support runtime transformations via conformin :handler (fn [{{{:keys [x y]} :body} :parameters}] {:status 200 :body {:total (+ x y)}})}}]] - {:data {:middleware [coercion/gen-wrap-coerce-exceptions - coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-exceptions-middleware + coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion spec/coercion}}))) ``` @@ -200,9 +200,9 @@ To plug in new validation engine, see the (defprotocol Coercion "Pluggable coercion protocol" (get-name [this] "Keyword name for the coercion") - (compile [this model name] "Compiles a coercion model") (get-apidocs [this model data] "???") - (make-open [this model] "Returns a new map model which doesn't fail on extra keys") + (compile-model [this model name] "Compiles a coercion model") + (open-model [this model] "Returns a new map model which doesn't fail on extra keys") (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")) diff --git a/doc/ring/compiling_middleware.md b/doc/ring/compiling_middleware.md index c522e8e8..11b62b08 100644 --- a/doc/ring/compiling_middleware.md +++ b/doc/ring/compiling_middleware.md @@ -51,7 +51,7 @@ To demonstrate the two approaches, below are response coercion middleware writte ```clj (require '[reitit.ring.middleware :as middleware]) -(def gen-wrap-coerce-response +(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." diff --git a/examples/just-coercion-with-ring/.gitignore b/examples/just-coercion-with-ring/.gitignore new file mode 100644 index 00000000..c53038ec --- /dev/null +++ b/examples/just-coercion-with-ring/.gitignore @@ -0,0 +1,11 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +.hgignore +.hg/ diff --git a/examples/just-coercion-with-ring/README.md b/examples/just-coercion-with-ring/README.md new file mode 100644 index 00000000..f1da29e8 --- /dev/null +++ b/examples/just-coercion-with-ring/README.md @@ -0,0 +1,39 @@ +# Just Coercion With Ring + +A Sample project showing how to use the reitit coercion with pure ring. + +* `Middleware` are turned into normal ring middleware via `reitit.middleware/chain` +* Endpoint parameters are given to middleware as arguments +* Coerced parameters are available from `:parameters` + +## Usage + +```clj +> lein repl + +(require '[example.server :as server]) + +;; the manually coerced version +(require '[example.naive :as naive]) +(server/restart naive/app) + +;; schema-coercion +(require '[example.schema :as schema]) +(server/restart schema/app) + +;; spec-coercion +(require '[example.spec :as spec]) +(server/restart spec/app) + +;; data-spec-coercion +(require '[example.dspec :as dspec]) +(server/restart dspec/app) +``` + +To test the endpoint: + +http://localhost:3000/?x=1&y=20 + +## License + +Copyright © 2017 Metosin Oy diff --git a/examples/just-coercion-with-ring/project.clj b/examples/just-coercion-with-ring/project.clj new file mode 100644 index 00000000..11bbb381 --- /dev/null +++ b/examples/just-coercion-with-ring/project.clj @@ -0,0 +1,6 @@ +(defproject just-coercion-with-ring "0.1.0-SNAPSHOT" + :description "Reitit coercion with vanilla ring" + :dependencies [[org.clojure/clojure "1.9.0-RC2"] + [ring "1.6.3"] + [metosin/muuntaja "0.4.1"] + [metosin/reitit "0.1.0-SNAPSHOT"]]) diff --git a/examples/just-coercion-with-ring/src/example/dspec.clj b/examples/just-coercion-with-ring/src/example/dspec.clj new file mode 100644 index 00000000..0a52b145 --- /dev/null +++ b/examples/just-coercion-with-ring/src/example/dspec.clj @@ -0,0 +1,15 @@ +(ns example.dspec + (:require [reitit.ring.coercion :as coercion] + [reitit.ring.coercion.spec :as spec-coercion] + [example.server :as server])) + +(defn handler [{{{:keys [x y]} :query} :parameters}] + {:status 200 + :body {:result (+ x y) + :source :data-spec}}) + +(def app + (-> #'handler + (server/wrap-coercion + {:parameters {:query {:x int?, :y int?}} + :coercion spec-coercion/coercion}))) diff --git a/examples/just-coercion-with-ring/src/example/naive.clj b/examples/just-coercion-with-ring/src/example/naive.clj new file mode 100644 index 00000000..5fa8569d --- /dev/null +++ b/examples/just-coercion-with-ring/src/example/naive.clj @@ -0,0 +1,27 @@ +(ns example.naive + (:require [clojure.spec.alpha :as s] + [clojure.walk :as walk] + [example.server :as server])) + +(s/def ::x int?) +(s/def ::y int?) +(s/def ::request (s/keys :req-un [::x ::y])) + +(defn ->long [x] + (try + (Long/parseLong x) + (catch Exception _ + x))) + +(defn app [request] + (println (:query-params request)) + (let [{:keys [x y] :as params} (-> (:query-params request) + (walk/keywordize-keys) + (update :x ->long) + (update :y ->long))] + (if (s/valid? ::request params) + {:status 200 + :body {:result (+ x y) + :source :naive}} + {:status 400 + :body "invalid input"}))) diff --git a/examples/just-coercion-with-ring/src/example/schema.clj b/examples/just-coercion-with-ring/src/example/schema.clj new file mode 100644 index 00000000..198ecb87 --- /dev/null +++ b/examples/just-coercion-with-ring/src/example/schema.clj @@ -0,0 +1,15 @@ +(ns example.schema + (:require [reitit.ring.coercion :as coercion] + [reitit.ring.coercion.schema :as schema-coercion] + [example.server :as server])) + +(defn handler [{{{:keys [x y]} :query} :parameters}] + {:status 200 + :body {:result (+ x y) + :source :schema}}) + +(def app + (-> #'handler + (server/wrap-coercion + {:parameters {:query {:x Long, :y Long}} + :coercion schema-coercion/coercion}))) diff --git a/examples/just-coercion-with-ring/src/example/server.clj b/examples/just-coercion-with-ring/src/example/server.clj new file mode 100644 index 00000000..33b1e029 --- /dev/null +++ b/examples/just-coercion-with-ring/src/example/server.clj @@ -0,0 +1,28 @@ +(ns example.server + (:require [ring.adapter.jetty :as jetty] + [reitit.ring.middleware :as middleware] + [reitit.ring.coercion :as coercion])) + +(defonce ^:private server (atom nil)) + +;; unlift Middleware Record into vanilla Ring middleware +;; NOTE: to support format-based body coercion, an options map needs +;; to be set with :extract-request-format and extract-response-format +(defn wrap-coercion [handler resource] + (middleware/chain + [coercion/coerce-request-middleware + coercion/coerce-response-middleware + coercion/coerce-exceptions-middleware] + handler + resource)) + +(defn restart [handler] + (let [app (-> handler + (ring.middleware.params/wrap-params) + (muuntaja.middleware/wrap-format))] + (swap! server (fn [x] + (when x (.stop x)) + (jetty/run-jetty + handler + {:port 3000, :join? false}))) + (println "server running in port 3000"))) diff --git a/examples/just-coercion-with-ring/src/example/spec.clj b/examples/just-coercion-with-ring/src/example/spec.clj new file mode 100644 index 00000000..90f5a40c --- /dev/null +++ b/examples/just-coercion-with-ring/src/example/spec.clj @@ -0,0 +1,23 @@ +(ns example.spec + (:require [clojure.spec.alpha :as s] + [spec-tools.spec :as spec] + [reitit.ring.coercion :as coercion] + [reitit.ring.coercion.spec :as spec-coercion] + [example.server :as server])) + +;; wrap into Spec Records to enable runtime conforming +(s/def ::x spec/int?) +(s/def ::y spec/int?) +(s/def ::request (s/keys :req-un [::x ::y])) + +;; read coerced parameters under :parameters +(defn handler [{{{:keys [x y]} :query} :parameters}] + {:status 200 + :body {:result (+ x y) + :source :spec}}) + +(def app + (-> #'handler + (server/wrap-coercion + {:parameters {:query ::request} + :coercion spec-coercion/coercion}))) diff --git a/examples/ring-example/.gitignore b/examples/ring-example/.gitignore new file mode 100644 index 00000000..c53038ec --- /dev/null +++ b/examples/ring-example/.gitignore @@ -0,0 +1,11 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +.hgignore +.hg/ diff --git a/examples/ring-example/README.md b/examples/ring-example/README.md new file mode 100644 index 00000000..a3811b30 --- /dev/null +++ b/examples/ring-example/README.md @@ -0,0 +1,33 @@ +# Ring example + +A Sample project with ring. + +## Usage + +```clj +> lein repl + +(require '[example.server :as server]) + +(server/restart) +``` + +To test the endpoints using [httpie](https://httpie.org/): + +```bash +# Schema +http GET :3000/schema/plus x==1 y==20 +http POST :3000/schema/plus x:=1 y:=20 + +# Data-specs +http GET :3000/dspec/plus x==1 y==20 +http POST :3000/dspec/plus x:=1 y:=20 + +# Specs +http GET :3000/spec/plus x==1 y==20 +http POST :3000/spec/plus x:=1 y:=20 +``` + +## License + +Copyright © 2017 Metosin Oy diff --git a/examples/ring-example/project.clj b/examples/ring-example/project.clj new file mode 100644 index 00000000..11bbb381 --- /dev/null +++ b/examples/ring-example/project.clj @@ -0,0 +1,6 @@ +(defproject just-coercion-with-ring "0.1.0-SNAPSHOT" + :description "Reitit coercion with vanilla ring" + :dependencies [[org.clojure/clojure "1.9.0-RC2"] + [ring "1.6.3"] + [metosin/muuntaja "0.4.1"] + [metosin/reitit "0.1.0-SNAPSHOT"]]) diff --git a/examples/ring-example/src/example/dspec.clj b/examples/ring-example/src/example/dspec.clj new file mode 100644 index 00000000..7c336214 --- /dev/null +++ b/examples/ring-example/src/example/dspec.clj @@ -0,0 +1,19 @@ +(ns example.dspec + (:require [reitit.ring.coercion :as coercion] + [reitit.ring.coercion.spec :as spec-coercion])) + +(def routes + ["/dspec" + ["/plus" {:name ::plus + :coercion spec-coercion/coercion + :responses {200 {:schema {:total int?}}} + :get {:summary "plus with query-params" + :parameters {:query {:x int?, :y int?}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200 + :body {:total (+ x y)}})} + :post {:summary "plus with body-params" + :parameters {:body {:x int?, :y int?}} + :handler (fn [{{{:keys [x y]} :body} :parameters}] + {:status 200 + :body {:total (+ x y)}})}}]]) diff --git a/examples/ring-example/src/example/schema.clj b/examples/ring-example/src/example/schema.clj new file mode 100644 index 00000000..55c3179a --- /dev/null +++ b/examples/ring-example/src/example/schema.clj @@ -0,0 +1,20 @@ +(ns example.schema + (:require [schema.core :as s] + [reitit.ring.coercion :as coercion] + [reitit.ring.coercion.schema :as schema-coercion])) + +(def routes + ["/schema" + ["/plus" {:name ::plus + :coercion schema-coercion/coercion + :responses {200 {:schema {:total s/Int}}} + :get {:summary "plus with query-params" + :parameters {:query {:x s/Int, :y s/Int}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200 + :body {:total (+ x y)}})} + :post {:summary "plus with body-params" + :parameters {:body {:x s/Int, :y s/Int}} + :handler (fn [{{{:keys [x y]} :body} :parameters}] + {:status 200 + :body {:total (+ x y)}})}}]]) diff --git a/examples/ring-example/src/example/server.clj b/examples/ring-example/src/example/server.clj new file mode 100644 index 00000000..0181c606 --- /dev/null +++ b/examples/ring-example/src/example/server.clj @@ -0,0 +1,33 @@ +(ns example.server + (:require [ring.adapter.jetty :as jetty] + [ring.middleware.params] + [muuntaja.middleware] + [reitit.ring :as ring] + [reitit.ring.coercion :as coercion] + [example.dspec] + [example.schema] + [example.spec])) + +(defonce ^:private server (atom nil)) + +(def app + (ring/ring-handler + (ring/router + [example.schema/routes + example.dspec/routes + example.spec/routes] + {:data {:middleware [ring.middleware.params/wrap-params + muuntaja.middleware/wrap-format + coercion/coerce-exceptions-middleware + coercion/coerce-request-middleware + coercion/coerce-response-middleware]}}))) + +(defn restart [] + (swap! server (fn [x] + (when x (.stop x)) + (jetty/run-jetty + app + {:port 3000, :join? false}))) + (println "server running in port 3000")) + +(restart) diff --git a/examples/ring-example/src/example/spec.clj b/examples/ring-example/src/example/spec.clj new file mode 100644 index 00000000..e6acc570 --- /dev/null +++ b/examples/ring-example/src/example/spec.clj @@ -0,0 +1,26 @@ +(ns example.spec + (:require [clojure.spec.alpha :as s] + [spec-tools.spec :as spec] + [reitit.ring.coercion :as coercion] + [reitit.ring.coercion.spec :as spec-coercion])) + +;; wrap into Spec Records to enable runtime conforming +(s/def ::x spec/int?) +(s/def ::y spec/int?) +(s/def ::total spec/int?) + +(def routes + ["/spec" + ["/plus" {:name ::plus + :coercion spec-coercion/coercion + :responses {200 {:schema (s/keys :req-un [::total])}} + :get {:summary "plus with query-params" + :parameters {:query (s/keys :req-un [::x ::y])} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200 + :body {:total (+ x y)}})} + :post {:summary "plus with body-params" + :parameters {:body (s/keys :req-un [::x ::y])} + :handler (fn [{{{:keys [x y]} :body} :parameters}] + {:status 200 + :body {:total (+ x y)}})}}]]) diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc index 3dda55dc..7ef4a807 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion.cljc @@ -15,16 +15,14 @@ (defrecord ParameterCoercion [in style keywordize? open?]) -(def valid-type? #{::request-coercion ::response-coercion}) - -(def ring-parameter-coercion +(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 request-coercion-failed! [result coercion value in request] +(defn ^:no-doc request-coercion-failed! [result coercion value in request] (throw (ex-info (str "Request coercion failed: " (pr-str result)) @@ -36,7 +34,7 @@ :in [:request in] :request request})))) -(defn response-coercion-failed! [result coercion value request response] +(defn ^:no-doc response-coercion-failed! [result coercion value request response] (throw (ex-info (str "Response coercion failed: " (pr-str result)) @@ -50,27 +48,23 @@ :response response})))) ;; TODO: support faster key walking, walk/keywordize-keys is quite slow... - -(defn request-coercer [coercion type model] +(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/make-open coercion model) model) + model (if open? (protocol/open-model coercion model) model) coercer (protocol/request-coercer coercion style model)] (fn [request] (let [value (transform request) - format (some-> request :muuntaja/request :format) + format (extract-request-format request) result (coercer value format)] (if (protocol/error? result) (request-coercion-failed! result coercion value in request) result)))))) -#_(defn muuntaja-response-format [request response] - (or (-> response :muuntaja/content-type) - (some-> request :muuntaja/response :format))) - -(defn response-coercer [coercion model {:keys [extract-response-format] - :or {extract-response-format (constantly nil)}}] +(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] @@ -81,28 +75,28 @@ (response-coercion-failed! result coercion value request response) result)))))) -(defn encode-error [data] +(defn ^:no-doc encode-error [data] (-> data (dissoc :request :response) (update :coercion protocol/get-name) (->> (protocol/encode-error (:coercion data))))) -(defn- coerce-request [coercers request] +(defn ^:no-doc coerce-request [coercers request] (reduce-kv (fn [acc k coercer] (impl/fast-assoc acc k (coercer request))) {} coercers)) -(defn- coerce-response [coercers request response] +(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] +(defn ^:no-doc request-coercers [coercion parameters opts] (->> (for [[k v] parameters :when v] - [k (request-coercer coercion k v)]) + [k (request-coercer coercion k v opts)]) (into {}))) (defn ^:no-doc response-coercers [coercion responses opts] @@ -110,7 +104,7 @@ [status (response-coercer coercion schema opts)]) (into {}))) -(defn handle-coercion-exception [e respond raise] +(defn ^:no-doc handle-coercion-exception [e respond raise] (let [data (ex-data e)] (if-let [status (condp = (:type data) ::request-coercion 400 @@ -125,15 +119,15 @@ ;; middleware ;; -(def gen-wrap-coerce-parameters +(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 - :gen-wrap (fn [{:keys [coercion parameters]} _] + :gen-wrap (fn [{:keys [coercion parameters]} opts] (if (and coercion parameters) - (let [coercers (request-coercers coercion parameters)] + (let [coercers (request-coercers coercion parameters opts)] (fn [handler] (fn ([request] @@ -143,13 +137,13 @@ (let [coerced (coerce-request coercers request)] (handler (impl/fast-assoc request :parameters coerced) respond raise))))))))})) -(def gen-wrap-coerce-response +(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 - :gen-wrap (fn [{:keys [coercion responses opts]} _] + :gen-wrap (fn [{:keys [coercion responses]} opts] (if (and coercion responses) (let [coercers (response-coercers coercion responses opts)] (fn [handler] @@ -159,7 +153,7 @@ ([request respond raise] (handler request #(respond (coerce-response coercers request %)) raise)))))))})) -(def gen-wrap-coerce-exceptions +(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." diff --git a/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc b/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc index eb8a961f..12838b55 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc @@ -1,12 +1,11 @@ -(ns reitit.ring.coercion.protocol - (:refer-clojure :exclude [compile])) +(ns reitit.ring.coercion.protocol) (defprotocol Coercion "Pluggable coercion protocol" (get-name [this] "Keyword name for the coercion") - (compile [this model name] "Compiles a coercion model") (get-apidocs [this model data] "???") - (make-open [this model] "Returns a new map model which doesn't fail on extra keys") + (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")) diff --git a/modules/reitit-ring/src/reitit/ring/middleware.cljc b/modules/reitit-ring/src/reitit/ring/middleware.cljc index f76e8045..ffd00217 100644 --- a/modules/reitit-ring/src/reitit/ring/middleware.cljc +++ b/modules/reitit-ring/src/reitit/ring/middleware.cljc @@ -101,3 +101,11 @@ :result :handler)) {::router router})) + +(defn chain + "Creates a vanilla ring middleware chain out of sequence of + IntoMiddleware thingies." + ([middleware handler data] + (chain middleware handler data nil)) + ([middleware handler data opts] + (compile-handler (expand middleware data opts) handler))) diff --git a/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc b/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc index aebddea1..f514fc47 100644 --- a/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc @@ -38,15 +38,14 @@ protocol/Coercion (get-name [_] name) - (compile [_ model _] - model) - (get-apidocs [_ _ {:keys [parameters responses] :as info}] (cond-> (dissoc info :parameters :responses) parameters (assoc ::swagger/parameters parameters) responses (assoc ::swagger/responses responses))) - (make-open [_ schema] (st/open-schema schema)) + (compile-model [_ model _] model) + + (open-model [_ schema] (st/open-schema schema)) (encode-error [_ error] (-> error diff --git a/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc b/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc index cd2aba4f..90dd3f2d 100644 --- a/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc @@ -46,10 +46,6 @@ (into-spec [this _] (st/create-spec {:spec this}))) -;; TODO: proper name! -(def memoized-into-spec - (memoize #(into-spec %1 (gensym "spec")))) - (defn stringify-pred [pred] (str (if (seq? pred) (seq pred) pred))) @@ -61,33 +57,33 @@ protocol/Coercion (get-name [_] name) - (compile [_ model _] - (memoized-into-spec model)) - - (get-apidocs [_ _ {: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 memoized-into-spec]))) + [k (protocol/compile-model this v nil)]))) responses (assoc ::swagger/responses (into (empty responses) (for [[k response] responses] - [k (update response :schema memoized-into-spec)]))))) + [k (update response :schema #(protocol/compile-model this % nil))]))))) - (make-open [_ spec] spec) + (compile-model [_ model _] + (into-spec model (or name (gensym "spec")))) + + (open-model [_ spec] spec) (encode-error [_ error] (-> error (update :spec (comp str s/form)) (update :problems (partial mapv #(update % :pred stringify-pred))))) - (request-coercer [_ type spec] - (let [spec (memoized-into-spec spec) + (request-coercer [this type spec] + (let [spec (protocol/compile-model this spec nil) {:keys [formats default]} (conforming type)] (fn [value format] (if-let [conforming (or (get formats format) default)] diff --git a/perf-test/clj/reitit/coercion_perf_test.clj b/perf-test/clj/reitit/coercion_perf_test.clj index fcce9741..e664f1f0 100644 --- a/perf-test/clj/reitit/coercion_perf_test.clj +++ b/perf-test/clj/reitit/coercion_perf_test.clj @@ -87,9 +87,9 @@ (defrecord NoOpCoercion [] protocol/Coercion (get-name [_] :no-op) - (compile [_ model _] model) (get-apidocs [_ _ {:keys [parameters responses] :as info}]) - (make-open [_ spec] spec) + (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))) @@ -107,24 +107,24 @@ app (ring/ring-handler (ring/router routes - {:data {:middleware [coercion/wrap-coerce-parameters] + {:data {:middleware [coercion/coerce-request-middleware] :coercion coercion}})) app2 (ring/ring-handler (ring/router routes - {:data {:middleware [coercion/gen-wrap-coerce-parameters] + {:data {:middleware [coercion/coerce-request-middleware] :coercion coercion}})) app3 (ring/ring-handler (ring/router routes - {:data {:middleware [coercion/wrap-coerce-parameters + {:data {:middleware [coercion/coerce-request-middleware coercion/wrap-coerce-response] :coercion coercion}})) app4 (ring/ring-handler (ring/router routes - {:data {:middleware [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion coercion}})) req {:request-method :get :uri "/api/ping" @@ -138,17 +138,17 @@ ;; 175ns (-19%) ;; 360ns (-64%) ;; 4080ns (-30%) - (bench! "gen-wrap-coerce-parameters" (app2 req)) + (bench! "coerce-request-middleware" (app2 req)) ;; 300ns ;; 1740ns ;; 9400ns - (bench! "wrap-coerce-parameters & responses" (app3 req)) + (bench! "wrap-coerce-request & responses" (app3 req)) ;; 175ns (-42%) ;; 384ns (-78%) ;; 6100ns (-35%) - (bench! "gen-wrap-coerce-parameters & responses" (app4 req))))) + (bench! "coerce-request-middleware & responses" (app4 req))))) (comment (do @@ -161,8 +161,8 @@ :get {:handler (fn [{{{:keys [x y]} :body} :parameters}] {:status 200 :body {:total (+ x y)}})}}]] - {:data {:middleware [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion spec/coercion}}))) (app @@ -205,8 +205,8 @@ (let [body (-> request :parameters :body)] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:data {:middleware [[mm/wrap-format m] - coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion schema/coercion}})) request {:request-method :post :uri "/plus" @@ -228,8 +228,8 @@ :handler (fn [request] (let [body (-> request :parameters :body)] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}] - {:data {:middleware [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion schema/coercion}})) request {:request-method :post :uri "/plus" @@ -251,8 +251,8 @@ :handler (fn [request] (let [body (-> request :parameters :body)] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}] - {:data {:middleware [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion spec/coercion}})) request {:request-method :post :uri "/plus" @@ -279,8 +279,8 @@ :handler (fn [request] (let [body (-> request :parameters :body)] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}] - {:data {:middleware [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] + {:data {:middleware [coercion/coerce-request-middleware + coercion/coerce-response-middleware] :coercion spec/coercion}})) request {:request-method :post :uri "/plus" diff --git a/project.clj b/project.clj index e12d6f36..81402f3a 100644 --- a/project.clj +++ b/project.clj @@ -45,6 +45,7 @@ [expound "0.3.2"] [orchestra "2017.08.13"] + [ring "1.6.3"] [metosin/muuntaja "0.4.1"] [metosin/jsonista "0.1.0-SNAPSHOT"] diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index c9d5ec71..fcd7c8ae 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -53,8 +53,8 @@ :coercion spec/coercion}})))] (testing "withut exception handling" - (let [app (create [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response])] + (let [app (create [coercion/coerce-request-middleware + coercion/coerce-response-middleware])] (testing "all good" (is (= {:status 200 @@ -74,9 +74,9 @@ (app invalid-request2)))))) (testing "with exception handling" - (let [app (create [coercion/gen-wrap-coerce-exceptions - coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response])] + (let [app (create [coercion/coerce-exceptions-middleware + coercion/coerce-request-middleware + coercion/coerce-response-middleware])] (testing "all good" (is (= {:status 200 @@ -108,8 +108,8 @@ :coercion schema/coercion}})))] (testing "withut exception handling" - (let [app (create [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response])] + (let [app (create [coercion/coerce-request-middleware + coercion/coerce-response-middleware])] (testing "all good" (is (= {:status 200 @@ -129,9 +129,9 @@ (app invalid-request2)))) (testing "with exception handling" - (let [app (create [coercion/gen-wrap-coerce-exceptions - coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response])] + (let [app (create [coercion/coerce-exceptions-middleware + coercion/coerce-request-middleware + coercion/coerce-response-middleware])] (testing "all good" (is (= {:status 200 diff --git a/test/cljc/reitit/middleware_test.cljc b/test/cljc/reitit/middleware_test.cljc index 3a66de61..9fc40a4a 100644 --- a/test/cljc/reitit/middleware_test.cljc +++ b/test/cljc/reitit/middleware_test.cljc @@ -170,3 +170,20 @@ :middleware (map :name)))))))))) +(deftest chain-test + (testing "chain can produce middlware chain of any IntoMiddleware" + (let [mw (fn [handler value] + #(conj (handler (conj % value)) value)) + handler #(conj % :ok) + mw1 {:gen-wrap (constantly #(mw % ::mw1))} + mw2 {:gen-wrap (constantly nil)} + mw3 {:wrap #(mw % ::mw3)} + mw4 #(mw % ::mw4) + mw5 {:gen-wrap (fn [{:keys [mount?]} _] + (when mount? + #(mw % ::mw5)))} + chain1 (middleware/chain [mw1 mw2 mw3 mw4 mw5] handler {:mount? true}) + chain2 (middleware/chain [mw1 mw2 mw3 mw4 mw5] handler {:mount? false})] + (is (= [::mw1 ::mw3 ::mw4 ::mw5 :ok ::mw5 ::mw4 ::mw3 ::mw1] (chain1 []))) + (is (= [::mw1 ::mw3 ::mw4 :ok ::mw4 ::mw3 ::mw1] (chain2 [])))))) +