Merge pull request #56 from metosin/coercion

Coercion re-visited
This commit is contained in:
Tommi Reiman 2017-12-10 18:02:39 +02:00 committed by GitHub
commit c471837d9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1226 additions and 497 deletions

View file

@ -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) * 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/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_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/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 * Extendable
* Modular * Modular
* [Fast](https://metosin.github.io/reitit/performance.html) * [Fast](https://metosin.github.io/reitit/performance.html)

View file

@ -7,7 +7,7 @@
* First-class [route data](./basics/route_data.md) * First-class [route data](./basics/route_data.md)
* Bi-directional routing * Bi-directional routing
* [Ring-router](./ring/ring.html) with [data-driven middleware](./ring/data_driven_middleware.html) * [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 * Extendable
* Modular * Modular
* [Fast](performance.md) * [Fast](performance.md)

View file

@ -2,22 +2,27 @@
* [Introduction](README.md) * [Introduction](README.md)
* [Basics](basics/README.md) * [Basics](basics/README.md)
* [Route syntax](basics/route_syntax.md) * [Route Syntax](basics/route_syntax.md)
* [Router](basics/router.md) * [Router](basics/router.md)
* [Path-based Routing](basics/path_based_routing.md) * [Path-based Routing](basics/path_based_routing.md)
* [Name-based Routing](basics/name_based_routing.md) * [Name-based Routing](basics/name_based_routing.md)
* [Route data](basics/route_data.md) * [Route Data](basics/route_data.md)
* [Route conflicts](basics/route_conflicts.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) * [Advanced](advanced/README.md)
* [Configuring routers](advanced/configuring_routers.md) * [Configuring Routers](advanced/configuring_routers.md)
* [Different Routers](advanced/different_routers.md) * [Different Routers](advanced/different_routers.md)
* [Route Validation](advanced/route_validation.md) * [Route Validation](advanced/route_validation.md)
* [Ring](ring/README.md) * [Ring](ring/README.md)
* [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)
* [Pluggable Coercion](ring/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) * [FAQ](faq.md)
* TODO: Swagger & OpenAPI * TODO: Swagger & OpenAPI

View file

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

View file

@ -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. | `: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). | `: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. | `: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: The router name can be asked from the router:

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
View file

@ -0,0 +1,182 @@
# Coercion Explained
Coercion is a process of transforming parameters (and responses) from one format into another. Reitit separates routing and coercion into separate steps.
By default, all wildcard and catch-all parameters are parsed as Strings:
```clj
(require '[reitit.core :as r])
(def router
(r/router
["/:company/users/:user-id" ::user-view]))
```
Here's a match with the String `:params`:
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view},
; :result nil,
; :params {:company "metosin", :user-id "123"},
; :path "/metosin/users/123"}
```
To enable parameter coercion, we need to do few things:
1. Define a `Coercion` for the routes
2. Define types for the parameters
3. Compile coercers for the types
4. Apply the coercion
## Define Coercion
`reitit.coercion/Coercion` is a protocol defining how types are defined, coerced and inventoried.
Reitit has the following coercion modules:
* `reitit.coercion.schema/coercion` for [plumatic schema](https://github.com/plumatic/schema).
* `reitit.coercion.spec/coercion` for both [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs).
Coercion can be attached to route data under `:coercion` key. There can be multiple `Coercion` implementation into a single router, normal [scoping rules](../basics/route_data.html#nested-route-data) apply.
## Defining parameters
Route parameters can be defined via route data `:parameters`. It has keys for different type of parameters: `:query`, `:body`, `:form`, `:header` and `:path`. Syntax for the actual parameters is defined by the `Coercion`.
Here's the example with Schema path-parameters:
```clj
(require '[reitit.coercion.schema])
(require '[schema.core :as s])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.schema/coercion
:parameters {:path {:company s/Str
:user-id s/Int}}}]))
```
Routing again:
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...}
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :result nil,
; :params {:company "metosin", :user-id "123"},
; :path "/metosin/users/123"}
```
Coercion was not applied. Why? All we did was just added more data to the route and the routing functions are just responsible for routing, not coercion.
But now we should have enough data on the match to apply the coercion.
## Compiling coercers
Before the actual coercion, we need to compile the coercers against the route data. This is because compiled coercers yield much better performance. A separate step makes thing explicit and non-magical. Compiling could be done via a Middleware, Interceptor but we can also do it at Router-level, effecting all routes.
There is a helper function for the coercer compiling in `reitit.coercion`:
```clj
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.schema])
(require '[schema.core :as s])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.schema/coercion
:parameters {:path {:company s/Str
:user-id s/Int}}}]
{:compile coercion/compile-request-coercers}))
```
Routing again:
```clj
(r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...}
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :result {:path #object[reitit.coercion$request_coercer$]},
; :params {:company "metosin", :user-id "123"},
; :path "/metosin/users/123"}
```
The compiler added a `:result` key into the match (done just once, at router creation time), which holds the compiled coercers. We are almost done.
## Applying coercion
We can use a helper function to do the actual coercion, based on a `Match`:
```clj
(coercion/coerce!
(r/match-by-path router "/metosin/users/123"))
; {:path {:company "metosin", :user-id 123}}
```
If a coercion fails, a typed (`:reitit.coercion/request-coercion`) ExceptionInfo is thrown, with descriptive data about the actual error:
```clj
(coercion/coerce!
(r/match-by-path router "/metosin/users/ikitommi"))
; => ExceptionInfo Request coercion failed:
; #CoercionError{:schema {:company java.lang.String, :user-id Int, Any Any},
; :errors {:user-id (not (integer? "ikitommi"))}}
; clojure.core/ex-info (core.clj:4739)
```
## Full example
Here's an full example of routing + coercion.
```clj
(require '[reitit.coercion.schema])
(require '[reitit.coercion :as coercion])
(require '[schema.core :as s])
(def router
(r/router
["/:company/users/:user-id" {:name ::user-view
:coercion reitit.coercion.schema/coercion
:parameters {:path {:company s/Str
:user-id s/Int}}}]
{:compile coercion/compile-request-coercers}))
(defn route-and-coerce! [path]
(if-let [match (r/match-by-path router path)]
(assoc match :parameters (coercion/coerce! match))))
(route-and-coerce! "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...}
; :parameters {:path {:company java.lang.String,
; :user-id Int}}},
; :result {:path #object[reitit.coercion$request_coercer$]},
; :params {:company "metosin", :user-id "123"},
; :parameters {:path {:company "metosin", :user-id 123}}
; :path "/metosin/users/123"}
(route-and-coerce! "/metosin/users/ikitommi")
; => ExceptionInfo Request coercion failed...
```
## Ring Coercion
For a full-blown http-coercion, see the [ring coercion](../ring/coercion.md).
## Thanks to
Most of the thing are just polished version of the original implementations. Big thanks to:
* [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol
* [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example) for the syntax of the `:paramters` (and `:responses`).

View 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...
```

View 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...
```

View file

@ -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. 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.
![Opensensors perf test](images/opensensors.png) ![Opensensors perf test](images/opensensors.png)
@ -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). 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.
![Opensensors perf test](images/lupapiste.png) ![Opensensors perf test](images/lupapiste.png)
**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? ### Why measure?

View file

@ -1,7 +1,7 @@
# Ring # Ring
* [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)
* [Pluggable Coercion](coercion.md) * [Pluggable Coercion](coercion.md)
* [Compiling middleware](compiling_middleware.md) * [Compiling Middleware](compiling_middleware.md)

View file

@ -1,11 +1,11 @@
# Pluggable Coercion # 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 ships with the following coercion modules:
* `reitit.ring.coercion.schema/SchemaCoercion` for [plumatic schema](https://github.com/plumatic/schema). * `reitit.coercion.schema/coercion` 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.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 ### 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. * `: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` 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): 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-middleware/coerce-request-middleware`
* `reitit.ring.coercion/coerce-response-middleware` * `reitit.ring.coercion-middleware/coerce-response-middleware`
If the request coercion succeeds, the coerced parameters are injected into request under `:parameters`. 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 ### Example with Schema
```clj ```clj
(require '[reitit.ring :as ring]) (require '[reitit.ring :as ring])
(require '[reitit.ring.coercion :as coercion]) (require '[reitit.ring.coercion-middleware :as coercion-middleware])
(require '[reitit.ring.coercion.schema :as schema]) (require '[reitit.coercion.schema :as schema])
(require '[schema.core :as s]) (require '[schema.core :as s])
(def app (def app
@ -36,13 +36,13 @@ If either request or response coercion fails, an descriptive error is thrown. To
(ring/router (ring/router
["/api" ["/api"
["/ping" {:post {:parameters {:body {:x s/Int, :y s/Int}} ["/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}] :handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/coerce-exceptions-middleware {:data {:middleware [coercion-middleware/coerce-exceptions-middleware
coercion/coerce-request-middleware coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion schema/coercion}}))) :coercion schema/coercion}})))
``` ```
@ -65,7 +65,7 @@ Invalid request:
:uri "/api/ping" :uri "/api/ping"
:body-params {:x 1, :y "2"}}) :body-params {:x 1, :y "2"}})
; {:status 400, ; {:status 400,
; :body {:type :reitit.ring.coercion/request-coercion ; :body {:type :reitit.coercion/request-coercion
; :coercion :schema ; :coercion :schema
; :in [:request :body-params] ; :in [:request :body-params]
; :value {:x 1, :y "2"} ; :value {:x 1, :y "2"}
@ -77,8 +77,8 @@ Invalid request:
```clj ```clj
(require '[reitit.ring :as ring]) (require '[reitit.ring :as ring])
(require '[reitit.ring.coercion :as coercion]) (require '[reitit.ring.coercion-middleware :as coercion-middleware])
(require '[reitit.ring.coercion.spec :as spec]) (require '[reitit.coercion.spec :as spec])
(def app (def app
(ring/ring-handler (ring/ring-handler
@ -89,9 +89,9 @@ Invalid request:
:handler (fn [{{{:keys [x y]} :body} :parameters}] :handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/coerce-exceptions-middleware {:data {:middleware [coercion-middleware/coerce-exceptions-middleware
coercion/coerce-request-middleware coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion spec/coercion}}))) :coercion spec/coercion}})))
``` ```
@ -132,8 +132,8 @@ Currently, `clojure.spec` [doesn't support runtime transformations via conformin
```clj ```clj
(require '[reitit.ring :as ring]) (require '[reitit.ring :as ring])
(require '[reitit.ring.coercion :as coercion]) (require '[reitit.ring.coercion-middleware :as coercion-middleware])
(require '[reitit.ring.coercion.spec :as spec]) (require '[reitit.coercion.spec :as spec])
(require '[clojure.spec.alpha :as s]) (require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st]) (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}] :handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/coerce-exceptions-middleware {:data {:middleware [coercion-middleware/coerce-exceptions-middleware
coercion/coerce-request-middleware coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion spec/coercion}}))) :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. Both Schema and Spec Coercion can be configured via options, see the source code for details.
To plug in new validation engine, see the To plug in new validation engine, see the
`reitit.ring.coercion.protocol/Coercion` protocol. `reitit.coercion/Coercion` protocol.
```clj ```clj
(defprotocol Coercion (defprotocol Coercion
"Pluggable coercion protocol" "Pluggable coercion protocol"
(get-name [this] "Keyword name for the coercion") (-get-name [this] "Keyword name for the coercion")
(get-apidocs [this model data] "???") (-get-apidocs [this model data] "???")
(compile-model [this model name] "Compiles a coercion model") (-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") (-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") (-encode-error [this error] "Converts error in to a serializable format")
(request-coercer [this type model] "Returns a `value format => value` request coercion function") (-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")) (-response-coercer [this model] "Returns a `value format => value` response coercion function"))
``` ```

View file

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

View file

@ -1,6 +1,5 @@
(ns example.dspec (ns example.dspec
(:require [reitit.ring.coercion :as coercion] (:require [reitit.coercion.spec :as spec-coercion]
[reitit.ring.coercion.spec :as spec-coercion]
[example.server :as server])) [example.server :as server]))
(defn handler [{{{:keys [x y]} :query} :parameters}] (defn handler [{{{:keys [x y]} :query} :parameters}]

View file

@ -1,6 +1,5 @@
(ns example.schema (ns example.schema
(:require [reitit.ring.coercion :as coercion] (:require [reitit.coercion.schema :as schema-coercion]
[reitit.ring.coercion.schema :as schema-coercion]
[example.server :as server])) [example.server :as server]))
(defn handler [{{{:keys [x y]} :query} :parameters}] (defn handler [{{{:keys [x y]} :query} :parameters}]

View file

@ -1,7 +1,7 @@
(ns example.server (ns example.server
(:require [ring.adapter.jetty :as jetty] (:require [ring.adapter.jetty :as jetty]
[reitit.middleware :as middleware] [reitit.middleware :as middleware]
[reitit.ring.coercion :as coercion])) [reitit.ring.coercion-middleware :as coercion-middleware]))
(defonce ^:private server (atom nil)) (defonce ^:private server (atom nil))
@ -10,9 +10,9 @@
;; to be set with :extract-request-format and extract-response-format ;; to be set with :extract-request-format and extract-response-format
(defn wrap-coercion [handler resource] (defn wrap-coercion [handler resource]
(middleware/chain (middleware/chain
[coercion/coerce-request-middleware [coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware coercion-middleware/coerce-response-middleware
coercion/coerce-exceptions-middleware] coercion-middleware/coerce-exceptions-middleware]
handler handler
resource)) resource))

View file

@ -1,8 +1,7 @@
(ns example.spec (ns example.spec
(:require [clojure.spec.alpha :as s] (:require [clojure.spec.alpha :as s]
[spec-tools.spec :as spec] [spec-tools.spec :as spec]
[reitit.ring.coercion :as coercion] [reitit.coercion.spec :as spec-coercion]
[reitit.ring.coercion.spec :as spec-coercion]
[example.server :as server])) [example.server :as server]))
;; wrap into Spec Records to enable runtime conforming ;; wrap into Spec Records to enable runtime conforming

View file

@ -1,11 +1,9 @@
(ns example.dspec (ns example.dspec
(:require [reitit.ring.coercion :as coercion] (:require [reitit.coercion.spec :as spec-coercion]))
[reitit.ring.coercion.spec :as spec-coercion]))
(def routes (def routes
["/dspec" ["/dspec" {:coercion spec-coercion/coercion}
["/plus" {:name ::plus ["/plus" {:name ::plus
:coercion spec-coercion/coercion
:responses {200 {:schema {:total int?}}} :responses {200 {:schema {:total int?}}}
:get {:summary "plus with query-params" :get {:summary "plus with query-params"
:parameters {:query {:x int?, :y int?}} :parameters {:query {:x int?, :y int?}}

View file

@ -1,12 +1,10 @@
(ns example.schema (ns example.schema
(:require [schema.core :as s] (:require [schema.core :as s]
[reitit.ring.coercion :as coercion] [reitit.coercion.schema :as schema-coercion]))
[reitit.ring.coercion.schema :as schema-coercion]))
(def routes (def routes
["/schema" ["/schema" {:coercion schema-coercion/coercion}
["/plus" {:name ::plus ["/plus" {:name ::plus
:coercion schema-coercion/coercion
:responses {200 {:schema {:total s/Int}}} :responses {200 {:schema {:total s/Int}}}
:get {:summary "plus with query-params" :get {:summary "plus with query-params"
:parameters {:query {:x s/Int, :y s/Int}} :parameters {:query {:x s/Int, :y s/Int}}

View file

@ -3,7 +3,7 @@
[ring.middleware.params] [ring.middleware.params]
[muuntaja.middleware] [muuntaja.middleware]
[reitit.ring :as ring] [reitit.ring :as ring]
[reitit.ring.coercion :as coercion] [reitit.ring.coercion-middleware :as coercion-middleware]
[example.dspec] [example.dspec]
[example.schema] [example.schema]
[example.spec])) [example.spec]))
@ -18,9 +18,9 @@
example.spec/routes] example.spec/routes]
{:data {:middleware [ring.middleware.params/wrap-params {:data {:middleware [ring.middleware.params/wrap-params
muuntaja.middleware/wrap-format muuntaja.middleware/wrap-format
coercion/coerce-exceptions-middleware coercion-middleware/coerce-exceptions-middleware
coercion/coerce-request-middleware coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware]}}))) coercion-middleware/coerce-response-middleware]}})))
(defn restart [] (defn restart []
(swap! server (fn [x] (swap! server (fn [x]

View file

@ -1,8 +1,7 @@
(ns example.spec (ns example.spec
(:require [clojure.spec.alpha :as s] (:require [clojure.spec.alpha :as s]
[spec-tools.spec :as spec] [spec-tools.spec :as spec]
[reitit.ring.coercion :as coercion] [reitit.coercion.spec :as spec-coercion]))
[reitit.ring.coercion.spec :as spec-coercion]))
;; wrap into Spec Records to enable runtime conforming ;; wrap into Spec Records to enable runtime conforming
(s/def ::x spec/int?) (s/def ::x spec/int?)
@ -10,9 +9,8 @@
(s/def ::total spec/int?) (s/def ::total spec/int?)
(def routes (def routes
["/spec" ["/spec" {:coercion spec-coercion/coercion}
["/plus" {:name ::plus ["/plus" {:name ::plus
:coercion spec-coercion/coercion
:responses {200 {:schema (s/keys :req-un [::total])}} :responses {200 {:schema (s/keys :req-un [::total])}}
:get {:summary "plus with query-params" :get {:summary "plus with query-params"
:parameters {:query (s/keys :req-un [::x ::y])} :parameters {:query (s/keys :req-un [::x ::y])}

View 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)})))

View file

@ -116,7 +116,7 @@
(defn wild-route? [[path]] (defn wild-route? [[path]]
(contains-wilds? path)) (contains-wilds? path))
(defn conflicting-routes? [[p1 :as route1] [p2 :as route2]] (defn conflicting-routes? [[p1] [p2]]
(loop [[s1 & ss1] (segments p1) (loop [[s1 & ss1] (segments p1)
[s2 & ss2] (segments p2)] [s2 & ss2] (segments p2)]
(cond (cond

View 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"}})

View file

@ -9,7 +9,7 @@
(defrecord Middleware [name wrap]) (defrecord Middleware [name wrap])
(defrecord Endpoint [data handler middleware]) (defrecord Endpoint [data handler middleware])
(defn create [{:keys [name wrap compile] :as m}] (defn create [{:keys [wrap compile] :as m}]
(when (and wrap compile) (when (and wrap compile)
(throw (throw
(ex-info (ex-info
@ -51,7 +51,7 @@
(when (>= compiled *max-compile-depth*) (when (>= compiled *max-compile-depth*)
(throw (throw
(ex-info (ex-info
(str "Too deep middleware compilation - " compiled) (str "Too deep Middleware compilation - " compiled)
{:this this, :data data, :opts opts}))) {:this this, :data data, :opts opts})))
(if-let [middeware (into-middleware (compile data opts) data opts)] (if-let [middeware (into-middleware (compile data opts) data opts)]
(map->Middleware (map->Middleware

View file

@ -10,19 +10,19 @@
(extend-protocol Segment (extend-protocol Segment
nil nil
(-insert [this ps data]) (-insert [_ _ _])
(-lookup [this ps params])) (-lookup [_ _ _]))
(defn- -catch-all [children catch-all data params p ps] (defn- -catch-all [children catch-all params p ps]
(if catch-all (if catch-all
(-lookup (-lookup
(impl/fast-get children catch-all) (impl/fast-get children catch-all)
nil nil
(assoc data :params (assoc params catch-all (str/join "/" (cons p ps))))))) (assoc params catch-all (str/join "/" (cons p ps))))))
(defn- segment (defn- segment
([] (segment {} #{} nil nil)) ([] (segment {} #{} nil nil))
([children wilds catch-all data] ([children wilds catch-all match]
(let [children' (impl/fast-map children)] (let [children' (impl/fast-map children)]
^{:type ::segment} ^{:type ::segment}
(reify (reify
@ -34,13 +34,13 @@
wilds (if w (conj wilds w) wilds) wilds (if w (conj wilds w) wilds)
catch-all (or c catch-all) catch-all (or c catch-all)
children (update children (or w c p) #(-insert (or % (segment)) ps d))] 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] (-lookup [_ [p & ps] params]
(if (nil? p) (if (nil? p)
(if data (assoc data :params params)) (if match (assoc match :params params))
(or (-lookup (impl/fast-get children' p) ps params) (or (-lookup (impl/fast-get children' p) ps params)
(some #(-lookup (impl/fast-get children' %) ps (assoc params % p)) wilds) (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] (defn insert [root path data]
(-insert (or root (segment)) (impl/segments path) (map->Match {:data data}))) (-insert (or root (segment)) (impl/segments path) (map->Match {:data data})))
@ -53,11 +53,3 @@
(defn lookup [segment path] (defn lookup [segment path]
(-lookup segment (impl/segments path) {})) (-lookup segment (impl/segments path) {}))
(comment
(-> [["/:abba" 1]
["/:abba/:dabba" 2]
["/kikka/*kakka" 3]]
(create)
(lookup "/kikka/1/2")
(./aprint)))

View file

@ -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))))))))}))

View file

@ -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))

View 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))))))))}))

View file

@ -6,5 +6,5 @@
:plugins [[lein-parent "0.3.2"]] :plugins [[lein-parent "0.3.2"]]
:parent-project {:path "../../project.clj" :parent-project {:path "../../project.clj"
:inherit [:deploy-repositories :managed-dependencies]} :inherit [:deploy-repositories :managed-dependencies]}
:dependencies [[metosin/reitit-ring] :dependencies [[metosin/reitit-core]
[metosin/schema-tools]]) [metosin/schema-tools]])

View file

@ -1,12 +1,12 @@
(ns reitit.ring.coercion.schema (ns reitit.coercion.schema
(:require [schema.core :as s] (:require [clojure.walk :as walk]
[schema.core :as s]
[schema-tools.core :as st] [schema-tools.core :as st]
[schema.coerce :as sc] [schema.coerce :as sc]
[schema.utils :as su] [schema.utils :as su]
[schema-tools.coerce :as stc] [schema-tools.coerce :as stc]
[spec-tools.swagger.core :as swagger] [spec-tools.swagger.core :as swagger]
[clojure.walk :as walk] [reitit.coercion :as coercion]))
[reitit.ring.coercion.protocol :as protocol]))
(def string-coercion-matcher (def string-coercion-matcher
stc/string-coercion-matcher) stc/string-coercion-matcher)
@ -35,24 +35,24 @@
(defrecord SchemaCoercion [name matchers coerce-response?] (defrecord SchemaCoercion [name matchers coerce-response?]
protocol/Coercion coercion/Coercion
(get-name [_] name) (-get-name [_] name)
(get-apidocs [_ _ {:keys [parameters responses] :as info}] (-get-apidocs [_ _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses) (cond-> (dissoc info :parameters :responses)
parameters (assoc ::swagger/parameters parameters) parameters (assoc ::swagger/parameters parameters)
responses (assoc ::swagger/responses responses))) 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 (-> error
(update :schema stringify) (update :schema stringify)
(update :errors stringify))) (update :errors stringify)))
(request-coercer [_ type schema] (-request-coercer [_ type schema]
(let [{:keys [formats default]} (matchers type) (let [{:keys [formats default]} (matchers type)
coercers (->> (for [m (conj (vals formats) default)] coercers (->> (for [m (conj (vals formats) default)]
[m (sc/coercer schema m)]) [m (sc/coercer schema m)])
@ -62,15 +62,15 @@
(let [coercer (coercers matcher) (let [coercer (coercers matcher)
coerced (coercer value)] coerced (coercer value)]
(if-let [error (su/error-val coerced)] (if-let [error (su/error-val coerced)]
(protocol/map->CoercionError (coercion/map->CoercionError
{:schema schema {:schema schema
:errors error}) :errors error})
coerced)) coerced))
value)))) value))))
(response-coercer [this schema] (-response-coercer [this schema]
(if (coerce-response? schema) (if (coerce-response? schema)
(protocol/request-coercer this :response schema)))) (coercion/-request-coercer this :response schema))))
(def default-options (def default-options
{:coerce-response? coerce-response? {:coerce-response? coerce-response?

View file

@ -6,5 +6,5 @@
:plugins [[lein-parent "0.3.2"]] :plugins [[lein-parent "0.3.2"]]
:parent-project {:path "../../project.clj" :parent-project {:path "../../project.clj"
:inherit [:deploy-repositories :managed-dependencies]} :inherit [:deploy-repositories :managed-dependencies]}
:dependencies [[metosin/reitit-ring] :dependencies [[metosin/reitit-core]
[metosin/spec-tools]]) [metosin/spec-tools]])

View file

@ -1,10 +1,10 @@
(ns reitit.ring.coercion.spec (ns reitit.coercion.spec
(:require [clojure.spec.alpha :as s] (:require [clojure.spec.alpha :as s]
[spec-tools.core :as st #?@(:cljs [:refer [Spec]])] [spec-tools.core :as st #?@(:cljs [:refer [Spec]])]
[spec-tools.data-spec :as ds] [spec-tools.data-spec :as ds]
[spec-tools.conform :as conform] [spec-tools.conform :as conform]
[spec-tools.swagger.core :as swagger] [spec-tools.swagger.core :as swagger]
[reitit.ring.coercion.protocol :as protocol]) [reitit.coercion :as coercion])
#?(:clj #?(:clj
(:import (spec_tools.core Spec)))) (:import (spec_tools.core Spec))))
@ -54,51 +54,51 @@
(defrecord SpecCoercion [name conforming coerce-response?] (defrecord SpecCoercion [name conforming coerce-response?]
protocol/Coercion coercion/Coercion
(get-name [_] name) (-get-name [_] name)
(get-apidocs [this _ {:keys [parameters responses] :as info}] (-get-apidocs [this _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses) (cond-> (dissoc info :parameters :responses)
parameters (assoc parameters (assoc
::swagger/parameters ::swagger/parameters
(into (into
(empty parameters) (empty parameters)
(for [[k v] parameters] (for [[k v] parameters]
[k (protocol/compile-model this v nil)]))) [k (coercion/-compile-model this v nil)])))
responses (assoc responses (assoc
::swagger/responses ::swagger/responses
(into (into
(empty responses) (empty responses)
(for [[k response] 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")))) (into-spec model (or name (gensym "spec"))))
(open-model [_ spec] spec) (-open-model [_ spec] spec)
(encode-error [_ error] (-encode-error [_ error]
(-> error (-> error
(update :spec (comp str s/form)) (update :spec (comp str s/form))
(update :problems (partial mapv #(update % :pred stringify-pred))))) (update :problems (partial mapv #(update % :pred stringify-pred)))))
(request-coercer [this type spec] (-request-coercer [this type spec]
(let [spec (protocol/compile-model this spec nil) (let [spec (coercion/-compile-model this spec nil)
{:keys [formats default]} (conforming type)] {:keys [formats default]} (conforming type)]
(fn [value format] (fn [value format]
(if-let [conforming (or (get formats format) default)] (if-let [conforming (or (get formats format) default)]
(let [conformed (st/conform spec value conforming)] (let [conformed (st/conform spec value conforming)]
(if (s/invalid? conformed) (if (s/invalid? conformed)
(let [problems (st/explain-data spec value conforming)] (let [problems (st/explain-data spec value conforming)]
(protocol/map->CoercionError (coercion/map->CoercionError
{:spec spec {:spec spec
:problems (::s/problems problems)})) :problems (::s/problems problems)}))
(s/unform spec conformed))) (s/unform spec conformed)))
value)))) value))))
(response-coercer [this spec] (-response-coercer [this spec]
(if (coerce-response? spec) (if (coerce-response? spec)
(protocol/request-coercer this :response spec)))) (coercion/-request-coercer this :response spec))))
(def default-options (def default-options
{:coerce-response? coerce-response? {:coerce-response? coerce-response?

View file

@ -4,18 +4,16 @@
[reitit.perf-utils :refer :all] [reitit.perf-utils :refer :all]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[spec-tools.core :as st] [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] [spec-tools.data-spec :as ds]
[muuntaja.middleware :as mm] [muuntaja.middleware :as mm]
[muuntaja.core :as m] [muuntaja.core :as m]
[muuntaja.format.jsonista :as jsonista-format] [muuntaja.format.jsonista :as jsonista-format]
[jsonista.core :as j] [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]) [reitit.core :as r])
(:import (java.io ByteArrayInputStream))) (:import (java.io ByteArrayInputStream)))
@ -41,14 +39,14 @@
(s/def ::k (s/keys :req-un [::x ::y])) (s/def ::k (s/keys :req-un [::x ::y]))
(let [spec (spec/into-spec {:x int?, :y int?} ::jeah) (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"} params {:x "1", :y "2"}
request {:body-params {:x "1", :y "2"}}] request {:body-params {:x "1", :y "2"}}]
;; 4600ns ;; 4600ns
(bench! (bench!
"coerce-parameters" "coerce-parameters"
(#'coercion/coerce-parameters coercers request)) (#'coercion-middleware/coerce-parameters coercers request))
;; 2700ns ;; 2700ns
(bench! (bench!
@ -85,14 +83,14 @@
params)))))) params))))))
(defrecord NoOpCoercion [] (defrecord NoOpCoercion []
protocol/Coercion coercion/Coercion
(get-name [_] :no-op) (-get-name [_] :no-op)
(get-apidocs [_ _ {:keys [parameters responses] :as info}]) (-get-apidocs [_ _ {:keys [parameters responses] :as info}])
(compile-model [_ model _] model) (-compile-model [_ model _] model)
(open-model [_ spec] spec) (-open-model [_ spec] spec)
(encode-error [_ error] error) (-encode-error [_ error] error)
(request-coercer [_ type spec] (fn [value format] value)) (-request-coercer [_ type spec] (fn [value format] value))
(response-coercer [this spec] (protocol/request-coercer this :response spec))) (-response-coercer [this spec] (protocol/request-coercer this :response spec)))
(comment (comment
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]] (doseq [coercion [nil (->NoOpCoercion) spec/coercion]]
@ -107,24 +105,24 @@
app (ring/ring-handler app (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/coerce-request-middleware] {:data {:middleware [coercion-middleware/coerce-request-middleware]
:coercion coercion}})) :coercion coercion}}))
app2 (ring/ring-handler app2 (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/coerce-request-middleware] {:data {:middleware [coercion-middleware/coerce-request-middleware]
:coercion coercion}})) :coercion coercion}}))
app3 (ring/ring-handler app3 (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/coerce-request-middleware {:data {:middleware [coercion-middleware/coerce-request-middleware
coercion/wrap-coerce-response] coercion-middleware/wrap-coerce-response]
:coercion coercion}})) :coercion coercion}}))
app4 (ring/ring-handler app4 (ring/ring-handler
(ring/router (ring/router
routes routes
{:data {:middleware [coercion/coerce-request-middleware {:data {:middleware [coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion coercion}})) :coercion coercion}}))
req {:request-method :get req {:request-method :get
:uri "/api/ping" :uri "/api/ping"
@ -161,8 +159,8 @@
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}] :get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200 {:status 200
:body {:total (+ x y)}})}}]] :body {:total (+ x y)}})}}]]
{:data {:middleware [coercion/coerce-request-middleware {:data {:middleware [coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion spec/coercion}}))) :coercion spec/coercion}})))
(app (app
@ -205,8 +203,8 @@
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [[mm/wrap-format m] {:data {:middleware [[mm/wrap-format m]
coercion/coerce-request-middleware coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion schema/coercion}})) :coercion schema/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"
@ -228,8 +226,8 @@
:handler (fn [request] :handler (fn [request]
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [coercion/coerce-request-middleware {:data {:middleware [coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion schema/coercion}})) :coercion schema/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"
@ -251,8 +249,8 @@
:handler (fn [request] :handler (fn [request]
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [coercion/coerce-request-middleware {:data {:middleware [coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion spec/coercion}})) :coercion spec/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"
@ -279,8 +277,8 @@
:handler (fn [request] :handler (fn [request]
(let [body (-> request :parameters :body)] (let [body (-> request :parameters :body)]
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}] {:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
{:data {:middleware [coercion/coerce-request-middleware {:data {:middleware [coercion-middleware/coerce-request-middleware
coercion/coerce-response-middleware] coercion-middleware/coerce-response-middleware]
:coercion spec/coercion}})) :coercion spec/coercion}}))
request {:request-method :post request {:request-method :post
:uri "/plus" :uri "/plus"

View 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))

View 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))

View file

@ -22,7 +22,7 @@
:plugins [[jonase/eastwood "0.2.5"] :plugins [[jonase/eastwood "0.2.5"]
[lein-doo "0.1.8"] [lein-doo "0.1.8"]
[lein-cljsbuild "1.1.7"] [lein-cljsbuild "1.1.7"]
[lein-cloverage "1.0.9"] [lein-cloverage "1.0.10"]
[lein-codox "0.10.3"] [lein-codox "0.10.3"]
[metosin/boot-alt-test "0.4.0-20171019.180106-3"]] [metosin/boot-alt-test "0.4.0-20171019.180106-3"]]
@ -35,19 +35,19 @@
"modules/reitit-spec/src" "modules/reitit-spec/src"
"modules/reitit-schema/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"] [org.clojure/clojurescript "1.9.946"]
;; modules dependencies ;; modules dependencies
[metosin/reitit] [metosin/reitit]
[metosin/schema-tools "0.10.0-SNAPSHOT"] [metosin/schema-tools "0.10.0-SNAPSHOT"]
[expound "0.3.2"] [expound "0.3.4"]
[orchestra "2017.08.13"] [orchestra "2017.11.12-1"]
[ring "1.6.3"] [ring "1.6.3"]
[metosin/muuntaja "0.4.1"] [metosin/muuntaja "0.4.1"]
[metosin/jsonista "0.1.0-SNAPSHOT"] [metosin/jsonista "0.1.0"]
[criterium "0.4.4"] [criterium "0.4.4"]
[org.clojure/test.check "0.9.0"] [org.clojure/test.check "0.9.0"]
@ -58,8 +58,9 @@
"-Dclojure.compiler.direct-linking=true"] "-Dclojure.compiler.direct-linking=true"]
:test-paths ["perf-test/clj"] :test-paths ["perf-test/clj"]
:dependencies [[compojure "1.6.0"] :dependencies [[compojure "1.6.0"]
[org.immutant/immutant "2.1.9"]
[io.pedestal/pedestal.route "0.5.3"] [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"] [ataraxy "0.4.0"]
[bidi "2.1.2"]]} [bidi "2.1.2"]]}
:analyze {:jvm-opts ^:replace ["-server" :analyze {:jvm-opts ^:replace ["-server"

View file

@ -1,147 +1,51 @@
(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] [schema.core :as s]
[reitit.ring :as ring] [reitit.core :as r]
[reitit.ring.coercion :as coercion] [reitit.coercion :as coercion]
[reitit.ring.coercion.spec :as spec] [reitit.coercion.spec :as spec]
[reitit.ring.coercion.schema :as schema]) [reitit.coercion.schema :as schema])
#?(:clj #?(:clj
(:import (clojure.lang ExceptionInfo)))) (: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 (deftest spec-coercion-test
(let [create (fn [middleware] (let [r (r/router
(ring/ring-handler [["/schema" {:coercion schema/coercion}
(ring/router ["/:number/:keyword" {:name ::user
["/api" :parameters {:path {:number s/Int
["/plus/:e" :keyword s/Keyword}}}]]
{:get {:parameters {:query {:a int?} ["/spec" {:coercion spec/coercion}
:body {:b int?} ["/:number/:keyword" {:name ::user
:form {:c int?} :parameters {:path {:number int?
:header {:d int?} :keyword keyword?}}}]]
:path {:e int?}} ["/none"
:responses {200 {:schema {:total pos-int?}}} ["/:number/:keyword" {:name ::user
:handler handler}}]] :parameters {:path {:number int?
{:data {:middleware middleware :keyword keyword?}}}]]]
:coercion spec/coercion}})))] {:compile coercion/compile-request-coercers})]
(testing "withut exception handling" (testing "schema-coercion"
(let [app (create [coercion/coerce-request-middleware (testing "succeeds"
coercion/coerce-response-middleware])] (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" (testing "spec-coercion"
(is (= {:status 200 (testing "succeeds"
:body {:total 15}} (let [m (r/match-by-path r "/spec/1/abba")]
(app valid-request)))) (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" (testing "no coercion defined"
(is (thrown-with-msg? (testing "doesn't coerce"
ExceptionInfo (let [m (r/match-by-path r "/none/1/abba")]
#"Request coercion failed" (is (= nil (coercion/coerce! m))))
(app invalid-request)))) (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))))))))))

View 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))))))))))