mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 00:41:12 +00:00
209 lines
7.5 KiB
Markdown
209 lines
7.5 KiB
Markdown
# Pluggable Coercion
|
|
|
|
Reitit provides pluggable parameter coercion via `reitit.coercion/Coercion` protocol, originally introduced in [compojure-api](https://clojars.org/metosin/compojure-api).
|
|
|
|
Reitit ships with the following coercion modules:
|
|
|
|
* `reitit.coercion.schema/coercion` for [plumatic schema](https://github.com/plumatic/schema).
|
|
* `reitit.coercion.spec/coercion` for both [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs).
|
|
|
|
### 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](https://github.com/metosin/ring-swagger#more-complete-example):
|
|
* `: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.
|
|
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-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
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(app
|
|
{:request-method :post
|
|
:uri "/api/ping"
|
|
:body-params {:x 1, :y 2}})
|
|
; {:status 200
|
|
; :body {:total 3}}
|
|
```
|
|
|
|
Invalid request:
|
|
|
|
```clj
|
|
(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
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(app
|
|
{:request-method :post
|
|
:uri "/api/ping"
|
|
:body-params {:x 1, :y 2}})
|
|
; {:status 200
|
|
; :body {:total 3}}
|
|
```
|
|
|
|
Invalid request:
|
|
|
|
```clj
|
|
(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](https://dev.clojure.org/jira/browse/CLJ-2116), so one needs to wrap all specs with `spec-tools.core/spec`.
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(app
|
|
{:request-method :post
|
|
:uri "/api/ping"
|
|
:body-params {:x 1, :y 2}})
|
|
; {:status 200
|
|
; :body {:total 3}}
|
|
```
|
|
|
|
Invalid request:
|
|
|
|
```clj
|
|
(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.
|
|
|
|
```clj
|
|
(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"))
|
|
```
|