reitit/doc/coercion/coercion.md

185 lines
6.8 KiB
Markdown
Raw Normal View History

2017-12-10 14:57:09 +00:00
# Coercion Explained
2017-12-10 19:48:06 +00:00
Coercion is a process of transforming parameters (and responses) from one format into another. Reitit separates routing and coercion into two separate steps.
2017-12-10 14:57:09 +00:00
By default, all wildcard and catch-all parameters are parsed as Strings:
```clj
(require '[reitit.core :as r])
(def router
(r/router
["/:company/users/:user-id" ::user-view]))
```
2017-12-15 06:20:53 +00:00
Match with the parsed `:params` as Strings:
2017-12-10 14:57:09 +00:00
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view},
; :result nil,
2018-02-01 14:23:44 +00:00
; :path-params {:company "metosin", :user-id "123"},
2017-12-10 14:57:09 +00:00
; :path "/metosin/users/123"}
```
2017-12-10 19:48:06 +00:00
To enable parameter coercion, the following things need to be done:
2017-12-10 14:57:09 +00:00
1. Define a `Coercion` for the routes
2. Define types for the parameters
3. Compile coercers for the types
4. Apply the coercion
## Define Coercion
`reitit.coercion/Coercion` is a protocol defining how types are defined, coerced and inventoried.
2017-12-10 19:48:06 +00:00
Reitit ships with the following coercion modules:
2017-12-10 14:57:09 +00:00
2017-12-15 06:20:53 +00:00
* `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)
2017-12-10 14:57:09 +00:00
2017-12-10 19:48:06 +00:00
Coercion can be attached to route data under `:coercion` key. There can be multiple `Coercion` implementations within a single router, normal [scoping rules](../basics/route_data.html#nested-route-data) apply.
2017-12-10 14:57:09 +00:00
## Defining parameters
2017-12-10 19:48:06 +00:00
Route parameters can be defined via route data `:parameters`. It has keys for different type of parameters: `:query`, `:body`, `:form`, `:header` and `:path`. Syntax for the actual parameters depends on the `Coercion` implementation.
2017-12-10 14:57:09 +00:00
2017-12-10 19:48:06 +00:00
Example with Schema path-parameters:
2017-12-10 14:57:09 +00:00
```clj
(require '[reitit.coercion.schema])
(require '[schema.core :as s])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.schema/coercion
:parameters {:path {:company s/Str
:user-id s/Int}}}]))
```
2017-12-10 19:48:06 +00:00
A Match:
2017-12-10 14:57:09 +00:00
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion <<:schema>>
2017-12-10 14:57:09 +00:00
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :result nil,
2018-02-01 14:23:44 +00:00
; :path-params {:company "metosin", :user-id "123"},
2017-12-10 14:57:09 +00:00
; :path "/metosin/users/123"}
```
2017-12-10 19:48:06 +00:00
Coercion was not applied. Why? In Reitit, routing and coercion are separate processes and we haven't applied the coercion yet. We need to apply it ourselves after the successfull routing.
2017-12-10 14:57:09 +00:00
But now we should have enough data on the match to apply the coercion.
## Compiling coercers
2017-12-10 19:48:06 +00:00
Before the actual coercion, we need to compile the coercers against the route data. Compiled coercers yield much better performance and the manual step of adding a coercion compiler makes things explicit and non-magical.
Compiling can be done via a Middleware, Interceptor or a Router. We apply it now at router-level, effecting all routes (with `:parameters` and `:coercion` defined).
2017-12-10 14:57:09 +00:00
2017-12-10 19:48:06 +00:00
There is a helper function `reitit.coercion/compile-request-coercers` just for this:
2017-12-10 14:57:09 +00:00
```clj
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.schema])
(require '[schema.core :as s])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.schema/coercion
:parameters {:path {:company s/Str
:user-id s/Int}}}]
{:compile coercion/compile-request-coercers}))
```
Routing again:
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion <<:schema>>
2017-12-10 14:57:09 +00:00
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :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 14:57:09 +00:00
; :path "/metosin/users/123"}
```
The compiler added a `:result` key into the match (done just once, at router creation time), which holds the compiled coercers. We are almost done.
## Applying coercion
2017-12-10 19:48:06 +00:00
We can use a helper function `reitit.coercion/coerce!` to do the actual coercion, based on a `Match`:
2017-12-10 14:57:09 +00:00
```clj
(coercion/coerce!
(r/match-by-path router "/metosin/users/123"))
; {:path {:company "metosin", :user-id 123}}
```
2017-12-10 19:48:06 +00:00
We get the coerced paremeters back. If a coercion fails, a typed (`:reitit.coercion/request-coercion`) ExceptionInfo is thrown, with data about the actual error:
2017-12-10 14:57:09 +00:00
```clj
(coercion/coerce!
(r/match-by-path router "/metosin/users/ikitommi"))
; => ExceptionInfo Request coercion failed:
; #CoercionError{:schema {:company java.lang.String, :user-id Int, Any Any},
; :errors {:user-id (not (integer? "ikitommi"))}}
; clojure.core/ex-info (core.clj:4739)
```
## Full example
2017-12-15 17:37:04 +00:00
Here's an full example for doing routing and coercion with Reitit and Schema:
2017-12-10 14:57:09 +00:00
```clj
(require '[reitit.coercion.schema])
(require '[reitit.coercion :as coercion])
(require '[reitit.core :as r])
2017-12-10 14:57:09 +00:00
(require '[schema.core :as s])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.schema/coercion
:parameters {:path {:company s/Str
:user-id s/Int}}}]
{:compile coercion/compile-request-coercers}))
2017-12-10 19:48:06 +00:00
(defn match-by-path-and-coerce! [path]
2017-12-10 14:57:09 +00:00
(if-let [match (r/match-by-path router path)]
(assoc match :parameters (coercion/coerce! match))))
2017-12-10 19:48:06 +00:00
(match-by-path-and-coerce! "/metosin/users/123")
2017-12-10 14:57:09 +00:00
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion <<:schema>>
2017-12-10 14:57:09 +00:00
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :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 14:57:09 +00:00
; :parameters {:path {:company "metosin", :user-id 123}}
; :path "/metosin/users/123"}
2017-12-10 19:48:06 +00:00
(match-by-path-and-coerce! "/metosin/users/ikitommi")
2017-12-10 14:57:09 +00:00
; => ExceptionInfo Request coercion failed...
```
## Ring Coercion
For a full-blown http-coercion, see the [ring coercion](../ring/coercion.md).
## Thanks to
* [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol
2017-12-15 06:20:53 +00:00
* [schema](https://github.com/plumatic/schema) and [schema-tools](https://github.com/metosin/schema-tools) for Schema Coercion
* [spec-tools](https://github.com/metosin/spec-tools) for Spec Coercion