reitit/doc/ring/coercion.md
2017-12-09 23:21:03 +02:00

7.5 KiB

Pluggable Coercion

Reitit provides pluggable parameter coercion via reitit.coercion/Coercion protocol, originally introduced in compojure-api.

Reitit ships with the following coercion modules:

Ring request and response coercion

To use Coercion with Ring, one needs to do the following:

  1. Define parameters and responses as data into route data, in format adopted from ring-swagger:
  • :parameters map, with submaps for different parameters: :query, :body, :form, :header and :path. Parameters are defined in the format understood by the Coercion.
  • :responses map, with response status codes as keys (or :default for "everything else") with maps with :schema and optionally :description as values.
  1. Set a Coercion implementation to route data under :coercion
  2. 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-middleware/coerce-request-middleware
  • reitit.ring.coercion-middleware/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-middleware/coerce-exceptions-middleware middleware

Example with Schema

(require '[reitit.ring :as ring])
(require '[reitit.ring.coercion-middleware :as coercion-middleware])
(require '[reitit.coercion.schema :as schema])
(require '[schema.core :as s])

(def app
  (ring/ring-handler
    (ring/router
      ["/api"
       ["/ping" {:post {:parameters {:body {:x s/Int, :y s/Int}}
                        :responses {200 {:schema {:total (s/constrained s/Int pos?)}}}
                        :handler (fn [{{{:keys [x y]} :body} :parameters}]
                                   {:status 200
                                    :body {:total (+ x y)}})}}]]
      {:data {:middleware [coercion-middleware/coerce-exceptions-middleware
                           coercion-middleware/coerce-request-middleware
                           coercion-middleware/coerce-response-middleware]
              :coercion schema/coercion}})))

Valid request:

(app
  {:request-method :post
   :uri "/api/ping"
   :body-params {:x 1, :y 2}})
; {:status 200
;  :body {:total 3}}

Invalid request:

(app
  {:request-method :post
   :uri "/api/ping"
   :body-params {:x 1, :y "2"}})
; {:status 400,
;  :body {:type :reitit.coercion/request-coercion
;         :coercion :schema
;         :in [:request :body-params]
;         :value {:x 1, :y "2"}
;         :schema {:x "Int", :y "Int"}
;         :errors {:y "(not (integer? \"2\"))"}}}

Example with data-specs

(require '[reitit.ring :as ring])
(require '[reitit.ring.coercion-middleware :as coercion-middleware])
(require '[reitit.coercion.spec :as spec])

(def app
  (ring/ring-handler
    (ring/router
      ["/api"
       ["/ping" {:post {:parameters {:body {:x int?, :y int?}}
                        :responses {200 {:schema {:total pos-int?}}}
                        :handler (fn [{{{:keys [x y]} :body} :parameters}]
                                   {:status 200
                                    :body {:total (+ x y)}})}}]]
      {:data {:middleware [coercion-middleware/coerce-exceptions-middleware
                           coercion-middleware/coerce-request-middleware
                           coercion-middleware/coerce-response-middleware]
              :coercion spec/coercion}})))

Valid request:

(app
  {:request-method :post
   :uri "/api/ping"
   :body-params {:x 1, :y 2}})
; {:status 200
;  :body {:total 3}}

Invalid request:

(app
  {:request-method :post
   :uri "/api/ping"
   :body-params {:x 1, :y "2"}})
; {:status 400,
;  :body {:type ::coercion/request-coercion
;         :coercion :spec
;         :in [:request :body-params]
;         :value {:x 1, :y "2"}
;         :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec37747/x :$spec37747/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})"
;         :problems [{:path [:y]
;                     :pred "clojure.core/int?"
;                     :val "2"
;                     :via [:$spec37747/y]
;                     :in [:y]}]}}

Example with clojure.spec

Currently, clojure.spec doesn't support runtime transformations via conforming, so one needs to wrap all specs with spec-tools.core/spec.

(require '[reitit.ring :as ring])
(require '[reitit.ring.coercion-middleware :as coercion-middleware])
(require '[reitit.coercion.spec :as spec])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])

(s/def ::x (st/spec int?))
(s/def ::y (st/spec int?))
(s/def ::total int?)
(s/def ::request (s/keys :req-un [::x ::y]))
(s/def ::response (s/keys :req-un [::total]))

(def app
  (ring/ring-handler
    (ring/router
      ["/api"
       ["/ping" {:post {:parameters {:body ::request}
                        :responses {200 {:schema ::response}}
                        :handler (fn [{{{:keys [x y]} :body} :parameters}]
                                   {:status 200
                                    :body {:total (+ x y)}})}}]]
      {:data {:middleware [coercion-middleware/coerce-exceptions-middleware
                           coercion-middleware/coerce-request-middleware
                           coercion-middleware/coerce-response-middleware]
              :coercion spec/coercion}})))

Valid request:

(app
  {:request-method :post
   :uri "/api/ping"
   :body-params {:x 1, :y 2}})
; {:status 200
;  :body {:total 3}}

Invalid request:

(app
  {:request-method :post
   :uri "/api/ping"
   :body-params {:x 1, :y "2"}})
; {:status 400,
;  :body {:type ::coercion/request-coercion
;         :coercion :spec
;         :in [:request :body-params]
;         :value {:x 1, :y "2"}
;         :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:reitit.coercion-test/x :reitit.coercion-test/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})"
;         :problems [{:path [:y]
;                     :pred "clojure.core/int?"
;                     :val "2"
;                     :via [::request ::y]
;                     :in [:y]}]}}

Custom coercion

Both Schema and Spec Coercion can be configured via options, see the source code for details.

To plug in new validation engine, see the reitit.coercion/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 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"))