mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 16:31:11 +00:00
commit
1e2d1be09b
32 changed files with 1096 additions and 102 deletions
|
|
@ -18,9 +18,9 @@ jobs:
|
||||||
command: ./scripts/test.sh clj
|
command: ./scripts/test.sh clj
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: ~/test/target/junit.xml
|
path: ~/test/target/junit.xml
|
||||||
- run:
|
# - run:
|
||||||
name: Run coverage
|
# name: Run coverage
|
||||||
command: ./scripts/submit-to-coveralls.sh clj
|
# command: ./scripts/submit-to-coveralls.sh clj
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: 'v1-test-{{ checksum "project.clj" }}'
|
key: 'v1-test-{{ checksum "project.clj" }}'
|
||||||
paths:
|
paths:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ install:
|
||||||
- npm install
|
- npm install
|
||||||
script:
|
script:
|
||||||
- ./scripts/test.sh $TEST
|
- ./scripts/test.sh $TEST
|
||||||
- ./scripts/submit-to-coveralls.sh $TEST
|
# - ./scripts/submit-to-coveralls.sh $TEST
|
||||||
env:
|
env:
|
||||||
matrix:
|
matrix:
|
||||||
- TEST=clj
|
- TEST=clj
|
||||||
|
|
|
||||||
71
README.md
71
README.md
|
|
@ -7,11 +7,12 @@ 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
|
||||||
* [Pluggable coercion](https://metosin.github.io/reitit/coercion/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))
|
||||||
* [Ring-router](https://metosin.github.io/reitit/ring/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html)
|
|
||||||
* Extendable
|
* Extendable
|
||||||
* Modular
|
* Modular
|
||||||
* [Fast](https://metosin.github.io/reitit/performance.html)
|
* [Fast](https://metosin.github.io/reitit/performance.html)
|
||||||
|
|
||||||
|
There are also [Ring-router](https://metosin.github.io/reitit/ring/ring.html) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html) as a separate module.
|
||||||
|
|
||||||
See the [full documentation](https://metosin.github.io/reitit/) for details.
|
See the [full documentation](https://metosin.github.io/reitit/) for details.
|
||||||
|
|
||||||
## Latest version
|
## Latest version
|
||||||
|
|
@ -56,6 +57,74 @@ Optionally, the parts can be required separately:
|
||||||
; :path "/api/orders/2"}
|
; :path "/api/orders/2"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Ring example
|
||||||
|
|
||||||
|
A Ring routing app with input & output coercion using [data-specs](https://github.com/metosin/spec-tools/blob/master/README.md#data-specs).
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.ring :as ring])
|
||||||
|
(require '[reitit.coercion.spec])
|
||||||
|
(require '[reitit.ring.coercion-middleware :as mw])
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api"
|
||||||
|
["/math" {:name ::math
|
||||||
|
:get {:coercion reitit.coercion.spec/coercion
|
||||||
|
:parameters {:query {:x int?, :y int?}}
|
||||||
|
:responses {200 {:schema {:total pos-int?}}}
|
||||||
|
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:total (+ x y)}})}}]]
|
||||||
|
{:data {:middleware [mw/coerce-exceptions-middleware
|
||||||
|
mw/coerce-request-middleware
|
||||||
|
mw/coerce-response-middleware]}})))
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid request:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(app {:request-method :get
|
||||||
|
:uri "/api/math"
|
||||||
|
:query-params {:x "1", :y "2"}})
|
||||||
|
; {:status 200
|
||||||
|
; :body {:total 3}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Invalid request:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(app {:request-method :get
|
||||||
|
:uri "/api/math"
|
||||||
|
:query-params {:x "1", :y "a"}})
|
||||||
|
;{:status 400,
|
||||||
|
; :body {:type :reitit.coercion/request-coercion,
|
||||||
|
; :coercion :spec,
|
||||||
|
; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec20745/x :$spec20745/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})",
|
||||||
|
; :problems [{:path [:y],
|
||||||
|
; :pred "clojure.core/int?",
|
||||||
|
; :val "a",
|
||||||
|
; :via [:$spec20745/y],
|
||||||
|
; :in [:y]}],
|
||||||
|
; :value {:x "1", :y "a"},
|
||||||
|
; :in [:request :query-params]}}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse routing:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.core :as r])
|
||||||
|
|
||||||
|
(-> app
|
||||||
|
(ring/get-router)
|
||||||
|
(r/match-by-name ::math)
|
||||||
|
:path)
|
||||||
|
;; "/api/math"
|
||||||
|
```
|
||||||
|
|
||||||
## More info
|
## More info
|
||||||
|
|
||||||
[Check out the full documentation!](https://metosin.github.io/reitit/)
|
[Check out the full documentation!](https://metosin.github.io/reitit/)
|
||||||
|
|
|
||||||
18
dev-resources/logback.xml
Normal file
18
dev-resources/logback.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<configuration scan="true" scanPeriod="10 seconds">
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>INFO</level>
|
||||||
|
</filter>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<logger name="user" level="ALL" />
|
||||||
|
|
||||||
|
</configuration>
|
||||||
|
|
@ -6,12 +6,13 @@
|
||||||
* [Route conflict resolution](./basics/route_conflicts.md)
|
* [Route conflict resolution](./basics/route_conflicts.md)
|
||||||
* First-class [route data](./basics/route_data.md)
|
* First-class [route data](./basics/route_data.md)
|
||||||
* Bi-directional routing
|
* Bi-directional routing
|
||||||
* [Ring-router](./ring/ring.html) with [data-driven middleware](./ring/data_driven_middleware.html)
|
* [Pluggable coercion](./coercion/coercion.md) ([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)
|
||||||
|
|
||||||
|
There are also [Ring-router](./ring/ring.md) with [data-driven middleware](./ring/data_driven_middleware.md) as a separate module.
|
||||||
|
|
||||||
To use Reitit, add the following dependecy to your project:
|
To use Reitit, add the following dependecy to your project:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
|
|
@ -23,7 +24,7 @@ Optionally, the parts can be required separately:
|
||||||
```clj
|
```clj
|
||||||
[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router
|
[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router
|
||||||
[metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router
|
[metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router
|
||||||
[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec-coercion
|
[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec coercion
|
||||||
[metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion
|
[metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
* [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 Data Validation](basics/route_data_validation.md)
|
||||||
* [Route Conflicts](basics/route_conflicts.md)
|
* [Route Conflicts](basics/route_conflicts.md)
|
||||||
* [Coercion](coercion/README.md)
|
* [Coercion](coercion/README.md)
|
||||||
* [Coercion Explained](coercion/coercion.md)
|
* [Coercion Explained](coercion/coercion.md)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
# Configuring Routers
|
# Configuring Routers
|
||||||
|
|
||||||
Routers can be configured via options. Options allow things like [`clojure.spec`](https://clojure.org/about/spec) validation for route data and fast, compiled handlers. The following options are available for the `reitit.core/router`:
|
Routers can be configured via options. The following options are available for the `reitit.core/router`:
|
||||||
|
|
||||||
| key | description |
|
| key | description |
|
||||||
| -------------|-------------|
|
| -------------|-------------|
|
||||||
| `:path` | Base-path for routes
|
| `:path` | Base-path for routes
|
||||||
| `:routes` | Initial resolved routes (default `[]`)
|
| `:routes` | Initial resolved routes (default `[]`)
|
||||||
| `:data` | Initial route data (default `{}`)
|
| `:data` | Initial route data (default `{}`)
|
||||||
|
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
|
||||||
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
|
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
|
||||||
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
|
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
|
||||||
| `:compile` | Function of `route opts => result` to compile a route handler
|
| `:compile` | Function of `route opts => result` to compile a route handler
|
||||||
| `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`)
|
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
|
||||||
|
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`)
|
||||||
| `:router` | Function of `routes opts => router` to override the actual router implementation
|
| `:router` | Function of `routes opts => router` to override the actual router implementation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec) definitions for raw-routes, routes, router and router options.
|
Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec) definitions for raw-routes, routes, router and router options.
|
||||||
|
|
||||||
**NOTE:** Use of specs requires to use Clojure 1.9.0 or higher.
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
|
|
@ -26,12 +24,12 @@ Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec)
|
||||||
|
|
||||||
## At development time
|
## At development time
|
||||||
|
|
||||||
`reitit.core/router` can be instrumented and use something like [expound](https://github.com/bhb/expound) to pretty-print the spec problems.
|
`reitit.core/router` can be instrumented and use a tool like [expound](https://github.com/bhb/expound) to pretty-print the spec problems.
|
||||||
|
|
||||||
First add a `:dev` dependency to:
|
First add a `:dev` dependency to:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[expound "0.3.0"] ; or higher
|
[expound "0.4.0"] ; or higher
|
||||||
```
|
```
|
||||||
|
|
||||||
Some bootstrapping:
|
Some bootstrapping:
|
||||||
|
|
@ -162,7 +160,3 @@ And we are ready to go:
|
||||||
; -------------------------
|
; -------------------------
|
||||||
; Detected 2 errors
|
; Detected 2 errors
|
||||||
```
|
```
|
||||||
|
|
||||||
# Validating route data
|
|
||||||
|
|
||||||
*TODO*
|
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,5 @@
|
||||||
* [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 Data Validation](route_data_validation.md)
|
||||||
* [Route Conflicts](route_conflicts.md)
|
* [Route Conflicts](route_conflicts.md)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# 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.
|
Most routing libraries allow conflicting paths within a router. On lookup, the first match is used making rest of the matching routes effecively unreachable. This is not good, especially if route tree is merged from multiple sources.
|
||||||
|
|
||||||
Reitit resolves this by running explicit conflicit resolution when a `router` is called. Conflicting routes are passed into a `:conflicts` callback. Default implementation throws `ex-info` with a descriptive message.
|
Reitit resolves this by running explicit conflicit resolution when a Router is created. Conflicting routes are passed into a `:conflicts` callback. Default implementation throws `ex-info` with a descriptive message.
|
||||||
|
|
||||||
Examples router with conflicting routes:
|
Examples router with conflicting routes:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 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. Together with `clojure.spec` -validation this enables co-existence of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components.
|
||||||
|
|
||||||
Routes can have a non-sequential route argument that is expanded into route data map when a router is created.
|
Routes can have a non-sequential route argument that is expanded into route data map when a router is created.
|
||||||
|
|
||||||
|
|
@ -75,8 +75,6 @@ Resolved route tree:
|
||||||
By default, `reitit/Expand` protocol is used to expand the route arguments. It expands keywords into `:name` and functions into `:handler` key in the route data map. It's easy to add custom expanders and one can chenge the whole expand implementation via [router options](../advanced/configuring_routers.md).
|
By default, `reitit/Expand` protocol is used to expand the route arguments. It expands keywords into `:name` and functions into `:handler` key in the route data map. It's easy to add custom expanders and one can chenge the whole expand implementation via [router options](../advanced/configuring_routers.md).
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[reitit.core :as r])
|
|
||||||
|
|
||||||
(def router
|
(def router
|
||||||
(r/router
|
(r/router
|
||||||
[["/ping" ::ping]
|
[["/ping" ::ping]
|
||||||
|
|
|
||||||
129
doc/basics/route_data_validation.md
Normal file
129
doc/basics/route_data_validation.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Route Data Validation
|
||||||
|
|
||||||
|
Route data can be anything, so it's easy to do mistakes. Accidentally using a `:role` key instead of `:roles` might render the whole routing app without any authorization in place.
|
||||||
|
|
||||||
|
To fail fast, we could use the custom `:coerce` and `:compile` hooks to apply data validation and throw exceptions on first sighted problem.
|
||||||
|
|
||||||
|
But there is a better way. Router also has a `:validation` hook to validate the whole route tree after it's successfuly compiled. It expects a 2-arity function `routes opts => ()` that can side-effect in case of validation errors.
|
||||||
|
|
||||||
|
## clojure.spec
|
||||||
|
|
||||||
|
Namespace `reitit.spec` contains specs for main parts of `reitit.core` and a helper function `validate-spec!` that runs spec validation for all route data and throws an exception if any errors are found.
|
||||||
|
|
||||||
|
A Router with invalid route data:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.core :as r])
|
||||||
|
|
||||||
|
(r/router
|
||||||
|
["/api" {:handler "identity"}])
|
||||||
|
; #object[reitit.core$...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Fails fast with `clojure.spec` validation turned on:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.spec :as rs])
|
||||||
|
|
||||||
|
(r/router
|
||||||
|
["/api" {:handler "identity"}]
|
||||||
|
{:validate rs/validate-spec!})
|
||||||
|
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||||
|
;
|
||||||
|
; -- On route -----------------------
|
||||||
|
;
|
||||||
|
; "/api"
|
||||||
|
;
|
||||||
|
; In: [:handler] val: "identity" fails spec: :reitit.spec/handler at: [:handler] predicate: fn?
|
||||||
|
;
|
||||||
|
; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler "identity"}, :spec :reitit.spec/default-data, :problems #:clojure.spec.alpha{:problems ({:path [:handler], :pred clojure.core/fn?, :val "identity", :via [:reitit.spec/default-data :reitit.spec/handler], :in [:handler]}), :spec :reitit.spec/default-data, :value {:handler "identity"}}})}, compiling: ...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customizing spec validation
|
||||||
|
|
||||||
|
`rs/validate-spec!` reads the following router options:
|
||||||
|
|
||||||
|
| key | description |
|
||||||
|
| ---------------|-------------|
|
||||||
|
| `:spec` | the spec to verify the route data (default `::rs/default-data`)
|
||||||
|
| `::rs/explain` | custom explain function (default `clojure.spec.alpha/explain-str`)
|
||||||
|
|
||||||
|
**NOTE**: `clojure.spec` implicitly validates all values with fully-qualified keys if specs exist with the same name.
|
||||||
|
|
||||||
|
Below is an example of using [expound](https://github.com/bhb/expound) to pretty-print route data problems.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[clojure.spec.alpha :as s])
|
||||||
|
(require '[expound.alpha :as e])
|
||||||
|
|
||||||
|
(s/def ::role #{:admin :manager})
|
||||||
|
(s/def ::roles (s/coll-of ::role :into #{}))
|
||||||
|
|
||||||
|
(r/router
|
||||||
|
["/api" {:handler identity
|
||||||
|
::roles #{:adminz}}]
|
||||||
|
{::rs/explain e/expound-str
|
||||||
|
:validate rs/validate-spec!})
|
||||||
|
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||||
|
;
|
||||||
|
; -- On route -----------------------
|
||||||
|
;
|
||||||
|
; "/api"
|
||||||
|
;
|
||||||
|
; -- Spec failed --------------------
|
||||||
|
;
|
||||||
|
; {:handler ..., :user/roles #{:adminz}}
|
||||||
|
; ^^^^^^^
|
||||||
|
;
|
||||||
|
; should be one of: `:admin`,`:manager`
|
||||||
|
;
|
||||||
|
; -- Relevant specs -------
|
||||||
|
;
|
||||||
|
; :user/role:
|
||||||
|
; #{:admin :manager}
|
||||||
|
; :user/roles:
|
||||||
|
; (clojure.spec.alpha/coll-of :user/role :into #{})
|
||||||
|
; :reitit.spec/default-data:
|
||||||
|
; (clojure.spec.alpha/keys
|
||||||
|
; :opt-un
|
||||||
|
; [:reitit.spec/name :reitit.spec/handler])
|
||||||
|
;
|
||||||
|
; -------------------------
|
||||||
|
; Detected 1 error
|
||||||
|
;
|
||||||
|
; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"], :user/roles #{:adminz}}, :spec :reitit.spec/default-data, :problems #:clojure.spec.alpha{:problems ({:path [:user/roles], :pred #{:admin :manager}, :val :adminz, :via [:reitit.spec/default-data :user/roles :user/role], :in [:user/roles 0]}), :spec :reitit.spec/default-data, :value {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"], :user/roles #{:adminz}}}})}, compiling: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Explicitly requiring a `::roles` key in a route data:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(r/router
|
||||||
|
["/api" {:handler identity}]
|
||||||
|
{:spec (s/merge (s/keys :req [::roles]) ::rs/default-data)
|
||||||
|
::rs/explain e/expound-str
|
||||||
|
:validate rs/validate-spec!})
|
||||||
|
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
|
||||||
|
;
|
||||||
|
; -- On route -----------------------
|
||||||
|
;
|
||||||
|
; "/api"
|
||||||
|
;
|
||||||
|
; -- Spec failed --------------------
|
||||||
|
;
|
||||||
|
; {:handler
|
||||||
|
; #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}
|
||||||
|
;
|
||||||
|
; should contain key: `:user/roles`
|
||||||
|
;
|
||||||
|
; | key | spec |
|
||||||
|
; |-------------+----------------------------------------|
|
||||||
|
; | :user/roles | (coll-of #{:admin :manager} :into #{}) |
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
; -------------------------
|
||||||
|
; Detected 1 error
|
||||||
|
;
|
||||||
|
; {:problems (#reitit.spec.Problem{:path "/api", :scope nil, :data {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}, :spec #object[clojure.spec.alpha$merge_spec_impl$reify__2124 0x7461744b "clojure.spec.alpha$merge_spec_impl$reify__2124@7461744b"], :problems #:clojure.spec.alpha{:problems ({:path [], :pred (clojure.core/fn [%] (clojure.core/contains? % :user/roles)), :val {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}, :via [], :in []}), :spec #object[clojure.spec.alpha$merge_spec_impl$reify__2124 0x7461744b "clojure.spec.alpha$merge_spec_impl$reify__2124@7461744b"], :value {:handler #object[clojure.core$identity 0x15b59b0e "clojure.core$identity@15b59b0e"]}}})}, compiling:(/Users/tommi/projects/metosin/reitit/test/cljc/reitit/spec_test.cljc:151:1)
|
||||||
|
```
|
||||||
|
|
@ -138,7 +138,7 @@ We get the coerced paremeters back. If a coercion fails, a typed (`:reitit.coerc
|
||||||
|
|
||||||
## Full example
|
## Full example
|
||||||
|
|
||||||
Here's an full example for doing both routing and coercion with Reitit:
|
Here's an full example for doing routing and coercion with Reitit and Schema:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[reitit.coercion.schema])
|
(require '[reitit.coercion.schema])
|
||||||
|
|
@ -179,9 +179,6 @@ For a full-blown http-coercion, see the [ring coercion](../ring/coercion.md).
|
||||||
|
|
||||||
## Thanks to
|
## Thanks to
|
||||||
|
|
||||||
Most of the thing are just polished version of the original implementations. Thanks to:
|
|
||||||
|
|
||||||
* [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol
|
* [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 `:parameters` and `:responses` syntax.
|
|
||||||
* [schema](https://github.com/plumatic/schema) and [schema-tools](https://github.com/metosin/schema-tools) for Schema Coercion
|
* [schema](https://github.com/plumatic/schema) and [schema-tools](https://github.com/metosin/schema-tools) for Schema Coercion
|
||||||
* [spec-tools](https://github.com/metosin/spec-tools) for Spec Coercion
|
* [spec-tools](https://github.com/metosin/spec-tools) for Spec Coercion
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,17 @@ Defining a coercion for a route data doesn't do anything, as it's just data. We
|
||||||
* `coerce-response-middleware` for the response coercion
|
* `coerce-response-middleware` for the response coercion
|
||||||
* `coerce-exceptions-middleware` to turn coercion exceptions into pretty responses
|
* `coerce-exceptions-middleware` to turn coercion exceptions into pretty responses
|
||||||
|
|
||||||
### Example with Schema
|
### Full example
|
||||||
|
|
||||||
|
Here's an full example for applying coercion with Reitit, Ring and Schema:
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(require '[reitit.ring.coercion-middleware :as mw])
|
(require '[reitit.ring.coercion-middleware :as mw])
|
||||||
|
(require '[reitit.coercion.schema])
|
||||||
|
(require '[reitit.ring :as ring])
|
||||||
|
(require '[schema.core :as s])
|
||||||
|
|
||||||
|
(def PositiveInt (s/constrained s/Int pos? 'PositiveInt))
|
||||||
|
|
||||||
(def app
|
(def app
|
||||||
(ring/ring-handler
|
(ring/ring-handler
|
||||||
|
|
@ -126,7 +133,7 @@ Invalid response:
|
||||||
|
|
||||||
### Optimizations
|
### Optimizations
|
||||||
|
|
||||||
The coercion middleware are [compiled againts a route](compiling_middleware,md). This enables them to compile and cache the actual coercers for the defined models ahead of time. They also unmount if a route doesn't have `:coercion` and `:parameters` or `:responses` defined.
|
The coercion middleware are [compiled againts a route](compiling_middleware,md). In the compile step the actual coercer implementations are compiled for the defined models. Also, the mw doesn't mount itself if a route doesn't have `:coercion` and `:parameters` or `:responses` defined.
|
||||||
|
|
||||||
We can query the compiled middleware chain for the routes:
|
We can query the compiled middleware chain for the routes:
|
||||||
|
|
||||||
|
|
@ -138,7 +145,7 @@ We can query the compiled middleware chain for the routes:
|
||||||
:result :post :middleware
|
:result :post :middleware
|
||||||
(->> (mapv :name)))
|
(->> (mapv :name)))
|
||||||
; [::mw/coerce-exceptions
|
; [::mw/coerce-exceptions
|
||||||
; ::mw/coerce-parameters
|
; ::mw/coerce-request
|
||||||
; ::mw/coerce-response]
|
; ::mw/coerce-response]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -160,8 +167,6 @@ Has no mounted middleware:
|
||||||
```
|
```
|
||||||
## Thanks to
|
## Thanks to
|
||||||
|
|
||||||
Most of the thing are just polished version of the original implementations. Thanks to:
|
|
||||||
|
|
||||||
* [compojure-api](https://clojars.org/metosin/compojure-api) for the initial `Coercion` protocol
|
* [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 `:parameters` and `:responses` syntax.
|
* [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example) for the `:parameters` and `:responses` syntax.
|
||||||
* [schema](https://github.com/plumatic/schema) and [schema-tools](https://github.com/metosin/schema-tools) for Schema Coercion
|
* [schema](https://github.com/plumatic/schema) and [schema-tools](https://github.com/metosin/schema-tools) for Schema Coercion
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,7 @@
|
||||||
([routes]
|
([routes]
|
||||||
(linear-router routes {}))
|
(linear-router routes {}))
|
||||||
([routes opts]
|
([routes opts]
|
||||||
(let [compiled (compile-routes routes opts)
|
(let [names (find-names routes opts)
|
||||||
names (find-names routes opts)
|
|
||||||
[pl nl] (reduce
|
[pl nl] (reduce
|
||||||
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
||||||
(let [{:keys [params] :as route} (impl/create [p data result])
|
(let [{:keys [params] :as route} (impl/create [p data result])
|
||||||
|
|
@ -152,7 +151,7 @@
|
||||||
(->PartialMatch p data result % params))]
|
(->PartialMatch p data result % params))]
|
||||||
[(conj pl route)
|
[(conj pl route)
|
||||||
(if name (assoc nl name f) nl)]))
|
(if name (assoc nl name f) nl)]))
|
||||||
[[] {}] compiled)
|
[[] {}] routes)
|
||||||
lookup (impl/fast-map nl)]
|
lookup (impl/fast-map nl)]
|
||||||
^{:type ::router}
|
^{:type ::router}
|
||||||
(reify
|
(reify
|
||||||
|
|
@ -160,7 +159,7 @@
|
||||||
(router-name [_]
|
(router-name [_]
|
||||||
:linear-router)
|
:linear-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
compiled)
|
routes)
|
||||||
(options [_]
|
(options [_]
|
||||||
opts)
|
opts)
|
||||||
(route-names [_]
|
(route-names [_]
|
||||||
|
|
@ -190,14 +189,13 @@
|
||||||
(str "can't create :lookup-router with wildcard routes: " wilds)
|
(str "can't create :lookup-router with wildcard routes: " wilds)
|
||||||
{:wilds wilds
|
{:wilds wilds
|
||||||
:routes routes})))
|
:routes routes})))
|
||||||
(let [compiled (compile-routes routes opts)
|
(let [names (find-names routes opts)
|
||||||
names (find-names routes opts)
|
|
||||||
[pl nl] (reduce
|
[pl nl] (reduce
|
||||||
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
||||||
[(assoc pl p (->Match p data result {} p))
|
[(assoc pl p (->Match p data result {} p))
|
||||||
(if name
|
(if name
|
||||||
(assoc nl name #(->Match p data result % p))
|
(assoc nl name #(->Match p data result % p))
|
||||||
nl)]) [{} {}] compiled)
|
nl)]) [{} {}] routes)
|
||||||
data (impl/fast-map pl)
|
data (impl/fast-map pl)
|
||||||
lookup (impl/fast-map nl)]
|
lookup (impl/fast-map nl)]
|
||||||
^{:type ::router}
|
^{:type ::router}
|
||||||
|
|
@ -205,7 +203,7 @@
|
||||||
(router-name [_]
|
(router-name [_]
|
||||||
:lookup-router)
|
:lookup-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
compiled)
|
routes)
|
||||||
(options [_]
|
(options [_]
|
||||||
opts)
|
opts)
|
||||||
(route-names [_]
|
(route-names [_]
|
||||||
|
|
@ -225,8 +223,7 @@
|
||||||
([routes]
|
([routes]
|
||||||
(segment-router routes {}))
|
(segment-router routes {}))
|
||||||
([routes opts]
|
([routes opts]
|
||||||
(let [compiled (compile-routes routes opts)
|
(let [names (find-names routes opts)
|
||||||
names (find-names routes opts)
|
|
||||||
[pl nl] (reduce
|
[pl nl] (reduce
|
||||||
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
(fn [[pl nl] [p {:keys [name] :as data} result]]
|
||||||
(let [{:keys [params] :as route} (impl/create [p data result])
|
(let [{:keys [params] :as route} (impl/create [p data result])
|
||||||
|
|
@ -235,7 +232,7 @@
|
||||||
(->PartialMatch p data result % params))]
|
(->PartialMatch p data result % params))]
|
||||||
[(segment/insert pl p (->Match p data result nil nil))
|
[(segment/insert pl p (->Match p data result nil nil))
|
||||||
(if name (assoc nl name f) nl)]))
|
(if name (assoc nl name f) nl)]))
|
||||||
[nil {}] compiled)
|
[nil {}] routes)
|
||||||
lookup (impl/fast-map nl)]
|
lookup (impl/fast-map nl)]
|
||||||
^{:type ::router}
|
^{:type ::router}
|
||||||
(reify
|
(reify
|
||||||
|
|
@ -243,7 +240,7 @@
|
||||||
(router-name [_]
|
(router-name [_]
|
||||||
:segment-router)
|
:segment-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
compiled)
|
routes)
|
||||||
(options [_]
|
(options [_]
|
||||||
opts)
|
opts)
|
||||||
(route-names [_]
|
(route-names [_]
|
||||||
|
|
@ -272,7 +269,7 @@
|
||||||
(str ":single-static-path-router requires exactly 1 static route: " routes)
|
(str ":single-static-path-router requires exactly 1 static route: " routes)
|
||||||
{:routes routes})))
|
{:routes routes})))
|
||||||
(let [[n :as names] (find-names routes opts)
|
(let [[n :as names] (find-names routes opts)
|
||||||
[[p data result] :as compiled] (compile-routes routes opts)
|
[[p data result] :as compiled] routes
|
||||||
p #?(:clj (.intern ^String p) :cljs p)
|
p #?(:clj (.intern ^String p) :cljs p)
|
||||||
match (->Match p data result {} p)]
|
match (->Match p data result {} p)]
|
||||||
^{:type ::router}
|
^{:type ::router}
|
||||||
|
|
@ -280,7 +277,7 @@
|
||||||
(router-name [_]
|
(router-name [_]
|
||||||
:single-static-path-router)
|
:single-static-path-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
compiled)
|
routes)
|
||||||
(options [_]
|
(options [_]
|
||||||
opts)
|
opts)
|
||||||
(route-names [_]
|
(route-names [_]
|
||||||
|
|
@ -304,7 +301,6 @@
|
||||||
(mixed-router routes {}))
|
(mixed-router routes {}))
|
||||||
([routes opts]
|
([routes opts]
|
||||||
(let [{wild true, lookup false} (group-by impl/wild-route? routes)
|
(let [{wild true, lookup false} (group-by impl/wild-route? routes)
|
||||||
compiled (compile-routes routes opts)
|
|
||||||
->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router)
|
->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router)
|
||||||
wildcard-router (segment-router wild opts)
|
wildcard-router (segment-router wild opts)
|
||||||
static-router (->static-router lookup opts)
|
static-router (->static-router lookup opts)
|
||||||
|
|
@ -314,7 +310,7 @@
|
||||||
(router-name [_]
|
(router-name [_]
|
||||||
:mixed-router)
|
:mixed-router)
|
||||||
(routes [_]
|
(routes [_]
|
||||||
compiled)
|
routes)
|
||||||
(options [_]
|
(options [_]
|
||||||
opts)
|
opts)
|
||||||
(route-names [_]
|
(route-names [_]
|
||||||
|
|
@ -339,10 +335,12 @@
|
||||||
| `:path` | Base-path for routes
|
| `:path` | Base-path for routes
|
||||||
| `:routes` | Initial resolved routes (default `[]`)
|
| `:routes` | Initial resolved routes (default `[]`)
|
||||||
| `:data` | Initial route data (default `{}`)
|
| `:data` | Initial route data (default `{}`)
|
||||||
|
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
|
||||||
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
|
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
|
||||||
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
|
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
|
||||||
| `:compile` | Function of `route opts => result` to compile a route handler
|
| `:compile` | Function of `route opts => result` to compile a route handler
|
||||||
| `:conflicts` | Function of `{route #{route}} => side-effect` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`)
|
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
|
||||||
|
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes (default `reitit.core/throw-on-conflicts!`)
|
||||||
| `:router` | Function of `routes opts => router` to override the actual router implementation"
|
| `:router` | Function of `routes opts => router` to override the actual router implementation"
|
||||||
([raw-routes]
|
([raw-routes]
|
||||||
(router raw-routes {}))
|
(router raw-routes {}))
|
||||||
|
|
@ -350,6 +348,7 @@
|
||||||
(let [{:keys [router] :as opts} (meta-merge default-router-options opts)
|
(let [{:keys [router] :as opts} (meta-merge default-router-options opts)
|
||||||
routes (resolve-routes raw-routes opts)
|
routes (resolve-routes raw-routes opts)
|
||||||
conflicting (conflicting-routes routes)
|
conflicting (conflicting-routes routes)
|
||||||
|
routes (compile-routes routes opts)
|
||||||
wilds? (boolean (some impl/wild-route? routes))
|
wilds? (boolean (some impl/wild-route? routes))
|
||||||
all-wilds? (every? impl/wild-route? routes)
|
all-wilds? (every? impl/wild-route? routes)
|
||||||
router (cond
|
router (cond
|
||||||
|
|
@ -360,6 +359,9 @@
|
||||||
all-wilds? segment-router
|
all-wilds? segment-router
|
||||||
:else mixed-router)]
|
:else mixed-router)]
|
||||||
|
|
||||||
|
(when-let [validate (:validate opts)]
|
||||||
|
(validate routes opts))
|
||||||
|
|
||||||
(when-let [conflicts (:conflicts opts)]
|
(when-let [conflicts (:conflicts opts)]
|
||||||
(when conflicting (conflicts conflicting)))
|
(when conflicting (conflicts conflicting)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
(ns reitit.segment
|
(ns reitit.segment
|
||||||
|
(:refer-clojure :exclude [-lookup])
|
||||||
(:require [reitit.impl :as impl]
|
(:require [reitit.impl :as impl]
|
||||||
[clojure.string :as str]))
|
[clojure.string :as str]))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@
|
||||||
;; routes
|
;; routes
|
||||||
;;
|
;;
|
||||||
|
|
||||||
(s/def ::path (s/with-gen (s/and string? #(or (str/blank? %) (str/starts-with? % "/")))
|
(s/def ::path (s/with-gen string? #(gen/fmap (fn [s] (str "/" s)) (s/gen string?))))
|
||||||
#(gen/fmap (fn [s] (str "/" s)) (s/gen string?))))
|
|
||||||
|
|
||||||
(s/def ::arg (s/and any? (complement vector?)))
|
(s/def ::arg (s/and any? (complement vector?)))
|
||||||
(s/def ::data (s/map-of keyword? any?))
|
(s/def ::data (s/map-of keyword? any?))
|
||||||
|
|
@ -33,6 +32,14 @@
|
||||||
(s/or :route ::route
|
(s/or :route ::route
|
||||||
:routes (s/coll-of ::route :into [])))
|
:routes (s/coll-of ::route :into [])))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Default data
|
||||||
|
;;
|
||||||
|
|
||||||
|
(s/def ::name keyword?)
|
||||||
|
(s/def ::handler fn?)
|
||||||
|
(s/def ::default-data (s/keys :opt-un [::name ::handler]))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; router
|
;; router
|
||||||
;;
|
;;
|
||||||
|
|
@ -62,3 +69,37 @@
|
||||||
:args (s/or :1arity (s/cat :data (s/spec ::raw-routes))
|
:args (s/or :1arity (s/cat :data (s/spec ::raw-routes))
|
||||||
:2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts))
|
:2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts))
|
||||||
:ret ::router)
|
:ret ::router)
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Route data validator
|
||||||
|
;;
|
||||||
|
|
||||||
|
|
||||||
|
(defrecord Problem [path scope data spec problems])
|
||||||
|
|
||||||
|
(defn problems-str [problems explain]
|
||||||
|
(apply str "Invalid route data:\n\n"
|
||||||
|
(mapv
|
||||||
|
(fn [{:keys [path scope data spec]}]
|
||||||
|
(str "-- On route -----------------------\n\n"
|
||||||
|
(pr-str path) (if scope (str " " (pr-str scope))) "\n\n" (explain spec data) "\n"))
|
||||||
|
problems)))
|
||||||
|
|
||||||
|
(defn throw-on-problems! [problems explain]
|
||||||
|
(throw
|
||||||
|
(ex-info
|
||||||
|
(problems-str problems explain)
|
||||||
|
{:problems problems})))
|
||||||
|
|
||||||
|
(defn validate-route-data [routes spec]
|
||||||
|
(->> (for [[p d _] routes]
|
||||||
|
(when-let [problems (and spec (s/explain-data spec d))]
|
||||||
|
(->Problem p nil d spec problems)))
|
||||||
|
(keep identity) (seq)))
|
||||||
|
|
||||||
|
(defn validate-spec!
|
||||||
|
[routes {:keys [spec ::explain]
|
||||||
|
:or {explain s/explain-str
|
||||||
|
spec ::default-data}}]
|
||||||
|
(when-let [problems (validate-route-data routes spec)]
|
||||||
|
(throw-on-problems! problems explain)))
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(def http-methods #{:get :head :patch :delete :options :post :put})
|
(def http-methods #{:get :head :patch :delete :options :post :put})
|
||||||
(defrecord Methods [get head post put delete trace options connect patch any])
|
(defrecord Methods [get head post put delete trace options connect patch any])
|
||||||
|
(defrecord Endpoint [data handler path method middleware])
|
||||||
|
|
||||||
(defn- group-keys [data]
|
(defn- group-keys [data]
|
||||||
(reduce-kv
|
(reduce-kv
|
||||||
|
|
@ -58,14 +59,19 @@
|
||||||
acc)) data http-methods)])
|
acc)) data http-methods)])
|
||||||
|
|
||||||
(defn compile-result [[path data] opts]
|
(defn compile-result [[path data] opts]
|
||||||
(let [[top childs] (group-keys data)]
|
(let [[top childs] (group-keys data)
|
||||||
|
->endpoint (fn [p d m s]
|
||||||
|
(-> (middleware/compile-result [p d] opts s)
|
||||||
|
(map->Endpoint)
|
||||||
|
(assoc :path p)
|
||||||
|
(assoc :method m)))]
|
||||||
(if-not (seq childs)
|
(if-not (seq childs)
|
||||||
(map->Methods {:any (middleware/compile-result [path top] opts)})
|
(map->Methods {:any (->endpoint path top :any nil)})
|
||||||
(reduce-kv
|
(reduce-kv
|
||||||
(fn [acc method data]
|
(fn [acc method data]
|
||||||
(let [data (meta-merge top data)]
|
(let [data (meta-merge top data)]
|
||||||
(assoc acc method (middleware/compile-result [path data] opts method))))
|
(assoc acc method (->endpoint path data method method))))
|
||||||
(map->Methods {:any (if (:handler top) (middleware/compile-result [path data] opts))})
|
(map->Methods {:any (if (:handler top) (->endpoint path data :any nil))})
|
||||||
childs))))
|
childs))))
|
||||||
|
|
||||||
(defn router
|
(defn router
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"Middleware for pluggable request coercion.
|
"Middleware for pluggable request coercion.
|
||||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||||
and :parameters from route data, otherwise does not mount."
|
and :parameters from route data, otherwise does not mount."
|
||||||
{:name ::coerce-parameters
|
{:name ::coerce-request
|
||||||
:compile (fn [{:keys [coercion parameters]} opts]
|
:compile (fn [{:keys [coercion parameters]} opts]
|
||||||
(if (and coercion parameters)
|
(if (and coercion parameters)
|
||||||
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||||
|
|
|
||||||
33
modules/reitit-ring/src/reitit/ring/spec.cljc
Normal file
33
modules/reitit-ring/src/reitit/ring/spec.cljc
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
(ns reitit.ring.spec
|
||||||
|
(:require [clojure.spec.alpha :as s]
|
||||||
|
[reitit.middleware #?@(:cljs [:refer [Middleware]])]
|
||||||
|
[reitit.spec :as rs])
|
||||||
|
#?(:clj
|
||||||
|
(:import (reitit.middleware Middleware))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Specs
|
||||||
|
;;
|
||||||
|
|
||||||
|
(s/def ::middleware (s/coll-of (partial instance? Middleware)))
|
||||||
|
|
||||||
|
(s/def ::data
|
||||||
|
(s/keys :req-un [::rs/handler]
|
||||||
|
:opt-un [::rs/name ::middleware]))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Validator
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn- validate-ring-route-data [routes spec]
|
||||||
|
(->> (for [[p _ c] routes
|
||||||
|
[method {:keys [data] :as endpoint}] c
|
||||||
|
:when endpoint]
|
||||||
|
(when-let [problems (and spec (s/explain-data spec data))]
|
||||||
|
(rs/->Problem p method data spec problems)))
|
||||||
|
(keep identity) (seq)))
|
||||||
|
|
||||||
|
(defn validate-spec!
|
||||||
|
[routes {:keys [spec ::rs/explain] :or {explain s/explain-str, spec ::data}}]
|
||||||
|
(when-let [problems (validate-ring-route-data routes spec)]
|
||||||
|
(rs/throw-on-problems! problems explain)))
|
||||||
|
|
@ -4,18 +4,15 @@
|
||||||
[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]
|
||||||
[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.ring.coercion-middleware :as coercion-middleware]
|
||||||
[reitit.coercion.spec :as spec]
|
[reitit.coercion.spec :as spec]
|
||||||
[reitit.coercion.schema :as schema]
|
[reitit.coercion.schema :as schema]
|
||||||
[reitit.coercion :as coercion]
|
[reitit.coercion :as coercion]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]))
|
||||||
[reitit.core :as r])
|
|
||||||
(:import (java.io ByteArrayInputStream)))
|
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; start repl with `lein perf repl`
|
;; start repl with `lein perf repl`
|
||||||
|
|
@ -46,7 +43,7 @@
|
||||||
;; 4600ns
|
;; 4600ns
|
||||||
(bench!
|
(bench!
|
||||||
"coerce-parameters"
|
"coerce-parameters"
|
||||||
(#'coercion-middleware/coerce-parameters coercers request))
|
(#'coercion-middleware/coerce-request-middleware coercers request))
|
||||||
|
|
||||||
;; 2700ns
|
;; 2700ns
|
||||||
(bench!
|
(bench!
|
||||||
|
|
@ -90,7 +87,7 @@
|
||||||
(-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] (coercion/request-coercer this :response spec {})))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]]
|
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]]
|
||||||
|
|
@ -175,11 +172,15 @@
|
||||||
(cc/quick-bench (app req)))))
|
(cc/quick-bench (app req)))))
|
||||||
|
|
||||||
(defn json-perf-test []
|
(defn json-perf-test []
|
||||||
|
(title "json")
|
||||||
(let [m (m/create (jsonista-format/with-json-format m/default-options))
|
(let [m (m/create (jsonista-format/with-json-format m/default-options))
|
||||||
app (ring/ring-handler
|
app (ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
["/plus" {:post {:handler (fn [{{:keys [x y]} :body-params}]
|
["/plus" {:post {:handler (fn [request]
|
||||||
{:status 200, :body {:result (+ x y)}})}}]
|
(let [body (:body-params request)
|
||||||
|
x (:x body)
|
||||||
|
y (:y body)]
|
||||||
|
{:status 200, :body {:result (+ x y)}}))}}]
|
||||||
{:data {:middleware [[mm/wrap-format m]]}}))
|
{:data {:middleware [[mm/wrap-format m]]}}))
|
||||||
request {:request-method :post
|
request {:request-method :post
|
||||||
:uri "/plus"
|
:uri "/plus"
|
||||||
|
|
@ -194,6 +195,7 @@
|
||||||
(-> request app :body slurp))))
|
(-> request app :body slurp))))
|
||||||
|
|
||||||
(defn schema-json-perf-test []
|
(defn schema-json-perf-test []
|
||||||
|
(title "schema-json")
|
||||||
(let [m (m/create (jsonista-format/with-json-format m/default-options))
|
(let [m (m/create (jsonista-format/with-json-format m/default-options))
|
||||||
app (ring/ring-handler
|
app (ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
|
|
@ -219,6 +221,7 @@
|
||||||
(-> request app :body slurp))))
|
(-> request app :body slurp))))
|
||||||
|
|
||||||
(defn schema-perf-test []
|
(defn schema-perf-test []
|
||||||
|
(title "schema")
|
||||||
(let [app (ring/ring-handler
|
(let [app (ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
["/plus" {:post {:responses {200 {:schema {:result Long}}}
|
["/plus" {:post {:responses {200 {:schema {:result Long}}}
|
||||||
|
|
@ -242,6 +245,7 @@
|
||||||
(call))))
|
(call))))
|
||||||
|
|
||||||
(defn data-spec-perf-test []
|
(defn data-spec-perf-test []
|
||||||
|
(title "data-spec")
|
||||||
(let [app (ring/ring-handler
|
(let [app (ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
["/plus" {:post {:responses {200 {:schema {:result int?}}}
|
["/plus" {:post {:responses {200 {:schema {:result int?}}}
|
||||||
|
|
@ -270,6 +274,7 @@
|
||||||
(s/def ::response (s/keys :req-un [::result]))
|
(s/def ::response (s/keys :req-un [::result]))
|
||||||
|
|
||||||
(defn spec-perf-test []
|
(defn spec-perf-test []
|
||||||
|
(title "spec")
|
||||||
(let [app (ring/ring-handler
|
(let [app (ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
["/plus" {:post {:responses {200 {:schema ::response}}
|
["/plus" {:post {:responses {200 {:schema ::response}}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
;;
|
;;
|
||||||
|
|
||||||
(defn h [path]
|
(defn h [path]
|
||||||
(fn [req]
|
(fn [_]
|
||||||
{:status 200, :body path}))
|
{:status 200, :body path}))
|
||||||
|
|
||||||
(defn add [handler routes route]
|
(defn add [handler routes route]
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,15 @@
|
||||||
(ns reitit.lupapiste-perf-test
|
(ns reitit.lupapiste-perf-test
|
||||||
(:require [clojure.test :refer [deftest testing is]]
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
[criterium.core :as cc]
|
|
||||||
[reitit.perf-utils :refer :all]
|
[reitit.perf-utils :refer :all]
|
||||||
[cheshire.core :as json]
|
|
||||||
[clojure.string :as str]
|
|
||||||
[reitit.core :as reitit]
|
[reitit.core :as reitit]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
|
|
||||||
[bidi.bidi :as bidi]
|
[bidi.bidi :as bidi]
|
||||||
|
|
||||||
[ataraxy.core :as ataraxy]
|
|
||||||
|
|
||||||
[compojure.core :as compojure]
|
[compojure.core :as compojure]
|
||||||
|
|
||||||
[io.pedestal.http.route.definition.table :as table]
|
[io.pedestal.http.route.definition.table :as table]
|
||||||
[io.pedestal.http.route.map-tree :as map-tree]
|
[io.pedestal.http.route.map-tree :as map-tree]
|
||||||
[io.pedestal.http.route.router :as pedestal]
|
[io.pedestal.http.route.router :as pedestal]))
|
||||||
[io.pedestal.http.route :as route]))
|
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; start repl with `lein perf repl`
|
;; start repl with `lein perf repl`
|
||||||
|
|
|
||||||
250
perf-test/clj/reitit/middleware_interceptor_perf.clj
Normal file
250
perf-test/clj/reitit/middleware_interceptor_perf.clj
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
(ns reitit.middleware-interceptor-perf
|
||||||
|
(:require [criterium.core :as cc]
|
||||||
|
[reitit.perf-utils :refer :all]
|
||||||
|
[reitit.middleware :as middleware]
|
||||||
|
[reitit.interceptor :as interceptor]
|
||||||
|
|
||||||
|
reitit.chain
|
||||||
|
io.pedestal.interceptor
|
||||||
|
io.pedestal.interceptor.chain))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; 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
|
||||||
|
;;
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; middleware
|
||||||
|
;;
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(defrecord RequestOrContext [values queue stack])
|
||||||
|
|
||||||
|
(def +items+ 100)
|
||||||
|
|
||||||
|
(defn expected! [x]
|
||||||
|
(assert (= (range +items+) (:values x))))
|
||||||
|
|
||||||
|
(defn middleware [handler value]
|
||||||
|
(fn [request]
|
||||||
|
(let [values (or (:values request) [])]
|
||||||
|
(handler (assoc request :values (conj values value))))))
|
||||||
|
|
||||||
|
(defn middleware-test []
|
||||||
|
(let [mw (map (fn [value] [middleware value]) (range +items+))
|
||||||
|
app (middleware/chain mw identity)
|
||||||
|
map-request {}
|
||||||
|
record-request (map->RequestOrContext map-request)]
|
||||||
|
|
||||||
|
;; 10.8 µs
|
||||||
|
(title "middleware - map")
|
||||||
|
(expected! (app map-request))
|
||||||
|
(cc/quick-bench
|
||||||
|
(app map-request))
|
||||||
|
|
||||||
|
;; 4.7 µs
|
||||||
|
(title "middleware - record")
|
||||||
|
(expected! (app record-request))
|
||||||
|
(cc/quick-bench
|
||||||
|
(app record-request))
|
||||||
|
|
||||||
|
(title "middleware - dynamic")
|
||||||
|
(expected! ((middleware/chain mw identity) record-request))
|
||||||
|
(cc/quick-bench
|
||||||
|
((middleware/chain mw identity) record-request))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Reduce
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn test-reduce []
|
||||||
|
(let [ints (vec (range +items+))
|
||||||
|
size (count ints)]
|
||||||
|
|
||||||
|
;; 64µs
|
||||||
|
(cc/quick-bench
|
||||||
|
(reduce #(+ ^int %1 ^int %2) ints))
|
||||||
|
|
||||||
|
;; 123µs
|
||||||
|
(cc/quick-bench
|
||||||
|
(loop [sum 0, i 0]
|
||||||
|
(if (= i size)
|
||||||
|
sum
|
||||||
|
(recur (+ sum ^int (nth ints i)) (inc i)))))
|
||||||
|
|
||||||
|
;; 34µs
|
||||||
|
(cc/quick-bench
|
||||||
|
(let [iter (clojure.lang.RT/iter ints)]
|
||||||
|
(loop [sum 0]
|
||||||
|
(if (.hasNext iter)
|
||||||
|
(recur (+ sum ^int (.next iter)))
|
||||||
|
sum))))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Interceptor
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn interceptor [value]
|
||||||
|
(fn [context]
|
||||||
|
(let [values (or (:values context) [])]
|
||||||
|
(assoc context :values (conj values value)))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Pedestal
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn pedestal-chain-text []
|
||||||
|
(let [is (map io.pedestal.interceptor/interceptor
|
||||||
|
(map (fn [value]
|
||||||
|
{:enter (interceptor value)}) (range +items+)))
|
||||||
|
ctx (io.pedestal.interceptor.chain/enqueue nil is)]
|
||||||
|
|
||||||
|
;; 78 µs
|
||||||
|
(title "pedestal")
|
||||||
|
(cc/quick-bench
|
||||||
|
(io.pedestal.interceptor.chain/execute ctx))))
|
||||||
|
|
||||||
|
(defn pedestal-tuned-chain-text []
|
||||||
|
(let [is (map io.pedestal.interceptor/interceptor
|
||||||
|
(map (fn [value]
|
||||||
|
{:enter (interceptor value)}) (range +items+)))
|
||||||
|
ctx (reitit.chain/map->Context (reitit.chain/enqueue nil is))]
|
||||||
|
|
||||||
|
;; 67 µs
|
||||||
|
(title "pedestal - tuned")
|
||||||
|
(cc/quick-bench
|
||||||
|
(reitit.chain/execute ctx))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; Naive chain
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn execute [ctx f] (f ctx))
|
||||||
|
|
||||||
|
(defn executor-reduce [interceptors]
|
||||||
|
(fn [ctx]
|
||||||
|
(as-> ctx $
|
||||||
|
(reduce execute $ (keep :enter interceptors))
|
||||||
|
(reduce execute $ (reverse (keep :leave interceptors))))))
|
||||||
|
|
||||||
|
(defn interceptor-test []
|
||||||
|
(let [interceptors (map (fn [value] [interceptor value]) (range +items+))
|
||||||
|
app (executor-reduce (interceptor/chain interceptors identity))
|
||||||
|
map-request {}
|
||||||
|
record-request (map->RequestOrContext map-request)]
|
||||||
|
|
||||||
|
;; 13.5 µs (Map)
|
||||||
|
(title "interceptors - map")
|
||||||
|
(expected! (app map-request))
|
||||||
|
(cc/quick-bench
|
||||||
|
(app map-request))
|
||||||
|
|
||||||
|
;; 7.2 µs (Record)
|
||||||
|
(title "interceptors - record")
|
||||||
|
(expected! (app record-request))
|
||||||
|
(cc/quick-bench
|
||||||
|
(app record-request))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; different reducers
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn enqueue [ctx interceptors]
|
||||||
|
(let [queue (or (:queue ctx) clojure.lang.PersistentQueue/EMPTY)]
|
||||||
|
(assoc ctx :queue (into queue interceptors))))
|
||||||
|
|
||||||
|
(defn queue [ctx interceptors]
|
||||||
|
(let [queue (or (:queue ctx) clojure.lang.PersistentQueue/EMPTY)]
|
||||||
|
(into queue interceptors)))
|
||||||
|
|
||||||
|
(defn leavel-all-queue [ctx stack]
|
||||||
|
(let [it (clojure.lang.RT/iter stack)]
|
||||||
|
(loop [ctx ctx]
|
||||||
|
(if (.hasNext it)
|
||||||
|
(if-let [leave (-> it .next :leave)]
|
||||||
|
(recur (leave ctx))
|
||||||
|
(recur ctx))
|
||||||
|
ctx))))
|
||||||
|
|
||||||
|
(defn executor-queue [interceptors]
|
||||||
|
(fn [ctx]
|
||||||
|
(loop [queue (queue ctx interceptors)
|
||||||
|
stack nil
|
||||||
|
ctx ctx]
|
||||||
|
(if-let [interceptor (peek queue)]
|
||||||
|
(let [queue (pop queue)
|
||||||
|
stack (conj stack interceptor)
|
||||||
|
f (or (:enter interceptor) identity)]
|
||||||
|
(recur queue stack (f ctx)))
|
||||||
|
(leavel-all-queue ctx stack)))))
|
||||||
|
|
||||||
|
(defn leave-all-ctx-queue [ctx stack]
|
||||||
|
(let [it (clojure.lang.RT/iter stack)]
|
||||||
|
(loop [ctx ctx]
|
||||||
|
(if (.hasNext it)
|
||||||
|
(if-let [leave (-> it .next :leave)]
|
||||||
|
(recur (leave ctx))
|
||||||
|
(recur ctx))
|
||||||
|
ctx))))
|
||||||
|
|
||||||
|
(defn executor-ctx-queue [interceptors]
|
||||||
|
(fn [ctx]
|
||||||
|
(loop [ctx (assoc ctx :queue (queue ctx interceptors))]
|
||||||
|
(let [queue ^clojure.lang.PersistentQueue (:queue ctx)
|
||||||
|
stack (:stack ctx)]
|
||||||
|
(if-let [interceptor (peek queue)]
|
||||||
|
(let [queue (pop queue)
|
||||||
|
stack (conj stack interceptor)
|
||||||
|
f (or (:enter interceptor) identity)]
|
||||||
|
(recur (-> ctx (assoc :queue queue) (assoc :stac stack) f)))
|
||||||
|
(leave-all-ctx-queue ctx stack))))))
|
||||||
|
|
||||||
|
(defn interceptor-chain-test []
|
||||||
|
(let [interceptors (map (fn [value] [interceptor value]) (range +items+))
|
||||||
|
app-reduce (executor-reduce (interceptor/chain interceptors identity))
|
||||||
|
app-queue (executor-queue (interceptor/chain interceptors identity))
|
||||||
|
app-ctx-queue (executor-ctx-queue (interceptor/chain interceptors identity))
|
||||||
|
request {}]
|
||||||
|
|
||||||
|
;; 14.2 µs
|
||||||
|
(title "interceptors - reduce")
|
||||||
|
(expected! (app-reduce request))
|
||||||
|
(cc/quick-bench
|
||||||
|
(app-reduce request))
|
||||||
|
|
||||||
|
;; 19.4 µs
|
||||||
|
(title "interceptors - queue")
|
||||||
|
(expected! (app-queue request))
|
||||||
|
(cc/quick-bench
|
||||||
|
(app-queue request))
|
||||||
|
|
||||||
|
;; 30.9 µs
|
||||||
|
(title "interceptors - ctx-queue")
|
||||||
|
(expected! (app-ctx-queue request))
|
||||||
|
(cc/quick-bench
|
||||||
|
(app-ctx-queue request))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(interceptor-test)
|
||||||
|
(middleware-test)
|
||||||
|
(pedestal-chain-text)
|
||||||
|
(pedestal-tuned-chain-text)
|
||||||
|
(interceptor-chain-test))
|
||||||
|
|
||||||
|
; Middleware (static chain) => 5µs
|
||||||
|
; Middleware (dynamic chain) => 60µs
|
||||||
|
|
||||||
|
; Interceptor (static queue) => 20µs
|
||||||
|
; Interceptor (context queues) => 30µs
|
||||||
|
; Pedestal (context queues) => 79µs
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
(ns reitit.nodejs-perf-test
|
(ns reitit.nodejs-perf-test
|
||||||
(:require [criterium.core :as cc]
|
(:require [reitit.perf-utils :refer :all]
|
||||||
[reitit.perf-utils :refer :all]
|
|
||||||
[immutant.web :as web]
|
[immutant.web :as web]
|
||||||
[reitit.ring :as ring]))
|
[reitit.ring :as ring]))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
(ns reitit.opensensors-perf-test
|
(ns reitit.opensensors-perf-test
|
||||||
(:require [clojure.test :refer [deftest testing is]]
|
(:require [reitit.perf-utils :refer :all]
|
||||||
[criterium.core :as cc]
|
|
||||||
[reitit.perf-utils :refer :all]
|
|
||||||
[cheshire.core :as json]
|
[cheshire.core :as json]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[reitit.core :as reitit]
|
[reitit.core :as reitit]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
|
|
||||||
[bidi.bidi :as bidi]
|
[bidi.bidi :as bidi]
|
||||||
|
|
||||||
[ataraxy.core :as ataraxy]
|
[ataraxy.core :as ataraxy]
|
||||||
|
|
||||||
[compojure.core :refer [routes context ANY]]
|
[compojure.core :refer [routes context ANY]]
|
||||||
|
|
||||||
[io.pedestal.http.route.definition.table :as table]
|
[io.pedestal.http.route.definition.table :as table]
|
||||||
[io.pedestal.http.route.map-tree :as map-tree]
|
[io.pedestal.http.route.map-tree :as map-tree]
|
||||||
[io.pedestal.http.route.router :as pedestal]
|
[io.pedestal.http.route.router :as pedestal]))
|
||||||
[io.pedestal.http.route :as route]))
|
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; start repl with `lein perf repl`
|
;; start repl with `lein perf repl`
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,7 @@
|
||||||
|
|
||||||
(defn bench-routes [routes req f]
|
(defn bench-routes [routes req f]
|
||||||
(let [router (reitit/router routes)
|
(let [router (reitit/router routes)
|
||||||
urls (valid-urls router)
|
urls (valid-urls router)]
|
||||||
random-url #(rand-nth urls)
|
|
||||||
log-time #(let [now (System/nanoTime)] (%) (- (System/nanoTime) now))
|
|
||||||
total 10000
|
|
||||||
dropped (int (* total 0.45))]
|
|
||||||
(mapv
|
(mapv
|
||||||
(fn [path]
|
(fn [path]
|
||||||
(let [request (map->Request (req path))
|
(let [request (map->Request (req path))
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,10 @@
|
||||||
nil routes))
|
nil routes))
|
||||||
|
|
||||||
#_(def reitit-tree
|
#_(def reitit-tree
|
||||||
(reduce
|
(reduce
|
||||||
(fn [acc [p d]]
|
(fn [acc [p d]]
|
||||||
(trie/insert acc p d))
|
(trie/insert acc p d))
|
||||||
nil routes))
|
nil routes))
|
||||||
|
|
||||||
(def reitit-segment
|
(def reitit-segment
|
||||||
(segment/create routes))
|
(segment/create routes))
|
||||||
|
|
@ -101,8 +101,8 @@
|
||||||
;; 0.8ms (return route-data)
|
;; 0.8ms (return route-data)
|
||||||
;; 0.8ms (fix payloads)
|
;; 0.8ms (fix payloads)
|
||||||
#_(cc/quick-bench
|
#_(cc/quick-bench
|
||||||
(dotimes [_ 1000]
|
(dotimes [_ 1000]
|
||||||
(trie/lookup reitit-tree "/v1/orgs/1/topics" {})))
|
(trie/lookup reitit-tree "/v1/orgs/1/topics" {})))
|
||||||
|
|
||||||
;; 0.9ms (initial)
|
;; 0.9ms (initial)
|
||||||
;; 0.5ms (protocols)
|
;; 0.5ms (protocols)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
[metosin/reitit]
|
[metosin/reitit]
|
||||||
[metosin/schema-tools "0.10.0-SNAPSHOT"]
|
[metosin/schema-tools "0.10.0-SNAPSHOT"]
|
||||||
|
|
||||||
[expound "0.3.4"]
|
[expound "0.4.0"]
|
||||||
[orchestra "2017.11.12-1"]
|
[orchestra "2017.11.12-1"]
|
||||||
|
|
||||||
[ring "1.6.3"]
|
[ring "1.6.3"]
|
||||||
|
|
|
||||||
382
test/cljc/reitit/chain.clj
Normal file
382
test/cljc/reitit/chain.clj
Normal file
|
|
@ -0,0 +1,382 @@
|
||||||
|
; Copyright 2013 Relevance, Inc.
|
||||||
|
; Copyright 2014-2016 Cognitect, Inc.
|
||||||
|
|
||||||
|
; The use and distribution terms for this software are covered by the
|
||||||
|
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0)
|
||||||
|
; which can be found in the file epl-v10.html at the root of this distribution.
|
||||||
|
;
|
||||||
|
; By using this software in any fashion, you are agreeing to be bound by
|
||||||
|
; the terms of this license.
|
||||||
|
;
|
||||||
|
; You must not remove this notice, or any other, from this software.
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(ns reitit.chain
|
||||||
|
"Interceptor pattern. Executes a chain of Interceptor functions on a
|
||||||
|
common \"context\" map, maintaining a virtual \"stack\", with error
|
||||||
|
handling and support for asynchronous execution."
|
||||||
|
(:refer-clojure :exclude (name))
|
||||||
|
(:require [clojure.core.async :as async :refer [<! go]]
|
||||||
|
[io.pedestal.log :as log]
|
||||||
|
[io.pedestal.interceptor :as interceptor])
|
||||||
|
(:import java.util.concurrent.atomic.AtomicLong))
|
||||||
|
|
||||||
|
(defrecord Context [execution-id stack queue terminators supressed async-info rebind])
|
||||||
|
|
||||||
|
(declare execute)
|
||||||
|
(declare execute-only)
|
||||||
|
|
||||||
|
(defn- channel? [c] (instance? clojure.core.async.impl.protocols.Channel c))
|
||||||
|
|
||||||
|
;; This is used for printing out interceptors within debug messages
|
||||||
|
(defn- name [interceptor]
|
||||||
|
(:name interceptor (pr-str interceptor)))
|
||||||
|
|
||||||
|
(defn- throwable->ex-info [^Throwable t execution-id interceptor stage]
|
||||||
|
(let [iname (name interceptor)
|
||||||
|
throwable-str (pr-str (type t))]
|
||||||
|
(ex-info (str throwable-str " in Interceptor " iname " - " (.getMessage t))
|
||||||
|
(merge {:execution-id execution-id
|
||||||
|
:stage stage
|
||||||
|
:interceptor iname
|
||||||
|
:exception-type (keyword throwable-str)
|
||||||
|
:exception t}
|
||||||
|
(ex-data t))
|
||||||
|
t)))
|
||||||
|
|
||||||
|
(defn- try-f
|
||||||
|
"If f is not nil, invokes it on context. If f throws an exception,
|
||||||
|
assoc's it on to context as :error."
|
||||||
|
[context interceptor stage]
|
||||||
|
(let [execution-id (:execution-id context)]
|
||||||
|
(if-let [f (stage interceptor)]
|
||||||
|
(try (log/debug :interceptor (name interceptor)
|
||||||
|
:stage stage
|
||||||
|
:execution-id execution-id
|
||||||
|
:fn f)
|
||||||
|
(f context)
|
||||||
|
(catch Throwable t
|
||||||
|
(log/debug :throw t :execution-id execution-id)
|
||||||
|
(assoc context :error (throwable->ex-info t execution-id interceptor stage))))
|
||||||
|
(do (log/trace :interceptor (name interceptor)
|
||||||
|
:skipped? true
|
||||||
|
:stage stage
|
||||||
|
:execution-id execution-id)
|
||||||
|
context))))
|
||||||
|
|
||||||
|
(defn- try-error
|
||||||
|
"If error-fn is not nil, invokes it on context and the current :error
|
||||||
|
from context."
|
||||||
|
[context interceptor]
|
||||||
|
(let [execution-id (:execution-id context)]
|
||||||
|
(if-let [error-fn (:error interceptor)]
|
||||||
|
(let [ex (:error context)
|
||||||
|
stage :error]
|
||||||
|
(log/debug :interceptor (name interceptor)
|
||||||
|
:stage :error
|
||||||
|
:execution-id execution-id)
|
||||||
|
(try (error-fn (assoc context :error nil) ex)
|
||||||
|
(catch Throwable t
|
||||||
|
(if (identical? (type t) (type (:exception ex)))
|
||||||
|
(do (log/debug :rethrow t :execution-id execution-id)
|
||||||
|
context)
|
||||||
|
(do (log/debug :throw t :suppressed (:exception-type ex) :execution-id execution-id)
|
||||||
|
(-> context
|
||||||
|
(assoc :error (throwable->ex-info t execution-id interceptor :error))
|
||||||
|
(update :suppressed conj ex)))))))
|
||||||
|
(do (log/trace :interceptor (name interceptor)
|
||||||
|
:skipped? true
|
||||||
|
:stage :error
|
||||||
|
:execution-id execution-id)
|
||||||
|
context))))
|
||||||
|
|
||||||
|
(defn- check-terminators
|
||||||
|
"Invokes each predicate in :terminators on context. If any predicate
|
||||||
|
returns true, removes :queue from context."
|
||||||
|
[context]
|
||||||
|
(if (some #(% context) (:terminators context))
|
||||||
|
(let [execution-id (:execution-id context)]
|
||||||
|
(log/debug :in 'check-terminators
|
||||||
|
:terminate? true
|
||||||
|
:execution-id execution-id)
|
||||||
|
(assoc context :queue nil))
|
||||||
|
context))
|
||||||
|
|
||||||
|
(defn- prepare-for-async
|
||||||
|
"Call all of the :enter-async functions in a context. The purpose of these
|
||||||
|
functions is to ready backing servlets or any other machinery for preparing
|
||||||
|
an asynchronous response."
|
||||||
|
[{:keys [enter-async] :as context}]
|
||||||
|
(doseq [enter-async-fn enter-async]
|
||||||
|
(enter-async-fn context)))
|
||||||
|
|
||||||
|
(defn- go-async
|
||||||
|
"When presented with a channel as the return value of an enter function,
|
||||||
|
wait for the channel to return a new-context (via a go block). When a new
|
||||||
|
context is received, restart execution of the interceptor chain with that
|
||||||
|
context.
|
||||||
|
This function is non-blocking, returning nil immediately (a signal to halt
|
||||||
|
further execution on this thread)."
|
||||||
|
([old-context context-channel]
|
||||||
|
(prepare-for-async old-context)
|
||||||
|
(go
|
||||||
|
(if-let [new-context (<! context-channel)]
|
||||||
|
(execute new-context)
|
||||||
|
(execute (assoc (assoc old-context :queue nil :async-info nil)
|
||||||
|
:stack (get-in old-context [:async-info :stack])
|
||||||
|
:error (ex-info "Async Interceptor closed Context Channel before delivering a Context"
|
||||||
|
{:execution-id (:execution-id old-context)
|
||||||
|
:stage (get-in old-context [:async-info :stage])
|
||||||
|
:interceptor (name (get-in old-context [:async-info :interceptor]))
|
||||||
|
:exception-type :PedestalChainAsyncPrematureClose})))))
|
||||||
|
nil)
|
||||||
|
([old-context context-channel interceptor-key]
|
||||||
|
(prepare-for-async old-context)
|
||||||
|
(go
|
||||||
|
(if-let [new-context (<! context-channel)]
|
||||||
|
(execute-only new-context interceptor-key)
|
||||||
|
(execute-only (assoc (assoc old-context :queue nil :async-info nil)
|
||||||
|
:stack (get-in old-context [:async-info :stack])
|
||||||
|
:error (ex-info "Async Interceptor closed Context Channel before delivering a Context"
|
||||||
|
{:execution-id (:execution-id old-context)
|
||||||
|
:stage (get-in old-context [:async-info :stage])
|
||||||
|
:interceptor (name (get-in old-context [:async-info :interceptor]))
|
||||||
|
:exception-type :PedestalChainAsyncPrematureClose}))
|
||||||
|
interceptor-key)))
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(defn- process-all-with-binding
|
||||||
|
"Invokes `interceptor-key` functions of all Interceptors on the execution
|
||||||
|
:queue of context, saves them on the :stack of context.
|
||||||
|
Returns updated context.
|
||||||
|
By default, `interceptor-key` is :enter"
|
||||||
|
([context]
|
||||||
|
(process-all-with-binding context :enter))
|
||||||
|
([context interceptor-key]
|
||||||
|
(log/debug :in 'process-all :handling interceptor-key :execution-id (:execution-id context))
|
||||||
|
(loop [context context]
|
||||||
|
(let [queue (:queue context)
|
||||||
|
stack (:stack context)]
|
||||||
|
(log/trace :context context)
|
||||||
|
(if (empty? queue)
|
||||||
|
context
|
||||||
|
(let [interceptor (peek queue)
|
||||||
|
old-context context
|
||||||
|
new-queue (pop queue)
|
||||||
|
;; conj on nil returns a list, acts like a stack:
|
||||||
|
new-stack (conj stack interceptor)
|
||||||
|
context (-> context
|
||||||
|
(assoc :queue new-queue)
|
||||||
|
(assoc :stack new-stack)
|
||||||
|
(try-f interceptor interceptor-key))]
|
||||||
|
(cond
|
||||||
|
(channel? context) (go-async (assoc old-context
|
||||||
|
:async-info {:interceptor interceptor
|
||||||
|
:stage interceptor-key
|
||||||
|
:stack new-stack})
|
||||||
|
context)
|
||||||
|
(:error context) (assoc context :queue nil)
|
||||||
|
(not= (:bindings context) (:bindings old-context)) (assoc context :rebind true)
|
||||||
|
true (recur (check-terminators context)))))))))
|
||||||
|
|
||||||
|
(defn- process-all
|
||||||
|
[context interceptor-key]
|
||||||
|
;; If we're processing leave handlers, reverse the queue
|
||||||
|
(let [context (if (= interceptor-key :leave) (update context :queue reverse) context)
|
||||||
|
context (with-bindings (or (:bindings context)
|
||||||
|
{})
|
||||||
|
(process-all-with-binding context interceptor-key))]
|
||||||
|
(if (:rebind context)
|
||||||
|
(recur (assoc context :rebind nil) interceptor-key)
|
||||||
|
context)))
|
||||||
|
|
||||||
|
(defn- process-any-errors-with-binding
|
||||||
|
"Unwinds the context by invoking :error functions of Interceptors on
|
||||||
|
the :stack of context, but **only** if there is an :error present in the context."
|
||||||
|
[context]
|
||||||
|
(log/debug :in 'process-any-errors :execution-id (:execution-id context))
|
||||||
|
(loop [context context]
|
||||||
|
(let [stack (:stack context)]
|
||||||
|
(log/trace :context context)
|
||||||
|
(if (empty? stack)
|
||||||
|
context
|
||||||
|
(let [interceptor (peek stack)
|
||||||
|
pre-bindings (:bindings context)
|
||||||
|
old-context context
|
||||||
|
context (assoc context :stack (pop stack))
|
||||||
|
context (if (:error context)
|
||||||
|
(try-error context interceptor)
|
||||||
|
context)]
|
||||||
|
(cond
|
||||||
|
(channel? context) (go-async old-context context)
|
||||||
|
(not= (:bindings context) pre-bindings) (assoc context :rebind true)
|
||||||
|
true (recur context)))))))
|
||||||
|
|
||||||
|
(defn- process-any-errors
|
||||||
|
"Establish the bindings present in `context` as thread local
|
||||||
|
bindings, and then invoke process-any-errors-with-binding.
|
||||||
|
Conditionally re-establish bindings if a change in bindings is made by an
|
||||||
|
interceptor."
|
||||||
|
[context]
|
||||||
|
(let [context (with-bindings (or (:bindings context) {})
|
||||||
|
(process-any-errors-with-binding context))]
|
||||||
|
(if (:rebind context)
|
||||||
|
(recur (assoc context :rebind nil))
|
||||||
|
context)))
|
||||||
|
|
||||||
|
(defn- enter-all
|
||||||
|
"Establish the bindings present in `context` as thread local
|
||||||
|
bindings, and then invoke enter-all-with-binding. Conditionally
|
||||||
|
re-establish bindings if a change in bindings is made by an
|
||||||
|
interceptor."
|
||||||
|
[context]
|
||||||
|
(process-all context :enter))
|
||||||
|
|
||||||
|
(defn- leave-all-with-binding
|
||||||
|
"Unwinds the context by invoking :leave functions of Interceptors on
|
||||||
|
the :stack of context. Returns updated context."
|
||||||
|
[context]
|
||||||
|
(log/debug :in 'leave-all :execution-id (:execution-id context))
|
||||||
|
(loop [context context]
|
||||||
|
(let [stack (:stack context)]
|
||||||
|
(log/trace :context context)
|
||||||
|
(if (empty? stack)
|
||||||
|
context
|
||||||
|
(let [interceptor (peek stack)
|
||||||
|
pre-bindings (:bindings context)
|
||||||
|
old-context context
|
||||||
|
context (assoc context :stack (pop stack))
|
||||||
|
context (if (:error context)
|
||||||
|
(try-error context interceptor)
|
||||||
|
(try-f context interceptor :leave))]
|
||||||
|
(cond
|
||||||
|
(channel? context) (go-async old-context context)
|
||||||
|
(not= (:bindings context) pre-bindings) (assoc context :rebind true)
|
||||||
|
true (recur context)))))))
|
||||||
|
|
||||||
|
(defn- leave-all
|
||||||
|
"Establish the bindings present in `context` as thread local
|
||||||
|
bindings, and then invoke leave-all-with-binding. Conditionally
|
||||||
|
re-establish bindings if a change in bindings is made by an
|
||||||
|
interceptor."
|
||||||
|
[context]
|
||||||
|
(let [context (with-bindings (or (:bindings context) {})
|
||||||
|
(leave-all-with-binding context))]
|
||||||
|
(if (:rebind context)
|
||||||
|
(recur (assoc context :rebind nil))
|
||||||
|
context)))
|
||||||
|
|
||||||
|
(defn enqueue
|
||||||
|
"Adds interceptors to the end of context's execution queue. Creates
|
||||||
|
the queue if necessary. Returns updated context."
|
||||||
|
[context interceptors]
|
||||||
|
{:pre (every? interceptor/interceptor? interceptors)}
|
||||||
|
(log/trace :enqueue (map name interceptors) :context context)
|
||||||
|
(update context :queue
|
||||||
|
(fnil into clojure.lang.PersistentQueue/EMPTY)
|
||||||
|
interceptors))
|
||||||
|
|
||||||
|
(defn enqueue*
|
||||||
|
"Like 'enqueue' but vararg.
|
||||||
|
If the last argument is a sequence of interceptors,
|
||||||
|
they're unpacked and to added to the context's execution queue."
|
||||||
|
[context & interceptors-and-seq]
|
||||||
|
(if (seq? (last interceptors-and-seq))
|
||||||
|
(enqueue context (apply list* interceptors-and-seq))
|
||||||
|
(enqueue context interceptors-and-seq)))
|
||||||
|
|
||||||
|
(defn terminate
|
||||||
|
"Removes all remaining interceptors from context's execution queue.
|
||||||
|
This effectively short-circuits execution of Interceptors' :enter
|
||||||
|
functions and begins executing the :leave functions."
|
||||||
|
[context]
|
||||||
|
(log/trace :in 'terminate :context context)
|
||||||
|
(assoc context :queue nil))
|
||||||
|
|
||||||
|
(defn terminate-when
|
||||||
|
"Adds pred as a terminating condition of the context. pred is a
|
||||||
|
function that takes a context as its argument. It will be invoked
|
||||||
|
after every Interceptor's :enter function. If pred returns logical
|
||||||
|
true, execution will stop at that Interceptor."
|
||||||
|
[context pred]
|
||||||
|
(update context :terminators conj pred))
|
||||||
|
|
||||||
|
(def ^:private ^AtomicLong execution-id (AtomicLong.))
|
||||||
|
|
||||||
|
(defn- begin [context]
|
||||||
|
(if (:execution-id context)
|
||||||
|
context
|
||||||
|
(let [execution-id (.incrementAndGet execution-id)]
|
||||||
|
(log/debug :in 'begin :execution-id execution-id)
|
||||||
|
(log/trace :context context)
|
||||||
|
(assoc context :execution-id execution-id))))
|
||||||
|
|
||||||
|
(defn- end [context]
|
||||||
|
(if (:execution-id context)
|
||||||
|
(do
|
||||||
|
(log/debug :in 'end :execution-id (:execution-id context) :context-keys (keys context))
|
||||||
|
(log/trace :context context)
|
||||||
|
(assoc context :stack nil :execution-id nil))
|
||||||
|
context))
|
||||||
|
|
||||||
|
(defn execute-only
|
||||||
|
"Like `execute`, but only processes the interceptors in a single direction,
|
||||||
|
using `interceptor-key` (i.e. :enter, :leave) to determine which functions
|
||||||
|
to call.
|
||||||
|
---
|
||||||
|
Executes a queue of Interceptors attached to the context. Context
|
||||||
|
must be a map, Interceptors are added with 'enqueue'.
|
||||||
|
An Interceptor Record has keys :enter, :leave, and :error.
|
||||||
|
The value of each key is a function; missing
|
||||||
|
keys or nil values are ignored. When executing a context, all
|
||||||
|
the `interceptor-key` functions are invoked in order. As this happens, the
|
||||||
|
Interceptors are pushed on to a stack."
|
||||||
|
([context interceptor-key]
|
||||||
|
(let [context (some-> context
|
||||||
|
map->Context
|
||||||
|
begin
|
||||||
|
(process-all interceptor-key)
|
||||||
|
terminate
|
||||||
|
process-any-errors
|
||||||
|
end)]
|
||||||
|
(if-let [ex (:error context)]
|
||||||
|
(throw ex)
|
||||||
|
context)))
|
||||||
|
([context interceptor-key interceptors]
|
||||||
|
(execute-only (enqueue context interceptors) interceptor-key)))
|
||||||
|
|
||||||
|
(defn execute
|
||||||
|
"Executes a queue of Interceptors attached to the context. Context
|
||||||
|
must be a map, Interceptors are added with 'enqueue'.
|
||||||
|
An Interceptor is a map or map-like object with the keys :enter,
|
||||||
|
:leave, and :error. The value of each key is a function; missing
|
||||||
|
keys or nil values are ignored. When executing a context, first all
|
||||||
|
the :enter functions are invoked in order. As this happens, the
|
||||||
|
Interceptors are pushed on to a stack.
|
||||||
|
When execution reaches the end of the queue, it begins popping
|
||||||
|
Interceptors off the stack and calling their :leave functions.
|
||||||
|
Therefore :leave functions are called in the opposite order from
|
||||||
|
:enter functions.
|
||||||
|
Both the :enter and :leave functions are called on a single
|
||||||
|
argument, the context map, and return an updated context.
|
||||||
|
If any Interceptor function throws an exception, execution stops and
|
||||||
|
begins popping Interceptors off the stack and calling their :error
|
||||||
|
functions. The :error function takes two arguments: the context and
|
||||||
|
an exception. It may either handle the exception, in which case the
|
||||||
|
execution continues with the next :leave function on the stack; or
|
||||||
|
re-throw the exception, passing control to the :error function on
|
||||||
|
the stack. If the exception reaches the end of the stack without
|
||||||
|
being handled, execute will throw it."
|
||||||
|
([context]
|
||||||
|
(let [context (some-> context
|
||||||
|
begin
|
||||||
|
enter-all
|
||||||
|
terminate
|
||||||
|
leave-all
|
||||||
|
end)]
|
||||||
|
(if-let [ex (:error context)]
|
||||||
|
(throw ex)
|
||||||
|
context)))
|
||||||
|
([context interceptors]
|
||||||
|
(execute (enqueue context interceptors))))
|
||||||
|
)
|
||||||
52
test/cljc/reitit/ring_spec_test.cljc
Normal file
52
test/cljc/reitit/ring_spec_test.cljc
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
(ns reitit.ring-spec-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[reitit.ring.spec :as rrs]
|
||||||
|
[reitit.core :as r]
|
||||||
|
[reitit.spec :as rs])
|
||||||
|
#?(:clj
|
||||||
|
(:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
||||||
|
|
||||||
|
(deftest route-data-validation-test
|
||||||
|
(testing "validation is turned off by default"
|
||||||
|
(is (true? (r/router?
|
||||||
|
(r/router
|
||||||
|
["/api" {:handler "identity"}])))))
|
||||||
|
|
||||||
|
(testing "with default spec validates :name, :handler and :middleware"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Invalid route data"
|
||||||
|
(ring/router
|
||||||
|
["/api" {:handler "identity"}]
|
||||||
|
{:validate rrs/validate-spec!})))
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Invalid route data"
|
||||||
|
(ring/router
|
||||||
|
["/api" {:handler identity
|
||||||
|
:name "kikka"}]
|
||||||
|
{:validate rrs/validate-spec!})))
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Invalid route data"
|
||||||
|
(ring/router
|
||||||
|
["/api" {:handler identity
|
||||||
|
:middleware [{}]}]
|
||||||
|
{:validate rrs/validate-spec!}))))
|
||||||
|
|
||||||
|
(testing "all endpoints are validated"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Invalid route data"
|
||||||
|
(ring/router
|
||||||
|
["/api" {:patch {:handler "identity"}}]
|
||||||
|
{:validate rrs/validate-spec!}))))
|
||||||
|
|
||||||
|
(testing "spec can be overridden"
|
||||||
|
(is (true? (r/router?
|
||||||
|
(ring/router
|
||||||
|
["/api" {:handler "identity"}]
|
||||||
|
{:spec any?
|
||||||
|
:validate rrs/validate-spec!}))))))
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
[#?(:clj clojure.spec.test.alpha :cljs cljs.spec.test.alpha) :as stest]
|
[#?(:clj clojure.spec.test.alpha :cljs cljs.spec.test.alpha) :as stest]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
[reitit.spec :as spec])
|
[reitit.spec :as rs]
|
||||||
|
[expound.alpha :as e])
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import (clojure.lang ExceptionInfo))))
|
(:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
||||||
|
|
@ -19,6 +20,8 @@
|
||||||
|
|
||||||
["/api" {}]
|
["/api" {}]
|
||||||
|
|
||||||
|
["api" {}]
|
||||||
|
|
||||||
[["/api" {}]]
|
[["/api" {}]]
|
||||||
|
|
||||||
["/api"
|
["/api"
|
||||||
|
|
@ -34,9 +37,6 @@
|
||||||
(r/router
|
(r/router
|
||||||
data)))
|
data)))
|
||||||
|
|
||||||
;; missing slash
|
|
||||||
["invalid" {}]
|
|
||||||
|
|
||||||
;; path
|
;; path
|
||||||
[:invalid {}]
|
[:invalid {}]
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
["/ipa"]])))
|
["/ipa"]])))
|
||||||
|
|
||||||
(testing "routes conform to spec (can't spec protocol functions)"
|
(testing "routes conform to spec (can't spec protocol functions)"
|
||||||
(is (= true (s/valid? ::spec/routes (r/routes (r/router ["/ping"]))))))
|
(is (= true (s/valid? ::rs/routes (r/routes (r/router ["/ping"]))))))
|
||||||
|
|
||||||
(testing "options"
|
(testing "options"
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
(r/router
|
(r/router
|
||||||
["/api"] opts)))
|
["/api"] opts)))
|
||||||
|
|
||||||
{:path "api"}
|
{:path :api}
|
||||||
{:path nil}
|
{:path nil}
|
||||||
{:data nil}
|
{:data nil}
|
||||||
{:expand nil}
|
{:expand nil}
|
||||||
|
|
@ -75,3 +75,28 @@
|
||||||
{:compile nil}
|
{:compile nil}
|
||||||
{:conflicts nil}
|
{:conflicts nil}
|
||||||
{:router nil}))))
|
{:router nil}))))
|
||||||
|
|
||||||
|
(deftest route-data-validation-test
|
||||||
|
(testing "validation is turned off by default"
|
||||||
|
(is (true? (r/router? (r/router
|
||||||
|
["/api" {:handler "identity"}])))))
|
||||||
|
|
||||||
|
(testing "with default spec validates :name and :handler"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Invalid route data"
|
||||||
|
(r/router
|
||||||
|
["/api" {:handler "identity"}]
|
||||||
|
{:validate rs/validate-spec!})))
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Invalid route data"
|
||||||
|
(r/router
|
||||||
|
["/api" {:name "kikka"}]
|
||||||
|
{:validate rs/validate-spec!}))))
|
||||||
|
|
||||||
|
(testing "spec can be overridden"
|
||||||
|
(is (true? (r/router? (r/router
|
||||||
|
["/api" {:handler "identity"}]
|
||||||
|
{:spec any?
|
||||||
|
:validate rs/validate-spec!}))))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue