mirror of
https://github.com/metosin/reitit.git
synced 2025-12-30 05:08:25 +00:00
Coercion docs
This commit is contained in:
parent
7af3f470d6
commit
715968a5d2
14 changed files with 341 additions and 21 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
3
doc/coercion/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Coercion
|
||||
|
||||
* [Coercion Explained](coercion.md)
|
||||
182
doc/coercion/coercion.md
Normal file
182
doc/coercion/coercion.md
Normal 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
104
doc/coercion/coercion2.md
Normal 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`).
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)})))
|
||||
|
|
|
|||
|
|
@ -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))))))))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue