mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 00:41:12 +00:00
commit
c471837d9c
45 changed files with 1226 additions and 497 deletions
|
|
@ -7,7 +7,7 @@ A friendly data-driven router for Clojure(Script).
|
|||
* First-class [route data](https://metosin.github.io/reitit/basics/route_data.html)
|
||||
* Bi-directional routing
|
||||
* [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))
|
||||
* [Pluggable coercion](https://metosin.github.io/reitit/coercion/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)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* First-class [route data](./basics/route_data.md)
|
||||
* Bi-directional routing
|
||||
* [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))
|
||||
* [Pluggable coercion](./coercion/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
||||
* Extendable
|
||||
* Modular
|
||||
* [Fast](performance.md)
|
||||
|
|
|
|||
|
|
@ -2,22 +2,27 @@
|
|||
|
||||
* [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)
|
||||
* [Plumatic Schema](coercion/schema_coercion.md)
|
||||
* [Clojure.spec](coercion/clojure_spec_coercion.md)
|
||||
* [Data-specs](coercion/data_spec_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)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ Reitit ships with several different implementations for the `Router` protocol, o
|
|||
| ------------------------------|-------------|
|
||||
| `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. Slow, but works with all route trees.
|
||||
| `:lookup-router` | Fast router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters and there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:mixed-router` | Creates internally a `:prefix-tree-router` and a `:lookup-router` and used them to effectively get best-of-both-worlds. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:mixed-router` | Creates internally a `:segment-router` for wildcard routes and a `:lookup-router` or `:single-static-path-router` for static routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:single-static-path-router` | Super fast router: sting-matches the route. Valid only if there is one static route.
|
||||
| `:prefix-tree-router` | Router that creates a [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) out of an route table. Much faster than `:linear-router`. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
| `:segment-router` | Router that creates a optimized [search trie](https://en.wikipedia.org/wiki/Trie) out of an route table. Much faster than `:linear-router` for wildcard routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md).
|
||||
|
||||
The router name can be asked from the router:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
6
doc/coercion/README.md
Normal file
6
doc/coercion/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Coercion
|
||||
|
||||
* [Coercion Explained](coercion.md)
|
||||
* [Plumatic Schema](schema_coercion.md)
|
||||
* [Clojure.spec](clojure_spec_coercion.md)
|
||||
* [Data-specs](data_spec_coercion.md)
|
||||
50
doc/coercion/clojure_spec_coercion.md
Normal file
50
doc/coercion/clojure_spec_coercion.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Clojure.spec Coercion
|
||||
|
||||
The [clojure.spec](https://clojure.org/guides/spec) library specifies the structure of data, validates or destructures it, and can generate data based on the spec.
|
||||
|
||||
**NOTE**: 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 into [Spec Records](https://github.com/metosin/spec-tools/blob/master/README.md#spec-records) to get the coercion working.
|
||||
|
||||
```clj
|
||||
(require '[reitit.coercion.spec])
|
||||
(require '[reitit.coercion :as coercion])
|
||||
(require '[spec-tools.spec :as spec])
|
||||
(require '[clojure.spec.alpha :as s])
|
||||
(require '[reitit.core :as r])
|
||||
|
||||
;; need to wrap the primitives!
|
||||
(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}}]
|
||||
{: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))))
|
||||
```
|
||||
|
||||
Successful coercion:
|
||||
|
||||
```clj
|
||||
(route-and-coerce! "/metosin/users/123")
|
||||
; #Match{:template "/:company/users/:user-id",
|
||||
; :data {:name :user/user-view,
|
||||
; :coercion #SpecCoercion{...}
|
||||
; :parameters {:path ::path-params}},
|
||||
; :result {:path #object[reitit.coercion$request_coercer$]},
|
||||
; :params {:company "metosin", :user-id "123"},
|
||||
; :parameters {:path {:company "metosin", :user-id 123}}
|
||||
; :path "/metosin/users/123"}
|
||||
```
|
||||
|
||||
Failing coercion:
|
||||
|
||||
```clj
|
||||
(route-and-coerce! "/metosin/users/ikitommi")
|
||||
; => ExceptionInfo Request coercion failed...
|
||||
```
|
||||
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`).
|
||||
43
doc/coercion/data_spec_coercion.md
Normal file
43
doc/coercion/data_spec_coercion.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Data-spec Coercion
|
||||
|
||||
[Data-specs](https://github.com/metosin/spec-tools#data-specs) is alternative, macro-free syntax to define `clojure.spec`s. As a bonus, supports the [runtime transformations via conforming](https://dev.clojure.org/jira/browse/CLJ-2116) out-of-the-box.
|
||||
|
||||
```clj
|
||||
(require '[reitit.coercion.spec])
|
||||
(require '[reitit.coercion :as coercion])
|
||||
(require '[reitit.core :as r])
|
||||
|
||||
(def router
|
||||
(r/router
|
||||
["/:company/users/:user-id" {:name ::user-view
|
||||
:coercion reitit.coercion.spec/coercion
|
||||
:parameters {:path {:company string?
|
||||
:user-id 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))))
|
||||
```
|
||||
|
||||
Successful coercion:
|
||||
|
||||
```clj
|
||||
(route-and-coerce! "/metosin/users/123")
|
||||
; #Match{:template "/:company/users/:user-id",
|
||||
; :data {:name :user/user-view,
|
||||
; :coercion #SpecCoercion{...}
|
||||
; :parameters {:path {:company 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"}
|
||||
```
|
||||
|
||||
Failing coercion:
|
||||
|
||||
```clj
|
||||
(route-and-coerce! "/metosin/users/ikitommi")
|
||||
; => ExceptionInfo Request coercion failed...
|
||||
```
|
||||
44
doc/coercion/schema_coercion.md
Normal file
44
doc/coercion/schema_coercion.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Plumatic Schema Coercion
|
||||
|
||||
[Plumatic Schema](https://github.com/plumatic/schema) is a Clojure(Script) library for declarative data description and validation.
|
||||
|
||||
```clj
|
||||
(require '[reitit.coercion.schema])
|
||||
(require '[reitit.coercion :as coercion])
|
||||
(require '[schema.core :as s])
|
||||
(require '[reitit.core :as r])
|
||||
|
||||
(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))))
|
||||
```
|
||||
|
||||
Successful coercion:
|
||||
|
||||
```clj
|
||||
(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"}
|
||||
```
|
||||
|
||||
Failing coercion:
|
||||
|
||||
```clj
|
||||
(route-and-coerce! "/metosin/users/ikitommi")
|
||||
; => ExceptionInfo Request coercion failed...
|
||||
```
|
||||
|
|
@ -77,7 +77,7 @@ So, we need to test something more realistic.
|
|||
|
||||
To get better view on the real life routing performance, there is [test](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/opensensors_perf_test.clj) of a mid-size rest(ish) http api with 50+ routes, having a lot of path parameters. The route definitions are pulled off from the [OpenSensors](https://opensensors.io/) swagger definitions.
|
||||
|
||||
Thanks to the [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) algorithm, `reitit-ring` and Pedestal are fastest here.
|
||||
Thanks to the snappy [segment-tree](https://github.com/metosin/reitit/blob/master/modules/reitit-core/src/reitit/segment.cljc) algorithm, `reitit-ring` is fastest here. Pedestal is also fast with it's [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) implementation.
|
||||
|
||||

|
||||
|
||||
|
|
@ -85,11 +85,11 @@ Thanks to the [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) algorithm,
|
|||
|
||||
Another real-life [test scenario](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj) is a [CQRS](https://martinfowler.com/bliki/CQRS.html)-style route tree, where all the paths are static, e.g. `/api/command/add-order`. The route definitions are pulled out from [Lupapiste](https://github.com/lupapiste/lupapiste). The test consists of ~300 static routes (just the commands here, there would be ~200 queries too).
|
||||
|
||||
Again, both `reitit-ring` and Pedestal shine here, thanks to the fast lookup-routers. On average, they are two orders of magnitude faster and on best/worst case, three orders of magnitude faster than the other tested libs. Ataraxy failed this test on `Method code too large` error.
|
||||
Again, both `reitit-ring` and Pedestal shine here, thanks to the fast lookup-routers. On average, they are **two** and on best case, **three orders of magnitude faster** than the other tested libs. Ataraxy failed this test on `Method code too large!` error.
|
||||
|
||||

|
||||
|
||||
**NOTE**: If there would be even one wildcard route in the route-tree, Pedestal would fallback from lookup-router to the prefix-tree router, yielding constant, but order of magnitude slower perf. Reitit instead fallbacks to `:mixed-router`, still serving the static routes with lookup-router, just the wildcard route(s) with prefix-tree. So, the performance would not notably degrade.
|
||||
**NOTE**: If there would be even one wildcard route in the route-tree, Pedestal would fallback from lookup-router to the prefix-tree router, yielding nearly constant, but an order of magnitude slower perf. Reitit instead fallbacks to `:mixed-router`, serving all the static routes with `:lookup-router`, just the wildcard route(s) with `:segment-tree`. So, the performance would not notably degrade.
|
||||
|
||||
### Why measure?
|
||||
|
||||
|
|
|
|||
|
|
@ -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,11 +1,11 @@
|
|||
# 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 provides pluggable parameter coercion via `reitit.coercion/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).
|
||||
* `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).
|
||||
|
||||
### Ring request and response coercion
|
||||
|
||||
|
|
@ -16,19 +16,19 @@ To use `Coercion` with Ring, one needs to do the following:
|
|||
* `: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/coerce-request-middleware`
|
||||
* `reitit.ring.coercion/coerce-response-middleware`
|
||||
* `reitit.ring.coercion-middleware/coerce-request-middleware`
|
||||
* `reitit.ring.coercion-middleware/coerce-response-middleware`
|
||||
|
||||
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/coerce-exceptions-middleware` middleware
|
||||
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-middleware/coerce-exceptions-middleware` middleware
|
||||
|
||||
### Example with Schema
|
||||
|
||||
```clj
|
||||
(require '[reitit.ring :as ring])
|
||||
(require '[reitit.ring.coercion :as coercion])
|
||||
(require '[reitit.ring.coercion.schema :as schema])
|
||||
(require '[reitit.ring.coercion-middleware :as coercion-middleware])
|
||||
(require '[reitit.coercion.schema :as schema])
|
||||
(require '[schema.core :as s])
|
||||
|
||||
(def app
|
||||
|
|
@ -36,13 +36,13 @@ If either request or response coercion fails, an descriptive error is thrown. To
|
|||
(ring/router
|
||||
["/api"
|
||||
["/ping" {:post {:parameters {:body {:x s/Int, :y s/Int}}
|
||||
:responses {200 {:schema {:total (s/constrained s/Int pos?}}}
|
||||
:responses {200 {:schema {:total (s/constrained s/Int pos?)}}}
|
||||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion schema/coercion}})))
|
||||
```
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ Invalid request:
|
|||
:uri "/api/ping"
|
||||
:body-params {:x 1, :y "2"}})
|
||||
; {:status 400,
|
||||
; :body {:type :reitit.ring.coercion/request-coercion
|
||||
; :body {:type :reitit.coercion/request-coercion
|
||||
; :coercion :schema
|
||||
; :in [:request :body-params]
|
||||
; :value {:x 1, :y "2"}
|
||||
|
|
@ -77,8 +77,8 @@ Invalid request:
|
|||
|
||||
```clj
|
||||
(require '[reitit.ring :as ring])
|
||||
(require '[reitit.ring.coercion :as coercion])
|
||||
(require '[reitit.ring.coercion.spec :as spec])
|
||||
(require '[reitit.ring.coercion-middleware :as coercion-middleware])
|
||||
(require '[reitit.coercion.spec :as spec])
|
||||
|
||||
(def app
|
||||
(ring/ring-handler
|
||||
|
|
@ -89,9 +89,9 @@ Invalid request:
|
|||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}})))
|
||||
```
|
||||
|
||||
|
|
@ -132,8 +132,8 @@ Currently, `clojure.spec` [doesn't support runtime transformations via conformin
|
|||
|
||||
```clj
|
||||
(require '[reitit.ring :as ring])
|
||||
(require '[reitit.ring.coercion :as coercion])
|
||||
(require '[reitit.ring.coercion.spec :as spec])
|
||||
(require '[reitit.ring.coercion-middleware :as coercion-middleware])
|
||||
(require '[reitit.coercion.spec :as spec])
|
||||
(require '[clojure.spec.alpha :as s])
|
||||
(require '[spec-tools.core :as st])
|
||||
|
||||
|
|
@ -152,9 +152,9 @@ Currently, `clojure.spec` [doesn't support runtime transformations via conformin
|
|||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}})))
|
||||
```
|
||||
|
||||
|
|
@ -194,16 +194,16 @@ Invalid request:
|
|||
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.
|
||||
`reitit.coercion/Coercion` protocol.
|
||||
|
||||
```clj
|
||||
(defprotocol Coercion
|
||||
"Pluggable coercion protocol"
|
||||
(get-name [this] "Keyword name for the coercion")
|
||||
(get-apidocs [this model data] "???")
|
||||
(compile-model [this model name] "Compiles a coercion model")
|
||||
(open-model [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"))
|
||||
(-get-name [this] "Keyword name for the coercion")
|
||||
(-get-apidocs [this model data] "???")
|
||||
(-compile-model [this model name] "Compiles a coercion model")
|
||||
(-open-model [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"))
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
(ns example.dspec
|
||||
(:require [reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]
|
||||
(:require [reitit.coercion.spec :as spec-coercion]
|
||||
[example.server :as server]))
|
||||
|
||||
(defn handler [{{{:keys [x y]} :query} :parameters}]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
(ns example.schema
|
||||
(:require [reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.schema :as schema-coercion]
|
||||
(:require [reitit.coercion.schema :as schema-coercion]
|
||||
[example.server :as server]))
|
||||
|
||||
(defn handler [{{{:keys [x y]} :query} :parameters}]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
(ns example.server
|
||||
(:require [ring.adapter.jetty :as jetty]
|
||||
[reitit.middleware :as middleware]
|
||||
[reitit.ring.coercion :as coercion]))
|
||||
[reitit.ring.coercion-middleware :as coercion-middleware]))
|
||||
|
||||
(defonce ^:private server (atom nil))
|
||||
|
||||
|
|
@ -10,9 +10,9 @@
|
|||
;; to be set with :extract-request-format and extract-response-format
|
||||
(defn wrap-coercion [handler resource]
|
||||
(middleware/chain
|
||||
[coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware
|
||||
coercion/coerce-exceptions-middleware]
|
||||
[coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware
|
||||
coercion-middleware/coerce-exceptions-middleware]
|
||||
handler
|
||||
resource))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
(ns example.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[spec-tools.spec :as spec]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]
|
||||
[reitit.coercion.spec :as spec-coercion]
|
||||
[example.server :as server]))
|
||||
|
||||
;; wrap into Spec Records to enable runtime conforming
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
(ns example.dspec
|
||||
(:require [reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]))
|
||||
(:require [reitit.coercion.spec :as spec-coercion]))
|
||||
|
||||
(def routes
|
||||
["/dspec"
|
||||
["/dspec" {:coercion spec-coercion/coercion}
|
||||
["/plus" {:name ::plus
|
||||
:coercion spec-coercion/coercion
|
||||
:responses {200 {:schema {:total int?}}}
|
||||
:get {:summary "plus with query-params"
|
||||
:parameters {:query {:x int?, :y int?}}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
(ns example.schema
|
||||
(:require [schema.core :as s]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.schema :as schema-coercion]))
|
||||
[reitit.coercion.schema :as schema-coercion]))
|
||||
|
||||
(def routes
|
||||
["/schema"
|
||||
["/schema" {:coercion schema-coercion/coercion}
|
||||
["/plus" {:name ::plus
|
||||
:coercion schema-coercion/coercion
|
||||
:responses {200 {:schema {:total s/Int}}}
|
||||
:get {:summary "plus with query-params"
|
||||
:parameters {:query {:x s/Int, :y s/Int}}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
[ring.middleware.params]
|
||||
[muuntaja.middleware]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion-middleware :as coercion-middleware]
|
||||
[example.dspec]
|
||||
[example.schema]
|
||||
[example.spec]))
|
||||
|
|
@ -18,9 +18,9 @@
|
|||
example.spec/routes]
|
||||
{:data {:middleware [ring.middleware.params/wrap-params
|
||||
muuntaja.middleware/wrap-format
|
||||
coercion/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]}})))
|
||||
coercion-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]}})))
|
||||
|
||||
(defn restart []
|
||||
(swap! server (fn [x]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
(ns example.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[spec-tools.spec :as spec]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec-coercion]))
|
||||
[reitit.coercion.spec :as spec-coercion]))
|
||||
|
||||
;; wrap into Spec Records to enable runtime conforming
|
||||
(s/def ::x spec/int?)
|
||||
|
|
@ -10,9 +9,8 @@
|
|||
(s/def ::total spec/int?)
|
||||
|
||||
(def routes
|
||||
["/spec"
|
||||
["/spec" {:coercion spec-coercion/coercion}
|
||||
["/plus" {:name ::plus
|
||||
:coercion spec-coercion/coercion
|
||||
:responses {200 {:schema (s/keys :req-un [::total])}}
|
||||
:get {:summary "plus with query-params"
|
||||
:parameters {:query (s/keys :req-un [::x ::y])}
|
||||
|
|
|
|||
155
modules/reitit-core/src/reitit/coercion.cljc
Normal file
155
modules/reitit-core/src/reitit/coercion.cljc
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
(ns reitit.coercion
|
||||
(:require [clojure.walk :as walk]
|
||||
[spec-tools.core :as st]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
;;
|
||||
;; Protocol
|
||||
;;
|
||||
|
||||
(defprotocol Coercion
|
||||
"Pluggable coercion protocol"
|
||||
(-get-name [this] "Keyword name for the coercion")
|
||||
(-get-apidocs [this model data] "???")
|
||||
(-compile-model [this model name] "Compiles a model")
|
||||
(-open-model [this model] "Returns a new model which allows extra keys in maps")
|
||||
(-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"))
|
||||
|
||||
(defrecord CoercionError [])
|
||||
|
||||
(defn error? [x]
|
||||
(instance? CoercionError x))
|
||||
|
||||
;;
|
||||
;; api-docs
|
||||
;;
|
||||
|
||||
#_(defn get-apidocs [coercion spec info]
|
||||
(protocol/get-apidocs coercion spec info))
|
||||
|
||||
;;
|
||||
;; coercer
|
||||
;;
|
||||
|
||||
(defrecord ParameterCoercion [in style keywordize? open?])
|
||||
|
||||
(def ^:no-doc ring-parameter-coercion
|
||||
{:query (->ParameterCoercion :query-params :string true true)
|
||||
:body (->ParameterCoercion :body-params :body false false)
|
||||
:form (->ParameterCoercion :form-params :string true true)
|
||||
:header (->ParameterCoercion :header-params :string true true)
|
||||
:path (->ParameterCoercion :path-params :string true true)})
|
||||
|
||||
(defn ^:no-doc request-coercion-failed! [result coercion value in request]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Request coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::request-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:request in]
|
||||
:request request}))))
|
||||
|
||||
(defn ^:no-doc response-coercion-failed! [result coercion value request response]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Response coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::response-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:response :body]
|
||||
:request request
|
||||
:response response}))))
|
||||
|
||||
;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
|
||||
(defn request-coercer [coercion type model {:keys [extract-request-format]
|
||||
:or {extract-request-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type)
|
||||
transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||
model (if open? (-open-model coercion model) model)
|
||||
coercer (-request-coercer coercion style model)]
|
||||
(fn [request]
|
||||
(let [value (transform request)
|
||||
format (extract-request-format request)
|
||||
result (coercer value format)]
|
||||
(if (error? result)
|
||||
(request-coercion-failed! result coercion value in request)
|
||||
result))))))
|
||||
|
||||
(defn response-coercer [coercion model {:keys [extract-response-format]
|
||||
:or {extract-response-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [coercer (-response-coercer coercion model)]
|
||||
(fn [request response]
|
||||
(let [format (extract-response-format request response)
|
||||
value (:body response)
|
||||
result (coercer value format)]
|
||||
(if (error? result)
|
||||
(response-coercion-failed! result coercion value request response)
|
||||
result))))))
|
||||
|
||||
(defn encode-error [data]
|
||||
(-> data
|
||||
(dissoc :request :response)
|
||||
(update :coercion -get-name)
|
||||
(->> (-encode-error (:coercion data)))))
|
||||
|
||||
(defn coerce-request [coercers request]
|
||||
(reduce-kv
|
||||
(fn [acc k coercer]
|
||||
(impl/fast-assoc acc k (coercer request)))
|
||||
{}
|
||||
coercers))
|
||||
|
||||
(defn coerce-response [coercers request response]
|
||||
(if response
|
||||
(if-let [coercer (or (coercers (:status response)) (coercers :default))]
|
||||
(impl/fast-assoc response :body (coercer request response)))))
|
||||
|
||||
(defn request-coercers [coercion parameters opts]
|
||||
(->> (for [[k v] parameters
|
||||
:when v]
|
||||
[k (request-coercer coercion k v opts)])
|
||||
(into {})))
|
||||
|
||||
(defn response-coercers [coercion responses opts]
|
||||
(->> (for [[status {:keys [schema]}] responses :when schema]
|
||||
[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
|
||||
"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!
|
||||
"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)})))
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
(defn wild-route? [[path]]
|
||||
(contains-wilds? path))
|
||||
|
||||
(defn conflicting-routes? [[p1 :as route1] [p2 :as route2]]
|
||||
(defn conflicting-routes? [[p1] [p2]]
|
||||
(loop [[s1 & ss1] (segments p1)
|
||||
[s2 & ss2] (segments p2)]
|
||||
(cond
|
||||
|
|
|
|||
136
modules/reitit-core/src/reitit/interceptor.cljc
Normal file
136
modules/reitit-core/src/reitit/interceptor.cljc
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
(ns reitit.interceptor
|
||||
(:require [meta-merge.core :refer [meta-merge]]
|
||||
[reitit.core :as r]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
(defprotocol IntoInterceptor
|
||||
(into-interceptor [this data opts]))
|
||||
|
||||
(defrecord Interceptor [name enter leave error])
|
||||
(defrecord Endpoint [data interceptors])
|
||||
|
||||
(defn create [{:keys [name wrap compile] :as m}]
|
||||
(when (and wrap compile)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Interceptor can't have both :wrap and :compile defined " m) m)))
|
||||
(map->Interceptor m))
|
||||
|
||||
(def ^:dynamic *max-compile-depth* 10)
|
||||
|
||||
(extend-protocol IntoInterceptor
|
||||
|
||||
#?(:clj clojure.lang.APersistentVector
|
||||
:cljs cljs.core.PersistentVector)
|
||||
(into-interceptor [[f & args] data opts]
|
||||
(if-let [{:keys [wrap] :as mw} (into-interceptor f data opts)]
|
||||
(assoc mw :wrap #(apply wrap % args))))
|
||||
|
||||
#?(:clj clojure.lang.Fn
|
||||
:cljs function)
|
||||
(into-interceptor [this _ _]
|
||||
(map->Interceptor
|
||||
{:enter this}))
|
||||
|
||||
#?(:clj clojure.lang.PersistentArrayMap
|
||||
:cljs cljs.core.PersistentArrayMap)
|
||||
(into-interceptor [this data opts]
|
||||
(into-interceptor (create this) data opts))
|
||||
|
||||
#?(:clj clojure.lang.PersistentHashMap
|
||||
:cljs cljs.core.PersistentHashMap)
|
||||
(into-interceptor [this data opts]
|
||||
(into-interceptor (create this) data opts))
|
||||
|
||||
Interceptor
|
||||
(into-interceptor [{:keys [compile] :as this} data opts]
|
||||
(if-not compile
|
||||
this
|
||||
(let [compiled (::compiled opts 0)
|
||||
opts (assoc opts ::compiled (inc compiled))]
|
||||
(when (>= compiled *max-compile-depth*)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Too deep Interceptor compilation - " compiled)
|
||||
{:this this, :data data, :opts opts})))
|
||||
(if-let [interceptor (into-interceptor (compile data opts) data opts)]
|
||||
(map->Interceptor
|
||||
(merge
|
||||
(dissoc this :create)
|
||||
(impl/strip-nils interceptor)))))))
|
||||
|
||||
nil
|
||||
(into-interceptor [_ _ _]))
|
||||
|
||||
(defn- ensure-handler! [path data scope]
|
||||
(when-not (:handler data)
|
||||
(throw (ex-info
|
||||
(str "path \"" path "\" doesn't have a :handler defined"
|
||||
(if scope (str " for " scope)))
|
||||
(merge {:path path, :data data}
|
||||
(if scope {:scope scope}))))))
|
||||
|
||||
(defn expand [interceptors data opts]
|
||||
(->> interceptors
|
||||
(keep #(into-interceptor % data opts))
|
||||
(into [])))
|
||||
|
||||
(defn interceptor-chain [interceptors handler data opts]
|
||||
(expand (conj interceptors handler) data opts))
|
||||
|
||||
(defn compile-result
|
||||
([route opts]
|
||||
(compile-result route opts nil))
|
||||
([[path {:keys [interceptors handler] :as data}]
|
||||
{:keys [::transform] :or {transform identity} :as opts} scope]
|
||||
(ensure-handler! path data scope)
|
||||
(let [interceptors (expand (transform (expand interceptors data opts)) data opts)]
|
||||
(map->Endpoint
|
||||
{:interceptors (interceptor-chain interceptors handler data opts)
|
||||
:data data}))))
|
||||
|
||||
(defn router
|
||||
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
|
||||
support for Interceptors. See [docs](https://metosin.github.io/reitit/) for details.
|
||||
|
||||
Example:
|
||||
|
||||
(router
|
||||
[\"/api\" {:interceptors [i/format i/oauth2]}
|
||||
[\"/users\" {:interceptors [i/delete]
|
||||
:handler get-user}]])
|
||||
|
||||
See router options from [[reitit.core/router]]."
|
||||
([data]
|
||||
(router data nil))
|
||||
([data opts]
|
||||
(let [opts (meta-merge {:compile compile-result} opts)]
|
||||
(r/router data opts))))
|
||||
|
||||
(defn interceptor-handler [router]
|
||||
(with-meta
|
||||
(fn [path]
|
||||
(some->> path
|
||||
(r/match-by-path router)
|
||||
:result
|
||||
:interceptors))
|
||||
{::router router}))
|
||||
|
||||
(defn execute [r {{:keys [uri]} :request :as ctx}]
|
||||
(if-let [interceptors (-> (r/match-by-path r uri)
|
||||
:result
|
||||
:interceptors)]
|
||||
(as-> ctx $
|
||||
(reduce #(%2 %1) $ (keep :enter interceptors))
|
||||
(reduce #(%2 %1) $ (keep :leave interceptors)))))
|
||||
|
||||
(def r
|
||||
(router
|
||||
["/api" {:interceptors [{:name ::add
|
||||
:enter (fn [ctx]
|
||||
(assoc ctx :enter true))
|
||||
:leave (fn [ctx]
|
||||
(assoc ctx :leave true))}]}
|
||||
["/ping" (fn [ctx] (assoc ctx :response "ok"))]]))
|
||||
|
||||
(execute r {:request {:uri "/api/ping"}})
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
(defrecord Middleware [name wrap])
|
||||
(defrecord Endpoint [data handler middleware])
|
||||
|
||||
(defn create [{:keys [name wrap compile] :as m}]
|
||||
(defn create [{:keys [wrap compile] :as m}]
|
||||
(when (and wrap compile)
|
||||
(throw
|
||||
(ex-info
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
(when (>= compiled *max-compile-depth*)
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Too deep middleware compilation - " compiled)
|
||||
(str "Too deep Middleware compilation - " compiled)
|
||||
{:this this, :data data, :opts opts})))
|
||||
(if-let [middeware (into-middleware (compile data opts) data opts)]
|
||||
(map->Middleware
|
||||
|
|
|
|||
|
|
@ -10,19 +10,19 @@
|
|||
|
||||
(extend-protocol Segment
|
||||
nil
|
||||
(-insert [this ps data])
|
||||
(-lookup [this ps params]))
|
||||
(-insert [_ _ _])
|
||||
(-lookup [_ _ _]))
|
||||
|
||||
(defn- -catch-all [children catch-all data params p ps]
|
||||
(defn- -catch-all [children catch-all params p ps]
|
||||
(if catch-all
|
||||
(-lookup
|
||||
(impl/fast-get children catch-all)
|
||||
nil
|
||||
(assoc data :params (assoc params catch-all (str/join "/" (cons p ps)))))))
|
||||
(assoc params catch-all (str/join "/" (cons p ps))))))
|
||||
|
||||
(defn- segment
|
||||
([] (segment {} #{} nil nil))
|
||||
([children wilds catch-all data]
|
||||
([children wilds catch-all match]
|
||||
(let [children' (impl/fast-map children)]
|
||||
^{:type ::segment}
|
||||
(reify
|
||||
|
|
@ -34,13 +34,13 @@
|
|||
wilds (if w (conj wilds w) wilds)
|
||||
catch-all (or c catch-all)
|
||||
children (update children (or w c p) #(-insert (or % (segment)) ps d))]
|
||||
(segment children wilds catch-all data))))
|
||||
(segment children wilds catch-all match))))
|
||||
(-lookup [_ [p & ps] params]
|
||||
(if (nil? p)
|
||||
(if data (assoc data :params params))
|
||||
(if match (assoc match :params params))
|
||||
(or (-lookup (impl/fast-get children' p) ps params)
|
||||
(some #(-lookup (impl/fast-get children' %) ps (assoc params % p)) wilds)
|
||||
(-catch-all children' catch-all data params p ps))))))))
|
||||
(-catch-all children' catch-all params p ps))))))))
|
||||
|
||||
(defn insert [root path data]
|
||||
(-insert (or root (segment)) (impl/segments path) (map->Match {:data data})))
|
||||
|
|
@ -53,11 +53,3 @@
|
|||
|
||||
(defn lookup [segment path]
|
||||
(-lookup segment (impl/segments path) {}))
|
||||
|
||||
(comment
|
||||
(-> [["/:abba" 1]
|
||||
["/:abba/:dabba" 2]
|
||||
["/kikka/*kakka" 3]]
|
||||
(create)
|
||||
(lookup "/kikka/1/2")
|
||||
(./aprint)))
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
(ns reitit.ring.coercion
|
||||
(:require [clojure.walk :as walk]
|
||||
[spec-tools.core :as st]
|
||||
[reitit.middleware :as middleware]
|
||||
[reitit.ring.coercion.protocol :as protocol]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
#_(defn get-apidocs [coercion spec info]
|
||||
(protocol/get-apidocs coercion spec info))
|
||||
|
||||
;;
|
||||
;; coercer
|
||||
;;
|
||||
|
||||
(defrecord ParameterCoercion [in style keywordize? open?])
|
||||
|
||||
(def ^:no-doc ring-parameter-coercion
|
||||
{:query (->ParameterCoercion :query-params :string true true)
|
||||
:body (->ParameterCoercion :body-params :body false false)
|
||||
:form (->ParameterCoercion :form-params :string true true)
|
||||
:header (->ParameterCoercion :header-params :string true true)
|
||||
:path (->ParameterCoercion :path-params :string true true)})
|
||||
|
||||
(defn ^:no-doc request-coercion-failed! [result coercion value in request]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Request coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::request-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:request in]
|
||||
:request request}))))
|
||||
|
||||
(defn ^:no-doc response-coercion-failed! [result coercion value request response]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Response coercion failed: " (pr-str result))
|
||||
(merge
|
||||
(into {} result)
|
||||
{:type ::response-coercion
|
||||
:coercion coercion
|
||||
:value value
|
||||
:in [:response :body]
|
||||
:request request
|
||||
:response response}))))
|
||||
|
||||
;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
|
||||
(defn ^:no-doc request-coercer [coercion type model {:keys [extract-request-format]
|
||||
:or {extract-request-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [{:keys [keywordize? open? in style]} (ring-parameter-coercion type)
|
||||
transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||
model (if open? (protocol/open-model coercion model) model)
|
||||
coercer (protocol/request-coercer coercion style model)]
|
||||
(fn [request]
|
||||
(let [value (transform request)
|
||||
format (extract-request-format request)
|
||||
result (coercer value format)]
|
||||
(if (protocol/error? result)
|
||||
(request-coercion-failed! result coercion value in request)
|
||||
result))))))
|
||||
|
||||
(defn ^:no-doc response-coercer [coercion model {:keys [extract-response-format]
|
||||
:or {extract-response-format (constantly nil)}}]
|
||||
(if coercion
|
||||
(let [coercer (protocol/response-coercer coercion model)]
|
||||
(fn [request response]
|
||||
(let [format (extract-response-format request response)
|
||||
value (:body response)
|
||||
result (coercer value format)]
|
||||
(if (protocol/error? result)
|
||||
(response-coercion-failed! result coercion value request response)
|
||||
result))))))
|
||||
|
||||
(defn ^:no-doc encode-error [data]
|
||||
(-> data
|
||||
(dissoc :request :response)
|
||||
(update :coercion protocol/get-name)
|
||||
(->> (protocol/encode-error (:coercion data)))))
|
||||
|
||||
(defn ^:no-doc coerce-request [coercers request]
|
||||
(reduce-kv
|
||||
(fn [acc k coercer]
|
||||
(impl/fast-assoc acc k (coercer request)))
|
||||
{}
|
||||
coercers))
|
||||
|
||||
(defn ^:no-doc coerce-response [coercers request response]
|
||||
(if response
|
||||
(if-let [coercer (or (coercers (:status response)) (coercers :default))]
|
||||
(impl/fast-assoc response :body (coercer request response)))))
|
||||
|
||||
(defn ^:no-doc request-coercers [coercion parameters opts]
|
||||
(->> (for [[k v] parameters
|
||||
:when v]
|
||||
[k (request-coercer coercion k v opts)])
|
||||
(into {})))
|
||||
|
||||
(defn ^:no-doc response-coercers [coercion responses opts]
|
||||
(->> (for [[status {:keys [schema]}] responses :when schema]
|
||||
[status (response-coercer coercion schema opts)])
|
||||
(into {})))
|
||||
|
||||
(defn ^:no-doc handle-coercion-exception [e respond raise]
|
||||
(let [data (ex-data e)]
|
||||
(if-let [status (condp = (:type data)
|
||||
::request-coercion 400
|
||||
::response-coercion 500
|
||||
nil)]
|
||||
(respond
|
||||
{:status status
|
||||
:body (encode-error data)})
|
||||
(raise e))))
|
||||
|
||||
;;
|
||||
;; middleware
|
||||
;;
|
||||
|
||||
(def coerce-request-middleware
|
||||
"Middleware for pluggable request coercion.
|
||||
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
||||
and :parameters from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-parameters
|
||||
:compile (fn [{:keys [coercion parameters]} opts]
|
||||
(if (and coercion parameters)
|
||||
(let [coercers (request-coercers coercion parameters opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(let [coerced (coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced))))
|
||||
([request respond raise]
|
||||
(let [coerced (coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
|
||||
|
||||
(def coerce-response-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
|
||||
:compile (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)))))))}))
|
||||
|
||||
(def coerce-exceptions-middleware
|
||||
"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
|
||||
:compile (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))))))))}))
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
(ns reitit.ring.coercion.protocol)
|
||||
|
||||
(defprotocol Coercion
|
||||
"Pluggable coercion protocol"
|
||||
(get-name [this] "Keyword name for the coercion")
|
||||
(get-apidocs [this model data] "???")
|
||||
(compile-model [this model name] "Compiles a model")
|
||||
(open-model [this model] "Returns a new model which allows extra keys in maps")
|
||||
(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"))
|
||||
|
||||
(defrecord CoercionError [])
|
||||
|
||||
(defn error? [x]
|
||||
(instance? CoercionError x))
|
||||
74
modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc
Normal file
74
modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
(ns reitit.ring.coercion-middleware
|
||||
(:require [reitit.middleware :as middleware]
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
(defn handle-coercion-exception [e respond raise]
|
||||
(let [data (ex-data e)]
|
||||
(if-let [status (condp = (:type data)
|
||||
::coercion/request-coercion 400
|
||||
::coercion/response-coercion 500
|
||||
nil)]
|
||||
(respond
|
||||
{:status status
|
||||
:body (coercion/encode-error data)})
|
||||
(raise e))))
|
||||
|
||||
;;
|
||||
;; middleware
|
||||
;;
|
||||
|
||||
(def coerce-request-middleware
|
||||
"Middleware for pluggable request coercion.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-parameters
|
||||
:compile (fn [{:keys [coercion parameters]} opts]
|
||||
(if (and coercion parameters)
|
||||
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(let [coerced (coercion/coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced))))
|
||||
([request respond raise]
|
||||
(let [coerced (coercion/coerce-request coercers request)]
|
||||
(handler (impl/fast-assoc request :parameters coerced) respond raise))))))))}))
|
||||
|
||||
(def coerce-response-middleware
|
||||
"Middleware for pluggable response coercion.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :responses from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-response
|
||||
:compile (fn [{:keys [coercion responses]} opts]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (coercion/response-coercers coercion responses opts)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(coercion/coerce-response coercers request (handler request)))
|
||||
([request respond raise]
|
||||
(handler request #(respond (coercion/coerce-response coercers request %)) raise)))))))}))
|
||||
|
||||
(def coerce-exceptions-middleware
|
||||
"Middleware for handling coercion exceptions.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters or :responses from route data, otherwise does not mount."
|
||||
(middleware/create
|
||||
{:name ::coerce-exceptions
|
||||
:compile (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))))))))}))
|
||||
|
|
@ -6,5 +6,5 @@
|
|||
:plugins [[lein-parent "0.3.2"]]
|
||||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-ring]
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[metosin/schema-tools]])
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
(ns reitit.ring.coercion.schema
|
||||
(:require [schema.core :as s]
|
||||
(ns reitit.coercion.schema
|
||||
(:require [clojure.walk :as walk]
|
||||
[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]))
|
||||
[reitit.coercion :as coercion]))
|
||||
|
||||
(def string-coercion-matcher
|
||||
stc/string-coercion-matcher)
|
||||
|
|
@ -35,24 +35,24 @@
|
|||
|
||||
(defrecord SchemaCoercion [name matchers coerce-response?]
|
||||
|
||||
protocol/Coercion
|
||||
(get-name [_] name)
|
||||
coercion/Coercion
|
||||
(-get-name [_] name)
|
||||
|
||||
(get-apidocs [_ _ {:keys [parameters responses] :as info}]
|
||||
(-get-apidocs [_ _ {:keys [parameters responses] :as info}]
|
||||
(cond-> (dissoc info :parameters :responses)
|
||||
parameters (assoc ::swagger/parameters parameters)
|
||||
responses (assoc ::swagger/responses responses)))
|
||||
|
||||
(compile-model [_ model _] model)
|
||||
(-compile-model [_ model _] model)
|
||||
|
||||
(open-model [_ schema] (st/open-schema schema))
|
||||
(-open-model [_ schema] (st/open-schema schema))
|
||||
|
||||
(encode-error [_ error]
|
||||
(-encode-error [_ error]
|
||||
(-> error
|
||||
(update :schema stringify)
|
||||
(update :errors stringify)))
|
||||
|
||||
(request-coercer [_ type schema]
|
||||
(-request-coercer [_ type schema]
|
||||
(let [{:keys [formats default]} (matchers type)
|
||||
coercers (->> (for [m (conj (vals formats) default)]
|
||||
[m (sc/coercer schema m)])
|
||||
|
|
@ -62,15 +62,15 @@
|
|||
(let [coercer (coercers matcher)
|
||||
coerced (coercer value)]
|
||||
(if-let [error (su/error-val coerced)]
|
||||
(protocol/map->CoercionError
|
||||
(coercion/map->CoercionError
|
||||
{:schema schema
|
||||
:errors error})
|
||||
coerced))
|
||||
value))))
|
||||
|
||||
(response-coercer [this schema]
|
||||
(-response-coercer [this schema]
|
||||
(if (coerce-response? schema)
|
||||
(protocol/request-coercer this :response schema))))
|
||||
(coercion/-request-coercer this :response schema))))
|
||||
|
||||
(def default-options
|
||||
{:coerce-response? coerce-response?
|
||||
|
|
@ -6,5 +6,5 @@
|
|||
:plugins [[lein-parent "0.3.2"]]
|
||||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-ring]
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[metosin/spec-tools]])
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
(ns reitit.ring.coercion.spec
|
||||
(ns reitit.coercion.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[spec-tools.core :as st #?@(:cljs [:refer [Spec]])]
|
||||
[spec-tools.data-spec :as ds]
|
||||
[spec-tools.conform :as conform]
|
||||
[spec-tools.swagger.core :as swagger]
|
||||
[reitit.ring.coercion.protocol :as protocol])
|
||||
[reitit.coercion :as coercion])
|
||||
#?(:clj
|
||||
(:import (spec_tools.core Spec))))
|
||||
|
||||
|
|
@ -54,51 +54,51 @@
|
|||
|
||||
(defrecord SpecCoercion [name conforming coerce-response?]
|
||||
|
||||
protocol/Coercion
|
||||
(get-name [_] name)
|
||||
coercion/Coercion
|
||||
(-get-name [_] name)
|
||||
|
||||
(get-apidocs [this _ {:keys [parameters responses] :as info}]
|
||||
(-get-apidocs [this _ {:keys [parameters responses] :as info}]
|
||||
(cond-> (dissoc info :parameters :responses)
|
||||
parameters (assoc
|
||||
::swagger/parameters
|
||||
(into
|
||||
(empty parameters)
|
||||
(for [[k v] parameters]
|
||||
[k (protocol/compile-model this v nil)])))
|
||||
[k (coercion/-compile-model this v nil)])))
|
||||
responses (assoc
|
||||
::swagger/responses
|
||||
(into
|
||||
(empty responses)
|
||||
(for [[k response] responses]
|
||||
[k (update response :schema #(protocol/compile-model this % nil))])))))
|
||||
[k (update response :schema #(coercion/-compile-model this % nil))])))))
|
||||
|
||||
(compile-model [_ model _]
|
||||
(-compile-model [_ model _]
|
||||
(into-spec model (or name (gensym "spec"))))
|
||||
|
||||
(open-model [_ spec] spec)
|
||||
(-open-model [_ spec] spec)
|
||||
|
||||
(encode-error [_ error]
|
||||
(-encode-error [_ error]
|
||||
(-> error
|
||||
(update :spec (comp str s/form))
|
||||
(update :problems (partial mapv #(update % :pred stringify-pred)))))
|
||||
|
||||
(request-coercer [this type spec]
|
||||
(let [spec (protocol/compile-model this spec nil)
|
||||
(-request-coercer [this type spec]
|
||||
(let [spec (coercion/-compile-model this spec nil)
|
||||
{:keys [formats default]} (conforming type)]
|
||||
(fn [value format]
|
||||
(if-let [conforming (or (get formats format) default)]
|
||||
(let [conformed (st/conform spec value conforming)]
|
||||
(if (s/invalid? conformed)
|
||||
(let [problems (st/explain-data spec value conforming)]
|
||||
(protocol/map->CoercionError
|
||||
(coercion/map->CoercionError
|
||||
{:spec spec
|
||||
:problems (::s/problems problems)}))
|
||||
(s/unform spec conformed)))
|
||||
value))))
|
||||
|
||||
(response-coercer [this spec]
|
||||
(-response-coercer [this spec]
|
||||
(if (coerce-response? spec)
|
||||
(protocol/request-coercer this :response spec))))
|
||||
(coercion/-request-coercer this :response spec))))
|
||||
|
||||
(def default-options
|
||||
{:coerce-response? coerce-response?
|
||||
|
|
@ -4,18 +4,16 @@
|
|||
[reitit.perf-utils :refer :all]
|
||||
[clojure.spec.alpha :as s]
|
||||
[spec-tools.core :as st]
|
||||
|
||||
[reitit.core :as reitit]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec]
|
||||
[reitit.ring.coercion.schema :as schema]
|
||||
[reitit.ring.coercion.protocol :as protocol]
|
||||
[spec-tools.data-spec :as ds]
|
||||
[muuntaja.middleware :as mm]
|
||||
[muuntaja.core :as m]
|
||||
[muuntaja.format.jsonista :as jsonista-format]
|
||||
[jsonista.core :as j]
|
||||
[reitit.coercion-middleware :as coercion-middleware]
|
||||
[reitit.coercion.spec :as spec]
|
||||
[reitit.coercion.schema :as schema]
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.core :as r])
|
||||
(:import (java.io ByteArrayInputStream)))
|
||||
|
||||
|
|
@ -41,14 +39,14 @@
|
|||
(s/def ::k (s/keys :req-un [::x ::y]))
|
||||
|
||||
(let [spec (spec/into-spec {:x int?, :y int?} ::jeah)
|
||||
coercers (#'coercion/request-coercers spec/coercion {:body spec})
|
||||
coercers (#'coercion-middleware/request-coercers spec/coercion {:body spec})
|
||||
params {:x "1", :y "2"}
|
||||
request {:body-params {:x "1", :y "2"}}]
|
||||
|
||||
;; 4600ns
|
||||
(bench!
|
||||
"coerce-parameters"
|
||||
(#'coercion/coerce-parameters coercers request))
|
||||
(#'coercion-middleware/coerce-parameters coercers request))
|
||||
|
||||
;; 2700ns
|
||||
(bench!
|
||||
|
|
@ -85,14 +83,14 @@
|
|||
params))))))
|
||||
|
||||
(defrecord NoOpCoercion []
|
||||
protocol/Coercion
|
||||
(get-name [_] :no-op)
|
||||
(get-apidocs [_ _ {:keys [parameters responses] :as info}])
|
||||
(compile-model [_ model _] model)
|
||||
(open-model [_ spec] spec)
|
||||
(encode-error [_ error] error)
|
||||
(request-coercer [_ type spec] (fn [value format] value))
|
||||
(response-coercer [this spec] (protocol/request-coercer this :response spec)))
|
||||
coercion/Coercion
|
||||
(-get-name [_] :no-op)
|
||||
(-get-apidocs [_ _ {:keys [parameters responses] :as info}])
|
||||
(-compile-model [_ model _] model)
|
||||
(-open-model [_ spec] spec)
|
||||
(-encode-error [_ error] error)
|
||||
(-request-coercer [_ type spec] (fn [value format] value))
|
||||
(-response-coercer [this spec] (protocol/request-coercer this :response spec)))
|
||||
|
||||
(comment
|
||||
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]]
|
||||
|
|
@ -107,24 +105,24 @@
|
|||
app (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware]
|
||||
:coercion coercion}}))
|
||||
app2 (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware]
|
||||
:coercion coercion}}))
|
||||
app3 (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/wrap-coerce-response]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/wrap-coerce-response]
|
||||
:coercion coercion}}))
|
||||
app4 (ring/ring-handler
|
||||
(ring/router
|
||||
routes
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion coercion}}))
|
||||
req {:request-method :get
|
||||
:uri "/api/ping"
|
||||
|
|
@ -161,8 +159,8 @@
|
|||
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}})))
|
||||
|
||||
(app
|
||||
|
|
@ -205,8 +203,8 @@
|
|||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [[mm/wrap-format m]
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion schema/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
@ -228,8 +226,8 @@
|
|||
:handler (fn [request]
|
||||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion schema/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
@ -251,8 +249,8 @@
|
|||
:handler (fn [request]
|
||||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
@ -279,8 +277,8 @@
|
|||
:handler (fn [request]
|
||||
(let [body (-> request :parameters :body)]
|
||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||
{:data {:middleware [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware]
|
||||
{:data {:middleware [coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware]
|
||||
:coercion spec/coercion}}))
|
||||
request {:request-method :post
|
||||
:uri "/plus"
|
||||
|
|
|
|||
110
perf-test/clj/reitit/go_perf_test.clj
Normal file
110
perf-test/clj/reitit/go_perf_test.clj
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
(ns reitit.go-perf-test
|
||||
(:require [criterium.core :as cc]
|
||||
[reitit.perf-utils :refer :all]
|
||||
[reitit.ring :as ring]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;;
|
||||
;; start repl with `lein perf repl`
|
||||
;; perf measured with the following setup:
|
||||
;;
|
||||
;; Model Name: MacBook Pro
|
||||
;; Model Identifier: MacBookPro113
|
||||
;; Processor Name: Intel Core i7
|
||||
;; Processor Speed: 2,5 GHz
|
||||
;; Number of Processors: 1
|
||||
;; Total Number of Cores: 4
|
||||
;; L2 Cache (per Core): 256 KB
|
||||
;; L3 Cache: 6 MB
|
||||
;; Memory: 16 GB
|
||||
;;
|
||||
|
||||
(defn h [path]
|
||||
(fn [req]
|
||||
{:status 200, :body path}))
|
||||
|
||||
(defn add [handler routes route]
|
||||
(let [method (-> route keys first str/lower-case keyword)
|
||||
path (-> route vals first)
|
||||
h (handler path)]
|
||||
(if (some (partial = path) (map first routes))
|
||||
(mapv (fn [[p d]] (if (= path p) [p (assoc d method h)] [p d])) routes)
|
||||
(conj routes [path {method h}]))))
|
||||
|
||||
(def routes [{"POST", "/1/classes/:className"},
|
||||
{"GET", "/1/classes/:className/:objectId"},
|
||||
{"PUT", "/1/classes/:className/:objectId"},
|
||||
{"GET", "/1/classes/:className"},
|
||||
{"DELETE", "/1/classes/:className/:objectId"},
|
||||
|
||||
;; Users
|
||||
{"POST", "/1/users"},
|
||||
{"GET", "/1/login"},
|
||||
{"GET", "/1/users/:objectId"},
|
||||
{"PUT", "/1/users/:objectId"},
|
||||
{"GET", "/1/users"},
|
||||
{"DELETE", "/1/users/:objectId"},
|
||||
{"POST", "/1/requestPasswordReset"},
|
||||
|
||||
;; Roles
|
||||
{"POST", "/1/roles"},
|
||||
{"GET", "/1/roles/:objectId"},
|
||||
{"PUT", "/1/roles/:objectId"},
|
||||
{"GET", "/1/roles"},
|
||||
{"DELETE", "/1/roles/:objectId"},
|
||||
|
||||
;; Files
|
||||
{"POST", "/1/files/:fileName"},
|
||||
|
||||
;; Analytics
|
||||
{"POST", "/1/events/:eventName"},
|
||||
|
||||
;; Push Notifications
|
||||
{"POST", "/1/push"},
|
||||
|
||||
;; Installations
|
||||
{"POST", "/1/installations"},
|
||||
{"GET", "/1/installations/:objectId"},
|
||||
{"PUT", "/1/installations/:objectId"},
|
||||
{"GET", "/1/installations"},
|
||||
{"DELETE", "/1/installations/:objectId"},
|
||||
|
||||
;; Cloud Functions
|
||||
{"POST", "/1/functions"}])
|
||||
|
||||
|
||||
(def app
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
(reduce (partial add h) [] routes))))
|
||||
|
||||
(defn routing-test []
|
||||
;; https://github.com/julienschmidt/go-http-routing-benchmark
|
||||
;; coudn't run the GO tests, so reusing just the numbers (run on better hw?):
|
||||
;;
|
||||
;; Intel Core i5-2500K (4x 3,30GHz + Turbo Boost), CPU-governor: performance
|
||||
;; 2x 4 GiB DDR3-1333 RAM, dual-channel
|
||||
;; go version go1.3rc1 linux/amd64
|
||||
;; Ubuntu 14.04 amd64 (Linux Kernel 3.13.0-29), fresh installation
|
||||
|
||||
;; 37ns (2.0x)
|
||||
;; 180ns (4.0x)
|
||||
;; 200ns (4.8x)
|
||||
"httpRouter"
|
||||
|
||||
;; 77ns
|
||||
;; 700ns
|
||||
;; 890ns
|
||||
(title "reitit-ring")
|
||||
(let [r1 (map->Request {:request-method :get, :uri "/1/users"})
|
||||
r2 (map->Request {:request-method :get, :uri "/1/classes/go"})
|
||||
r3 (map->Request {:request-method :get, :uri "/1/classes/go/123456789"})]
|
||||
(assert (= {:status 200, :body "/1/users"} (app r1)))
|
||||
(assert (= {:status 200, :body "/1/classes/:className"} (app r2)))
|
||||
(assert (= {:status 200, :body "/1/classes/:className/:objectId"} (app r3)))
|
||||
(cc/quick-bench (app r1))
|
||||
(cc/quick-bench (app r2))
|
||||
(cc/quick-bench (app r3))))
|
||||
|
||||
(comment
|
||||
(routing-test))
|
||||
82
perf-test/clj/reitit/nodejs_perf_test.clj
Normal file
82
perf-test/clj/reitit/nodejs_perf_test.clj
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
(ns reitit.nodejs-perf-test
|
||||
(:require [criterium.core :as cc]
|
||||
[reitit.perf-utils :refer :all]
|
||||
[immutant.web :as web]
|
||||
[reitit.ring :as ring]))
|
||||
|
||||
;;
|
||||
;; start repl with `lein perf repl`
|
||||
;; perf measured with the following setup:
|
||||
;;
|
||||
;; Model Name: MacBook Pro
|
||||
;; Model Identifier: MacBookPro113
|
||||
;; Processor Name: Intel Core i7
|
||||
;; Processor Speed: 2,5 GHz
|
||||
;; Number of Processors: 1
|
||||
;; Total Number of Cores: 4
|
||||
;; L2 Cache (per Core): 256 KB
|
||||
;; L3 Cache: 6 MB
|
||||
;; Memory: 16 GB
|
||||
;;
|
||||
|
||||
(defn h [name req]
|
||||
(let [id (-> req :path-params :id)]
|
||||
{:status 200, :body (str "Got " name " id " id)}))
|
||||
|
||||
|
||||
(def app
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
(for [name ["product" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "twenty"]]
|
||||
[(str "/" name "/:id") {:get (partial h name)}]))))
|
||||
|
||||
(app {:request-method :get, :uri "/product/foo"})
|
||||
|
||||
(defn routing-test []
|
||||
|
||||
;; 21385 / 14337
|
||||
"barista"
|
||||
|
||||
;; 26259 / 25571
|
||||
"choreographer"
|
||||
|
||||
;; 24277 / 19174
|
||||
"clutch"
|
||||
|
||||
;; 26158 / 25584
|
||||
"connect"
|
||||
|
||||
;; 24614 / 25413
|
||||
"escort"
|
||||
|
||||
;; 21979 / 18595
|
||||
"express"
|
||||
|
||||
;; 23123 / 25405
|
||||
"find-my-way"
|
||||
|
||||
;; 24798 / 25286
|
||||
"http-hash"
|
||||
|
||||
;; 24215 / 23670
|
||||
"i40"
|
||||
|
||||
;; 23561 / 26278
|
||||
"light-router"
|
||||
|
||||
;; 28362 / 30056
|
||||
"http-raw"
|
||||
|
||||
;; 25310 / 25126
|
||||
"regex"
|
||||
|
||||
;; 84149 / 84867
|
||||
(title "reitit")
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/product/foo
|
||||
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/twenty/bar
|
||||
(assert (= {:status 200, :body "Got product id foo"} (app {:request-method :get, :uri "/product/foo"})))
|
||||
(assert (= {:status 200, :body "Got twenty id bar"} (app {:request-method :get, :uri "/twenty/bar"}))))
|
||||
|
||||
(comment
|
||||
(web/run app {:port 2048})
|
||||
(routing-test))
|
||||
13
project.clj
13
project.clj
|
|
@ -22,7 +22,7 @@
|
|||
:plugins [[jonase/eastwood "0.2.5"]
|
||||
[lein-doo "0.1.8"]
|
||||
[lein-cljsbuild "1.1.7"]
|
||||
[lein-cloverage "1.0.9"]
|
||||
[lein-cloverage "1.0.10"]
|
||||
[lein-codox "0.10.3"]
|
||||
[metosin/boot-alt-test "0.4.0-20171019.180106-3"]]
|
||||
|
||||
|
|
@ -35,19 +35,19 @@
|
|||
"modules/reitit-spec/src"
|
||||
"modules/reitit-schema/src"]
|
||||
|
||||
:dependencies [[org.clojure/clojure "1.9.0-RC1"]
|
||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||
[org.clojure/clojurescript "1.9.946"]
|
||||
|
||||
;; modules dependencies
|
||||
[metosin/reitit]
|
||||
[metosin/schema-tools "0.10.0-SNAPSHOT"]
|
||||
|
||||
[expound "0.3.2"]
|
||||
[orchestra "2017.08.13"]
|
||||
[expound "0.3.4"]
|
||||
[orchestra "2017.11.12-1"]
|
||||
|
||||
[ring "1.6.3"]
|
||||
[metosin/muuntaja "0.4.1"]
|
||||
[metosin/jsonista "0.1.0-SNAPSHOT"]
|
||||
[metosin/jsonista "0.1.0"]
|
||||
|
||||
[criterium "0.4.4"]
|
||||
[org.clojure/test.check "0.9.0"]
|
||||
|
|
@ -58,8 +58,9 @@
|
|||
"-Dclojure.compiler.direct-linking=true"]
|
||||
:test-paths ["perf-test/clj"]
|
||||
:dependencies [[compojure "1.6.0"]
|
||||
[org.immutant/immutant "2.1.9"]
|
||||
[io.pedestal/pedestal.route "0.5.3"]
|
||||
[org.clojure/core.async "0.3.443"]
|
||||
[org.clojure/core.async "0.3.465"]
|
||||
[ataraxy "0.4.0"]
|
||||
[bidi "2.1.2"]]}
|
||||
:analyze {:jvm-opts ^:replace ["-server"
|
||||
|
|
|
|||
|
|
@ -1,147 +1,51 @@
|
|||
(ns reitit.coercion-test
|
||||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[schema.core :as s]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.ring.coercion.spec :as spec]
|
||||
[reitit.ring.coercion.schema :as schema])
|
||||
[reitit.core :as r]
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.coercion.spec :as spec]
|
||||
[reitit.coercion.schema :as schema])
|
||||
#?(:clj
|
||||
(:import (clojure.lang ExceptionInfo))))
|
||||
|
||||
(defn handler [{{{: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)}})
|
||||
|
||||
(def valid-request
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get
|
||||
:query-params {"a" "1"}
|
||||
:body-params {:b 2}
|
||||
:form-params {:c 3}
|
||||
:header-params {:d 4}})
|
||||
|
||||
(def invalid-request
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get})
|
||||
|
||||
(def invalid-request2
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get
|
||||
:query-params {"a" "1"}
|
||||
:body-params {:b 2}
|
||||
:form-params {:c 3}
|
||||
:header-params {:d -40}})
|
||||
|
||||
(deftest spec-coercion-test
|
||||
(let [create (fn [middleware]
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api"
|
||||
["/plus/:e"
|
||||
{:get {:parameters {:query {:a int?}
|
||||
:body {:b int?}
|
||||
:form {:c int?}
|
||||
:header {:d int?}
|
||||
:path {:e int?}}
|
||||
:responses {200 {:schema {:total pos-int?}}}
|
||||
:handler handler}}]]
|
||||
{:data {:middleware middleware
|
||||
:coercion spec/coercion}})))]
|
||||
(let [r (r/router
|
||||
[["/schema" {:coercion schema/coercion}
|
||||
["/:number/:keyword" {:name ::user
|
||||
: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?}}}]]]
|
||||
{:compile coercion/compile-request-coercers})]
|
||||
|
||||
(testing "withut exception handling"
|
||||
(let [app (create [coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
(testing "schema-coercion"
|
||||
(testing "succeeds"
|
||||
(let [m (r/match-by-path r "/schema/1/abba")]
|
||||
(is (= {:path {:keyword :abba, :number 1}}
|
||||
(coercion/coerce! m)))))
|
||||
(testing "throws with invalid input"
|
||||
(let [m (r/match-by-path r "/schema/kikka/abba")]
|
||||
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||
|
||||
(testing "all good"
|
||||
(is (= {:status 200
|
||||
:body {:total 15}}
|
||||
(app valid-request))))
|
||||
(testing "spec-coercion"
|
||||
(testing "succeeds"
|
||||
(let [m (r/match-by-path r "/spec/1/abba")]
|
||||
(is (= {:path {:keyword :abba, :number 1}}
|
||||
(coercion/coerce! m)))))
|
||||
(testing "throws with invalid input"
|
||||
(let [m (r/match-by-path r "/spec/kikka/abba")]
|
||||
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||
|
||||
(testing "invalid request"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Request coercion failed"
|
||||
(app invalid-request))))
|
||||
(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))))))))
|
||||
|
||||
(testing "invalid response"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Response coercion failed"
|
||||
(app invalid-request2))))))
|
||||
|
||||
(testing "with exception handling"
|
||||
(let [app (create [coercion/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
|
||||
(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/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
|
||||
(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/coerce-exceptions-middleware
|
||||
coercion/coerce-request-middleware
|
||||
coercion/coerce-response-middleware])]
|
||||
|
||||
(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))))))))))
|
||||
|
|
|
|||
147
test/cljc/reitit/ring_coercion_test.cljc
Normal file
147
test/cljc/reitit/ring_coercion_test.cljc
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
(ns reitit.ring-coercion-test
|
||||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[schema.core :as s]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.coercion-middleware :as coercion-middleware]
|
||||
[reitit.coercion.spec :as spec]
|
||||
[reitit.coercion.schema :as schema])
|
||||
#?(:clj
|
||||
(:import (clojure.lang ExceptionInfo))))
|
||||
|
||||
(defn handler [{{{: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)}})
|
||||
|
||||
(def valid-request
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get
|
||||
:query-params {"a" "1"}
|
||||
:body-params {:b 2}
|
||||
:form-params {:c 3}
|
||||
:header-params {:d 4}})
|
||||
|
||||
(def invalid-request
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get})
|
||||
|
||||
(def invalid-request2
|
||||
{:uri "/api/plus/5"
|
||||
:request-method :get
|
||||
:query-params {"a" "1"}
|
||||
:body-params {:b 2}
|
||||
:form-params {:c 3}
|
||||
:header-params {:d -40}})
|
||||
|
||||
(deftest spec-coercion-test
|
||||
(let [create (fn [middleware]
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/api"
|
||||
["/plus/:e"
|
||||
{:get {:parameters {:query {:a int?}
|
||||
:body {:b int?}
|
||||
:form {:c int?}
|
||||
: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-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(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-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(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-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(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-middleware/coerce-exceptions-middleware
|
||||
coercion-middleware/coerce-request-middleware
|
||||
coercion-middleware/coerce-response-middleware])]
|
||||
|
||||
(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