mirror of
https://github.com/metosin/reitit.git
synced 2026-02-14 07:15:16 +00:00
commit
63b8cf924c
16 changed files with 535 additions and 240 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)
|
* 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)
|
* First-class [route data](https://metosin.github.io/reitit/basics/route_data.html)
|
||||||
* Bi-directional routing
|
* 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)
|
* [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/parameter_coercion.html) ([clojure.spec](https://clojure.org/about/spec))
|
* [Pluggable coercion](https://metosin.github.io/reitit/ring/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
||||||
* Extendable
|
* Extendable
|
||||||
|
* Modular
|
||||||
* [Fast](https://metosin.github.io/reitit/performance.html)
|
* [Fast](https://metosin.github.io/reitit/performance.html)
|
||||||
|
|
||||||
See the [full documentation](https://metosin.github.io/reitit/) for details.
|
See the [full documentation](https://metosin.github.io/reitit/) for details.
|
||||||
|
|
@ -26,7 +27,8 @@ Optionally, the parts can be required separately:
|
||||||
```clj
|
```clj
|
||||||
[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router
|
[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router
|
||||||
[metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-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
|
## Quick start
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
[Reitit](https://github.com/metosin/reitit) is a small Clojure(Script) library for data-driven routing.
|
[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)
|
* 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)
|
* First-class [route data](./basics/route_data.md)
|
||||||
* Bi-directional routing
|
* Bi-directional routing
|
||||||
* [Pluggable coercion](./ring/parameter_coercion.md) ([clojure.spec](https://clojure.org/about/spec))
|
* [Ring-router](./ring/ring.html) with [data-driven middleware](./ring/data_driven_middleware.html)
|
||||||
* supports both [Middleware](./ring/compiling_middleware.md) & Interceptors
|
* [Pluggable coercion](./ring/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
||||||
* Extendable
|
* Extendable
|
||||||
|
* Modular
|
||||||
* [Fast](performance.md)
|
* [Fast](performance.md)
|
||||||
|
|
||||||
To use Reitit, add the following dependecy to your project:
|
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-core "0.1.0-SNAPSHOT"] ; just the router
|
||||||
[metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-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
|
||||||
```
|
```
|
||||||
|
|
||||||
For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/).
|
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)
|
* [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)
|
* [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)
|
* [Compiling middleware](ring/compiling_middleware.md)
|
||||||
* [Performance](performance.md)
|
* [Performance](performance.md)
|
||||||
|
* [FAQ](faq.md)
|
||||||
* TODO: Swagger & OpenAPI
|
* TODO: Swagger & OpenAPI
|
||||||
* TODO: Interceptors
|
* 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)
|
* [Ring-router](ring.md)
|
||||||
* [Dynamic extensions](dynamic_extensions.md)
|
* [Dynamic extensions](dynamic_extensions.md)
|
||||||
* [Data-driven Middleware](data_driven_middleware.md)
|
* [Data-driven Middleware](data_driven_middleware.md)
|
||||||
* [Parameter coercion](parameter_coercion.md)
|
* [Pluggable Coercion](coercion.md)
|
||||||
* [Compiling middleware](compiling_middleware.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.
|
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 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.
|
* 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))))))
|
(handler request respond raise))))))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Compiled
|
## Compiled Middleware
|
||||||
|
|
||||||
* Route information is provided via a closure
|
* Route information is provided via a closure
|
||||||
* Pre-compiled coercers
|
* Pre-compiled coercers
|
||||||
|
|
@ -52,20 +52,20 @@ To demonstrate the two approaches, below are response coercion middleware writte
|
||||||
(require '[reitit.ring.middleware :as middleware])
|
(require '[reitit.ring.middleware :as middleware])
|
||||||
|
|
||||||
(def gen-wrap-coerce-response
|
(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`
|
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||||
and :responses from route data, otherwise does not mount."
|
and :responses from route data, otherwise does not mount."
|
||||||
(middleware/create
|
(middleware/create
|
||||||
{:name ::coerce-response
|
{:name ::coerce-response
|
||||||
:gen-wrap (fn [{:keys [responses coercion opts]} _]
|
:gen-wrap (fn [{:keys [coercion responses opts]} _]
|
||||||
(if (and coercion responses)
|
(if (and coercion responses)
|
||||||
(let [coercers (response-coercers coercion responses opts)]
|
(let [coercers (response-coercers coercion responses opts)]
|
||||||
(fn [handler]
|
(fn [handler]
|
||||||
(fn
|
(fn
|
||||||
([request]
|
([request]
|
||||||
(coerce-response coercers request (handler request)))
|
(coerce-response coercers request (handler request)))
|
||||||
([request respond raise]
|
([request respond raise]
|
||||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||||
```
|
```
|
||||||
|
|
||||||
The latter has 50% less code, is easier to reason about and is much faster.
|
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}}
|
|
||||||
```
|
|
||||||
|
|
@ -15,9 +15,11 @@
|
||||||
|
|
||||||
(defrecord ParameterCoercion [in style keywordize? open?])
|
(defrecord ParameterCoercion [in style keywordize? open?])
|
||||||
|
|
||||||
|
(def valid-type? #{::request-coercion ::response-coercion})
|
||||||
|
|
||||||
(def ring-parameter-coercion
|
(def ring-parameter-coercion
|
||||||
{:query (->ParameterCoercion :query-params :string true true)
|
{:query (->ParameterCoercion :query-params :string true true)
|
||||||
:body (->ParameterCoercion :body-params :string false true)
|
:body (->ParameterCoercion :body-params :body false false)
|
||||||
:form (->ParameterCoercion :form-params :string true true)
|
:form (->ParameterCoercion :form-params :string true true)
|
||||||
:header (->ParameterCoercion :header-params :string true true)
|
:header (->ParameterCoercion :header-params :string true true)
|
||||||
:path (->ParameterCoercion :path-params :string true true)})
|
:path (->ParameterCoercion :path-params :string true true)})
|
||||||
|
|
@ -47,6 +49,8 @@
|
||||||
:request request
|
:request request
|
||||||
:response response}))))
|
:response response}))))
|
||||||
|
|
||||||
|
;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
|
||||||
|
|
||||||
(defn request-coercer [coercion type model]
|
(defn request-coercer [coercion type model]
|
||||||
(if coercion
|
(if coercion
|
||||||
(let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type)
|
(let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type)
|
||||||
|
|
@ -77,11 +81,13 @@
|
||||||
(response-coercion-failed! result coercion value request response)
|
(response-coercion-failed! result coercion value request response)
|
||||||
result))))))
|
result))))))
|
||||||
|
|
||||||
;;
|
(defn encode-error [data]
|
||||||
;; middleware
|
(-> data
|
||||||
;;
|
(dissoc :request :response)
|
||||||
|
(update :coercion protocol/get-name)
|
||||||
|
(->> (protocol/encode-error (:coercion data)))))
|
||||||
|
|
||||||
(defn- coerce-parameters [coercers request]
|
(defn- coerce-request [coercers request]
|
||||||
(reduce-kv
|
(reduce-kv
|
||||||
(fn [acc k coercer]
|
(fn [acc k coercer]
|
||||||
(impl/fast-assoc acc k (coercer request)))
|
(impl/fast-assoc acc k (coercer request)))
|
||||||
|
|
@ -104,85 +110,46 @@
|
||||||
[status (response-coercer coercion schema opts)])
|
[status (response-coercer coercion schema opts)])
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
(defn wrap-coerce-parameters
|
(defn handle-coercion-exception [e respond raise]
|
||||||
"Pluggable request coercion middleware.
|
(let [data (ex-data e)]
|
||||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
(if-let [status (condp = (:type data)
|
||||||
and :parameters from route data, otherwise will do nothing."
|
::request-coercion 400
|
||||||
[handler]
|
::response-coercion 500
|
||||||
(fn
|
nil)]
|
||||||
([request]
|
(respond
|
||||||
(let [method (:request-method request)
|
{:status status
|
||||||
match (ring/get-match request)
|
:body (encode-error data)})
|
||||||
parameters (-> match :result method :data :parameters)
|
(raise e))))
|
||||||
coercion (-> match :data :coercion)]
|
|
||||||
(if (and coercion parameters)
|
;;
|
||||||
(let [coercers (request-coercers coercion parameters)
|
;; middleware
|
||||||
coerced (coerce-parameters coercers request)]
|
;;
|
||||||
(handler (impl/fast-assoc request :parameters coerced)))
|
|
||||||
(handler request))))
|
|
||||||
([request respond raise]
|
|
||||||
(let [method (:request-method request)
|
|
||||||
match (ring/get-match request)
|
|
||||||
parameters (-> match :result method :data :parameters)
|
|
||||||
coercion (-> match :data :coercion)]
|
|
||||||
(if (and coercion parameters)
|
|
||||||
(let [coercers (request-coercers coercion parameters)
|
|
||||||
coerced (coerce-parameters coercers request)]
|
|
||||||
(handler (impl/fast-assoc request :parameters coerced) respond raise)))))))
|
|
||||||
|
|
||||||
(def gen-wrap-coerce-parameters
|
(def gen-wrap-coerce-parameters
|
||||||
"Generator for pluggable request coercion middleware.
|
"Middleware for pluggable request coercion.
|
||||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||||
and :parameters from route data, otherwise does not mount."
|
and :parameters from route data, otherwise does not mount."
|
||||||
(middleware/create
|
(middleware/create
|
||||||
{:name ::coerce-parameters
|
{:name ::coerce-parameters
|
||||||
:gen-wrap (fn [{:keys [parameters coercion]} _]
|
:gen-wrap (fn [{:keys [coercion parameters]} _]
|
||||||
(if (and coercion parameters)
|
(if (and coercion parameters)
|
||||||
(let [coercers (request-coercers coercion parameters)]
|
(let [coercers (request-coercers coercion parameters)]
|
||||||
(fn [handler]
|
(fn [handler]
|
||||||
(fn
|
(fn
|
||||||
([request]
|
([request]
|
||||||
(let [coerced (coerce-parameters coercers request)]
|
(let [coerced (coerce-request coercers request)]
|
||||||
(handler (impl/fast-assoc request :parameters coerced))))
|
(handler (impl/fast-assoc request :parameters coerced))))
|
||||||
([request respond raise]
|
([request respond raise]
|
||||||
(let [coerced (coerce-parameters coercers request)]
|
(let [coerced (coerce-request coercers request)]
|
||||||
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
|
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
|
||||||
|
|
||||||
(defn wrap-coerce-response
|
|
||||||
"Pluggable response coercion middleware.
|
|
||||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
|
||||||
and :responses from route data, otherwise will do nothing."
|
|
||||||
[handler]
|
|
||||||
(fn
|
|
||||||
([request]
|
|
||||||
(let [response (handler request)
|
|
||||||
method (:request-method request)
|
|
||||||
match (ring/get-match request)
|
|
||||||
responses (-> match :result method :data :responses)
|
|
||||||
coercion (-> match :data :coercion)
|
|
||||||
opts (-> match :data :opts)]
|
|
||||||
(if (and coercion responses)
|
|
||||||
(let [coercers (response-coercers coercion responses opts)]
|
|
||||||
(coerce-response coercers request response))
|
|
||||||
response)))
|
|
||||||
([request respond raise]
|
|
||||||
(let [method (:request-method request)
|
|
||||||
match (ring/get-match request)
|
|
||||||
responses (-> match :result method :data :responses)
|
|
||||||
coercion (-> match :data :coercion)
|
|
||||||
opts (-> match :data :opts)]
|
|
||||||
(if (and coercion responses)
|
|
||||||
(let [coercers (response-coercers coercion responses opts)]
|
|
||||||
(handler request #(respond (coerce-response coercers request %))))
|
|
||||||
(handler request respond raise))))))
|
|
||||||
|
|
||||||
(def gen-wrap-coerce-response
|
(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`
|
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||||
and :responses from route data, otherwise does not mount."
|
and :responses from route data, otherwise does not mount."
|
||||||
(middleware/create
|
(middleware/create
|
||||||
{:name ::coerce-response
|
{:name ::coerce-response
|
||||||
:gen-wrap (fn [{:keys [responses coercion opts]} _]
|
:gen-wrap (fn [{:keys [coercion responses opts]} _]
|
||||||
(if (and coercion responses)
|
(if (and coercion responses)
|
||||||
(let [coercers (response-coercers coercion responses opts)]
|
(let [coercers (response-coercers coercion responses opts)]
|
||||||
(fn [handler]
|
(fn [handler]
|
||||||
|
|
@ -191,3 +158,24 @@
|
||||||
(coerce-response coercers request (handler request)))
|
(coerce-response coercers request (handler request)))
|
||||||
([request respond raise]
|
([request respond raise]
|
||||||
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
(handler request #(respond (coerce-response coercers request %)) raise)))))))}))
|
||||||
|
|
||||||
|
(def gen-wrap-coerce-exceptions
|
||||||
|
"Middleware for handling coercion exceptions.
|
||||||
|
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||||
|
and :parameters or :responses from route data, otherwise does not mount."
|
||||||
|
(middleware/create
|
||||||
|
{:name ::coerce-exceptions
|
||||||
|
:gen-wrap (fn [{:keys [coercion parameters responses]} _]
|
||||||
|
(if (and coercion (or parameters responses))
|
||||||
|
(fn [handler]
|
||||||
|
(fn
|
||||||
|
([request]
|
||||||
|
(try
|
||||||
|
(handler request)
|
||||||
|
(catch #?(:clj Exception :cljs js/Error) e
|
||||||
|
(handle-coercion-exception e identity #(throw %)))))
|
||||||
|
([request respond raise]
|
||||||
|
(try
|
||||||
|
(handler request respond #(handle-coercion-exception % respond raise))
|
||||||
|
(catch #?(:clj Exception :cljs js/Error) e
|
||||||
|
(handle-coercion-exception e respond raise))))))))}))
|
||||||
|
|
|
||||||
10
modules/reitit-schema/project.clj
Normal file
10
modules/reitit-schema/project.clj
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
(defproject metosin/reitit-schema "0.1.0-SNAPSHOT"
|
||||||
|
:description "Reitit: Plumatic Schema coercion"
|
||||||
|
:url "https://github.com/metosin/reitit"
|
||||||
|
:license {:name "Eclipse Public License"
|
||||||
|
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||||
|
:plugins [[lein-parent "0.3.2"]]
|
||||||
|
:parent-project {:path "../../project.clj"
|
||||||
|
:inherit [:deploy-repositories :managed-dependencies]}
|
||||||
|
:dependencies [[metosin/reitit-ring]
|
||||||
|
[metosin/schema-tools]])
|
||||||
84
modules/reitit-schema/src/reitit/ring/coercion/schema.cljc
Normal file
84
modules/reitit-schema/src/reitit/ring/coercion/schema.cljc
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
(ns reitit.ring.coercion.schema
|
||||||
|
(:require [schema.core :as s]
|
||||||
|
[schema-tools.core :as st]
|
||||||
|
[schema.coerce :as sc]
|
||||||
|
[schema.utils :as su]
|
||||||
|
[schema-tools.coerce :as stc]
|
||||||
|
[spec-tools.swagger.core :as swagger]
|
||||||
|
[clojure.walk :as walk]
|
||||||
|
[reitit.ring.coercion.protocol :as protocol]))
|
||||||
|
|
||||||
|
(def string-coercion-matcher
|
||||||
|
stc/string-coercion-matcher)
|
||||||
|
|
||||||
|
(def json-coercion-matcher
|
||||||
|
stc/json-coercion-matcher)
|
||||||
|
|
||||||
|
(def default-coercion-matcher
|
||||||
|
(constantly nil))
|
||||||
|
|
||||||
|
(defmulti coerce-response? identity :default ::default)
|
||||||
|
(defmethod coerce-response? ::default [_] true)
|
||||||
|
|
||||||
|
(defn stringify [schema]
|
||||||
|
(walk/prewalk
|
||||||
|
(fn [x]
|
||||||
|
(cond
|
||||||
|
#?@(:clj [(class? x) (.getName ^Class x)])
|
||||||
|
(instance? schema.core.OptionalKey x) (pr-str (list 'opt (:k x)))
|
||||||
|
(instance? schema.core.RequiredKey x) (pr-str (list 'req (:k x)))
|
||||||
|
(and (satisfies? s/Schema x) (record? x)) (try (pr-str (s/explain x)) (catch #?(:clj Exception :cljs js/Error) _ x))
|
||||||
|
(instance? schema.utils.ValidationError x) (str (su/validation-error-explain x))
|
||||||
|
(instance? schema.utils.NamedError x) (str (su/named-error-explain x))
|
||||||
|
:else x))
|
||||||
|
schema))
|
||||||
|
|
||||||
|
(defrecord SchemaCoercion [name matchers coerce-response?]
|
||||||
|
|
||||||
|
protocol/Coercion
|
||||||
|
(get-name [_] name)
|
||||||
|
|
||||||
|
(compile [_ model _]
|
||||||
|
model)
|
||||||
|
|
||||||
|
(get-apidocs [_ _ {:keys [parameters responses] :as info}]
|
||||||
|
(cond-> (dissoc info :parameters :responses)
|
||||||
|
parameters (assoc ::swagger/parameters parameters)
|
||||||
|
responses (assoc ::swagger/responses responses)))
|
||||||
|
|
||||||
|
(make-open [_ schema] (st/open-schema schema))
|
||||||
|
|
||||||
|
(encode-error [_ error]
|
||||||
|
(-> error
|
||||||
|
(update :schema stringify)
|
||||||
|
(update :errors stringify)))
|
||||||
|
|
||||||
|
;; TODO: create all possible coercers ahead of time
|
||||||
|
(request-coercer [_ type schema]
|
||||||
|
(let [{:keys [formats default]} (matchers type)]
|
||||||
|
(fn [value format]
|
||||||
|
(if-let [matcher (or (get formats format) default)]
|
||||||
|
(let [coercer (sc/coercer schema matcher)
|
||||||
|
coerced (coercer value)]
|
||||||
|
(if-let [error (su/error-val coerced)]
|
||||||
|
(protocol/map->CoercionError
|
||||||
|
{:schema schema
|
||||||
|
:errors error})
|
||||||
|
coerced))
|
||||||
|
value))))
|
||||||
|
|
||||||
|
(response-coercer [this schema]
|
||||||
|
(if (coerce-response? schema)
|
||||||
|
(protocol/request-coercer this :response schema))))
|
||||||
|
|
||||||
|
(def default-options
|
||||||
|
{:coerce-response? coerce-response?
|
||||||
|
:matchers {:body {:default default-coercion-matcher
|
||||||
|
:formats {"application/json" json-coercion-matcher}}
|
||||||
|
:string {:default string-coercion-matcher}
|
||||||
|
:response {:default default-coercion-matcher}}})
|
||||||
|
|
||||||
|
(defn create [{:keys [matchers coerce-response?]}]
|
||||||
|
(->SchemaCoercion :schema matchers coerce-response?))
|
||||||
|
|
||||||
|
(def coercion (create default-options))
|
||||||
|
|
@ -50,6 +50,9 @@
|
||||||
(def memoized-into-spec
|
(def memoized-into-spec
|
||||||
(memoize #(into-spec %1 (gensym "spec"))))
|
(memoize #(into-spec %1 (gensym "spec"))))
|
||||||
|
|
||||||
|
(defn stringify-pred [pred]
|
||||||
|
(str (if (seq? pred) (seq pred) pred)))
|
||||||
|
|
||||||
(defmulti coerce-response? identity :default ::default)
|
(defmulti coerce-response? identity :default ::default)
|
||||||
(defmethod coerce-response? ::default [_] true)
|
(defmethod coerce-response? ::default [_] true)
|
||||||
|
|
||||||
|
|
@ -79,7 +82,9 @@
|
||||||
(make-open [_ spec] spec)
|
(make-open [_ spec] spec)
|
||||||
|
|
||||||
(encode-error [_ error]
|
(encode-error [_ error]
|
||||||
(update error :spec (comp str s/form)))
|
(-> error
|
||||||
|
(update :spec (comp str s/form))
|
||||||
|
(update :problems (partial mapv #(update % :pred stringify-pred)))))
|
||||||
|
|
||||||
(request-coercer [_ type spec]
|
(request-coercer [_ type spec]
|
||||||
(let [spec (memoized-into-spec spec)
|
(let [spec (memoized-into-spec spec)
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@
|
||||||
:inherit [:deploy-repositories :managed-dependencies]}
|
:inherit [:deploy-repositories :managed-dependencies]}
|
||||||
:dependencies [[metosin/reitit-core]
|
:dependencies [[metosin/reitit-core]
|
||||||
[metosin/reitit-ring]
|
[metosin/reitit-ring]
|
||||||
[metosin/reitit-spec]])
|
[metosin/reitit-spec]
|
||||||
|
[metosin/reitit-schema]])
|
||||||
|
|
|
||||||
10
project.clj
10
project.clj
|
|
@ -13,9 +13,11 @@
|
||||||
[metosin/reitit-core "0.1.0-SNAPSHOT"]
|
[metosin/reitit-core "0.1.0-SNAPSHOT"]
|
||||||
[metosin/reitit-ring "0.1.0-SNAPSHOT"]
|
[metosin/reitit-ring "0.1.0-SNAPSHOT"]
|
||||||
[metosin/reitit-spec "0.1.0-SNAPSHOT"]
|
[metosin/reitit-spec "0.1.0-SNAPSHOT"]
|
||||||
|
[metosin/reitit-schema "0.1.0-SNAPSHOT"]
|
||||||
|
|
||||||
[meta-merge "1.0.0"]
|
[meta-merge "1.0.0"]
|
||||||
[metosin/spec-tools "0.5.1"]]
|
[metosin/spec-tools "0.5.1"]
|
||||||
|
[metosin/schema-tools "0.10.0-SNAPSHOT"]]
|
||||||
|
|
||||||
:plugins [[jonase/eastwood "0.2.5"]
|
:plugins [[jonase/eastwood "0.2.5"]
|
||||||
[lein-doo "0.1.8"]
|
[lein-doo "0.1.8"]
|
||||||
|
|
@ -30,13 +32,15 @@
|
||||||
:source-paths ["modules/reitit/src"
|
:source-paths ["modules/reitit/src"
|
||||||
"modules/reitit-core/src"
|
"modules/reitit-core/src"
|
||||||
"modules/reitit-ring/src"
|
"modules/reitit-ring/src"
|
||||||
"modules/reitit-spec/src"]
|
"modules/reitit-spec/src"
|
||||||
|
"modules/reitit-schema/src"]
|
||||||
|
|
||||||
:dependencies [[org.clojure/clojure "1.9.0-RC1"]
|
:dependencies [[org.clojure/clojure "1.9.0-RC1"]
|
||||||
[org.clojure/clojurescript "1.9.946"]
|
[org.clojure/clojurescript "1.9.946"]
|
||||||
|
|
||||||
;; all modules dependencies
|
;; modules dependencies
|
||||||
[metosin/reitit]
|
[metosin/reitit]
|
||||||
|
[metosin/schema-tools "0.10.0-SNAPSHOT"]
|
||||||
|
|
||||||
[expound "0.3.2"]
|
[expound "0.3.2"]
|
||||||
[orchestra "2017.08.13"]
|
[orchestra "2017.08.13"]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Modules
|
# Modules
|
||||||
for ext in reitit-core reitit-ring reitit-spec reitit; do
|
for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit; do
|
||||||
cd modules/$ext; lein "$@"; cd ../..;
|
cd modules/$ext; lein "$@"; cd ../..;
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,147 @@
|
||||||
(ns reitit.coercion-test
|
(ns reitit.coercion-test
|
||||||
(:require [clojure.test :refer [deftest testing is]]
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
|
[schema.core :as s]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
[reitit.ring.coercion :as coercion]
|
[reitit.ring.coercion :as coercion]
|
||||||
[reitit.ring.coercion.spec :as spec])
|
[reitit.ring.coercion.spec :as spec]
|
||||||
|
[reitit.ring.coercion.schema :as schema])
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import (clojure.lang ExceptionInfo))))
|
(:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
||||||
(defn handler
|
(defn handler [{{{:keys [a]} :query
|
||||||
([{:keys [::mw]}]
|
{:keys [b]} :body
|
||||||
{:status 200 :body (conj mw :ok)})
|
{:keys [c]} :form
|
||||||
([request respond raise]
|
{:keys [d]} :header
|
||||||
(respond (handler request))))
|
{:keys [e]} :path} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:total (+ a b c d e)}})
|
||||||
|
|
||||||
(deftest coercion-test
|
(def valid-request
|
||||||
(let [app (ring/ring-handler
|
{:uri "/api/plus/5"
|
||||||
(ring/router
|
:request-method :get
|
||||||
["/api"
|
:query-params {"a" "1"}
|
||||||
["/plus/:e"
|
:body-params {:b 2}
|
||||||
{:get {:parameters {:query {:a int?}
|
:form-params {:c 3}
|
||||||
:body {:b int?}
|
:header-params {:d 4}})
|
||||||
:form {:c int?}
|
|
||||||
:header {:d int?}
|
|
||||||
:path {:e int?}}
|
|
||||||
:responses {200 {:schema {:total pos-int?}}}
|
|
||||||
:handler (fn [{{{:keys [a]} :query
|
|
||||||
{:keys [b]} :body
|
|
||||||
{:keys [c]} :form
|
|
||||||
{:keys [d]} :header
|
|
||||||
{:keys [e]} :path} :parameters}]
|
|
||||||
{:status 200
|
|
||||||
:body {:total (+ a b c d e)}})}}]]
|
|
||||||
{:data {:middleware [coercion/gen-wrap-coerce-parameters
|
|
||||||
coercion/gen-wrap-coerce-response]
|
|
||||||
:coercion spec/coercion}}))]
|
|
||||||
|
|
||||||
(testing "all good"
|
(def invalid-request
|
||||||
(is (= {:status 200
|
{:uri "/api/plus/5"
|
||||||
:body {:total 15}}
|
:request-method :get})
|
||||||
(app {:uri "/api/plus/5"
|
|
||||||
:request-method :get
|
|
||||||
:query-params {"a" "1"}
|
|
||||||
:body-params {:b 2}
|
|
||||||
:form-params {:c 3}
|
|
||||||
:header-params {:d 4}}))))
|
|
||||||
|
|
||||||
(testing "invalid request"
|
(def invalid-request2
|
||||||
(is (thrown-with-msg?
|
{:uri "/api/plus/5"
|
||||||
ExceptionInfo
|
:request-method :get
|
||||||
#"Request coercion failed"
|
:query-params {"a" "1"}
|
||||||
(app {:uri "/api/plus/5"
|
:body-params {:b 2}
|
||||||
:request-method :get}))))
|
:form-params {:c 3}
|
||||||
|
:header-params {:d -40}})
|
||||||
|
|
||||||
(testing "invalid response"
|
(deftest spec-coercion-test
|
||||||
(is (thrown-with-msg?
|
(let [create (fn [middleware]
|
||||||
ExceptionInfo
|
(ring/ring-handler
|
||||||
#"Response coercion failed"
|
(ring/router
|
||||||
(app {:uri "/api/plus/5"
|
["/api"
|
||||||
:request-method :get
|
["/plus/:e"
|
||||||
:query-params {"a" "1"}
|
{:get {:parameters {:query {:a int?}
|
||||||
:body-params {:b 2}
|
:body {:b int?}
|
||||||
:form-params {:c 3}
|
:form {:c int?}
|
||||||
:header-params {:d -40}}))))))
|
:header {:d int?}
|
||||||
|
:path {:e int?}}
|
||||||
|
:responses {200 {:schema {:total pos-int?}}}
|
||||||
|
:handler handler}}]]
|
||||||
|
{:data {:middleware middleware
|
||||||
|
:coercion spec/coercion}})))]
|
||||||
|
|
||||||
|
(testing "withut exception handling"
|
||||||
|
(let [app (create [coercion/gen-wrap-coerce-parameters
|
||||||
|
coercion/gen-wrap-coerce-response])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Request coercion failed"
|
||||||
|
(app invalid-request))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Response coercion failed"
|
||||||
|
(app invalid-request2))))))
|
||||||
|
|
||||||
|
(testing "with exception handling"
|
||||||
|
(let [app (create [coercion/gen-wrap-coerce-exceptions
|
||||||
|
coercion/gen-wrap-coerce-parameters
|
||||||
|
coercion/gen-wrap-coerce-response])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(let [{:keys [status body]} (app invalid-request)]
|
||||||
|
(is (= 400 status))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(let [{:keys [status body]} (app invalid-request2)]
|
||||||
|
(is (= 500 status))))))))
|
||||||
|
|
||||||
|
(deftest schema-coercion-test
|
||||||
|
(let [create (fn [middleware]
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api"
|
||||||
|
["/plus/:e"
|
||||||
|
{:get {:parameters {:query {:a s/Int}
|
||||||
|
:body {:b s/Int}
|
||||||
|
:form {:c s/Int}
|
||||||
|
:header {:d s/Int}
|
||||||
|
:path {:e s/Int}}
|
||||||
|
:responses {200 {:schema {:total (s/constrained s/Int pos? 'positive)}}}
|
||||||
|
:handler handler}}]]
|
||||||
|
{:data {:middleware middleware
|
||||||
|
:coercion schema/coercion}})))]
|
||||||
|
|
||||||
|
(testing "withut exception handling"
|
||||||
|
(let [app (create [coercion/gen-wrap-coerce-parameters
|
||||||
|
coercion/gen-wrap-coerce-response])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Request coercion failed"
|
||||||
|
(app invalid-request))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Response coercion failed"
|
||||||
|
(app invalid-request2))))
|
||||||
|
|
||||||
|
(testing "with exception handling"
|
||||||
|
(let [app (create [coercion/gen-wrap-coerce-exceptions
|
||||||
|
coercion/gen-wrap-coerce-parameters
|
||||||
|
coercion/gen-wrap-coerce-response])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(let [{:keys [status body]} (app invalid-request)]
|
||||||
|
(is (= 400 status))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(let [{:keys [status body]} (app invalid-request2)]
|
||||||
|
(is (= 500 status))))))))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue