mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
Tune docs
This commit is contained in:
parent
97598ce194
commit
9538d74ae0
8 changed files with 237 additions and 118 deletions
|
|
@ -6,9 +6,10 @@ A friendly data-driven router for Clojure(Script).
|
|||
* Route [conflict resolution](https://metosin.github.io/reitit/basics/route_conflicts.html)
|
||||
* First-class [route data](https://metosin.github.io/reitit/basics/route_data.html)
|
||||
* Bi-directional routing
|
||||
* [Ring-router](https://metosin.github.io/reitit/ring.html) with data-driven [middleware](https://metosin.github.io/reitit/ring/compiling_middleware.html)
|
||||
* [Pluggable coercion](https://metosin.github.io/reitit/ring/parameter_coercion.html) ([clojure.spec](https://clojure.org/about/spec))
|
||||
* [Ring-router](https://metosin.github.io/reitit/ring/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html)
|
||||
* [Pluggable coercion](https://metosin.github.io/reitit/ring/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
||||
* Extendable
|
||||
* Modular
|
||||
* [Fast](https://metosin.github.io/reitit/performance.html)
|
||||
|
||||
See the [full documentation](https://metosin.github.io/reitit/) for details.
|
||||
|
|
@ -26,7 +27,8 @@ Optionally, the parts can be required separately:
|
|||
```clj
|
||||
[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router
|
||||
[metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router
|
||||
[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec-coercion
|
||||
[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec coercion
|
||||
[metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
[Reitit](https://github.com/metosin/reitit) is a small Clojure(Script) library for data-driven routing.
|
||||
|
||||
* Simple data-driven [route syntax](./basics/route_syntax.md)
|
||||
* [Route conflict resolution](./advanced/route_conflicts.md)
|
||||
* [Route conflict resolution](./basics/route_conflicts.md)
|
||||
* First-class [route data](./basics/route_data.md)
|
||||
* Bi-directional routing
|
||||
* [Pluggable coercion](./ring/parameter_coercion.md) ([clojure.spec](https://clojure.org/about/spec))
|
||||
* supports both [Middleware](./ring/compiling_middleware.md) & Interceptors
|
||||
* [Ring-router](./ring/ring.html) with [data-driven middleware](./ring/data_driven_middleware.html)
|
||||
* [Pluggable coercion](./ring/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
||||
* Extendable
|
||||
* Modular
|
||||
* [Fast](performance.md)
|
||||
|
||||
To use Reitit, add the following dependecy to your project:
|
||||
|
|
@ -23,6 +24,7 @@ Optionally, the parts can be required separately:
|
|||
[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router
|
||||
[metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router
|
||||
[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec-coercion
|
||||
[metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion
|
||||
```
|
||||
|
||||
For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/).
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@
|
|||
* [Ring-router](ring/ring.md)
|
||||
* [Dynamic extensions](ring/dynamic_extensions.md)
|
||||
* [Data-driven Middleware](ring/data_driven_middleware.md)
|
||||
* [Parameter coercion](ring/parameter_coercion.md)
|
||||
* [Pluggable Coercion](ring/coercion.md)
|
||||
* [Compiling middleware](ring/compiling_middleware.md)
|
||||
* [Performance](performance.md)
|
||||
* [FAQ](faq.md)
|
||||
* TODO: Swagger & OpenAPI
|
||||
* TODO: Interceptors
|
||||
|
|
|
|||
1
doc/faq.md
Normal file
1
doc/faq.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Frequently Asked Questions
|
||||
|
|
@ -3,5 +3,5 @@
|
|||
* [Ring-router](ring.md)
|
||||
* [Dynamic extensions](dynamic_extensions.md)
|
||||
* [Data-driven Middleware](data_driven_middleware.md)
|
||||
* [Parameter coercion](parameter_coercion.md)
|
||||
* [Pluggable Coercion](coercion.md)
|
||||
* [Compiling middleware](compiling_middleware.md)
|
||||
|
|
|
|||
209
doc/ring/coercion.md
Normal file
209
doc/ring/coercion.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# Pluggable Coercion
|
||||
|
||||
Reitit provides pluggable parameter coercion via `reitit.ring.coercion.protocol/Coercion` protocol, originally introduced in [compojure-api](https://clojars.org/metosin/compojure-api).
|
||||
|
||||
Reitit ships with the following coercion modules:
|
||||
|
||||
* `reitit.ring.coercion.schema/SchemaCoercion` for [plumatic schema](https://github.com/plumatic/schema).
|
||||
* `reitit.ring.coercion.spec/SpecCoercion` 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/gen-wrap-coerce-parameters`
|
||||
* `reitit.ring.coercion/gen-wrap-coerce-response`
|
||||
|
||||
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/gen-wrap-coerce-exceptions` middleware
|
||||
|
||||
### Example with Schema
|
||||
|
||||
```clj
|
||||
(require '[reitit.ring :as ring])
|
||||
(require '[reitit.ring.coercion :as coercion])
|
||||
(require '[reitit.ring.coercion.schema :as schema])
|
||||
(require '[schema.core :as s])
|
||||
|
||||
(def app
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api"
|
||||
["/ping" {:parameters {:body {:x s/Int, :y s/Int}}
|
||||
:responses {200 {:schema {:total (s/constrained s/Int pos?}}}
|
||||
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/gen-wrap-coerce-exceptions
|
||||
coercion/gen-wrap-coerce-parameters
|
||||
coercion/gen-wrap-coerce-response]
|
||||
:coercion schema/coercion}})))
|
||||
```
|
||||
|
||||
Valid request:
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
:uri "/api/ping"
|
||||
:body-params {:x 1, :y 2}})
|
||||
; {:status 200
|
||||
; :body {:total 3}}
|
||||
```
|
||||
|
||||
Invalid request:
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
:uri "/api/ping"
|
||||
:body-params {:x 1, :y "2"}})
|
||||
; {:status 400,
|
||||
; :body {:type :reitit.ring.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 :as coercion])
|
||||
(require '[reitit.ring.coercion.spec :as spec])
|
||||
|
||||
(def app
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api"
|
||||
["/ping" {:parameters {:body {:x int?, :y int?}}
|
||||
:responses {200 {:schema {:total pos-int?}}}
|
||||
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/gen-wrap-coerce-exceptions
|
||||
coercion/gen-wrap-coerce-parameters
|
||||
coercion/gen-wrap-coerce-response]
|
||||
:coercion spec/coercion}})))
|
||||
```
|
||||
|
||||
Valid request:
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
:uri "/api/ping"
|
||||
:body-params {:x 1, :y 2}})
|
||||
; {:status 200
|
||||
; :body {:total 3}}
|
||||
```
|
||||
|
||||
Invalid request:
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
: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 :as coercion])
|
||||
(require '[reitit.ring.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" {:parameters {:body ::request}
|
||||
:responses {200 {:schema ::response}}
|
||||
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/gen-wrap-coerce-exceptions
|
||||
coercion/gen-wrap-coerce-parameters
|
||||
coercion/gen-wrap-coerce-response]
|
||||
:coercion spec/coercion}})))
|
||||
```
|
||||
|
||||
Valid request:
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
:uri "/api/ping"
|
||||
:body-params {:x 1, :y 2}})
|
||||
; {:status 200
|
||||
; :body {:total 3}}
|
||||
```
|
||||
|
||||
Invalid request:
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
: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.ring.coercion.protocol/Coercion` protocol.
|
||||
|
||||
```clj
|
||||
(defprotocol Coercion
|
||||
"Pluggable coercion protocol"
|
||||
(get-name [this] "Keyword name for the coercion")
|
||||
(compile [this model name] "Compiles a coercion model")
|
||||
(get-apidocs [this model data] "???")
|
||||
(make-open [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"))
|
||||
```
|
||||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
The [dynamic extensions](dynamic_extensions.md) is a easy way to extend the system. To enable fast lookups into route data, we can compile them into any shape (records, functions etc.) we want, enabling fast access at request-time.
|
||||
|
||||
Still, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware/interceptor at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding much faster runtime processing. It can also decide not to mount itself by returning `nil`. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it?
|
||||
But, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware/interceptor at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding much faster runtime processing. It can also decide not to mount itself by returning `nil`. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it?
|
||||
|
||||
To enable this we use [middleware records](data_driven_middleware.md) `:gen-wrap` key instead of the normal `:wrap`. `:gen-wrap` expects a function of `route-data router-opts => ?wrap`.
|
||||
|
||||
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen-wrap`. Actual codes can be found in [`reitit.ring.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/ring/coercion.cljc):
|
||||
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen-wrap`.
|
||||
|
||||
## Naive
|
||||
## Normal Middleware
|
||||
|
||||
* Reads the compiled route information on every request.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
|||
(handler request respond raise))))))
|
||||
```
|
||||
|
||||
## Compiled
|
||||
## Compiled Middleware
|
||||
|
||||
* Route information is provided via a closure
|
||||
* Pre-compiled coercers
|
||||
|
|
@ -52,20 +52,20 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
|||
(require '[reitit.ring.middleware :as middleware])
|
||||
|
||||
(def gen-wrap-coerce-response
|
||||
"Generator for pluggable response coercion middleware.
|
||||
"Middleware for pluggable response coercion.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :responses from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-response
|
||||
:gen-wrap (fn [{:keys [responses coercion opts]} _]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(coerce-response coercers request (handler request)))
|
||||
([request respond raise]
|
||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||
:gen-wrap (fn [{:keys [coercion responses opts]} _]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(coerce-response coercers request (handler request)))
|
||||
([request respond raise]
|
||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||
```
|
||||
|
||||
The latter has 50% less code, is easier to reason about and is much faster.
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
# Parameter coercion
|
||||
|
||||
Reitit provides pluggable parameter coercion via `reitit.ring.coercion.protocol/Coercion` protocol, originally introduced in [compojure-api](https://clojars.org/metosin/compojure-api). Reitit ships with `reitit.ring.coercion.spec/SpecCoercion` providing implemenation for [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs).
|
||||
|
||||
**NOTE**: Before Clojure 1.9.0 is shipped, to use the spec-coercion, one needs to add the following dependencies manually to the project:
|
||||
|
||||
```clj
|
||||
[org.clojure/clojure "1.9.0-beta2"]
|
||||
[org.clojure/spec.alpha "0.1.123"]
|
||||
[metosin/spec-tools "0.4.0"]
|
||||
```
|
||||
|
||||
### 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. Define a `Coercion` to route data under `:coercion`
|
||||
3. Mount request & response coercion middleware to the routes (recommended to mount to all routes under router as they mounted only to routes which have the parameters / responses defined):
|
||||
* `reitit.ring.coercion/gen-wrap-coerce-parameters`
|
||||
* `gen-wrap-coerce-parameters/gen-wrap-coerce-responses`
|
||||
|
||||
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.
|
||||
|
||||
#### Example with data-specs
|
||||
|
||||
```clj
|
||||
(require '[reitit.ring :as ring])
|
||||
(require '[reitit.ring.coercion :as coercion])
|
||||
(require '[reitit.ring.coercion.spec :as spec])
|
||||
|
||||
(def app
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api"
|
||||
["/ping" {:parameters {:body {:x int?, :y int?}}
|
||||
:responses {200 {:schema {:total pos-int?}}}
|
||||
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/gen-wrap-coerce-parameters
|
||||
coercion/gen-wrap-coerce-response]
|
||||
:coercion spec/coercion}})))
|
||||
```
|
||||
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
:uri "/api/ping"
|
||||
:body-params {:x 1, :y 2}})
|
||||
; {:status 200, :body {:total 3}}
|
||||
```
|
||||
|
||||
#### Example with specs
|
||||
|
||||
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 :as coercion])
|
||||
(require '[reitit.ring.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" {:parameters {:body ::request}
|
||||
:responses {200 {:schema ::response}}
|
||||
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/gen-wrap-coerce-parameters
|
||||
coercion/gen-wrap-coerce-response]
|
||||
:coercion spec/coercion}})))
|
||||
```
|
||||
|
||||
```clj
|
||||
(app
|
||||
{:request-method :get
|
||||
:uri "/api/ping"
|
||||
:body-params {:x 1, :y 2}})
|
||||
; {:status 200, :body {:total 3}}
|
||||
```
|
||||
Loading…
Reference in a new issue