Merge pull request #58 from metosin/bowl-of-stuff-in

Bowl of stuff in
This commit is contained in:
Tommi Reiman 2017-12-28 10:59:15 +02:00 committed by GitHub
commit 1e2d1be09b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1096 additions and 102 deletions

View file

@ -18,9 +18,9 @@ jobs:
command: ./scripts/test.sh clj
- store_test_results:
path: ~/test/target/junit.xml
- run:
name: Run coverage
command: ./scripts/submit-to-coveralls.sh clj
# - run:
# name: Run coverage
# command: ./scripts/submit-to-coveralls.sh clj
- save_cache:
key: 'v1-test-{{ checksum "project.clj" }}'
paths:

View file

@ -7,7 +7,7 @@ install:
- npm install
script:
- ./scripts/test.sh $TEST
- ./scripts/submit-to-coveralls.sh $TEST
# - ./scripts/submit-to-coveralls.sh $TEST
env:
matrix:
- TEST=clj

View file

@ -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)
* 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))
* [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
* Modular
* [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.
## Latest version
@ -56,6 +57,74 @@ Optionally, the parts can be required separately:
; :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
[Check out the full documentation!](https://metosin.github.io/reitit/)

18
dev-resources/logback.xml Normal file
View 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>

View file

@ -6,12 +6,13 @@
* [Route conflict resolution](./basics/route_conflicts.md)
* 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](./coercion/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
* [Pluggable coercion](./coercion/coercion.md) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
* Extendable
* Modular
* [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:
```clj
@ -23,7 +24,7 @@ Optionally, the parts can be required separately:
```clj
[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the 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
```

View file

@ -7,6 +7,7 @@
* [Path-based Routing](basics/path_based_routing.md)
* [Name-based Routing](basics/name_based_routing.md)
* [Route Data](basics/route_data.md)
* [Route Data Validation](basics/route_data_validation.md)
* [Route Conflicts](basics/route_conflicts.md)
* [Coercion](coercion/README.md)
* [Coercion Explained](coercion/coercion.md)

View file

@ -1,15 +1,16 @@
# 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 |
| -------------|-------------|
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (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`)
| `: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
| `: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

View file

@ -2,8 +2,6 @@
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
```clj
@ -26,12 +24,12 @@ Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec)
## 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:
```clj
[expound "0.3.0"] ; or higher
[expound "0.4.0"] ; or higher
```
Some bootstrapping:
@ -162,7 +160,3 @@ And we are ready to go:
; -------------------------
; Detected 2 errors
```
# Validating route data
*TODO*

View file

@ -5,4 +5,5 @@
* [Path-based Routing](path_based_routing.md)
* [Name-based Routing](name_based_routing.md)
* [Route Data](route_data.md)
* [Route Data Validation](route_data_validation.md)
* [Route Conflicts](route_conflicts.md)

View file

@ -1,8 +1,8 @@
# 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:

View file

@ -1,6 +1,6 @@
# 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.
@ -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).
```clj
(require '[reitit.core :as r])
(def router
(r/router
[["/ping" ::ping]

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

View file

@ -138,7 +138,7 @@ We get the coerced paremeters back. If a coercion fails, a typed (`:reitit.coerc
## 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
(require '[reitit.coercion.schema])
@ -179,9 +179,6 @@ 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. 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 `:parameters` and `:responses` syntax.
* [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

View file

@ -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-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
(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
(ring/ring-handler
@ -126,7 +133,7 @@ Invalid response:
### 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:
@ -138,7 +145,7 @@ We can query the compiled middleware chain for the routes:
:result :post :middleware
(->> (mapv :name)))
; [::mw/coerce-exceptions
; ::mw/coerce-parameters
; ::mw/coerce-request
; ::mw/coerce-response]
```
@ -160,8 +167,6 @@ Has no mounted middleware:
```
## 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
* [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

View file

@ -142,8 +142,7 @@
([routes]
(linear-router routes {}))
([routes opts]
(let [compiled (compile-routes routes opts)
names (find-names routes opts)
(let [names (find-names routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [params] :as route} (impl/create [p data result])
@ -152,7 +151,7 @@
(->PartialMatch p data result % params))]
[(conj pl route)
(if name (assoc nl name f) nl)]))
[[] {}] compiled)
[[] {}] routes)
lookup (impl/fast-map nl)]
^{:type ::router}
(reify
@ -160,7 +159,7 @@
(router-name [_]
:linear-router)
(routes [_]
compiled)
routes)
(options [_]
opts)
(route-names [_]
@ -190,14 +189,13 @@
(str "can't create :lookup-router with wildcard routes: " wilds)
{:wilds wilds
:routes routes})))
(let [compiled (compile-routes routes opts)
names (find-names routes opts)
(let [names (find-names routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
[(assoc pl p (->Match p data result {} p))
(if name
(assoc nl name #(->Match p data result % p))
nl)]) [{} {}] compiled)
nl)]) [{} {}] routes)
data (impl/fast-map pl)
lookup (impl/fast-map nl)]
^{:type ::router}
@ -205,7 +203,7 @@
(router-name [_]
:lookup-router)
(routes [_]
compiled)
routes)
(options [_]
opts)
(route-names [_]
@ -225,8 +223,7 @@
([routes]
(segment-router routes {}))
([routes opts]
(let [compiled (compile-routes routes opts)
names (find-names routes opts)
(let [names (find-names routes opts)
[pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]]
(let [{:keys [params] :as route} (impl/create [p data result])
@ -235,7 +232,7 @@
(->PartialMatch p data result % params))]
[(segment/insert pl p (->Match p data result nil nil))
(if name (assoc nl name f) nl)]))
[nil {}] compiled)
[nil {}] routes)
lookup (impl/fast-map nl)]
^{:type ::router}
(reify
@ -243,7 +240,7 @@
(router-name [_]
:segment-router)
(routes [_]
compiled)
routes)
(options [_]
opts)
(route-names [_]
@ -272,7 +269,7 @@
(str ":single-static-path-router requires exactly 1 static route: " routes)
{:routes routes})))
(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)
match (->Match p data result {} p)]
^{:type ::router}
@ -280,7 +277,7 @@
(router-name [_]
:single-static-path-router)
(routes [_]
compiled)
routes)
(options [_]
opts)
(route-names [_]
@ -304,7 +301,6 @@
(mixed-router routes {}))
([routes opts]
(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)
wildcard-router (segment-router wild opts)
static-router (->static-router lookup opts)
@ -314,7 +310,7 @@
(router-name [_]
:mixed-router)
(routes [_]
compiled)
routes)
(options [_]
opts)
(route-names [_]
@ -339,10 +335,12 @@
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (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`)
| `: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
| `: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"
([raw-routes]
(router raw-routes {}))
@ -350,6 +348,7 @@
(let [{:keys [router] :as opts} (meta-merge default-router-options opts)
routes (resolve-routes raw-routes opts)
conflicting (conflicting-routes routes)
routes (compile-routes routes opts)
wilds? (boolean (some impl/wild-route? routes))
all-wilds? (every? impl/wild-route? routes)
router (cond
@ -360,6 +359,9 @@
all-wilds? segment-router
:else mixed-router)]
(when-let [validate (:validate opts)]
(validate routes opts))
(when-let [conflicts (:conflicts opts)]
(when conflicting (conflicts conflicting)))

View file

@ -1,4 +1,5 @@
(ns reitit.segment
(:refer-clojure :exclude [-lookup])
(:require [reitit.impl :as impl]
[clojure.string :as str]))

View file

@ -8,8 +8,7 @@
;; routes
;;
(s/def ::path (s/with-gen (s/and string? #(or (str/blank? %) (str/starts-with? % "/")))
#(gen/fmap (fn [s] (str "/" s)) (s/gen string?))))
(s/def ::path (s/with-gen string? #(gen/fmap (fn [s] (str "/" s)) (s/gen string?))))
(s/def ::arg (s/and any? (complement vector?)))
(s/def ::data (s/map-of keyword? any?))
@ -33,6 +32,14 @@
(s/or :route ::route
: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
;;
@ -62,3 +69,37 @@
:args (s/or :1arity (s/cat :data (s/spec ::raw-routes))
:2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts))
: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)))

View file

@ -6,6 +6,7 @@
(def http-methods #{:get :head :patch :delete :options :post :put})
(defrecord Methods [get head post put delete trace options connect patch any])
(defrecord Endpoint [data handler path method middleware])
(defn- group-keys [data]
(reduce-kv
@ -58,14 +59,19 @@
acc)) data http-methods)])
(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)
(map->Methods {:any (middleware/compile-result [path top] opts)})
(map->Methods {:any (->endpoint path top :any nil)})
(reduce-kv
(fn [acc method data]
(let [data (meta-merge top data)]
(assoc acc method (middleware/compile-result [path data] opts method))))
(map->Methods {:any (if (:handler top) (middleware/compile-result [path data] opts))})
(assoc acc method (->endpoint path data method method))))
(map->Methods {:any (if (:handler top) (->endpoint path data :any nil))})
childs))))
(defn router

View file

@ -21,7 +21,7 @@
"Middleware for pluggable request coercion.
Expects a :coercion of type `reitit.coercion/Coercion`
and :parameters from route data, otherwise does not mount."
{:name ::coerce-parameters
{:name ::coerce-request
:compile (fn [{:keys [coercion parameters]} opts]
(if (and coercion parameters)
(let [coercers (coercion/request-coercers coercion parameters opts)]

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

View file

@ -4,18 +4,15 @@
[reitit.perf-utils :refer :all]
[clojure.spec.alpha :as s]
[spec-tools.core :as st]
[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.ring.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)))
[reitit.ring :as ring]))
;;
;; start repl with `lein perf repl`
@ -46,7 +43,7 @@
;; 4600ns
(bench!
"coerce-parameters"
(#'coercion-middleware/coerce-parameters coercers request))
(#'coercion-middleware/coerce-request-middleware coercers request))
;; 2700ns
(bench!
@ -90,7 +87,7 @@
(-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)))
(-response-coercer [this spec] (coercion/request-coercer this :response spec {})))
(comment
(doseq [coercion [nil (->NoOpCoercion) spec/coercion]]
@ -175,11 +172,15 @@
(cc/quick-bench (app req)))))
(defn json-perf-test []
(title "json")
(let [m (m/create (jsonista-format/with-json-format m/default-options))
app (ring/ring-handler
(ring/router
["/plus" {:post {:handler (fn [{{:keys [x y]} :body-params}]
{:status 200, :body {:result (+ x y)}})}}]
["/plus" {:post {:handler (fn [request]
(let [body (:body-params request)
x (:x body)
y (:y body)]
{:status 200, :body {:result (+ x y)}}))}}]
{:data {:middleware [[mm/wrap-format m]]}}))
request {:request-method :post
:uri "/plus"
@ -194,6 +195,7 @@
(-> request app :body slurp))))
(defn schema-json-perf-test []
(title "schema-json")
(let [m (m/create (jsonista-format/with-json-format m/default-options))
app (ring/ring-handler
(ring/router
@ -219,6 +221,7 @@
(-> request app :body slurp))))
(defn schema-perf-test []
(title "schema")
(let [app (ring/ring-handler
(ring/router
["/plus" {:post {:responses {200 {:schema {:result Long}}}
@ -242,6 +245,7 @@
(call))))
(defn data-spec-perf-test []
(title "data-spec")
(let [app (ring/ring-handler
(ring/router
["/plus" {:post {:responses {200 {:schema {:result int?}}}
@ -270,6 +274,7 @@
(s/def ::response (s/keys :req-un [::result]))
(defn spec-perf-test []
(title "spec")
(let [app (ring/ring-handler
(ring/router
["/plus" {:post {:responses {200 {:schema ::response}}

View file

@ -20,7 +20,7 @@
;;
(defn h [path]
(fn [req]
(fn [_]
{:status 200, :body path}))
(defn add [handler routes route]

View file

@ -1,22 +1,15 @@
(ns reitit.lupapiste-perf-test
(:require [clojure.test :refer [deftest testing is]]
[criterium.core :as cc]
[reitit.perf-utils :refer :all]
[cheshire.core :as json]
[clojure.string :as str]
[reitit.core :as reitit]
[reitit.ring :as ring]
[bidi.bidi :as bidi]
[ataraxy.core :as ataraxy]
[compojure.core :as compojure]
[io.pedestal.http.route.definition.table :as table]
[io.pedestal.http.route.map-tree :as map-tree]
[io.pedestal.http.route.router :as pedestal]
[io.pedestal.http.route :as route]))
[io.pedestal.http.route.router :as pedestal]))
;;
;; start repl with `lein perf repl`

View 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

View file

@ -1,6 +1,5 @@
(ns reitit.nodejs-perf-test
(:require [criterium.core :as cc]
[reitit.perf-utils :refer :all]
(:require [reitit.perf-utils :refer :all]
[immutant.web :as web]
[reitit.ring :as ring]))

View file

@ -1,22 +1,17 @@
(ns reitit.opensensors-perf-test
(:require [clojure.test :refer [deftest testing is]]
[criterium.core :as cc]
[reitit.perf-utils :refer :all]
(:require [reitit.perf-utils :refer :all]
[cheshire.core :as json]
[clojure.string :as str]
[reitit.core :as reitit]
[reitit.ring :as ring]
[bidi.bidi :as bidi]
[ataraxy.core :as ataraxy]
[compojure.core :refer [routes context ANY]]
[io.pedestal.http.route.definition.table :as table]
[io.pedestal.http.route.map-tree :as map-tree]
[io.pedestal.http.route.router :as pedestal]
[io.pedestal.http.route :as route]))
[io.pedestal.http.route.router :as pedestal]))
;;
;; start repl with `lein perf repl`

View file

@ -30,11 +30,7 @@
(defn bench-routes [routes req f]
(let [router (reitit/router routes)
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))]
urls (valid-urls router)]
(mapv
(fn [path]
(let [request (map->Request (req path))

View file

@ -42,7 +42,7 @@
[metosin/reitit]
[metosin/schema-tools "0.10.0-SNAPSHOT"]
[expound "0.3.4"]
[expound "0.4.0"]
[orchestra "2017.11.12-1"]
[ring "1.6.3"]

382
test/cljc/reitit/chain.clj Normal file
View 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))))
)

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

View file

@ -3,7 +3,8 @@
[#?(:clj clojure.spec.test.alpha :cljs cljs.spec.test.alpha) :as stest]
[clojure.spec.alpha :as s]
[reitit.core :as r]
[reitit.spec :as spec])
[reitit.spec :as rs]
[expound.alpha :as e])
#?(:clj
(:import (clojure.lang ExceptionInfo))))
@ -19,6 +20,8 @@
["/api" {}]
["api" {}]
[["/api" {}]]
["/api"
@ -34,9 +37,6 @@
(r/router
data)))
;; missing slash
["invalid" {}]
;; path
[:invalid {}]
@ -45,7 +45,7 @@
["/ipa"]])))
(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"
@ -67,7 +67,7 @@
(r/router
["/api"] opts)))
{:path "api"}
{:path :api}
{:path nil}
{:data nil}
{:expand nil}
@ -75,3 +75,28 @@
{:compile nil}
{:conflicts 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!}))))))