diff --git a/README.md b/README.md index d5b5e9d6..c5a84b15 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/doc/README.md b/doc/README.md index bde4bab0..ad3a4549 100644 --- a/doc/README.md +++ b/doc/README.md @@ -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) diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index 8930d756..bf23d439 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.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 diff --git a/doc/advanced/README.md b/doc/advanced/README.md index d5080e77..cad41444 100644 --- a/doc/advanced/README.md +++ b/doc/advanced/README.md @@ -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) diff --git a/doc/advanced/different_routers.md b/doc/advanced/different_routers.md index 87bbd532..6b92d2b4 100644 --- a/doc/advanced/different_routers.md +++ b/doc/advanced/different_routers.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: diff --git a/doc/basics/README.md b/doc/basics/README.md index 1dec603b..65fc6a69 100644 --- a/doc/basics/README.md +++ b/doc/basics/README.md @@ -1,8 +1,8 @@ # Basics -* [Route syntax](route_syntax.md) +* [Route Syntax](route_syntax.md) * [Router](router.md) * [Path-based Routing](path_based_routing.md) * [Name-based Routing](name_based_routing.md) -* [Route data](route_data.md) -* [Route conflicts](route_conflicts.md) +* [Route Data](route_data.md) +* [Route Conflicts](route_conflicts.md) diff --git a/doc/basics/name_based_routing.md b/doc/basics/name_based_routing.md index ec01783d..7b16b9ae 100644 --- a/doc/basics/name_based_routing.md +++ b/doc/basics/name_based_routing.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. diff --git a/doc/basics/path_based_routing.md b/doc/basics/path_based_routing.md index 44fa8281..f9e5eaf6 100644 --- a/doc/basics/path_based_routing.md +++ b/doc/basics/path_based_routing.md @@ -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: diff --git a/doc/basics/route_conflicts.md b/doc/basics/route_conflicts.md index d38e2e51..401a2aa9 100644 --- a/doc/basics/route_conflicts.md +++ b/doc/basics/route_conflicts.md @@ -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. diff --git a/doc/basics/route_data.md b/doc/basics/route_data.md index cdaa29a2..81d918a9 100644 --- a/doc/basics/route_data.md +++ b/doc/basics/route_data.md @@ -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. diff --git a/doc/coercion/README.md b/doc/coercion/README.md new file mode 100644 index 00000000..2cc3d479 --- /dev/null +++ b/doc/coercion/README.md @@ -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) diff --git a/doc/coercion/clojure_spec_coercion.md b/doc/coercion/clojure_spec_coercion.md new file mode 100644 index 00000000..9aece914 --- /dev/null +++ b/doc/coercion/clojure_spec_coercion.md @@ -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... +``` diff --git a/doc/coercion/coercion.md b/doc/coercion/coercion.md new file mode 100644 index 00000000..0ef26c72 --- /dev/null +++ b/doc/coercion/coercion.md @@ -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`). diff --git a/doc/coercion/data_spec_coercion.md b/doc/coercion/data_spec_coercion.md new file mode 100644 index 00000000..8487d89d --- /dev/null +++ b/doc/coercion/data_spec_coercion.md @@ -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... +``` diff --git a/doc/coercion/schema_coercion.md b/doc/coercion/schema_coercion.md new file mode 100644 index 00000000..c42374f8 --- /dev/null +++ b/doc/coercion/schema_coercion.md @@ -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... +``` diff --git a/doc/performance.md b/doc/performance.md index 07a15cd6..2b9d0e89 100644 --- a/doc/performance.md +++ b/doc/performance.md @@ -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. ![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). -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) -**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? diff --git a/doc/ring/README.md b/doc/ring/README.md index 7db18acf..7ba874eb 100644 --- a/doc/ring/README.md +++ b/doc/ring/README.md @@ -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) diff --git a/doc/ring/coercion.md b/doc/ring/coercion.md index 4777b28a..376e1327 100644 --- a/doc/ring/coercion.md +++ b/doc/ring/coercion.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")) ``` diff --git a/doc/ring/dynamic_extensions.md b/doc/ring/dynamic_extensions.md index faaa573a..28df430a 100644 --- a/doc/ring/dynamic_extensions.md +++ b/doc/ring/dynamic_extensions.md @@ -1,4 +1,4 @@ -# Dynamic extensions +# Dynamic Extensions `ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build ad-hoc extensions to the system. diff --git a/examples/just-coercion-with-ring/src/example/dspec.clj b/examples/just-coercion-with-ring/src/example/dspec.clj index 0a52b145..f5c343b2 100644 --- a/examples/just-coercion-with-ring/src/example/dspec.clj +++ b/examples/just-coercion-with-ring/src/example/dspec.clj @@ -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}] diff --git a/examples/just-coercion-with-ring/src/example/schema.clj b/examples/just-coercion-with-ring/src/example/schema.clj index 198ecb87..307ada8d 100644 --- a/examples/just-coercion-with-ring/src/example/schema.clj +++ b/examples/just-coercion-with-ring/src/example/schema.clj @@ -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}] diff --git a/examples/just-coercion-with-ring/src/example/server.clj b/examples/just-coercion-with-ring/src/example/server.clj index 776a5127..e69bc828 100644 --- a/examples/just-coercion-with-ring/src/example/server.clj +++ b/examples/just-coercion-with-ring/src/example/server.clj @@ -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)) diff --git a/examples/just-coercion-with-ring/src/example/spec.clj b/examples/just-coercion-with-ring/src/example/spec.clj index 90f5a40c..7ff94f03 100644 --- a/examples/just-coercion-with-ring/src/example/spec.clj +++ b/examples/just-coercion-with-ring/src/example/spec.clj @@ -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 diff --git a/examples/ring-example/src/example/dspec.clj b/examples/ring-example/src/example/dspec.clj index 7c336214..7b32d737 100644 --- a/examples/ring-example/src/example/dspec.clj +++ b/examples/ring-example/src/example/dspec.clj @@ -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?}} diff --git a/examples/ring-example/src/example/schema.clj b/examples/ring-example/src/example/schema.clj index 55c3179a..bd773f08 100644 --- a/examples/ring-example/src/example/schema.clj +++ b/examples/ring-example/src/example/schema.clj @@ -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}} diff --git a/examples/ring-example/src/example/server.clj b/examples/ring-example/src/example/server.clj index 0181c606..68e6d19d 100644 --- a/examples/ring-example/src/example/server.clj +++ b/examples/ring-example/src/example/server.clj @@ -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] diff --git a/examples/ring-example/src/example/spec.clj b/examples/ring-example/src/example/spec.clj index e6acc570..6962041f 100644 --- a/examples/ring-example/src/example/spec.clj +++ b/examples/ring-example/src/example/spec.clj @@ -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])} diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc new file mode 100644 index 00000000..3870b91e --- /dev/null +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -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)}))) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 34c0db92..a879828a 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -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 diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc new file mode 100644 index 00000000..847500cb --- /dev/null +++ b/modules/reitit-core/src/reitit/interceptor.cljc @@ -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"}}) diff --git a/modules/reitit-core/src/reitit/middleware.cljc b/modules/reitit-core/src/reitit/middleware.cljc index 4d9e6de6..57d2536e 100644 --- a/modules/reitit-core/src/reitit/middleware.cljc +++ b/modules/reitit-core/src/reitit/middleware.cljc @@ -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 diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index 7a3aa86a..5fbad362 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -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))) diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc deleted file mode 100644 index 77d1f1b4..00000000 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ /dev/null @@ -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))))))))})) diff --git a/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc b/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc deleted file mode 100644 index 12838b55..00000000 --- a/modules/reitit-ring/src/reitit/ring/coercion/protocol.cljc +++ /dev/null @@ -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)) diff --git a/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc b/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc new file mode 100644 index 00000000..e2b2cf27 --- /dev/null +++ b/modules/reitit-ring/src/reitit/ring/coercion_middleware.cljc @@ -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))))))))})) diff --git a/modules/reitit-schema/project.clj b/modules/reitit-schema/project.clj index e405cfdb..c6c2de17 100644 --- a/modules/reitit-schema/project.clj +++ b/modules/reitit-schema/project.clj @@ -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]]) diff --git a/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc similarity index 81% rename from modules/reitit-schema/src/reitit/ring/coercion/schema.cljc rename to modules/reitit-schema/src/reitit/coercion/schema.cljc index f514fc47..fa130571 100644 --- a/modules/reitit-schema/src/reitit/ring/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -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? diff --git a/modules/reitit-spec/project.clj b/modules/reitit-spec/project.clj index c000fa46..e8613cdf 100644 --- a/modules/reitit-spec/project.clj +++ b/modules/reitit-spec/project.clj @@ -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]]) diff --git a/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc similarity index 81% rename from modules/reitit-spec/src/reitit/ring/coercion/spec.cljc rename to modules/reitit-spec/src/reitit/coercion/spec.cljc index 90dd3f2d..f316dd85 100644 --- a/modules/reitit-spec/src/reitit/ring/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -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? diff --git a/perf-test/clj/reitit/coercion_perf_test.clj b/perf-test/clj/reitit/coercion_perf_test.clj index e664f1f0..d3641446 100644 --- a/perf-test/clj/reitit/coercion_perf_test.clj +++ b/perf-test/clj/reitit/coercion_perf_test.clj @@ -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" diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj new file mode 100644 index 00000000..676c99a2 --- /dev/null +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -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)) diff --git a/perf-test/clj/reitit/nodejs_perf_test.clj b/perf-test/clj/reitit/nodejs_perf_test.clj new file mode 100644 index 00000000..fd31d31b --- /dev/null +++ b/perf-test/clj/reitit/nodejs_perf_test.clj @@ -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)) diff --git a/project.clj b/project.clj index 81402f3a..3e23ab29 100644 --- a/project.clj +++ b/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" diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index fcd7c8ae..bf33b52f 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -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)))))))))) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc new file mode 100644 index 00000000..eef9372d --- /dev/null +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -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))))))))))