Merge pull request #124 from metosin/http

Http-router (WIP)
This commit is contained in:
Tommi Reiman 2018-08-25 15:48:39 +03:00 committed by GitHub
commit 9cb2e01715
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1103 additions and 187 deletions

View file

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

View file

@ -0,0 +1 @@
{"number":100,"boolean":true,"list":[{"kikka":"kukka"}],"nested":{"map":"this is value","secret":1}}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"imu":42}

File diff suppressed because one or more lines are too long

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

View file

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

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

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

View file

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

View file

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

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

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

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

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

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

View file

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

View file

@ -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"]

View file

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

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

View file

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