Merge pull request #52 from metosin/coercion-samples

Cleanup coercion & samples
This commit is contained in:
Tommi Reiman 2017-12-03 17:52:48 +02:00 committed by GitHub
commit 653743a25f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 419 additions and 93 deletions

View file

@ -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. * `: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` 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): 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/coerce-request-middleware`
* `reitit.ring.coercion/gen-wrap-coerce-response` * `reitit.ring.coercion/coerce-response-middleware`
If the request coercion succeeds, the coerced parameters are injected into request under `:parameters`. 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 ### 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}] :handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/gen-wrap-coerce-exceptions {:data {:middleware [coercion/coerce-exceptions-middleware
coercion/gen-wrap-coerce-parameters coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion schema/coercion}}))) :coercion schema/coercion}})))
``` ```
@ -89,9 +89,9 @@ Invalid request:
:handler (fn [{{{:keys [x y]} :body} :parameters}] :handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/gen-wrap-coerce-exceptions {:data {:middleware [coercion/coerce-exceptions-middleware
coercion/gen-wrap-coerce-parameters coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion spec/coercion}}))) :coercion spec/coercion}})))
``` ```
@ -152,9 +152,9 @@ Currently, `clojure.spec` [doesn't support runtime transformations via conformin
:handler (fn [{{{:keys [x y]} :body} :parameters}] :handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/gen-wrap-coerce-exceptions {:data {:middleware [coercion/coerce-exceptions-middleware
coercion/gen-wrap-coerce-parameters coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion spec/coercion}}))) :coercion spec/coercion}})))
``` ```
@ -200,9 +200,9 @@ To plug in new validation engine, see the
(defprotocol Coercion (defprotocol Coercion
"Pluggable coercion protocol" "Pluggable coercion protocol"
(get-name [this] "Keyword name for the coercion") (get-name [this] "Keyword name for the coercion")
(compile [this model name] "Compiles a coercion model")
(get-apidocs [this model data] "???") (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") (encode-error [this error] "Converts error in to a serializable format")
(request-coercer [this type model] "Returns a `value format => value` request coercion function") (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")) (response-coercer [this model] "Returns a `value format => value` response coercion function"))

View file

@ -51,7 +51,7 @@ To demonstrate the two approaches, below are response coercion middleware writte
```clj ```clj
(require '[reitit.ring.middleware :as middleware]) (require '[reitit.ring.middleware :as middleware])
(def gen-wrap-coerce-response (def coerce-response-middleware
"Middleware for pluggable response coercion. "Middleware for pluggable response coercion.
Expects a :coercion of type `reitit.coercion.protocol/Coercion` Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responses from route data, otherwise does not mount." and :responses from route data, otherwise does not mount."

View file

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View file

@ -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

View file

@ -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"]])

View file

@ -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})))

View file

@ -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"})))

View file

@ -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})))

View file

@ -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")))

View file

@ -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})))

11
examples/ring-example/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View file

@ -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

View file

@ -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"]])

View file

@ -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)}})}}]])

View file

@ -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)}})}}]])

View file

@ -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)

View file

@ -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)}})}}]])

View file

@ -15,16 +15,14 @@
(defrecord ParameterCoercion [in style keywordize? open?]) (defrecord ParameterCoercion [in style keywordize? open?])
(def valid-type? #{::request-coercion ::response-coercion}) (def ^:no-doc ring-parameter-coercion
(def ring-parameter-coercion
{:query (->ParameterCoercion :query-params :string true true) {:query (->ParameterCoercion :query-params :string true true)
:body (->ParameterCoercion :body-params :body false false) :body (->ParameterCoercion :body-params :body false false)
:form (->ParameterCoercion :form-params :string true true) :form (->ParameterCoercion :form-params :string true true)
:header (->ParameterCoercion :header-params :string true true) :header (->ParameterCoercion :header-params :string true true)
:path (->ParameterCoercion :path-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 (throw
(ex-info (ex-info
(str "Request coercion failed: " (pr-str result)) (str "Request coercion failed: " (pr-str result))
@ -36,7 +34,7 @@
:in [:request in] :in [:request in]
:request request})))) :request request}))))
(defn response-coercion-failed! [result coercion value request response] (defn ^:no-doc response-coercion-failed! [result coercion value request response]
(throw (throw
(ex-info (ex-info
(str "Response coercion failed: " (pr-str result)) (str "Response coercion failed: " (pr-str result))
@ -50,27 +48,23 @@
:response response})))) :response response}))))
;; TODO: support faster key walking, walk/keywordize-keys is quite slow... ;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
(defn ^:no-doc request-coercer [coercion type model {:keys [extract-request-format]
(defn request-coercer [coercion type model] :or {extract-request-format (constantly nil)}}]
(if coercion (if coercion
(let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type) (let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type)
transform (comp (if keywordize? walk/keywordize-keys identity) in) 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)] coercer (protocol/request-coercer coercion style model)]
(fn [request] (fn [request]
(let [value (transform request) (let [value (transform request)
format (some-> request :muuntaja/request :format) format (extract-request-format request)
result (coercer value format)] result (coercer value format)]
(if (protocol/error? result) (if (protocol/error? result)
(request-coercion-failed! result coercion value in request) (request-coercion-failed! result coercion value in request)
result)))))) result))))))
#_(defn muuntaja-response-format [request response] (defn ^:no-doc response-coercer [coercion model {:keys [extract-response-format]
(or (-> response :muuntaja/content-type) :or {extract-response-format (constantly nil)}}]
(some-> request :muuntaja/response :format)))
(defn response-coercer [coercion model {:keys [extract-response-format]
:or {extract-response-format (constantly nil)}}]
(if coercion (if coercion
(let [coercer (protocol/response-coercer coercion model)] (let [coercer (protocol/response-coercer coercion model)]
(fn [request response] (fn [request response]
@ -81,28 +75,28 @@
(response-coercion-failed! result coercion value request response) (response-coercion-failed! result coercion value request response)
result)))))) result))))))
(defn encode-error [data] (defn ^:no-doc encode-error [data]
(-> data (-> data
(dissoc :request :response) (dissoc :request :response)
(update :coercion protocol/get-name) (update :coercion protocol/get-name)
(->> (protocol/encode-error (:coercion data))))) (->> (protocol/encode-error (:coercion data)))))
(defn- coerce-request [coercers request] (defn ^:no-doc coerce-request [coercers request]
(reduce-kv (reduce-kv
(fn [acc k coercer] (fn [acc k coercer]
(impl/fast-assoc acc k (coercer request))) (impl/fast-assoc acc k (coercer request)))
{} {}
coercers)) coercers))
(defn- coerce-response [coercers request response] (defn ^:no-doc coerce-response [coercers request response]
(if response (if response
(if-let [coercer (or (coercers (:status response)) (coercers :default))] (if-let [coercer (or (coercers (:status response)) (coercers :default))]
(impl/fast-assoc response :body (coercer request response))))) (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 (->> (for [[k v] parameters
:when v] :when v]
[k (request-coercer coercion k v)]) [k (request-coercer coercion k v opts)])
(into {}))) (into {})))
(defn ^:no-doc response-coercers [coercion responses opts] (defn ^:no-doc response-coercers [coercion responses opts]
@ -110,7 +104,7 @@
[status (response-coercer coercion schema opts)]) [status (response-coercer coercion schema opts)])
(into {}))) (into {})))
(defn handle-coercion-exception [e respond raise] (defn ^:no-doc handle-coercion-exception [e respond raise]
(let [data (ex-data e)] (let [data (ex-data e)]
(if-let [status (condp = (:type data) (if-let [status (condp = (:type data)
::request-coercion 400 ::request-coercion 400
@ -125,15 +119,15 @@
;; middleware ;; middleware
;; ;;
(def gen-wrap-coerce-parameters (def coerce-request-middleware
"Middleware for pluggable request coercion. "Middleware for pluggable request coercion.
Expects a :coercion of type `reitit.coercion.protocol/Coercion` Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :parameters from route data, otherwise does not mount." and :parameters from route data, otherwise does not mount."
(middleware/create (middleware/create
{:name ::coerce-parameters {:name ::coerce-parameters
:gen-wrap (fn [{:keys [coercion parameters]} _] :gen-wrap (fn [{:keys [coercion parameters]} opts]
(if (and coercion parameters) (if (and coercion parameters)
(let [coercers (request-coercers coercion parameters)] (let [coercers (request-coercers coercion parameters opts)]
(fn [handler] (fn [handler]
(fn (fn
([request] ([request]
@ -143,13 +137,13 @@
(let [coerced (coerce-request coercers request)] (let [coerced (coerce-request coercers request)]
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))})) (handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
(def gen-wrap-coerce-response (def coerce-response-middleware
"Middleware for pluggable response coercion. "Middleware for pluggable response coercion.
Expects a :coercion of type `reitit.coercion.protocol/Coercion` Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responses from route data, otherwise does not mount." and :responses from route data, otherwise does not mount."
(middleware/create (middleware/create
{:name ::coerce-response {:name ::coerce-response
:gen-wrap (fn [{:keys [coercion responses opts]} _] :gen-wrap (fn [{:keys [coercion responses]} opts]
(if (and coercion responses) (if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)] (let [coercers (response-coercers coercion responses opts)]
(fn [handler] (fn [handler]
@ -159,7 +153,7 @@
([request respond raise] ([request respond raise]
(handler request #(respond (coerce-response coercers request %)) raise)))))))})) (handler request #(respond (coerce-response coercers request %)) raise)))))))}))
(def gen-wrap-coerce-exceptions (def coerce-exceptions-middleware
"Middleware for handling coercion exceptions. "Middleware for handling coercion exceptions.
Expects a :coercion of type `reitit.coercion.protocol/Coercion` Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :parameters or :responses from route data, otherwise does not mount." and :parameters or :responses from route data, otherwise does not mount."

View file

@ -1,12 +1,11 @@
(ns reitit.ring.coercion.protocol (ns reitit.ring.coercion.protocol)
(:refer-clojure :exclude [compile]))
(defprotocol Coercion (defprotocol Coercion
"Pluggable coercion protocol" "Pluggable coercion protocol"
(get-name [this] "Keyword name for the coercion") (get-name [this] "Keyword name for the coercion")
(compile [this model name] "Compiles a coercion model")
(get-apidocs [this model data] "???") (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") (encode-error [this error] "Converts error in to a serializable format")
(request-coercer [this type model] "Returns a `value format => value` request coercion function") (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")) (response-coercer [this model] "Returns a `value format => value` response coercion function"))

View file

@ -101,3 +101,11 @@
:result :result
:handler)) :handler))
{::router router})) {::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)))

View file

@ -38,15 +38,14 @@
protocol/Coercion protocol/Coercion
(get-name [_] name) (get-name [_] name)
(compile [_ model _]
model)
(get-apidocs [_ _ {:keys [parameters responses] :as info}] (get-apidocs [_ _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses) (cond-> (dissoc info :parameters :responses)
parameters (assoc ::swagger/parameters parameters) parameters (assoc ::swagger/parameters parameters)
responses (assoc ::swagger/responses responses))) 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] (encode-error [_ error]
(-> error (-> error

View file

@ -46,10 +46,6 @@
(into-spec [this _] (into-spec [this _]
(st/create-spec {:spec this}))) (st/create-spec {:spec this})))
;; TODO: proper name!
(def memoized-into-spec
(memoize #(into-spec %1 (gensym "spec"))))
(defn stringify-pred [pred] (defn stringify-pred [pred]
(str (if (seq? pred) (seq pred) pred))) (str (if (seq? pred) (seq pred) pred)))
@ -61,33 +57,33 @@
protocol/Coercion protocol/Coercion
(get-name [_] name) (get-name [_] name)
(compile [_ model _] (get-apidocs [this _ {:keys [parameters responses] :as info}]
(memoized-into-spec model))
(get-apidocs [_ _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses) (cond-> (dissoc info :parameters :responses)
parameters (assoc parameters (assoc
::swagger/parameters ::swagger/parameters
(into (into
(empty parameters) (empty parameters)
(for [[k v] parameters] (for [[k v] parameters]
[k memoized-into-spec]))) [k (protocol/compile-model this v nil)])))
responses (assoc responses (assoc
::swagger/responses ::swagger/responses
(into (into
(empty responses) (empty responses)
(for [[k response] 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] (encode-error [_ error]
(-> error (-> error
(update :spec (comp str s/form)) (update :spec (comp str s/form))
(update :problems (partial mapv #(update % :pred stringify-pred))))) (update :problems (partial mapv #(update % :pred stringify-pred)))))
(request-coercer [_ type spec] (request-coercer [this type spec]
(let [spec (memoized-into-spec spec) (let [spec (protocol/compile-model this spec nil)
{:keys [formats default]} (conforming type)] {:keys [formats default]} (conforming type)]
(fn [value format] (fn [value format]
(if-let [conforming (or (get formats format) default)] (if-let [conforming (or (get formats format) default)]

View file

@ -87,9 +87,9 @@
(defrecord NoOpCoercion [] (defrecord NoOpCoercion []
protocol/Coercion protocol/Coercion
(get-name [_] :no-op) (get-name [_] :no-op)
(compile [_ model _] model)
(get-apidocs [_ _ {:keys [parameters responses] :as info}]) (get-apidocs [_ _ {:keys [parameters responses] :as info}])
(make-open [_ spec] spec) (compile-model [_ model _] model)
(open-model [_ spec] spec)
(encode-error [_ error] error) (encode-error [_ error] error)
(request-coercer [_ type spec] (fn [value format] value)) (request-coercer [_ type spec] (fn [value format] value))
(response-coercer [this spec] (protocol/request-coercer this :response spec))) (response-coercer [this spec] (protocol/request-coercer this :response spec)))
@ -107,24 +107,24 @@
app (ring/ring-handler app (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/wrap-coerce-parameters] {:data {:middleware [coercion/coerce-request-middleware]
:coercion coercion}})) :coercion coercion}}))
app2 (ring/ring-handler app2 (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/gen-wrap-coerce-parameters] {:data {:middleware [coercion/coerce-request-middleware]
:coercion coercion}})) :coercion coercion}}))
app3 (ring/ring-handler app3 (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/wrap-coerce-parameters {:data {:middleware [coercion/coerce-request-middleware
coercion/wrap-coerce-response] coercion/wrap-coerce-response]
:coercion coercion}})) :coercion coercion}}))
app4 (ring/ring-handler app4 (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/gen-wrap-coerce-parameters {:data {:middleware [coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion coercion}})) :coercion coercion}}))
req {:request-method :get req {:request-method :get
:uri "/api/ping" :uri "/api/ping"
@ -138,17 +138,17 @@
;; 175ns (-19%) ;; 175ns (-19%)
;; 360ns (-64%) ;; 360ns (-64%)
;; 4080ns (-30%) ;; 4080ns (-30%)
(bench! "gen-wrap-coerce-parameters" (app2 req)) (bench! "coerce-request-middleware" (app2 req))
;; 300ns ;; 300ns
;; 1740ns ;; 1740ns
;; 9400ns ;; 9400ns
(bench! "wrap-coerce-parameters & responses" (app3 req)) (bench! "wrap-coerce-request & responses" (app3 req))
;; 175ns (-42%) ;; 175ns (-42%)
;; 384ns (-78%) ;; 384ns (-78%)
;; 6100ns (-35%) ;; 6100ns (-35%)
(bench! "gen-wrap-coerce-parameters & responses" (app4 req))))) (bench! "coerce-request-middleware & responses" (app4 req)))))
(comment (comment
(do (do
@ -161,8 +161,8 @@
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}] :get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/gen-wrap-coerce-parameters {:data {:middleware [coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion spec/coercion}}))) :coercion spec/coercion}})))
(app (app
@ -205,8 +205,8 @@
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [[mm/wrap-format m] {:data {:middleware [[mm/wrap-format m]
coercion/gen-wrap-coerce-parameters coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion schema/coercion}})) :coercion schema/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"
@ -228,8 +228,8 @@
:handler (fn [request] :handler (fn [request]
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [coercion/gen-wrap-coerce-parameters {:data {:middleware [coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion schema/coercion}})) :coercion schema/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"
@ -251,8 +251,8 @@
:handler (fn [request] :handler (fn [request]
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [coercion/gen-wrap-coerce-parameters {:data {:middleware [coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion spec/coercion}})) :coercion spec/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"
@ -279,8 +279,8 @@
:handler (fn [request] :handler (fn [request]
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [coercion/gen-wrap-coerce-parameters {:data {:middleware [coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response] coercion/coerce-response-middleware]
:coercion spec/coercion}})) :coercion spec/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"

View file

@ -45,6 +45,7 @@
[expound "0.3.2"] [expound "0.3.2"]
[orchestra "2017.08.13"] [orchestra "2017.08.13"]
[ring "1.6.3"]
[metosin/muuntaja "0.4.1"] [metosin/muuntaja "0.4.1"]
[metosin/jsonista "0.1.0-SNAPSHOT"] [metosin/jsonista "0.1.0-SNAPSHOT"]

View file

@ -53,8 +53,8 @@
:coercion spec/coercion}})))] :coercion spec/coercion}})))]
(testing "withut exception handling" (testing "withut exception handling"
(let [app (create [coercion/gen-wrap-coerce-parameters (let [app (create [coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response])] coercion/coerce-response-middleware])]
(testing "all good" (testing "all good"
(is (= {:status 200 (is (= {:status 200
@ -74,9 +74,9 @@
(app invalid-request2)))))) (app invalid-request2))))))
(testing "with exception handling" (testing "with exception handling"
(let [app (create [coercion/gen-wrap-coerce-exceptions (let [app (create [coercion/coerce-exceptions-middleware
coercion/gen-wrap-coerce-parameters coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response])] coercion/coerce-response-middleware])]
(testing "all good" (testing "all good"
(is (= {:status 200 (is (= {:status 200
@ -108,8 +108,8 @@
:coercion schema/coercion}})))] :coercion schema/coercion}})))]
(testing "withut exception handling" (testing "withut exception handling"
(let [app (create [coercion/gen-wrap-coerce-parameters (let [app (create [coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response])] coercion/coerce-response-middleware])]
(testing "all good" (testing "all good"
(is (= {:status 200 (is (= {:status 200
@ -129,9 +129,9 @@
(app invalid-request2)))) (app invalid-request2))))
(testing "with exception handling" (testing "with exception handling"
(let [app (create [coercion/gen-wrap-coerce-exceptions (let [app (create [coercion/coerce-exceptions-middleware
coercion/gen-wrap-coerce-parameters coercion/coerce-request-middleware
coercion/gen-wrap-coerce-response])] coercion/coerce-response-middleware])]
(testing "all good" (testing "all good"
(is (= {:status 200 (is (= {:status 200

View file

@ -170,3 +170,20 @@
:middleware :middleware
(map :name)))))))))) (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 []))))))