mirror of
https://github.com/metosin/reitit.git
synced 2025-12-31 13:48:25 +00:00
commit
9cb2e01715
30 changed files with 1103 additions and 187 deletions
29
README.md
29
README.md
|
|
@ -7,6 +7,7 @@ A fast 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))
|
||||
* Helpers for [ring](https://metosin.github.io/reitit/ring/ring.html) & [the browser](https://metosin.github.io/reitit/frontend/basics.html)
|
||||
* Extendable
|
||||
* Modular
|
||||
* [Fast](https://metosin.github.io/reitit/performance.html)
|
||||
|
|
@ -15,22 +16,26 @@ Posts:
|
|||
* [Reitit, Data-Driven Routing with Clojure(Script)](https://www.metosin.fi/blog/reitit/)
|
||||
* [Data-Driven Ring with Reitit](https://www.metosin.fi/blog/reitit-ring/)
|
||||
|
||||
See the [full documentation](https://metosin.github.io/reitit/) for details.
|
||||
See the [full documentation](https://metosin.github.io/reitit/) for details.
|
||||
|
||||
There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians Slack](http://clojurians.net/) for discussion & help.
|
||||
|
||||
## Modules
|
||||
|
||||
* `reitit` - all bundled
|
||||
* `reitit-core` - the routing core
|
||||
* `reitit-ring` - a [ring router](https://metosin.github.io/reitit/ring/ring.html)
|
||||
* `reitit-middleware` - [common data-driven middleware](https://metosin.github.io/reitit/ring/default_middleware.html) for `reitit-ring`
|
||||
* `reitit-middleware` - [common middleware](https://metosin.github.io/reitit/ring/default_middleware.html) for `reitit-ring`
|
||||
* `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion
|
||||
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
|
||||
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
|
||||
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui)
|
||||
* [`reitit-frontend`](frontend/basics.md) Tools for frontend routing.
|
||||
|
||||
Bubblin' under:
|
||||
|
||||
* `reitit-http` with enchanced Pedestal-style Interceptors (WIP)
|
||||
* `reitit-frontend` with Keechma-style Controllers (WIP)
|
||||
* `reitit-http` http-routing with Pedestal-style Interceptors (WIP)
|
||||
* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors (WIP)
|
||||
|
||||
## Latest version
|
||||
|
||||
|
|
@ -44,11 +49,25 @@ Optionally, the parts can be required separately:
|
|||
|
||||
```clj
|
||||
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; coercion
|
||||
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; ring helpers
|
||||
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; swagger-support for ring & http
|
||||
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; frontend helpers (alpha)
|
||||
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; http with interceptors (alpha)
|
||||
[metosin/reitit-http "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-sieppari "0.2.0-SNAPSHOT"]
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
|
|
|||
1
dev-resources/json/json100b.json
Normal file
1
dev-resources/json/json100b.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"number":100,"boolean":true,"list":[{"kikka":"kukka"}],"nested":{"map":"this is value","secret":1}}
|
||||
1
dev-resources/json/json100k.json
Normal file
1
dev-resources/json/json100k.json
Normal file
File diff suppressed because one or more lines are too long
1
dev-resources/json/json10b.json
Normal file
1
dev-resources/json/json10b.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"imu":42}
|
||||
1
dev-resources/json/json10k.json
Normal file
1
dev-resources/json/json10k.json
Normal file
File diff suppressed because one or more lines are too long
1
dev-resources/json/json1k.json
Normal file
1
dev-resources/json/json1k.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"results":[{"gender":"male","name":{"title":"mr","first":"morris","last":"lambert"},"location":{"street":"7239 hillcrest rd","city":"nowra","state":"australian capital territory","postcode":7541},"email":"morris.lambert@example.com","login":{"username":"smallbird414","password":"carole","salt":"yO9OBSsk","md5":"658323a603522238fb32a86b82eafd55","sha1":"289f6e9a8ccd42b539e0c43283e788aeb8cd0f6e","sha256":"57bca99b2b4e78aa2171eda4db3f35e7631ca3b30f157bdc7ea089a855c66668"},"dob":"1950-07-13 09:18:34","registered":"2012-04-07 00:05:32","phone":"08-2274-7839","cell":"0452-558-702","id":{"name":"TFN","value":"740213762"},"picture":{"large":"https://randomuser.me/api/portraits/men/95.jpg","medium":"https://randomuser.me/api/portraits/med/men/95.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/men/95.jpg"},"nat":"AU"}],"info":{"seed":"fb0c2b3c7cedc7af","results":1,"page":1,"version":"1.1"}}
|
||||
|
|
@ -7,21 +7,31 @@
|
|||
* First-class [route data](./basics/route_data.md)
|
||||
* Bi-directional routing
|
||||
* [Pluggable coercion](./coercion/coercion.md) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
||||
* Helpers for [ring](./ring/ring.html) & [the browser](./frontend/basics.html)
|
||||
* Extendable
|
||||
* Modular
|
||||
* [Fast](performance.md)
|
||||
|
||||
Modules:
|
||||
|
||||
* `reitit` - all bundled
|
||||
* `reitit-core` - the routing core
|
||||
* [`reitit-ring`](ring/ring.md) with [data-driven middleware](https://metosin.github.io/reitit/ring/data_driven_middleware.html)
|
||||
* `reitit-ring` - a [ring router](./ring/ring.md)
|
||||
* `reitit-middleware` - [common middleware](./ring/default_middleware.md) for `reitit-ring`
|
||||
* `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion
|
||||
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
|
||||
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
|
||||
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui).
|
||||
* [`reitit-frontend`](frontend/basics.md) Tools for frontend routing.
|
||||
|
||||
To use Reitit, add the following dependency to your project:
|
||||
Bubblin' under:
|
||||
|
||||
* `reitit-http` http-routing with Pedestal-style Interceptors (WIP)
|
||||
* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors (WIP)
|
||||
|
||||
## Latest version
|
||||
|
||||
All bundled:
|
||||
|
||||
```clj
|
||||
[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||
|
|
@ -31,15 +41,28 @@ Optionally, the parts can be required separately:
|
|||
|
||||
```clj
|
||||
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; coercion
|
||||
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; ring helpers
|
||||
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; swagger-support for ring & http
|
||||
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
||||
[metosin/frontend "0.1.4-SNAPSHOT"]
|
||||
|
||||
;; frontend helpers (alpha)
|
||||
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
||||
|
||||
;; http with interceptors (alpha)
|
||||
[metosin/reitit-http "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-sieppari "0.2.0-SNAPSHOT"]
|
||||
```
|
||||
|
||||
For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/).
|
||||
There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians Slack](http://clojurians.net/) for discussion & help.
|
||||
|
||||
# Examples
|
||||
|
||||
|
|
|
|||
11
examples/http/.gitignore
vendored
Normal file
11
examples/http/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
.hgignore
|
||||
.hg/
|
||||
14
examples/http/README.md
Normal file
14
examples/http/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Http with Swagger example
|
||||
|
||||
## Usage
|
||||
|
||||
```clj
|
||||
> lein repl
|
||||
(start)
|
||||
```
|
||||
|
||||
Go with browser to http://localhost:3000
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2018 Metosin Oy
|
||||
6
examples/http/project.clj
Normal file
6
examples/http/project.clj
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
(defproject ring-example "0.1.0-SNAPSHOT"
|
||||
:description "Reitit Ring App with Swagger"
|
||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||
[ring "1.6.3"]
|
||||
[metosin/reitit "0.2.0-SNAPSHOT"]]
|
||||
:repl-options {:init-ns example.server})
|
||||
22
examples/http/src/example/server.clj
Normal file
22
examples/http/src/example/server.clj
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
(ns example.server
|
||||
(:require [reitit.http :as http]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.interceptor.sieppari]
|
||||
[ring.adapter.jetty :as jetty]))
|
||||
|
||||
(def app
|
||||
(http/ring-handler
|
||||
(http/router
|
||||
["/" {:get (fn [request]
|
||||
{:status 200
|
||||
:body "hello!"})}])
|
||||
(ring/routes
|
||||
(ring/create-default-handler))
|
||||
{:executor reitit.interceptor.sieppari/executor}))
|
||||
|
||||
(defn start []
|
||||
(jetty/run-jetty #'app {:port 3000, :join? false, :async? true})
|
||||
(println "server running in port 3000"))
|
||||
|
||||
(comment
|
||||
(start))
|
||||
|
|
@ -335,7 +335,7 @@
|
|||
(str ":single-static-path-router requires exactly 1 static route: " compiled-routes)
|
||||
{:routes compiled-routes})))
|
||||
(let [[n :as names] (find-names compiled-routes opts)
|
||||
[[p data result] :as compiled] compiled-routes
|
||||
[[p data result]] compiled-routes
|
||||
p #?(:clj (.intern ^String p) :cljs p)
|
||||
match (->Match p data result {} p)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,20 @@
|
|||
(into-interceptor [this data opts]))
|
||||
|
||||
(defrecord Interceptor [name enter leave error])
|
||||
(defrecord Endpoint [data interceptors])
|
||||
(defrecord Endpoint [data interceptors queue])
|
||||
(defrecord Context [request response exception])
|
||||
|
||||
(defprotocol Executor
|
||||
(queue
|
||||
[this interceptors]
|
||||
"takes a sequence of interceptors and compiles them to queue for the executor")
|
||||
(execute
|
||||
[this interceptors request]
|
||||
[this interceptors request respond raise]
|
||||
"executes the interceptor chain"))
|
||||
|
||||
(defn context [request]
|
||||
(map->Context {:request request}))
|
||||
|
||||
(def ^:dynamic *max-compile-depth* 10)
|
||||
|
||||
|
|
@ -44,9 +57,13 @@
|
|||
|
||||
#?(:clj clojure.lang.Fn
|
||||
:cljs function)
|
||||
(into-interceptor [this _ _]
|
||||
(map->Interceptor
|
||||
{:enter this}))
|
||||
(into-interceptor [this data opts]
|
||||
(into-interceptor
|
||||
{:name ::handler
|
||||
::handler this
|
||||
:enter (fn [ctx]
|
||||
(assoc ctx :response (this (:request ctx))))}
|
||||
data opts))
|
||||
|
||||
#?(:clj clojure.lang.PersistentArrayMap
|
||||
:cljs cljs.core.PersistentArrayMap)
|
||||
|
|
@ -78,45 +95,33 @@
|
|||
nil
|
||||
(into-interceptor [_ _ _]))
|
||||
|
||||
(defn- ensure-handler! [path data scope]
|
||||
(when-not (:handler data)
|
||||
(throw (ex-info
|
||||
(str "path \"" path "\" doesn't have a :handler defined"
|
||||
(if scope (str " for " scope)))
|
||||
(merge {:path path, :data data}
|
||||
(if scope {:scope scope}))))))
|
||||
|
||||
(defn- expand-and-transform
|
||||
[interceptors data {:keys [::transform] :or {transform identity} :as opts}]
|
||||
(->> interceptors
|
||||
(keep #(into-interceptor % data opts))
|
||||
(transform)
|
||||
(keep #(into-interceptor % data opts))
|
||||
(into [])))
|
||||
|
||||
;;
|
||||
;; public api
|
||||
;;
|
||||
|
||||
(defn chain
|
||||
"Creates a Interceptor chain out of sequence of IntoInterceptor
|
||||
and optionally a handler. Optionally takes route data and (Router) opts."
|
||||
([interceptors handler data]
|
||||
(chain interceptors handler data nil))
|
||||
([interceptors handler data opts]
|
||||
(let [interceptor (some-> (into-interceptor handler data opts)
|
||||
(assoc :name (:name data)))]
|
||||
(-> (expand-and-transform interceptors data opts)
|
||||
(cond-> interceptor (conj interceptor))))))
|
||||
Optionally takes route data and (Router) opts."
|
||||
([interceptors]
|
||||
(chain interceptors nil nil))
|
||||
([interceptors data]
|
||||
(chain interceptors data nil))
|
||||
([interceptors data {:keys [::transform] :or {transform identity} :as opts}]
|
||||
(->> interceptors
|
||||
(keep #(into-interceptor % data opts))
|
||||
(transform)
|
||||
(keep #(into-interceptor % data opts))
|
||||
(into []))))
|
||||
|
||||
(defn compile-result
|
||||
([route opts]
|
||||
(compile-result route opts nil))
|
||||
([[path {:keys [interceptors handler] :as data}] opts scope]
|
||||
(ensure-handler! path data scope)
|
||||
(map->Endpoint
|
||||
{:interceptors (chain interceptors handler data opts)
|
||||
:data data})))
|
||||
([[_ {:keys [interceptors handler] :as data}] {:keys [::queue] :as opts} _]
|
||||
(let [chain (chain (into (vec interceptors) [handler]) data opts)]
|
||||
(map->Endpoint
|
||||
{:interceptors chain
|
||||
:queue ((or queue identity) chain)
|
||||
:data data}))))
|
||||
|
||||
(defn router
|
||||
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
|
||||
|
|
@ -131,8 +136,8 @@
|
|||
|
||||
Options:
|
||||
|
||||
| key | description |
|
||||
| --------------------------------|-------------|
|
||||
| key | description
|
||||
| --------------------------------|-------------
|
||||
| `:reitit.interceptor/transform` | Function of [Interceptor] => [Interceptor] to transform the expanded Interceptors (default: identity).
|
||||
| `:reitit.interceptor/registry` | Map of `keyword => IntoInterceptor` to replace keyword references into Interceptor
|
||||
|
||||
|
|
|
|||
10
modules/reitit-http/project.clj
Normal file
10
modules/reitit-http/project.clj
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
(defproject metosin/reitit-http "0.2.0-SNAPSHOT"
|
||||
:description "Reitit: HTTP routing with interceptors"
|
||||
:url "https://github.com/metosin/reitit"
|
||||
:license {:name "Eclipse Public License"
|
||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||
:plugins [[lein-parent "0.3.2"]]
|
||||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[metosin/reitit-ring]])
|
||||
124
modules/reitit-http/src/reitit/http.cljc
Normal file
124
modules/reitit-http/src/reitit/http.cljc
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
(ns reitit.http
|
||||
(:require [meta-merge.core :refer [meta-merge]]
|
||||
[reitit.interceptor :as interceptor]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.core :as r]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
(defrecord Endpoint [data interceptors queue handler path method])
|
||||
|
||||
(defn coerce-handler [[path data] {:keys [expand] :as opts}]
|
||||
[path (reduce
|
||||
(fn [acc method]
|
||||
(if (contains? acc method)
|
||||
(update acc method expand opts)
|
||||
acc)) data ring/http-methods)])
|
||||
|
||||
(defn compile-result [[path data] opts]
|
||||
(let [[top childs] (ring/group-keys data)
|
||||
compile (fn [[path data] opts scope]
|
||||
(interceptor/compile-result [path data] opts scope))
|
||||
->endpoint (fn [p d m s]
|
||||
(let [compiled (compile [p d] opts s)]
|
||||
(-> compiled
|
||||
(map->Endpoint)
|
||||
(assoc :path p)
|
||||
(assoc :method m))))
|
||||
->methods (fn [any? data]
|
||||
(reduce
|
||||
(fn [acc method]
|
||||
(cond-> acc
|
||||
any? (assoc method (->endpoint path data method nil))))
|
||||
(ring/map->Methods {})
|
||||
ring/http-methods))]
|
||||
(if-not (seq childs)
|
||||
(->methods true top)
|
||||
(reduce-kv
|
||||
(fn [acc method data]
|
||||
(let [data (meta-merge top data)]
|
||||
(assoc acc method (->endpoint path data method method))))
|
||||
(->methods (:handler top) data)
|
||||
childs))))
|
||||
|
||||
(defn router
|
||||
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
|
||||
support for http-methods and Interceptors. See [docs](https://metosin.github.io/reitit/)
|
||||
for details.
|
||||
|
||||
Example:
|
||||
|
||||
(router
|
||||
[\"/api\" {:interceptors [format-i oauth2-i]}
|
||||
[\"/users\" {:get get-user
|
||||
:post update-user
|
||||
:delete {:interceptors [delete-i]
|
||||
:handler delete-user}}]])
|
||||
|
||||
See router options from [[reitit.core/router]] and [[reitit.middleware/router]]."
|
||||
([data]
|
||||
(router data nil))
|
||||
([data opts]
|
||||
(let [opts (meta-merge {:coerce coerce-handler, :compile compile-result} opts)]
|
||||
(r/router data opts))))
|
||||
|
||||
(defn ring-handler
|
||||
"Creates a ring-handler out of a http-router,
|
||||
a default ring-handler and options map, with the following keys:
|
||||
|
||||
| key | description |
|
||||
| ----------------|-------------|
|
||||
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
||||
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler"
|
||||
[router default-handler {:keys [executor interceptors]}]
|
||||
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
||||
default-queue (->> [default-handler]
|
||||
(concat interceptors)
|
||||
(map #(interceptor/into-interceptor % nil (r/options router)))
|
||||
(interceptor/queue executor))
|
||||
router-opts (-> (r/options router)
|
||||
(assoc ::interceptor/queue (partial interceptor/queue executor))
|
||||
(cond-> (seq interceptors)
|
||||
(update-in [:data :interceptors] (partial into (vec interceptors)))))
|
||||
router (reitit.http/router (r/routes router) router-opts)]
|
||||
(with-meta
|
||||
(fn
|
||||
([request]
|
||||
(if-let [match (r/match-by-path router (:uri request))]
|
||||
(let [method (:request-method request)
|
||||
path-params (:path-params match)
|
||||
endpoint (-> match :result method)
|
||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||
request (-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))]
|
||||
(or (interceptor/execute executor interceptors request)
|
||||
(interceptor/execute executor default-queue request)))
|
||||
(interceptor/execute executor default-queue request)))
|
||||
([request respond raise]
|
||||
(let [default #(interceptor/execute executor default-queue % respond raise)]
|
||||
(if-let [match (r/match-by-path router (:uri request))]
|
||||
(let [method (:request-method request)
|
||||
path-params (:path-params match)
|
||||
endpoint (-> match :result method)
|
||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||
request (-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))
|
||||
respond' (fn [response]
|
||||
(if response
|
||||
(respond response)
|
||||
(default request)))]
|
||||
(if interceptors
|
||||
(interceptor/execute executor interceptors request respond' raise)
|
||||
(default request)))
|
||||
(default request)))
|
||||
nil))
|
||||
{::r/router router})))
|
||||
|
||||
(defn get-router [handler]
|
||||
(-> handler meta ::r/router))
|
||||
|
||||
(defn get-match [request]
|
||||
(::r/match request))
|
||||
53
modules/reitit-http/src/reitit/http/coercion.cljc
Normal file
53
modules/reitit-http/src/reitit/http/coercion.cljc
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
(ns reitit.http.coercion
|
||||
(:require [reitit.coercion :as coercion]
|
||||
[reitit.spec :as rs]
|
||||
[reitit.impl :as impl]))
|
||||
|
||||
(def coerce-request-interceptor
|
||||
"Interceptor for pluggable request coercion.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters from route data, otherwise does not mount."
|
||||
{:name ::coerce-request
|
||||
:spec ::rs/parameters
|
||||
:compile (fn [{:keys [coercion parameters]} opts]
|
||||
(if (and coercion parameters)
|
||||
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||
{:enter
|
||||
(fn [ctx]
|
||||
(let [request (:request ctx)
|
||||
coerced (coercion/coerce-request coercers request)
|
||||
request (impl/fast-assoc request :parameters coerced)]
|
||||
(assoc ctx :request request)))})))})
|
||||
|
||||
(def coerce-response-interceptor
|
||||
"Interceptor for pluggable response coercion.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :responses from route data, otherwise does not mount."
|
||||
{:name ::coerce-response
|
||||
:spec ::rs/responses
|
||||
:compile (fn [{:keys [coercion responses]} opts]
|
||||
(if (and coercion responses)
|
||||
(let [coercers (coercion/response-coercers coercion responses opts)]
|
||||
{:leave
|
||||
(fn [ctx]
|
||||
(let [response (coercion/coerce-response coercers (:request ctx) (:response ctx))]
|
||||
(assoc ctx :response response)))})))})
|
||||
|
||||
(def coerce-exceptions-interceptor
|
||||
"Interceptor for handling coercion exceptions.
|
||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||
and :parameters or :responses from route data, otherwise does not mount."
|
||||
{:name ::coerce-exceptions
|
||||
:compile (fn [{:keys [coercion parameters responses]} _]
|
||||
(if (and coercion (or parameters responses))
|
||||
{:error (fn [ctx]
|
||||
(let [data (ex-data (:error ctx))]
|
||||
(if-let [status (case (:type data)
|
||||
::coercion/request-coercion 400
|
||||
::coercion/response-coercion 500
|
||||
nil)]
|
||||
(let [response {:status status, :body (coercion/encode-error data)}]
|
||||
(-> ctx
|
||||
(assoc :response response)
|
||||
(assoc :error nil)))
|
||||
ctx)))}))})
|
||||
23
modules/reitit-http/src/reitit/http/spec.cljc
Normal file
23
modules/reitit-http/src/reitit/http/spec.cljc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
(ns reitit.http.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[reitit.ring.spec :as rrs]
|
||||
[reitit.interceptor :as interceptor]
|
||||
[reitit.spec :as rs]))
|
||||
|
||||
;;
|
||||
;; Specs
|
||||
;;
|
||||
|
||||
(s/def ::interceptors (s/coll-of (partial satisfies? interceptor/IntoInterceptor)))
|
||||
|
||||
(s/def ::data
|
||||
(s/keys :opt-un [::rs/handler ::rs/name ::interceptors]))
|
||||
|
||||
;;
|
||||
;; Validator
|
||||
;;
|
||||
|
||||
(defn validate-spec!
|
||||
[routes {:keys [spec ::rs/explain] :or {explain s/explain-str, spec ::data}}]
|
||||
(when-let [problems (rrs/validate-route-data routes :interceptors spec)]
|
||||
(rs/throw-on-problems! problems explain)))
|
||||
|
|
@ -3,22 +3,76 @@
|
|||
[reitit.middleware :as middleware]
|
||||
[reitit.core :as r]
|
||||
[reitit.impl :as impl]
|
||||
#?@(:clj [
|
||||
[ring.util.mime-type :as mime-type]
|
||||
[ring.util.response :as response]])
|
||||
#?@(:clj [[ring.util.mime-type :as mime-type]
|
||||
[ring.util.response :as response]])
|
||||
[clojure.string :as str]))
|
||||
|
||||
(def http-methods #{:get :head :post :put :delete :connect :options :trace :patch})
|
||||
(defrecord Methods [get head post put delete connect options trace patch])
|
||||
(defrecord Endpoint [data handler path method middleware])
|
||||
|
||||
(defn- group-keys [data]
|
||||
(defn ^:no-wiki group-keys [data]
|
||||
(reduce-kv
|
||||
(fn [[top childs] k v]
|
||||
(if (http-methods k)
|
||||
[top (assoc childs k v)]
|
||||
[(assoc top k v) childs])) [{} {}] data))
|
||||
|
||||
(defn coerce-handler [[path data] {:keys [expand] :as opts}]
|
||||
[path (reduce
|
||||
(fn [acc method]
|
||||
(if (contains? acc method)
|
||||
(update acc method expand opts)
|
||||
acc)) data http-methods)])
|
||||
|
||||
(defn compile-result [[path data] opts]
|
||||
(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)))
|
||||
->methods (fn [any? data]
|
||||
(reduce
|
||||
(fn [acc method]
|
||||
(cond-> acc
|
||||
any? (assoc method (->endpoint path data method nil))))
|
||||
(map->Methods {})
|
||||
http-methods))]
|
||||
(if-not (seq childs)
|
||||
(->methods true top)
|
||||
(reduce-kv
|
||||
(fn [acc method data]
|
||||
(let [data (meta-merge top data)]
|
||||
(assoc acc method (->endpoint path data method method))))
|
||||
(->methods (:handler top) data)
|
||||
childs))))
|
||||
|
||||
;;
|
||||
;; public api
|
||||
;;
|
||||
|
||||
(defn router
|
||||
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
|
||||
support for http-methods and Middleware. See [docs](https://metosin.github.io/reitit/)
|
||||
for details.
|
||||
|
||||
Example:
|
||||
|
||||
(router
|
||||
[\"/api\" {:middleware [wrap-format wrap-oauth2]}
|
||||
[\"/users\" {:get get-user
|
||||
:post update-user
|
||||
:delete {:middleware [wrap-delete]
|
||||
:handler delete-user}}]])
|
||||
|
||||
See router options from [[reitit.core/router]] and [[reitit.middleware/router]]."
|
||||
([data]
|
||||
(router data nil))
|
||||
([data opts]
|
||||
(let [opts (meta-merge {:coerce coerce-handler, :compile compile-result} opts)]
|
||||
(r/router data opts))))
|
||||
|
||||
(defn routes
|
||||
"Create a ring handler by combining several handlers into one."
|
||||
[& handlers]
|
||||
|
|
@ -55,7 +109,7 @@
|
|||
(if-let [match (::r/match request)]
|
||||
(let [method (:request-method request :any)
|
||||
result (:result match)
|
||||
handler? (or (-> result method :handler) (-> result :any :handler))
|
||||
handler? (or (-> result method) (-> result :any))
|
||||
error-handler (if handler? not-acceptable method-not-allowed)]
|
||||
(error-handler request))
|
||||
(not-found request)))
|
||||
|
|
@ -156,7 +210,8 @@
|
|||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))]
|
||||
((routes handler default-handler) request respond raise))
|
||||
(default-handler request respond raise))))
|
||||
(default-handler request respond raise))
|
||||
nil))
|
||||
{::r/router router}))))
|
||||
|
||||
(defn get-router [handler]
|
||||
|
|
@ -164,54 +219,3 @@
|
|||
|
||||
(defn get-match [request]
|
||||
(::r/match request))
|
||||
|
||||
(defn coerce-handler [[path data] {:keys [expand] :as opts}]
|
||||
[path (reduce
|
||||
(fn [acc method]
|
||||
(if (contains? acc method)
|
||||
(update acc method expand opts)
|
||||
acc)) data http-methods)])
|
||||
|
||||
(defn compile-result [[path data] opts]
|
||||
(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)))
|
||||
->methods (fn [any? data]
|
||||
(reduce
|
||||
(fn [acc method]
|
||||
(cond-> acc
|
||||
any? (assoc method (->endpoint path data method nil))))
|
||||
(map->Methods {})
|
||||
http-methods))]
|
||||
(if-not (seq childs)
|
||||
(->methods true top)
|
||||
(reduce-kv
|
||||
(fn [acc method data]
|
||||
(let [data (meta-merge top data)]
|
||||
(assoc acc method (->endpoint path data method method))))
|
||||
(->methods (:handler top) data)
|
||||
childs))))
|
||||
|
||||
(defn router
|
||||
"Creates a [[reitit.core/Router]] from raw route data and optionally an options map with
|
||||
support for http-methods and Middleware. See [docs](https://metosin.github.io/reitit/)
|
||||
for details.
|
||||
|
||||
Example:
|
||||
|
||||
(router
|
||||
[\"/api\" {:middleware [wrap-format wrap-oauth2]}
|
||||
[\"/users\" {:get get-user
|
||||
:post update-user
|
||||
:delete {:middleware [wrap-delete]
|
||||
:handler delete-user}}]])
|
||||
|
||||
See router options from [[reitit.core/router]] and [[reitit.middleware/router]]."
|
||||
([data]
|
||||
(router data nil))
|
||||
([data opts]
|
||||
(let [opts (meta-merge {:coerce coerce-handler, :compile compile-result} opts)]
|
||||
(r/router data opts))))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
(defn handle-coercion-exception [e respond raise]
|
||||
(let [data (ex-data e)]
|
||||
(if-let [status (condp = (:type data)
|
||||
(if-let [status (case (:type data)
|
||||
::coercion/request-coercion 400
|
||||
::coercion/response-coercion 500
|
||||
nil)]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
;; Specs
|
||||
;;
|
||||
|
||||
(s/def ::middleware (s/coll-of #(satisfies? middleware/IntoMiddleware %)))
|
||||
(s/def ::middleware (s/coll-of (partial satisfies? middleware/IntoMiddleware)))
|
||||
|
||||
(s/def ::data
|
||||
(s/keys :req-un [::rs/handler]
|
||||
|
|
@ -26,11 +26,12 @@
|
|||
:non-specs non-specs})))
|
||||
(s/merge-spec-impl (vec specs) (vec specs) nil))
|
||||
|
||||
(defn- validate-ring-route-data [routes spec]
|
||||
(defn validate-route-data [routes key spec]
|
||||
(->> (for [[p _ c] routes
|
||||
[method {:keys [data middleware] :as endpoint}] c
|
||||
[method {:keys [data] :as endpoint}] c
|
||||
:when endpoint
|
||||
:let [mw-specs (seq (keep :spec middleware))
|
||||
:let [target (key endpoint)
|
||||
mw-specs (seq (keep :spec target))
|
||||
specs (keep identity (into [spec] mw-specs))
|
||||
spec (merge-specs specs)]]
|
||||
(when-let [problems (and spec (s/explain-data spec data))]
|
||||
|
|
@ -39,5 +40,5 @@
|
|||
|
||||
(defn validate-spec!
|
||||
[routes {:keys [spec ::rs/explain] :or {explain s/explain-str, spec ::data}}]
|
||||
(when-let [problems (validate-ring-route-data routes spec)]
|
||||
(when-let [problems (validate-route-data routes :middleware spec)]
|
||||
(rs/throw-on-problems! problems explain)))
|
||||
|
|
|
|||
10
modules/reitit-sieppari/project.clj
Normal file
10
modules/reitit-sieppari/project.clj
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
(defproject metosin/reitit-sieppari "0.2.0-SNAPSHOT"
|
||||
:description "Reitit: Sieppari Interceptors"
|
||||
:url "https://github.com/metosin/reitit"
|
||||
:license {:name "Eclipse Public License"
|
||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||
:plugins [[lein-parent "0.3.2"]]
|
||||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[metosin/sieppari]])
|
||||
18
modules/reitit-sieppari/src/reitit/interceptor/sieppari.clj
Normal file
18
modules/reitit-sieppari/src/reitit/interceptor/sieppari.clj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
(ns reitit.interceptor.sieppari
|
||||
(:require [reitit.interceptor :as interceptor]
|
||||
[sieppari.queue :as queue]
|
||||
[sieppari.core :as sieppari]))
|
||||
|
||||
(def executor
|
||||
(reify
|
||||
interceptor/Executor
|
||||
(queue [_ interceptors]
|
||||
(queue/into-queue
|
||||
(map
|
||||
(fn [{:keys [::interceptor/handler] :as interceptor}]
|
||||
(or handler interceptor))
|
||||
interceptors)))
|
||||
(execute [_ interceptors request]
|
||||
(sieppari/execute interceptors request))
|
||||
(execute [_ interceptors request respond raise]
|
||||
(sieppari/execute interceptors request respond raise))))
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
(s/def ::summary string?)
|
||||
(s/def ::description string?)
|
||||
|
||||
(s/def ::swagger (s/keys :req-un [::id]))
|
||||
(s/def ::swagger (s/keys :opt-un [::id]))
|
||||
(s/def ::spec (s/keys :opt-un [::swagger ::no-doc ::tags ::summary ::description]))
|
||||
|
||||
(def swagger-feature
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@
|
|||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:dependencies [[metosin/reitit-core]
|
||||
[metosin/reitit-ring]
|
||||
[metosin/reitit-middleware]
|
||||
[metosin/reitit-spec]
|
||||
[metosin/reitit-schema]
|
||||
[metosin/reitit-ring]
|
||||
[metosin/reitit-middleware]
|
||||
[metosin/reitit-http]
|
||||
[metosin/reitit-swagger]
|
||||
[metosin/reitit-swagger-ui]
|
||||
[metosin/reitit-frontend]])
|
||||
[metosin/reitit-frontend]
|
||||
[metosin/reitit-sieppari]])
|
||||
|
|
|
|||
86
perf-test/clj/reitit/json_size_perf.cljc
Normal file
86
perf-test/clj/reitit/json_size_perf.cljc
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
(ns reitit.json-size-perf
|
||||
(:require [criterium.core :as cc]
|
||||
[reitit.perf-utils :refer :all]
|
||||
[reitit.ring :as ring]
|
||||
[muuntaja.middleware :as mm]
|
||||
[reitit.coercion.spec]
|
||||
[reitit.ring.coercion]
|
||||
[jsonista.core :as j]))
|
||||
|
||||
;;
|
||||
;; start repl with `lein perf repl`
|
||||
;; perf measured with the following setup:
|
||||
;;
|
||||
;; Model Name: MacBook Pro
|
||||
;; Model Identifier: MacBookPro113
|
||||
;; Processor Name: Intel Core i7
|
||||
;; Processor Speed: 2,5 GHz
|
||||
;; Number of Processors: 1
|
||||
;; Total Number of Cores: 4
|
||||
;; L2 Cache (per Core): 256 KB
|
||||
;; L3 Cache: 6 MB
|
||||
;; Memory: 16 GB
|
||||
;;
|
||||
|
||||
(defn test! []
|
||||
(let [json-request (fn [data]
|
||||
{:uri "/echo"
|
||||
:request-method :post
|
||||
:headers {"content-type" "application/json"
|
||||
"accept" "application/json"}
|
||||
:body (j/write-value-as-string data)})
|
||||
request-stream (fn [request]
|
||||
(let [b (.getBytes ^String (:body request))]
|
||||
(fn []
|
||||
(assoc request :body (java.io.ByteArrayInputStream. b)))))
|
||||
app (ring/ring-handler
|
||||
(ring/router
|
||||
["/echo"
|
||||
{:post {:parameters {:body any?}
|
||||
:coercion reitit.coercion.spec/coercion
|
||||
:handler (fn [request]
|
||||
(let [body (-> request :parameters :body)]
|
||||
{:status 200
|
||||
:body body}))}}]
|
||||
{:data {:middleware [mm/wrap-format
|
||||
reitit.ring.coercion/coerce-request-middleware]}}))]
|
||||
(doseq [file ["dev-resources/json/json10b.json"
|
||||
"dev-resources/json/json100b.json"
|
||||
"dev-resources/json/json1k.json"
|
||||
"dev-resources/json/json10k.json"
|
||||
"dev-resources/json/json100k.json"]
|
||||
:let [data (j/read-value (slurp file))
|
||||
request (json-request data)
|
||||
request! (request-stream request)]]
|
||||
|
||||
"10b"
|
||||
;; 38µs (c-api 1.x)
|
||||
;; 14µs (c-api 2.0.0-alpha21)
|
||||
;; 6µs
|
||||
|
||||
"100b"
|
||||
;; 74µs (c-api 1.x)
|
||||
;; 16µs (c-api 2.0.0-alpha21)
|
||||
;; 8µs
|
||||
|
||||
"1k"
|
||||
;; 322µs (c-api 1.x)
|
||||
;; 24µs (c-api 2.0.0-alpha21)
|
||||
;; 16µs
|
||||
|
||||
"10k"
|
||||
;; 3300µs (c-api 1.x)
|
||||
;; 120µs (c-api 2.0.0-alpha21)
|
||||
;; 110µs
|
||||
|
||||
"100k"
|
||||
;; 10600µs (c-api 1.x)
|
||||
;; 1100µs (c-api 2.0.0-alpha21)
|
||||
;; 1100µs
|
||||
|
||||
(title file)
|
||||
#_(println (-> (request!) app :body slurp))
|
||||
(cc/quick-bench (app (request!))))))
|
||||
|
||||
(comment
|
||||
(test!))
|
||||
|
|
@ -5,6 +5,8 @@
|
|||
[reitit.interceptor :as interceptor]
|
||||
|
||||
reitit.chain
|
||||
sieppari.queue
|
||||
sieppari.core
|
||||
io.pedestal.interceptor
|
||||
io.pedestal.interceptor.chain))
|
||||
|
||||
|
|
@ -34,37 +36,80 @@
|
|||
(def +items+ 10)
|
||||
|
||||
(defn expected! [x]
|
||||
(assert (= (range +items+) (:values x))))
|
||||
(println x)
|
||||
#_(assert (= (range +items+) (:values x))))
|
||||
|
||||
(defn middleware [handler value]
|
||||
(fn [request]
|
||||
(let [values (or (:values request) [])]
|
||||
(handler (assoc request :values (conj values value))))))
|
||||
(handler request)))
|
||||
|
||||
(def map-request {})
|
||||
(def record-request (map->RequestOrContext map-request))
|
||||
|
||||
(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)]
|
||||
app (middleware/chain mw identity)]
|
||||
|
||||
;; 1000ns
|
||||
;; 18ns (identity)
|
||||
(title "middleware - map")
|
||||
(expected! (app map-request))
|
||||
(cc/quick-bench
|
||||
(app map-request))
|
||||
|
||||
;; 365ns
|
||||
;; 21ns (identity)
|
||||
(title "middleware - record")
|
||||
(expected! (app record-request))
|
||||
(cc/quick-bench
|
||||
(app record-request))
|
||||
|
||||
;; 6900ns
|
||||
;; 6900ns
|
||||
;; 10000ns (identity)
|
||||
(title "middleware - dynamic")
|
||||
(expected! ((middleware/chain mw identity) record-request))
|
||||
(cc/quick-bench
|
||||
((middleware/chain mw identity) record-request))))
|
||||
|
||||
(defn sieppari-test []
|
||||
(let [interceptors (conj
|
||||
(mapv
|
||||
(fn [value]
|
||||
{:enter identity})
|
||||
(range +items+))
|
||||
identity)
|
||||
queue (sieppari.queue/into-queue interceptors)
|
||||
app (fn [req] (sieppari.core/execute interceptors req))
|
||||
app2 (fn [req] (sieppari.core/execute queue req))]
|
||||
|
||||
;; 5500ns
|
||||
;; 4000ns (identity)
|
||||
(title "sieppari - map")
|
||||
(expected! (app map-request))
|
||||
(cc/quick-bench
|
||||
(app map-request))
|
||||
|
||||
;; 4600ns
|
||||
;; 3800ns (identity)
|
||||
(title "sieppari - record")
|
||||
(expected! (app record-request))
|
||||
(cc/quick-bench
|
||||
(app record-request))
|
||||
|
||||
;; 2200ns
|
||||
;; 1300ns (identity)
|
||||
(title "sieppari - map (compiled queue)")
|
||||
(expected! (app2 map-request))
|
||||
(cc/quick-bench
|
||||
(app2 map-request))
|
||||
|
||||
;; 1600ns
|
||||
;; 1300ns (identity)
|
||||
(title "sieppari - record (compiled queue)")
|
||||
(expected! (app2 record-request))
|
||||
(cc/quick-bench
|
||||
(app2 record-request))))
|
||||
|
||||
;;
|
||||
;; Reduce
|
||||
;;
|
||||
|
|
@ -108,25 +153,15 @@
|
|||
(defn pedestal-chain-text []
|
||||
(let [is (map io.pedestal.interceptor/interceptor
|
||||
(map (fn [value]
|
||||
{:enter (interceptor value)}) (range +items+)))
|
||||
{:enter identity}) (range +items+)))
|
||||
ctx (io.pedestal.interceptor.chain/enqueue nil is)]
|
||||
|
||||
;; 8400ns
|
||||
;; 7200ns (identity)
|
||||
(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
|
||||
;;
|
||||
|
|
@ -239,8 +274,8 @@
|
|||
(comment
|
||||
(interceptor-test)
|
||||
(middleware-test)
|
||||
(sieppari-test)
|
||||
(pedestal-chain-text)
|
||||
(pedestal-tuned-chain-text)
|
||||
(interceptor-chain-test))
|
||||
|
||||
; Middleware (static chain) => 5µs
|
||||
|
|
|
|||
19
project.clj
19
project.clj
|
|
@ -11,20 +11,23 @@
|
|||
|
||||
:managed-dependencies [[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-http "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
||||
[metosin/reitit-sieppari "0.2.0-SNAPSHOT"]
|
||||
[meta-merge "1.0.0"]
|
||||
[ring/ring-core "1.6.3"]
|
||||
[metosin/spec-tools "0.7.1"]
|
||||
[metosin/schema-tools "0.10.3"]
|
||||
[metosin/ring-swagger-ui "2.2.10"]
|
||||
[metosin/muuntaja "0.6.0-alpha1"]
|
||||
[metosin/jsonista "0.2.1"]]
|
||||
[metosin/muuntaja "0.6.0-alpha3"]
|
||||
[metosin/jsonista "0.2.1"]
|
||||
[metosin/sieppari "0.0.0-alpha4"]]
|
||||
|
||||
:plugins [[jonase/eastwood "0.2.6"]
|
||||
[lein-doo "0.1.10"]
|
||||
|
|
@ -39,12 +42,14 @@
|
|||
:source-paths ["modules/reitit/src"
|
||||
"modules/reitit-core/src"
|
||||
"modules/reitit-ring/src"
|
||||
"modules/reitit-http/src"
|
||||
"modules/reitit-middleware/src"
|
||||
"modules/reitit-spec/src"
|
||||
"modules/reitit-schema/src"
|
||||
"modules/reitit-swagger/src"
|
||||
"modules/reitit-swagger-ui/src"
|
||||
"modules/reitit-frontend/src"]
|
||||
"modules/reitit-frontend/src"
|
||||
"modules/reitit-sieppari/src"]
|
||||
|
||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||
[org.clojure/clojurescript "1.10.339"]
|
||||
|
|
@ -57,8 +62,9 @@
|
|||
|
||||
[ring "1.6.3"]
|
||||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||
[metosin/muuntaja "0.6.0-alpha1"]
|
||||
[metosin/muuntaja "0.6.0-alpha3"]
|
||||
[metosin/ring-swagger-ui "2.2.10"]
|
||||
[metosin/sieppari "0.0.0-alpha4"]
|
||||
[metosin/jsonista "0.2.1"]
|
||||
|
||||
[criterium "0.4.4"]
|
||||
|
|
@ -77,6 +83,7 @@
|
|||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||
[io.pedestal/pedestal.route "0.5.4"]
|
||||
[org.clojure/core.async "0.4.474"]
|
||||
[metosin/sieppari "0.0.0-alpha4"]
|
||||
[yada "1.2.13"]
|
||||
[ring/ring-defaults "0.3.1"]
|
||||
[ataraxy "0.4.0"]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,17 @@
|
|||
set -e
|
||||
|
||||
# Modules
|
||||
for ext in reitit-core reitit-ring reitit-middleware reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit-frontend reitit; do
|
||||
for ext in \
|
||||
reitit-core \
|
||||
reitit-spec \
|
||||
reitit-schema \
|
||||
reitit-ring \
|
||||
reitit-middleware \
|
||||
reitit-http \
|
||||
reitit-swagger \
|
||||
reitit-swagger-ui \
|
||||
reitit-frontend \
|
||||
reitit-sieppari \
|
||||
reitit; do
|
||||
cd modules/$ext; lein "$@"; cd ../..;
|
||||
done
|
||||
|
|
|
|||
422
test/clj/reitit/http_test.clj
Normal file
422
test/clj/reitit/http_test.clj
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
(ns reitit.http-test
|
||||
"just Clojure before Sieppari is ported into cljs"
|
||||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[clojure.set :as set]
|
||||
[reitit.interceptor :as interceptor]
|
||||
[reitit.interceptor.sieppari :as sieppari]
|
||||
[reitit.http :as http]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.core :as r]))
|
||||
|
||||
(defn interceptor [name]
|
||||
{:enter (fn [ctx] (update-in ctx [:request ::i] (fnil conj []) name))})
|
||||
|
||||
(defn handler [{:keys [::i]}]
|
||||
{:status 200 :body (conj i :ok)})
|
||||
|
||||
(deftest http-router-test
|
||||
|
||||
(testing "http-handler"
|
||||
(let [api-interceptor (interceptor :api)
|
||||
router (http/router
|
||||
["/api" {:interceptors [api-interceptor]}
|
||||
["/all" handler]
|
||||
["/get" {:get handler}]
|
||||
["/users" {:interceptors [[interceptor :users]]
|
||||
:get handler
|
||||
:post {:handler handler
|
||||
:interceptors [[interceptor :post]]}
|
||||
:handler handler}]])
|
||||
app (http/ring-handler router nil {:executor sieppari/executor})]
|
||||
|
||||
(testing "router can be extracted"
|
||||
(is (= (r/routes router)
|
||||
(r/routes (http/get-router app)))))
|
||||
|
||||
(testing "not found"
|
||||
(is (= nil (app {:uri "/favicon.ico"}))))
|
||||
|
||||
(testing "catch all handler"
|
||||
(is (= {:status 200, :body [:api :ok]}
|
||||
(app {:uri "/api/all" :request-method :get}))))
|
||||
|
||||
(testing "just get handler"
|
||||
(is (= {:status 200, :body [:api :ok]}
|
||||
(app {:uri "/api/get" :request-method :get})))
|
||||
(is (= nil (app {:uri "/api/get" :request-method :post}))))
|
||||
|
||||
(testing "expanded method handler"
|
||||
(is (= {:status 200, :body [:api :users :ok]}
|
||||
(app {:uri "/api/users" :request-method :get}))))
|
||||
|
||||
(testing "method handler with middleware"
|
||||
(is (= {:status 200, :body [:api :users :post :ok]}
|
||||
(app {:uri "/api/users" :request-method :post}))))
|
||||
|
||||
(testing "fallback handler"
|
||||
(is (= {:status 200, :body [:api :users :ok]}
|
||||
(app {:uri "/api/users" :request-method :put}))))
|
||||
|
||||
(testing "3-arity"
|
||||
(let [result (atom nil)
|
||||
respond (partial reset! result), raise ::not-called]
|
||||
(app {:uri "/api/users" :request-method :post} respond raise)
|
||||
(is (= {:status 200, :body [:api :users :post :ok]}
|
||||
@result))))))
|
||||
|
||||
(testing "named routes"
|
||||
(let [router (http/router
|
||||
[["/api"
|
||||
["/all" {:handler handler :name ::all}]
|
||||
["/get" {:get {:handler handler :name ::HIDDEN}
|
||||
:name ::get}]
|
||||
["/users" {:get handler
|
||||
:post handler
|
||||
:handler handler
|
||||
:name ::users}]]])
|
||||
app (http/ring-handler router nil {:executor sieppari/executor})]
|
||||
|
||||
(testing "router can be extracted"
|
||||
(is (= (r/routes router)
|
||||
(r/routes (http/get-router app)))))
|
||||
|
||||
(testing "only top-level route names are matched"
|
||||
(is (= [::all ::get ::users]
|
||||
(r/route-names router))))
|
||||
|
||||
(testing "all named routes can be matched"
|
||||
(doseq [name (r/route-names router)]
|
||||
(is (= name (-> (r/match-by-name router name) :data :name))))))))
|
||||
|
||||
(def enforce-roles-interceptor
|
||||
{:enter (fn [{{:keys [::roles] :as request} :request :as ctx}]
|
||||
(let [required (some-> request (http/get-match) :data ::roles)]
|
||||
(if (and (seq required) (not (set/intersection required roles)))
|
||||
(-> ctx
|
||||
(assoc :response {:status 403, :body "forbidden"})
|
||||
(assoc :queue nil))
|
||||
ctx)))})
|
||||
|
||||
(deftest enforcing-data-rules-at-runtime-test
|
||||
(let [handler (constantly {:status 200, :body "ok"})
|
||||
app (http/ring-handler
|
||||
(http/router
|
||||
[["/api"
|
||||
["/ping" handler]
|
||||
["/admin" {::roles #{:admin}}
|
||||
["/ping" handler]]]]
|
||||
{:data {:interceptors [enforce-roles-interceptor]}})
|
||||
nil {:executor sieppari/executor})]
|
||||
|
||||
(testing "public handler"
|
||||
(is (= {:status 200, :body "ok"}
|
||||
(app {:uri "/api/ping" :request-method :get}))))
|
||||
|
||||
(testing "runtime-enforced handler"
|
||||
(testing "without needed roles"
|
||||
(is (= {:status 403 :body "forbidden"}
|
||||
(app {:uri "/api/admin/ping"
|
||||
:request-method :get}))))
|
||||
(testing "with needed roles"
|
||||
(is (= {:status 200, :body "ok"}
|
||||
(app {:uri "/api/admin/ping"
|
||||
:request-method :get
|
||||
::roles #{:admin}})))))))
|
||||
|
||||
(deftest default-handler-test
|
||||
(let [response {:status 200, :body "ok"}
|
||||
router (http/router
|
||||
[["/ping" {:get (constantly response)}]
|
||||
["/pong" (constantly nil)]])
|
||||
app (http/ring-handler router nil {:executor sieppari/executor})]
|
||||
|
||||
(testing "match"
|
||||
(is (= response (app {:request-method :get, :uri "/ping"}))))
|
||||
|
||||
(testing "no match"
|
||||
(testing "with defaults"
|
||||
(testing "route doesn't match yields nil"
|
||||
(is (= nil (app {:request-method :get, :uri "/"}))))
|
||||
(testing "method doesn't match yields nil"
|
||||
(is (= nil (app {:request-method :post, :uri "/ping"}))))
|
||||
(testing "handler rejects yields nil"
|
||||
(is (= nil (app {:request-method :get, :uri "/pong"})))))
|
||||
|
||||
(testing "with default http responses"
|
||||
(let [app (http/ring-handler
|
||||
router
|
||||
(ring/create-default-handler)
|
||||
{:executor sieppari/executor})]
|
||||
(testing "route doesn't match yields 404"
|
||||
(is (= 404 (:status (app {:request-method :get, :uri "/"})))))
|
||||
(testing "method doesn't match yields 405"
|
||||
(is (= 405 (:status (app {:request-method :post, :uri "/ping"})))))
|
||||
(testing "handler rejects yields nil"
|
||||
(is (= 406 (:status (app {:request-method :get, :uri "/pong"})))))))
|
||||
|
||||
(testing "with custom http responses"
|
||||
(let [app (http/ring-handler
|
||||
router
|
||||
(ring/create-default-handler
|
||||
{:not-found (constantly {:status -404})
|
||||
:method-not-allowed (constantly {:status -405})
|
||||
:not-acceptable (constantly {:status -406})})
|
||||
{:executor sieppari/executor})]
|
||||
(testing "route doesn't match"
|
||||
(is (= -404 (:status (app {:request-method :get, :uri "/"})))))
|
||||
(testing "method doesn't match"
|
||||
(is (= -405 (:status (app {:request-method :post, :uri "/ping"})))))
|
||||
(testing "handler rejects"
|
||||
(is (= -406 (:status (app {:request-method :get, :uri "/pong"}))))))))))
|
||||
|
||||
(deftest async-http-test
|
||||
(let [promise #(let [value (atom ::nil)]
|
||||
(fn
|
||||
([] @value)
|
||||
([x]
|
||||
(reset! value x))))
|
||||
response {:status 200, :body "ok"}
|
||||
router (http/router
|
||||
[["/ping" {:get (fn [_] response)}]
|
||||
["/pong" (fn [_] nil)]])
|
||||
app (http/ring-handler router nil {:executor sieppari/executor})]
|
||||
|
||||
(testing "match"
|
||||
(let [respond (promise)
|
||||
raise (promise)]
|
||||
(app {:request-method :get, :uri "/ping"} respond raise)
|
||||
(is (= response (respond)))
|
||||
(is (= ::nil (raise)))))
|
||||
|
||||
(testing "no match"
|
||||
|
||||
(testing "with defaults"
|
||||
(testing "route doesn't match"
|
||||
(let [respond (promise)
|
||||
raise (promise)]
|
||||
(app {:request-method :get, :uri "/"} respond raise)
|
||||
(is (= nil (respond)))
|
||||
(is (= ::nil (raise)))))
|
||||
(testing "method doesn't match"
|
||||
(let [respond (promise)
|
||||
raise (promise)]
|
||||
(app {:request-method :post, :uri "/ping"} respond raise)
|
||||
(is (= nil (respond)))
|
||||
(is (= ::nil (raise)))))
|
||||
(testing "handler rejects"
|
||||
(let [respond (promise)
|
||||
raise (promise)]
|
||||
(app {:request-method :get, :uri "/pong"} respond raise)
|
||||
(is (= nil (respond)))
|
||||
(is (= ::nil (raise))))))
|
||||
|
||||
(testing "with default http responses"
|
||||
|
||||
(let [app (http/ring-handler router (ring/create-default-handler) {:executor sieppari/executor})]
|
||||
(testing "route doesn't match"
|
||||
(let [respond (promise)
|
||||
raise (promise)]
|
||||
(app {:request-method :get, :uri "/"} respond raise)
|
||||
(is (= 404 (:status (respond))))
|
||||
(is (= ::nil (raise)))))
|
||||
(testing "method doesn't match"
|
||||
(let [respond (promise)
|
||||
raise (promise)]
|
||||
(app {:request-method :post, :uri "/ping"} respond raise)
|
||||
(is (= 405 (:status (respond))))
|
||||
(is (= ::nil (raise)))))
|
||||
(testing "if handler rejects"
|
||||
(let [respond (promise)
|
||||
raise (promise)]
|
||||
(app {:request-method :get, :uri "/pong"} respond raise)
|
||||
(is (= 406 (:status (respond))))
|
||||
(is (= ::nil (raise))))))))))
|
||||
|
||||
(deftest interceptor-transform-test
|
||||
(let [interceptor (fn [name] {:name name
|
||||
:enter (fn [ctx]
|
||||
(update-in ctx [:request ::i] (fnil conj []) name))})
|
||||
handler (fn [{:keys [::i]}] {:status 200 :body (conj i :ok)})
|
||||
request {:uri "/api/avaruus" :request-method :get}
|
||||
create (fn [options]
|
||||
(http/ring-handler
|
||||
(http/router
|
||||
["/api" {:interceptors [(interceptor :olipa)]}
|
||||
["/avaruus" {:interceptors [(interceptor :kerran)]
|
||||
:get {:handler handler
|
||||
:interceptors [(interceptor :avaruus)]}}]]
|
||||
options)
|
||||
nil
|
||||
{:executor sieppari/executor}))]
|
||||
|
||||
(testing "by default, all middleware are applied in order"
|
||||
(let [app (create nil)]
|
||||
(is (= {:status 200, :body [:olipa :kerran :avaruus :ok]}
|
||||
(app request)))))
|
||||
|
||||
(testing "middleware can be re-ordered"
|
||||
(let [app (create {::interceptor/transform (partial sort-by :name)})]
|
||||
(is (= {:status 200, :body [:avaruus :kerran :olipa :ok]}
|
||||
(app request)))))
|
||||
|
||||
(testing "adding debug middleware between middleware"
|
||||
(let [app (create {::interceptor/transform #(interleave % (repeat (interceptor "debug")))})]
|
||||
(is (= {:status 200, :body [:olipa "debug" :kerran "debug" :avaruus "debug" :ok]}
|
||||
(app request)))))))
|
||||
|
||||
(deftest resource-handler-test
|
||||
(let [redirect (fn [uri] {:status 302, :body "", :headers {"Location" uri}})
|
||||
request (fn [uri] {:uri uri, :request-method :get})]
|
||||
(testing "inside a router"
|
||||
|
||||
(testing "from root"
|
||||
(let [app (http/ring-handler
|
||||
(http/router
|
||||
["/*" (ring/create-resource-handler)])
|
||||
(ring/create-default-handler)
|
||||
{:executor sieppari/executor})]
|
||||
(testing test
|
||||
(testing "different file-types"
|
||||
(let [response (app (request "/hello.json"))]
|
||||
(is (= "application/json" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
|
||||
(let [response (app (request "/hello.xml"))]
|
||||
(is (= "text/xml" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
|
||||
|
||||
(testing "index-files"
|
||||
(let [response (app (request "/docs"))]
|
||||
(is (= (redirect "/docs/index.html") response)))
|
||||
(let [response (app (request "/docs/"))]
|
||||
(is (= (redirect "/docs/index.html") response))))
|
||||
|
||||
(testing "not found"
|
||||
(let [response (app (request "/not-found"))]
|
||||
(is (= 404 (:status response)))))
|
||||
|
||||
(testing "3-arity"
|
||||
(let [result (atom nil)
|
||||
respond (partial reset! result)
|
||||
raise ::not-called]
|
||||
(app (request "/hello.xml") respond raise)
|
||||
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
|
||||
(is (get-in @result [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result)))))))))
|
||||
|
||||
(testing "from path"
|
||||
(let [app (http/ring-handler
|
||||
(http/router
|
||||
["/files/*" (ring/create-resource-handler)])
|
||||
(ring/create-default-handler)
|
||||
{:executor sieppari/executor})
|
||||
request #(request (str "/files" %))
|
||||
redirect #(redirect (str "/files" %))]
|
||||
(testing test
|
||||
(testing "different file-types"
|
||||
(let [response (app (request "/hello.json"))]
|
||||
(is (= "application/json" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
|
||||
(let [response (app (request "/hello.xml"))]
|
||||
(is (= "text/xml" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
|
||||
|
||||
(testing "index-files"
|
||||
(let [response (app (request "/docs"))]
|
||||
(is (= (redirect "/docs/index.html") response)))
|
||||
(let [response (app (request "/docs/"))]
|
||||
(is (= (redirect "/docs/index.html") response))))
|
||||
|
||||
(testing "not found"
|
||||
(let [response (app (request "/not-found"))]
|
||||
(is (= 404 (:status response)))))
|
||||
|
||||
(testing "3-arity"
|
||||
(let [result (atom nil)
|
||||
respond (partial reset! result)
|
||||
raise ::not-called]
|
||||
(app (request "/hello.xml") respond raise)
|
||||
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
|
||||
(is (get-in @result [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result))))))))))
|
||||
|
||||
(testing "outside a router"
|
||||
|
||||
(testing "from root"
|
||||
(let [app (http/ring-handler
|
||||
(http/router [])
|
||||
(ring/routes
|
||||
(ring/create-resource-handler {:path "/"})
|
||||
(ring/create-default-handler))
|
||||
{:executor sieppari/executor})]
|
||||
(testing test
|
||||
(testing "different file-types"
|
||||
(let [response (app (request "/hello.json"))]
|
||||
(is (= "application/json" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
|
||||
(let [response (app (request "/hello.xml"))]
|
||||
(is (= "text/xml" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
|
||||
|
||||
(testing "index-files"
|
||||
(let [response (app (request "/docs"))]
|
||||
(is (= (redirect "/docs/index.html") response)))
|
||||
(let [response (app (request "/docs/"))]
|
||||
(is (= (redirect "/docs/index.html") response))))
|
||||
|
||||
(testing "not found"
|
||||
(let [response (app (request "/not-found"))]
|
||||
(is (= 404 (:status response)))))
|
||||
|
||||
(testing "3-arity"
|
||||
(let [result (atom nil)
|
||||
respond (partial reset! result)
|
||||
raise ::not-called]
|
||||
(app (request "/hello.xml") respond raise)
|
||||
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
|
||||
(is (get-in @result [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result)))))))))
|
||||
|
||||
(testing "from path"
|
||||
(let [app (http/ring-handler
|
||||
(http/router [])
|
||||
(ring/routes
|
||||
(ring/create-resource-handler {:path "/files"})
|
||||
(ring/create-default-handler))
|
||||
{:executor sieppari/executor})
|
||||
request #(request (str "/files" %))
|
||||
redirect #(redirect (str "/files" %))]
|
||||
(testing test
|
||||
(testing "different file-types"
|
||||
(let [response (app (request "/hello.json"))]
|
||||
(is (= "application/json" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
|
||||
(let [response (app (request "/hello.xml"))]
|
||||
(is (= "text/xml" (get-in response [:headers "Content-Type"])))
|
||||
(is (get-in response [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
|
||||
|
||||
(testing "index-files"
|
||||
(let [response (app (request "/docs"))]
|
||||
(is (= (redirect "/docs/index.html") response)))
|
||||
(let [response (app (request "/docs/"))]
|
||||
(is (= (redirect "/docs/index.html") response))))
|
||||
|
||||
(testing "not found"
|
||||
(let [response (app (request "/not-found"))]
|
||||
(is (= 404 (:status response)))))
|
||||
|
||||
(testing "3-arity"
|
||||
(let [result (atom nil)
|
||||
respond (partial reset! result)
|
||||
raise ::not-called]
|
||||
(app (request "/hello.xml") respond raise)
|
||||
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
|
||||
(is (get-in @result [:headers "Last-Modified"]))
|
||||
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result))))))))))))
|
||||
|
|
@ -5,32 +5,39 @@
|
|||
#?(:clj
|
||||
(:import (clojure.lang ExceptionInfo))))
|
||||
|
||||
(def ctx (interceptor/context []))
|
||||
|
||||
(defn execute [interceptors ctx]
|
||||
(as-> ctx $
|
||||
(reduce #(%2 %1) $ (keep :enter interceptors))
|
||||
(reduce #(%2 %1) $ (reverse (keep :leave interceptors)))))
|
||||
(reduce #(%2 %1) $ (reverse (keep :leave interceptors)))
|
||||
(:response $)))
|
||||
|
||||
(def ctx [])
|
||||
(defn f [value ctx]
|
||||
(update ctx :request conj value))
|
||||
|
||||
(defn kws [k qk]
|
||||
(keyword (namespace qk) (str (name k) "_" (name qk))))
|
||||
|
||||
(defn interceptor [value]
|
||||
{:name value
|
||||
:enter #(conj % value)
|
||||
:leave #(conj % value)})
|
||||
:enter #(update % :request (fnil conj []) (kws :enter value))
|
||||
:leave #(update % :response (fnil conj []) (kws :leave value))})
|
||||
|
||||
(defn enter [value]
|
||||
{:name value
|
||||
:enter #(conj % value)})
|
||||
:enter (partial f value)})
|
||||
|
||||
(defn handler [ctx]
|
||||
(conj ctx :ok))
|
||||
(defn handler [request]
|
||||
(conj request :ok))
|
||||
|
||||
(defn create
|
||||
([interceptors]
|
||||
(create interceptors nil))
|
||||
(create interceptors nil))
|
||||
([interceptors opts]
|
||||
(let [chain (interceptor/chain
|
||||
interceptors
|
||||
handler :data opts)]
|
||||
(conj interceptors handler)
|
||||
:data opts)]
|
||||
(partial execute chain))))
|
||||
|
||||
(deftest expand-interceptor-test
|
||||
|
|
@ -41,8 +48,8 @@
|
|||
(let [calls (atom 0)
|
||||
enter (fn [value]
|
||||
(swap! calls inc)
|
||||
(fn [ctx]
|
||||
(conj ctx value)))]
|
||||
{:enter (fn [ctx]
|
||||
(update ctx :request conj value))})]
|
||||
|
||||
(testing "as function"
|
||||
(reset! calls 0)
|
||||
|
|
@ -73,14 +80,14 @@
|
|||
|
||||
(testing "as map"
|
||||
(reset! calls 0)
|
||||
(let [app (create [{:enter (enter :value)}])]
|
||||
(let [app (create [{:enter (:enter (enter :value))}])]
|
||||
(dotimes [_ 10]
|
||||
(is (= [:value :ok] (app ctx)))
|
||||
(is (= 1 @calls)))))
|
||||
|
||||
(testing "as Interceptor"
|
||||
(reset! calls 0)
|
||||
(let [app (create [(interceptor/map->Interceptor {:enter (enter :value)})])]
|
||||
(let [app (create [(interceptor/map->Interceptor {:enter (:enter (enter :value))})])]
|
||||
(dotimes [_ 10]
|
||||
(is (= [:value :ok] (app ctx)))
|
||||
(is (= 1 @calls)))))))
|
||||
|
|
@ -90,12 +97,12 @@
|
|||
i1 (fn [value]
|
||||
{:compile (fn [data _]
|
||||
(swap! calls inc)
|
||||
(fn [ctx]
|
||||
(into ctx [data value])))})
|
||||
{:enter (fn [ctx]
|
||||
(update ctx :request into [data value]))})})
|
||||
i3 (fn [value]
|
||||
{:compile (fn [data _]
|
||||
{:compile (fn [_ _]
|
||||
(swap! calls inc)
|
||||
{:compile (fn [data _]
|
||||
{:compile (fn [_ _]
|
||||
(swap! calls inc)
|
||||
(i1 value))})})]
|
||||
|
||||
|
|
@ -137,16 +144,10 @@
|
|||
(let [handler (interceptor/interceptor-handler router)]
|
||||
(fn [path]
|
||||
(when-let [interceptors (handler path)]
|
||||
(execute interceptors [])))))
|
||||
(execute interceptors ctx)))))
|
||||
|
||||
(deftest interceptor-handler-test
|
||||
|
||||
(testing "all paths should have a handler"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"path \"/ping\" doesn't have a :handler defined"
|
||||
(interceptor/router ["/ping"]))))
|
||||
|
||||
(testing "interceptor-handler"
|
||||
(let [api-interceptor (interceptor :api)
|
||||
router (interceptor/router
|
||||
|
|
@ -164,36 +165,37 @@
|
|||
(is (= [:ok] (app "/ping"))))
|
||||
|
||||
(testing "with interceptor"
|
||||
(is (= [:api :ok :api] (app "/api/ping"))))
|
||||
(is (= [:enter_api :ok :leave_api] (app "/api/ping"))))
|
||||
|
||||
(testing "with nested interceptor"
|
||||
(is (= [:api :admin :ok :admin :api] (app "/api/admin/ping"))))
|
||||
(is (= [:enter_api :enter_admin :ok :leave_admin :leave_api] (app "/api/admin/ping"))))
|
||||
|
||||
(testing ":compile interceptor can be unmounted at creation-time"
|
||||
(let [i1 {:name ::i1, :compile (constantly (interceptor ::i1))}
|
||||
i2 {:name ::i2, :compile (constantly nil)}
|
||||
i3 (interceptor ::i3)
|
||||
router (interceptor/router
|
||||
["/api" {:name ::api
|
||||
:interceptors [i1 i2 i3 i2]
|
||||
["/api" {:interceptors [i1 i2 i3 i2]
|
||||
:handler handler}])
|
||||
app (create-app router)]
|
||||
|
||||
(is (= [::i1 ::i3 :ok ::i3 ::i1] (app "/api")))
|
||||
(is (= [::enter_i1 ::enter_i3 :ok ::leave_i3 ::leave_i1] (app "/api")))
|
||||
|
||||
(testing "routes contain list of actually applied interceptors"
|
||||
(is (= [::i1 ::i3 ::api] (->> (r/compiled-routes router)
|
||||
first
|
||||
last
|
||||
:interceptors
|
||||
(map :name)))))
|
||||
(is (= [::i1 ::i3 ::interceptor/handler]
|
||||
(->> (r/compiled-routes router)
|
||||
first
|
||||
last
|
||||
:interceptors
|
||||
(map :name)))))
|
||||
|
||||
(testing "match contains list of actually applied interceptors"
|
||||
(is (= [::i1 ::i3 ::api] (->> "/api"
|
||||
(r/match-by-path router)
|
||||
:result
|
||||
:interceptors
|
||||
(map :name))))))))))
|
||||
(is (= [::i1 ::i3 ::interceptor/handler]
|
||||
(->> "/api"
|
||||
(r/match-by-path router)
|
||||
:result
|
||||
:interceptors
|
||||
(map :name))))))))))
|
||||
|
||||
(deftest chain-test
|
||||
(testing "chain can produce interceptor chain of any IntoInterceptor"
|
||||
|
|
@ -204,12 +206,12 @@
|
|||
i5 {:compile (fn [{:keys [mount?]} _]
|
||||
(when mount?
|
||||
(interceptor ::i5)))}
|
||||
chain1 (interceptor/chain [i1 i2 i3 i4 i5] handler {:mount? true})
|
||||
chain2 (interceptor/chain [i1 i2 i3 i4 i5] handler {:mount? false})
|
||||
chain3 (interceptor/chain [i1 i2 i3 i4 i5] nil {:mount? false})]
|
||||
(is (= [::i1 ::i3 ::i4 ::i5 :ok ::i5 ::i4 ::i3 ::i1] (execute chain1 [])))
|
||||
(is (= [::i1 ::i3 ::i4 :ok ::i4 ::i3 ::i1] (execute chain2 [])))
|
||||
(is (= [::i1 ::i3 ::i4 ::i4 ::i3 ::i1] (execute chain3 []))))))
|
||||
chain1 (interceptor/chain [i1 i2 i3 i4 i5 handler] {:mount? true})
|
||||
chain2 (interceptor/chain [i1 i2 i3 i4 i5 handler] {:mount? false})
|
||||
chain3 (interceptor/chain [i1 i2 i3 i4 i5] {:mount? false})]
|
||||
(is (= [::enter_i1 ::enter_i3 ::enter_i4 ::enter_i5 :ok ::leave_i5 ::leave_i4 ::leave_i3 ::leave_i1] (execute chain1 ctx)))
|
||||
(is (= [::enter_i1 ::enter_i3 ::enter_i4 :ok ::leave_i4 ::leave_i3 ::leave_i1] (execute chain2 ctx)))
|
||||
(is (= [::leave_i4 ::leave_i3 ::leave_i1] (execute chain3 ctx))))))
|
||||
|
||||
(deftest interceptor-transform-test
|
||||
(let [debug-i (enter ::debug)
|
||||
|
|
@ -227,7 +229,10 @@
|
|||
(is (= [::olipa ::kerran ::avaruus :ok] (app "/ping")))))
|
||||
|
||||
(testing "interceptors can be re-ordered"
|
||||
(let [app (create {::interceptor/transform (partial sort-by :name)})]
|
||||
(let [app (create {::interceptor/transform (fn [interceptors]
|
||||
(concat
|
||||
(sort-by :name (butlast interceptors))
|
||||
[(last interceptors)]))})]
|
||||
(is (= [::avaruus ::kerran ::olipa :ok] (app "/ping")))))
|
||||
|
||||
(testing "adding debug interceptor between interceptors"
|
||||
|
|
|
|||
Loading…
Reference in a new issue