Coercion docs

This commit is contained in:
Tommi Reiman 2017-12-10 16:57:09 +02:00
parent 7af3f470d6
commit 715968a5d2
14 changed files with 341 additions and 21 deletions

View file

@ -2,22 +2,24 @@
* [Introduction](README.md)
* [Basics](basics/README.md)
* [Route syntax](basics/route_syntax.md)
* [Route Syntax](basics/route_syntax.md)
* [Router](basics/router.md)
* [Path-based Routing](basics/path_based_routing.md)
* [Name-based Routing](basics/name_based_routing.md)
* [Route data](basics/route_data.md)
* [Route conflicts](basics/route_conflicts.md)
* [Route Data](basics/route_data.md)
* [Route Conflicts](basics/route_conflicts.md)
* [Coercion](coercion/README.md)
* [Coercion Explained](coercion/coercion.md)
* [Advanced](advanced/README.md)
* [Configuring routers](advanced/configuring_routers.md)
* [Configuring Routers](advanced/configuring_routers.md)
* [Different Routers](advanced/different_routers.md)
* [Route Validation](advanced/route_validation.md)
* [Ring](ring/README.md)
* [Ring-router](ring/ring.md)
* [Dynamic extensions](ring/dynamic_extensions.md)
* [Dynamic Extensions](ring/dynamic_extensions.md)
* [Data-driven Middleware](ring/data_driven_middleware.md)
* [Pluggable Coercion](ring/coercion.md)
* [Compiling middleware](ring/compiling_middleware.md)
* [Compiling Middleware](ring/compiling_middleware.md)
* [Performance](performance.md)
* [FAQ](faq.md)
* TODO: Swagger & OpenAPI

View file

@ -1,5 +1,5 @@
# Advanced
* [Configuring routers](configuring_routers.md)
* [Configuring Routers](configuring_routers.md)
* [Different Routers](different_routers.md)
* [Route Validation](route_validation.md)

View file

@ -1,8 +1,8 @@
# Basics
* [Route syntax](route_syntax.md)
* [Route Syntax](route_syntax.md)
* [Router](router.md)
* [Path-based Routing](path_based_routing.md)
* [Name-based Routing](name_based_routing.md)
* [Route data](route_data.md)
* [Route conflicts](route_conflicts.md)
* [Route Data](route_data.md)
* [Route Conflicts](route_conflicts.md)

View file

@ -1,4 +1,4 @@
## Name-based (reverse) routing
## Name-based (reverse) Routing
All routes which have `:name` route data defined, can also be matched by name.

View file

@ -1,4 +1,4 @@
## Path-based routing
## Path-based Routing
Path-based routing is done using the `reitit.core/match-by-path` function. It takes the router and path as arguments and returns one of the following:

View file

@ -1,4 +1,4 @@
# Route conflicts
# Route Conflicts
Many routing libraries allow multiple matches for a single path lookup. Usually, the first match is used and the rest are effecively unreachanle. This is not good, especially if route tree is merged from multiple sources.

View file

@ -1,4 +1,4 @@
# Route data
# Route Data
Route data is the heart of this library. Routes can have any data attachted to them. Data is interpeted either by the client application or the `Router` via it's `:coerce` and `:compile` hooks. This enables co-existence of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components.

3
doc/coercion/README.md Normal file
View file

@ -0,0 +1,3 @@
# Coercion
* [Coercion Explained](coercion.md)

182
doc/coercion/coercion.md Normal file
View file

@ -0,0 +1,182 @@
# Coercion Explained
Coercion is a process of transforming parameters (and responses) from one format into another. Reitit separates routing and coercion into separate steps.
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]))
```
Here's a match with the String `:params`:
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view},
; :result nil,
; :params {:company "metosin", :user-id "123"},
; :path "/metosin/users/123"}
```
To enable parameter coercion, we need to do few things:
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.
Reitit has 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).
Coercion can be attached to route data under `:coercion` key. There can be multiple `Coercion` implementation into a single router, normal [scoping rules](../basics/route_data.html#nested-route-data) apply.
## Defining parameters
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 is defined by the `Coercion`.
Here's the example with Schema path-parameters:
```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}}}]))
```
Routing again:
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...}
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :result nil,
; :params {:company "metosin", :user-id "123"},
; :path "/metosin/users/123"}
```
Coercion was not applied. Why? All we did was just added more data to the route and the routing functions are just responsible for routing, not coercion.
But now we should have enough data on the match to apply the coercion.
## Compiling coercers
Before the actual coercion, we need to compile the coercers against the route data. This is because compiled coercers yield much better performance. A separate step makes thing explicit and non-magical. Compiling could be done via a Middleware, Interceptor but we can also do it at Router-level, effecting all routes.
There is a helper function for the coercer compiling in `reitit.coercion`:
```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 #SchemaCoercion{...}
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :result {:path #object[reitit.coercion$request_coercer$]},
; :params {:company "metosin", :user-id "123"},
; :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
We can use a helper function to do the actual coercion, based on a `Match`:
```clj
(coercion/coerce!
(r/match-by-path router "/metosin/users/123"))
; {:path {:company "metosin", :user-id 123}}
```
If a coercion fails, a typed (`:reitit.coercion/request-coercion`) ExceptionInfo is thrown, with descriptive data about the actual error:
```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
Here's an full example of routing + coercion.
```clj
(require '[reitit.coercion.schema])
(require '[reitit.coercion :as coercion])
(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}))
(defn route-and-coerce! [path]
(if-let [match (r/match-by-path router path)]
(assoc match :parameters (coercion/coerce! match))))
(route-and-coerce! "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...}
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :result {:path #object[reitit.coercion$request_coercer$]},
; :params {:company "metosin", :user-id "123"},
; :parameters {:path {:company "metosin", :user-id 123}}
; :path "/metosin/users/123"}
(route-and-coerce! "/metosin/users/ikitommi")
; => ExceptionInfo Request coercion failed...
```
## Ring Coercion
For a full-blown http-coercion, see the [ring coercion](../ring/coercion.md).
## Thanks to
Most of the thing are just polished version of the original implementations. Big thanks to:
* [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol
* [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example) for the syntax of the `:paramters` (and `:responses`).

104
doc/coercion/coercion2.md Normal file
View file

@ -0,0 +1,104 @@
# Coercion
Coercion is a process of transforming parameters from one format into another.
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]))
```
Here's a match:
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view},
; :result nil,
; :params {:company "metosin", :user-id "123"},
; :path "/metosin/users/123"}
```
To enable parameter coercion, we need to do few things:
1. Choose a `Coercion` for the routes
2. Defined types for the parameters
3. Compile coercers for the types
4. Apply the coercion
## Coercion
`Coercion` is a protocol defining how types can be defined, coerced and inventoried.
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).
Coercion can be attached to routes using a `:coercion` key in the route data. There can be multiple `Coercion` implementation into a single router, normal [scoping rules](../basics/route_data.html#nested-route-data) apply here too.
## Defining types for parameters
Route parameters can be defined via route data `:parameters`. It can be submaps to define different types of parameters: `:query`, `:body`, `:form`, `:header` and `:path`. Syntax for the actual parameters is defined by the `Coercion` being used.
#### Schema
```clj
(require '[reitit.coercion.schema])
(require '[schema.core :as schema])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.schema/coercion
:parameters {:path {:company schema/Str
:user-id schema/Int}]))
```
#### 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` to get this working.
```clj
(require '[reitit.coercion.spec])
(require '[spec-tools.spec :as spec])
(require '[clojure.spec :as s])
(s/def ::company spec/string?
(s/def ::user-id spec/int?
(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]))
```
#### Data-specs
```clj
(require '[reitit.coercion.spec])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.spec/coercion
:parameters {:path {:company str?
:user-id int?}]))
```
So, now we have our
### Thanks to
Most of the thing are just redefined version of the original implementation. Big thanks to:
* [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol
* [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example) for the syntax for the `:paramters` (and `:responses`).

View file

@ -1,7 +1,7 @@
# Ring
* [Ring-router](ring.md)
* [Dynamic extensions](dynamic_extensions.md)
* [Dynamic Extensions](dynamic_extensions.md)
* [Data-driven Middleware](data_driven_middleware.md)
* [Pluggable Coercion](coercion.md)
* [Compiling middleware](compiling_middleware.md)
* [Compiling Middleware](compiling_middleware.md)

View file

@ -1,4 +1,4 @@
# Dynamic extensions
# Dynamic Extensions
`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build ad-hoc extensions to the system.

View file

@ -125,13 +125,31 @@
[status (response-coercer coercion schema opts)])
(into {})))
(defn- coercers-not-compiled! [match]
(throw
(ex-info
(str
"Match didn't have a compiled coercion attached.\n"
"Maybe you should have defined a router option:\n"
"{:compile reitit.coercion/compile-request-coercers}\n")
{:match match})))
;;
;; integration
;;
(defn compile-request-coercers [[p {:keys [parameters coercion]}] opts]
(defn compile-request-coercers
"A router :compile implementation which reads the `:parameters`
and `:coercion` data to create compiled coercers into Match under
`:result. A pre-requisite to use [[coerce!]]."
[[_ {:keys [parameters coercion]}] opts]
(if (and parameters coercion)
(request-coercers coercion parameters opts)))
(defn coerce! [match]
(coerce-request (:result match) {:path-params (:params match)}))
(defn coerce!
"Returns a map of coerced input parameters using pre-compiled
coercers under `:result` (provided by [[compile-request-coercers]].
If coercion or parameters are not defined, return `nil`"
[match]
(if-let [result (:result match)]
(coerce-request result {:path-params (:params match)})))

View file

@ -15,6 +15,10 @@
:parameters {:path {:number s/Int
:keyword s/Keyword}}}]]
["/spec" {:coercion spec/coercion}
["/:number/:keyword" {:name ::user
:parameters {:path {:number int?
:keyword keyword?}}}]]
["/none"
["/:number/:keyword" {:name ::user
:parameters {:path {:number int?
:keyword keyword?}}}]]]
@ -36,5 +40,12 @@
(coercion/coerce! m)))))
(testing "throws with invalid input"
(let [m (r/match-by-path r "/spec/kikka/abba")]
(is (thrown? ExceptionInfo (coercion/coerce! m))))))))
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
(testing "no coercion defined"
(testing "doesn't coerce"
(let [m (r/match-by-path r "/none/1/abba")]
(is (= nil (coercion/coerce! m))))
(let [m (r/match-by-path r "/none/kikka/abba")]
(is (= nil (coercion/coerce! m))))))))