2017-12-10 15:46:29 +00:00
# Clojure.spec Coercion
The [clojure.spec ](https://clojure.org/guides/spec ) library specifies the structure of data, validates or destructures it, and can generate data based on the spec.
2018-09-05 19:18:32 +00:00
## Warning
2018-10-21 17:06:28 +00:00
`clojure.spec` by itself doesn't support coercion. `reitit` uses [spec-tools ](https://github.com/metosin/spec-tools ) that adds coercion to spec. Like `clojure.spec` , it's alpha as it leans both on spec walking and `clojure.spec.alpha/conform` , which is concidered a spec internal, that might be changed or removed later.
2018-09-05 19:18:32 +00:00
2018-10-21 17:06:28 +00:00
## Usage
2018-09-05 19:18:32 +00:00
2018-10-21 17:06:28 +00:00
For simple specs (core predicates, `spec-tools.core/spec` , `s/and` , `s/or` , `s/coll-of` , `s/keys` , `s/map-of` , `s/nillable` and `s/every` ), the transformation is inferred using [spec-walker ](https://github.com/metosin/spec-tools#spec-walker ) and is automatic. To support all specs (like regex-specs), specs need to be wrapped into [Spec Records ](https://github.com/metosin/spec-tools/blob/master/README.md#spec-records ).
There are [CLJ-2116 ](https://dev.clojure.org/jira/browse/CLJ-2116 ) and [CLJ-2251 ](https://dev.clojure.org/jira/browse/CLJ-2251 ) that would help solve this elegantly. Go vote 'em up.
2018-09-05 19:18:32 +00:00
## Example
2017-12-10 15:46:29 +00:00
```clj
(require '[reitit.coercion.spec])
(require '[reitit.coercion :as coercion])
(require '[spec-tools.spec :as spec])
(require '[clojure.spec.alpha :as s])
(require '[reitit.core :as r])
2018-10-21 17:06:28 +00:00
;; simple specs, inferred
(s/def ::company string?)
(s/def ::user-id int?)
2017-12-10 15:46:29 +00:00
(s/def ::path-params (s/keys :req-un [::company ::user-id]))
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.spec/coercion
:parameters {:path ::path-params}}]
{:compile coercion/compile-request-coercers}))
2017-12-10 19:48:06 +00:00
(defn match-by-path-and-coerce! [path]
2017-12-10 15:46:29 +00:00
(if-let [match (r/match-by-path router path)]
(assoc match :parameters (coercion/coerce! match))))
```
Successful coercion:
```clj
2017-12-10 19:48:06 +00:00
(match-by-path-and-coerce! "/metosin/users/123")
2017-12-10 15:46:29 +00:00
; #Match {:template "/:company/users/:user-id",
; :data {:name :user/user-view,
2017-12-13 16:00:50 +00:00
; :coercion < < :spec > >
2017-12-10 15:46:29 +00:00
; :parameters {:path ::path-params}},
; :result {:path #object [reitit.coercion$request_coercer$]},
2018-02-01 14:23:44 +00:00
; :path-params {:company "metosin", :user-id "123"},
2017-12-10 15:46:29 +00:00
; :parameters {:path {:company "metosin", :user-id 123}}
; :path "/metosin/users/123"}
```
Failing coercion:
```clj
2017-12-10 19:48:06 +00:00
(match-by-path-and-coerce! "/metosin/users/ikitommi")
2017-12-10 15:46:29 +00:00
; => ExceptionInfo Request coercion failed...
```
2019-01-15 08:14:04 +00:00
## Error printing
Spec problems are exposed as-is into request & response coercion errors, enabling pretty-printers like expound to be used:
```clj
(require '[reitit.ring :as ring])
(require '[reitit.ring.middleware.exception :as exception])
(require '[reitit.ring.coercion :as coercion])
(require '[expound.alpha :as expound])
(defn coercion-error-handler [status]
(let [printer (expound/custom-printer {:theme :figwheel-theme, :print-specs? false})
handler (exception/create-coercion-handler status)]
(fn [exception request]
(printer (-> exception ex-data :problems))
(handler exception request))))
(def app
(ring/ring-handler
(ring/router
["/plus"
{:get
{:parameters {:query {:x int?, :y int?}}
:responses {200 {:body {:total pos-int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200, :body {:total (+ x y)}})}}]
{:data {:coercion reitit.coercion.spec/coercion
:middleware [(exception/create-exception-middleware
(merge
exception/default-handlers
{:reitit.coercion/request-coercion (coercion-error-handler 400)
:reitit.coercion/response-coercion (coercion-error-handler 500)}))
coercion/coerce-request-middleware
coercion/coerce-response-middleware]}})))
(app
{:uri "/plus"
:request-method :get
:query-params {"x" "1", "y" "fail"}})
; => ...
; -- Spec failed --------------------
;
; {:x ..., :y "fail"}
; ^^^^^^
;
; should satisfy
;
; int?
(app
{:uri "/plus"
:request-method :get
:query-params {"x" "1", "y" "-2"}})
; => ...
;-- Spec failed --------------------
;
; {:total -1}
; ^^
;
; should satisfy
;
; pos-int?
```