diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd3b7c23..95a2287f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+## 0.2.2-SNAPSHOT
+
+* better documentation for interceptors
+* sample apps:
+ * [Sieppari, reitit-http & swagger](https://github.com/metosin/reitit/blob/master/examples/http-swagger/src/example/server.clj)
+ * [Pedestal, reitit-http & swagger](https://github.com/metosin/reitit/blob/master/examples/pedestal-swagger/src/example/server.clj)
+
+## `reitit-middleware`
+
+* new middleware `reitit.ring.middleware.parameters/parameters-middleware` to wrap query & form params.
+
+## `reitit-interceptors`
+
+* new module like `reitit-middleware` but for interceptors. See the [Docs](https://metosin.github.io/reitit/http/default_interceptors.html).
+
## 0.2.1 (2018-09-04)
## `reitit-schema`
diff --git a/README.md b/README.md
index 924b3984..7d1fb353 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,8 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui)
* `reitit-frontend` Tools for [frontend routing]((https://metosin.github.io/reitit/frontend/basics.html))
-* `reitit-http` http-routing with Pedestal-style Interceptors (WIP)
-* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors (WIP)
+* `reitit-http` http-routing with Pedestal-style Interceptors
+* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors
## Latest version
@@ -63,7 +63,7 @@ Optionally, the parts can be required separately:
;; frontend helpers
[metosin/reitit-frontend "0.2.1"]
-;; http with interceptors (WIP)
+;; http with interceptors
[metosin/reitit-http "0.2.1"]
[metosin/reitit-sieppari "0.2.1"]
```
@@ -147,10 +147,6 @@ Invalid request:
; :in [:request :query-params]}}
```
-**NOTE**: Reitit is not a batteries included web-stack. You should also include at least:
-* content negotiation library like [Muuntaja](https://github.com/metosin/muuntaja)
-* some default Ring-middleware like `ring.middleware.params/wrap-params`
-
## More examples
* [`reitit-ring` with coercion, swagger and default middleware](https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj)
diff --git a/doc/README.md b/doc/README.md
index d93d9edd..0f617593 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -23,8 +23,8 @@ Modules:
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui).
* `reitit-frontend` Tools for [frontend routing](frontend/basics.md)
-* `reitit-http` http-routing with Pedestal-style Interceptors (WIP)
-* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors (WIP)
+* `reitit-http` http-routing with Pedestal-style Interceptors
+* `reitit-sieppari` support for [Sieppari](https://github.com/metosin/sieppari) Interceptors
## Latest version
@@ -54,7 +54,7 @@ Optionally, the parts can be required separately:
;; frontend helpers
[metosin/reitit-frontend "0.2.1"]
-;; http with interceptors (WIP)
+;; http with interceptors
[metosin/reitit-http "0.2.1"]
[metosin/reitit-sieppari "0.2.1"]
```
diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md
index e6b72f6c..e7909620 100644
--- a/doc/SUMMARY.md
+++ b/doc/SUMMARY.md
@@ -37,15 +37,18 @@
* [Compiling Middleware](ring/compiling_middleware.md)
* [Swagger Support](ring/swagger.md)
+## HTTP
+
+* [Interceptors](http/interceptors.md)
+* [Pedestal](http/pedestal.md)
+* [Sieppari](http/sieppari.md)
+* [Default Interceptors](http/default_interceptors.md)
+
## Frontend
* [Basics](frontend/basics.md)
* [Browser integration](frontend/browser.md)
-* [Controllers (WIP)](frontend/controllers.md)
-
-## HTTP
-
-* [Interceptors](http/interceptors.md)
+* [Controllers](frontend/controllers.md)
## Advanced
diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn
index 37e71e23..29778524 100644
--- a/doc/cljdoc.edn
+++ b/doc/cljdoc.edn
@@ -35,12 +35,15 @@
["Route Data Validation" {:file "doc/ring/route_data_validation.md"}]
["Compiling Middleware" {:file "doc/ring/compiling_middleware.md"}]
["Swagger Support" {:file "doc/ring/swagger.md"}]]
+ ["HTTP" {}
+ ["Interceptors" {:file "doc/http/interceptors.md"}]
+ ["Pedestal" {:file "doc/http/pedestal.md"}]
+ ["Sieppari" {:file "doc/http/sieppar.md"}]
+ ["Default Interceptors" {:file "doc/http/default_interceptors.md"}]]
["Frontend" {}
["Basics" {:file "doc/frontend/basics.md"}]
["Browser integration" {:file "doc/frontend/browser.md"}]
- ["Controllers (WIP)" {:file "doc/frontend/controllers.md"}]]
- ["HTTP" {}
- ["Interceptors" {:file "doc/http/interceptors.md"}]]
+ ["Controllers" {:file "doc/frontend/controllers.md"}]]
["Advanced" {}
["Configuring Routers" {:file "doc/advanced/configuring_routers.md"}]
["Composing Routers" {:file "doc/advanced/composing_routers.md"}]
diff --git a/doc/frontend/controllers.md b/doc/frontend/controllers.md
index 999205a2..19a00003 100644
--- a/doc/frontend/controllers.md
+++ b/doc/frontend/controllers.md
@@ -1,4 +1,4 @@
-# Controllers (WIP)
+# Controllers
* https://github.com/metosin/reitit/tree/master/examples/frontend-controllers
diff --git a/doc/http/default_interceptors.md b/doc/http/default_interceptors.md
new file mode 100644
index 00000000..e40e0e34
--- /dev/null
+++ b/doc/http/default_interceptors.md
@@ -0,0 +1,26 @@
+# Default Interceptors
+
+```clj
+[metosin/reitit-interceptors "0.2.1"]
+```
+
+Just like the [ring default middleware](../ring/default_middleware.md), but for interceptors. The default interceptors are:
+
+### Parameters handling
+* `reitit.http.interceptors.parameters/parameters-interceptor`
+
+### Exception handling
+* `reitit.http.interceptors.exception/exception-interceptor`
+
+### Content Negotiation
+* `reitit.http.interceptors.muuntaja/format-interceptor`
+* `reitit.http.interceptors.muuntaja/format-negotiate-interceptor`
+* `reitit.http.interceptors.muuntaja/format-request-interceptor`
+* `reitit.http.interceptors.muuntaja/format-response-interceptor`
+
+### Multipart request handling
+* `reitit.http.interceptors.multipart/multipart-interceptor`
+
+## Example app
+
+See an example app with the default interceptors in action: https://github.com/metosin/reitit/blob/master/examples/http-swagger/src/example/server.clj.
diff --git a/doc/http/interceptors.md b/doc/http/interceptors.md
index 5e79983f..83d314ce 100644
--- a/doc/http/interceptors.md
+++ b/doc/http/interceptors.md
@@ -1,10 +1,6 @@
-# Interceptors (WIP)
+# Interceptors
-Reitit also support for [Pedestal](pedestal.io)-style [interceptors](http://pedestal.io/reference/interceptors) as an alternative to using middleware. Basic interceptor handling is implemented in `reitit.interceptor` package. There is no interceptor executor shipped, but you can use libraries like [Pedestal Interceptor](https://github.com/pedestal/pedestal/tree/master/interceptor) or [Sieppari](https://github.com/metosin/sieppari) to execute the chains.
-
-## Current Status
-
-Work-in-progress and considered alpha quality.
+Reitit also support for [interceptors](http://pedestal.io/reference/interceptors) as an alternative to using middleware. Basic interceptor handling is implemented in `reitit.interceptor` package. There is no interceptor executor shipped, but you can use libraries like [Pedestal Interceptor](https://github.com/pedestal/pedestal/tree/master/interceptor) or [Sieppari](https://github.com/metosin/sieppari) to execute the chains.
## Reitit-http
@@ -12,18 +8,15 @@ Work-in-progress and considered alpha quality.
[metosin/reitit-http "0.2.1"]
```
-An module for http-routing using interceptors instead of middleware. Builds on top of the [`reitit-ring`](../ring/ring.md) module. The differences:
+An module for http-routing using interceptors instead of middleware. Builds on top of the [`reitit-ring`](../ring/ring.md) module having all the same features.
+
+The differences:
* instead of `:middleware`, uses `:interceptors`
-* compared to `reitit.http/http-router` takes an extra options map with mandatory key `:executor` (of type `reitit.interceptor/Executor`) and optional top level `:interceptors` - wrapping both routes and default handler.
-* optional entry poitn `reitit.http/routing-interceptor` to provide a routing interceptor, to be used with Pedestal.
+* compared to `reitit.ring/ring-router`, the `reitit.http/http-router` takes an extra options map with mandatory key `:executor` (of type `reitit.interceptor/Executor`) and optional top level `:interceptors` - wrapping both routes and default handler.
+* instead of creating a ring-handler, apps can be wrapped into a routing interceptor that enqueues the matched interceptors into the context. For this, there is `reitit.http/routing-interceptor`.
-## Examples
+## Why interceptors?
-### Sieppari
-
-See code at: https://github.com/metosin/reitit/tree/master/examples/http
-
-### Pedestal
-
-See example at: https://github.com/metosin/reitit/tree/master/examples/pedestal
+* https://quanttype.net/posts/2018-08-03-why-interceptors.html
+* https://www.reddit.com/r/Clojure/comments/9csmty/why_interceptors/
diff --git a/doc/http/pedestal.md b/doc/http/pedestal.md
new file mode 100644
index 00000000..d5113be3
--- /dev/null
+++ b/doc/http/pedestal.md
@@ -0,0 +1,25 @@
+# Pedestal
+
+[Pedestal](http://pedestal.io/) is a well known interceptor implmementation for Clojure. To use `reitit-http` with it, we need to change the default routing interceptor into a new one. Currently, there isn't a separate Pedestal-module in reitit, but the examples have the example code how to do this.
+
+## Caveat
+
+`reitit-http` defines Interceptors as `reitit.interceptor/Interceptor`. Compared to Pedestal (2-arity), reitit uses a simplified (1-arity) model for handling errors, described in the [Sieppari README](https://github.com/metosin/sieppari#differences-to-pedestal).
+
+* you can use any [pedestal-style interceptor](http://pedestal.io/reference/interceptors) within reitit router (as Pedestal is executing those anyway)
+* you can use any reitit-style interceptor that doesn't have `:error`-stage defined
+* using a reitit-style interceptor with `:error` defined will cause `ArityException` if invoked
+
+See the [error handling guide](http://pedestal.io/reference/error-handling) on how to handle errors with Pedestal.
+
+## Examples
+
+### Simple
+
+* simple example, with both sync & async code:
+ * https://github.com/metosin/reitit/tree/master/examples/pedestal
+
+### With batteries
+
+* with [default interceptors](default_interceptors.md), [coercion](../coercion/coercion.md) and [swagger](../ring/swagger.md)-support (note: exception handling is disabled):
+ * https://github.com/metosin/reitit/tree/master/examples/pedestal-swagger
diff --git a/doc/http/sieppari.md b/doc/http/sieppari.md
new file mode 100644
index 00000000..ea979619
--- /dev/null
+++ b/doc/http/sieppari.md
@@ -0,0 +1,71 @@
+# Sieppari
+
+```clj
+[metosin/reitit-sieppari "0.2.1"]
+```
+
+[Sieppari](https://github.com/metosin/sieppari) is a new and fast interceptor implementation with pluggable async ([core.async](https://github.com/clojure/core.async), [Manifold](https://github.com/ztellman/manifold) and [Promesa](http://funcool.github.io/promesa/latest)).
+
+To use Sieppari with `reitit-http`, there is `reitit-sieppari` module, which has an `reitit.interceptor.Executor` implementation for Sieppari. All reitit interceptors use the Sieppari Interceptor model, so they work seamlesly together.
+
+Synchronous Ring:
+
+```clj
+(require '[reitit.http :as http])
+(require '[reitit.interceptor.sieppari :as sieppari])
+
+(defn i [x]
+ {:enter (fn [ctx] (println "enter " x) ctx)
+ :leave (fn [ctx] (println "leave " x) ctx)})
+
+(defn handler [_]
+ (future {:status 200, :body "pong"}))
+
+(def app
+ (http/ring-handler
+ (http/router
+ ["/api"
+ {:interceptors [(i :api)]}
+
+ ["/ping"
+ {:interceptors [(i :ping)]
+ :get {:interceptors [(i :get)]
+ :handler handler}}]])
+ {:executor sieppari/executor}))
+
+(app {:request-method :get, :uri "/api/ping"})
+;enter :api
+;enter :ping
+;enter :get
+;leave :get
+;leave :ping
+;leave :api
+;=> {:status 200, :body "pong"}
+```
+
+Ring-async:
+
+```clj
+(let [respond (promise)]
+ (app {:request-method :get, :uri "/api/ping"} respond nil)
+ (deref respond 1000 ::timeout))
+;enter :api
+;enter :ping
+;enter :get
+;leave :get
+;leave :ping
+;leave :api
+;=> {:status 200, :body "pong"}
+```
+
+## Examples
+
+### Simple
+
+* simple example, with both sync & async code:
+ * https://github.com/metosin/reitit/tree/master/examples/http
+
+### With batteries
+
+* with [default interceptors](default_interceptors.md), [coercion](../coercion/coercion.md) and [swagger](../ring/swagger.md)-support:
+ * https://github.com/metosin/reitit/tree/master/examples/http-swagger
diff --git a/doc/ring/default_middleware.md b/doc/ring/default_middleware.md
index 9b1d64cc..30afc625 100644
--- a/doc/ring/default_middleware.md
+++ b/doc/ring/default_middleware.md
@@ -6,10 +6,18 @@
Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases, yield better performance. `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware.
+* [Parameter handling](#parameters-handling)
* [Exception handling](#exception-handling)
* [Content negotiation](#content-negotiation)
* [Multipart request handling](#multipart-request-handling)
+## Parameters handling
+
+`reitit.ring.middleware.parameters/parameters-middleware` to capture query- and form-params. Wraps
+`ring.middleware.params/wrap-params`.
+
+**NOTE**: will be factored into two parts: a query-parameters middleware and a Muuntaja format responsible for the the `application/x-www-form-urlencoded` body format.
+
## Exception handling
A polished version of [compojure-api](https://github.com/metosin/compojure-api) exception handling. Catches all exceptions and invokes configured exception handler.
diff --git a/doc/ring/swagger.md b/doc/ring/swagger.md
index a0a8179e..661eaabd 100644
--- a/doc/ring/swagger.md
+++ b/doc/ring/swagger.md
@@ -128,6 +128,7 @@ Whole example project is in [`/examples/ring-swagger`](https://github.com/metosi
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart]
+ [reitit.ring.middleware.parameters :as parameters]
[ring.middleware.params :as params]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
@@ -180,7 +181,7 @@ Whole example project is in [`/examples/ring-swagger`](https://github.com/metosi
{:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware [;; query-params & form-params
- params/wrap-params
+ parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
diff --git a/examples/http-swagger/.gitignore b/examples/http-swagger/.gitignore
new file mode 100644
index 00000000..c53038ec
--- /dev/null
+++ b/examples/http-swagger/.gitignore
@@ -0,0 +1,11 @@
+/target
+/classes
+/checkouts
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+/.lein-*
+/.nrepl-port
+.hgignore
+.hg/
diff --git a/examples/http-swagger/README.md b/examples/http-swagger/README.md
new file mode 100644
index 00000000..3fc6d580
--- /dev/null
+++ b/examples/http-swagger/README.md
@@ -0,0 +1,23 @@
+# Http with Swagger example
+
+## Usage
+
+```clj
+> lein repl
+(start)
+```
+
+To test the endpoints using [httpie](https://httpie.org/):
+
+```bash
+http GET :3000/math/plus x==1 y==20
+http POST :3000/math/spec/plus x:=1 y:=20
+
+http GET :3000/swagger.json
+```
+
+
+
+## License
+
+Copyright © 2018 Metosin Oy
diff --git a/examples/http-swagger/project.clj b/examples/http-swagger/project.clj
new file mode 100644
index 00000000..76594905
--- /dev/null
+++ b/examples/http-swagger/project.clj
@@ -0,0 +1,6 @@
+(defproject ring-example "0.1.0-SNAPSHOT"
+ :description "Reitit Http App with Swagger"
+ :dependencies [[org.clojure/clojure "1.9.0"]
+ [ring/ring-jetty-adapter "1.7.0-RC2"]
+ [metosin/reitit "0.2.1"]]
+ :repl-options {:init-ns example.server})
diff --git a/examples/http-swagger/resources/reitit.png b/examples/http-swagger/resources/reitit.png
new file mode 100644
index 00000000..c89c3654
Binary files /dev/null and b/examples/http-swagger/resources/reitit.png differ
diff --git a/examples/http-swagger/src/example/server.clj b/examples/http-swagger/src/example/server.clj
new file mode 100644
index 00000000..3d97dc3d
--- /dev/null
+++ b/examples/http-swagger/src/example/server.clj
@@ -0,0 +1,94 @@
+(ns example.server
+ (:require [reitit.ring :as ring]
+ [reitit.http :as http]
+ [reitit.swagger :as swagger]
+ [reitit.swagger-ui :as swagger-ui]
+ [reitit.http.coercion :as coercion]
+ [reitit.coercion.spec :as spec-coercion]
+ [reitit.http.interceptors.parameters :as parameters]
+ [reitit.http.interceptors.muuntaja :as muuntaja]
+ [reitit.http.interceptors.exception :as exception]
+ [reitit.http.interceptors.multipart :as multipart]
+ [reitit.interceptor.sieppari :as sieppari]
+ [ring.adapter.jetty :as jetty]
+ [muuntaja.core :as m]
+ [clojure.java.io :as io]))
+
+(def app
+ (http/ring-handler
+ (http/router
+ [["/swagger.json"
+ {:get {:no-doc true
+ :swagger {:info {:title "my-api"
+ :description "with reitit-http"}}
+ :handler (swagger/create-swagger-handler)}}]
+
+ ["/files"
+ {:swagger {:tags ["files"]}}
+
+ ["/upload"
+ {:post {:summary "upload a file"
+ :parameters {:multipart {:file multipart/temp-file-part}}
+ :responses {200 {:body {:name string?, :size int?}}}
+ :handler (fn [{{{:keys [file]} :multipart} :parameters}]
+ {:status 200
+ :body {:name (:filename file)
+ :size (:size file)}})}}]
+
+ ["/download"
+ {:get {:summary "downloads a file"
+ :swagger {:produces ["image/png"]}
+ :handler (fn [_]
+ {:status 200
+ :headers {"Content-Type" "image/png"}
+ :body (io/input-stream
+ (io/resource "reitit.png"))})}}]]
+
+ ["/math"
+ {:swagger {:tags ["math"]}}
+
+ ["/plus"
+ {:get {:summary "plus with spec query parameters"
+ :parameters {:query {:x int?, :y int?}}
+ :responses {200 {:body {:total int?}}}
+ :handler (fn [{{{:keys [x y]} :query} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}
+ :post {:summary "plus with spec body parameters"
+ :parameters {:body {:x int?, :y int?}}
+ :responses {200 {:body {:total int?}}}
+ :handler (fn [{{{:keys [x y]} :body} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]]
+
+ {:data {:coercion spec-coercion/coercion
+ :muuntaja m/instance
+ :interceptors [;; query-params & form-params
+ (parameters/parameters-interceptor)
+ ;; content-negotiation
+ (muuntaja/format-negotiate-interceptor)
+ ;; encoding response body
+ (muuntaja/format-response-interceptor)
+ ;; exception handling
+ (exception/exception-interceptor)
+ ;; decoding request body
+ (muuntaja/format-request-interceptor)
+ ;; coercing response bodys
+ (coercion/coerce-response-interceptor)
+ ;; coercing request parameters
+ (coercion/coerce-request-interceptor)
+ ;; multipart
+ (multipart/multipart-interceptor)]}})
+ (ring/routes
+ (swagger-ui/create-swagger-ui-handler
+ {:path "/"
+ :config {:validatorUrl nil}})
+ (ring/create-default-handler))
+ {:executor sieppari/executor}))
+
+(defn start []
+ (jetty/run-jetty #'app {:port 3000, :join? false})
+ (println "server running in port 3000"))
+
+(comment
+ (start))
diff --git a/examples/http-swagger/swagger.png b/examples/http-swagger/swagger.png
new file mode 100644
index 00000000..9d5a55b8
Binary files /dev/null and b/examples/http-swagger/swagger.png differ
diff --git a/examples/http/project.clj b/examples/http/project.clj
index 07231498..a8c6efad 100644
--- a/examples/http/project.clj
+++ b/examples/http/project.clj
@@ -4,6 +4,6 @@
[org.clojure/core.async "0.4.474"]
[funcool/promesa "1.9.0"]
[manifold "0.1.8"]
- [ring "1.6.3"]
+ [ring/ring-jetty-adapter "1.7.0-RC2"]
[metosin/reitit "0.2.1"]]
:repl-options {:init-ns example.server})
diff --git a/examples/just-coercion-with-ring/project.clj b/examples/just-coercion-with-ring/project.clj
index 44753b85..913b3836 100644
--- a/examples/just-coercion-with-ring/project.clj
+++ b/examples/just-coercion-with-ring/project.clj
@@ -1,6 +1,6 @@
(defproject just-coercion-with-ring "0.1.0-SNAPSHOT"
:description "Reitit coercion with vanilla ring"
:dependencies [[org.clojure/clojure "1.9.0"]
- [ring "1.6.3"]
+ [ring/ring-jetty-adapter "1.7.0-RC2"]
[metosin/muuntaja "0.4.1"]
[metosin/reitit "0.2.1"]])
diff --git a/examples/pedestal-swagger/.gitignore b/examples/pedestal-swagger/.gitignore
new file mode 100644
index 00000000..c53038ec
--- /dev/null
+++ b/examples/pedestal-swagger/.gitignore
@@ -0,0 +1,11 @@
+/target
+/classes
+/checkouts
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+/.lein-*
+/.nrepl-port
+.hgignore
+.hg/
diff --git a/examples/pedestal-swagger/README.md b/examples/pedestal-swagger/README.md
new file mode 100644
index 00000000..59326fc4
--- /dev/null
+++ b/examples/pedestal-swagger/README.md
@@ -0,0 +1,23 @@
+# Pedestal with reitit-http & Swagger example
+
+## Usage
+
+```clj
+> lein repl
+(start)
+```
+
+To test the endpoints using [httpie](https://httpie.org/):
+
+```bash
+http GET :3000/math/plus x==1 y==20
+http POST :3000/math/spec/plus x:=1 y:=20
+
+http GET :3000/swagger.json
+```
+
+
+
+## License
+
+Copyright © 2018 Metosin Oy
diff --git a/examples/pedestal-swagger/project.clj b/examples/pedestal-swagger/project.clj
new file mode 100644
index 00000000..1bfb2525
--- /dev/null
+++ b/examples/pedestal-swagger/project.clj
@@ -0,0 +1,7 @@
+(defproject ring-example "0.1.0-SNAPSHOT"
+ :description "Reitit-http with pedestal"
+ :dependencies [[org.clojure/clojure "1.9.0"]
+ [io.pedestal/pedestal.service "0.5.4"]
+ [io.pedestal/pedestal.jetty "0.5.4"]
+ [metosin/reitit "0.2.1"]]
+ :repl-options {:init-ns example.server})
diff --git a/examples/pedestal-swagger/resources/reitit.png b/examples/pedestal-swagger/resources/reitit.png
new file mode 100644
index 00000000..c89c3654
Binary files /dev/null and b/examples/pedestal-swagger/resources/reitit.png differ
diff --git a/examples/pedestal-swagger/src/example/server.clj b/examples/pedestal-swagger/src/example/server.clj
new file mode 100644
index 00000000..6f80a29c
--- /dev/null
+++ b/examples/pedestal-swagger/src/example/server.clj
@@ -0,0 +1,112 @@
+(ns example.server
+ (:require [io.pedestal.http]
+ [reitit.interceptor.pedestal :as pedestal]
+ [reitit.ring :as ring]
+ [reitit.http :as http]
+ [reitit.swagger :as swagger]
+ [reitit.swagger-ui :as swagger-ui]
+ [reitit.http.coercion :as coercion]
+ [reitit.coercion.spec :as spec-coercion]
+ [reitit.http.interceptors.parameters :as parameters]
+ [reitit.http.interceptors.muuntaja :as muuntaja]
+ #_[reitit.http.interceptors.exception :as exception]
+ [reitit.http.interceptors.multipart :as multipart]
+ [muuntaja.core :as m]
+ [clojure.java.io :as io]))
+
+(def routing-interceptor
+ (pedestal/routing-interceptor
+ (http/router
+ [["/swagger.json"
+ {:get {:no-doc true
+ :swagger {:info {:title "my-api"
+ :description "with pedestal & reitit-http"}}
+ :handler (swagger/create-swagger-handler)}}]
+
+ ["/files"
+ {:swagger {:tags ["files"]}}
+
+ ["/upload"
+ {:post {:summary "upload a file"
+ :parameters {:multipart {:file multipart/temp-file-part}}
+ :responses {200 {:body {:name string?, :size int?}}}
+ :handler (fn [{{{:keys [file]} :multipart} :parameters}]
+ {:status 200
+ :body {:name (:filename file)
+ :size (:size file)}})}}]
+
+ ["/download"
+ {:get {:summary "downloads a file"
+ :swagger {:produces ["image/png"]}
+ :handler (fn [_]
+ {:status 200
+ :headers {"Content-Type" "image/png"}
+ :body (io/input-stream
+ (io/resource "reitit.png"))})}}]]
+
+ ["/math"
+ {:swagger {:tags ["math"]}}
+
+ ["/plus"
+ {:get {:summary "plus with spec query parameters"
+ :parameters {:query {:x int?, :y int?}}
+ :responses {200 {:body {:total int?}}}
+ :handler (fn [{{{:keys [x y]} :query} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}
+ :post {:summary "plus with spec body parameters"
+ :parameters {:body {:x int?, :y int?}}
+ :responses {200 {:body {:total int?}}}
+ :handler (fn [{{{:keys [x y]} :body} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]]
+
+ {:data {:coercion spec-coercion/coercion
+ :muuntaja m/instance
+ :interceptors [;; query-params & form-params
+ (parameters/parameters-interceptor)
+ ;; content-negotiation
+ (muuntaja/format-negotiate-interceptor)
+ ;; encoding response body
+ (muuntaja/format-response-interceptor)
+ ;; exception handling - doesn't work
+ ;;(exception/exception-interceptor)
+ ;; decoding request body
+ (muuntaja/format-request-interceptor)
+ ;; coercing response bodys
+ (coercion/coerce-response-interceptor)
+ ;; coercing request parameters
+ (coercion/coerce-request-interceptor)
+ ;; multipart
+ (multipart/multipart-interceptor)]}})
+
+ ;; optional default ring handler (if no routes have matched)
+ (ring/routes
+ (swagger-ui/create-swagger-ui-handler
+ {:path "/"
+ :config {:validatorUrl nil}})
+ (ring/create-default-handler))))
+
+(defonce server (atom nil))
+
+(defn start []
+ (when @server
+ (io.pedestal.http/stop @server)
+ (println "server stopped"))
+ (-> {:env :prod
+ :io.pedestal.http/routes []
+ :io.pedestal.http/resource-path "/public"
+ :io.pedestal.http/type :jetty
+ :io.pedestal.http/port 3000}
+ (merge {:env :dev
+ :io.pedestal.http/join? false
+ :io.pedestal.http/allowed-origins {:creds true :allowed-origins (constantly true)}})
+ (pedestal/default-interceptors routing-interceptor)
+ io.pedestal.http/dev-interceptors
+ io.pedestal.http/create-server
+ io.pedestal.http/start
+ (->> (reset! server)))
+ (println "server running in port 3000"))
+
+(comment
+ (start))
diff --git a/examples/pedestal-swagger/src/reitit/interceptor/pedestal.clj b/examples/pedestal-swagger/src/reitit/interceptor/pedestal.clj
new file mode 100644
index 00000000..362cbd5d
--- /dev/null
+++ b/examples/pedestal-swagger/src/reitit/interceptor/pedestal.clj
@@ -0,0 +1,38 @@
+(ns reitit.interceptor.pedestal
+ (:require [io.pedestal.interceptor.chain :as chain]
+ [io.pedestal.interceptor :as interceptor]
+ [io.pedestal.http :as http]
+ [reitit.interceptor]
+ [reitit.http])
+ (:import (reitit.interceptor Executor)))
+
+(def pedestal-executor
+ (reify
+ Executor
+ (queue [_ interceptors]
+ (->> interceptors
+ (map (fn [{:keys [::interceptor/handler] :as interceptor}]
+ (or handler interceptor)))
+ (map interceptor/interceptor)))
+ (enqueue [_ context interceptors]
+ (chain/enqueue context interceptors))))
+
+(defn routing-interceptor
+ ([router]
+ (routing-interceptor router nil))
+ ([router default-handler]
+ (routing-interceptor router default-handler nil))
+ ([router default-handler {:keys [interceptors]}]
+ (interceptor/interceptor
+ (reitit.http/routing-interceptor
+ router
+ default-handler
+ {:executor pedestal-executor
+ :interceptors interceptors}))))
+
+(defn default-interceptors [spec router]
+ (-> spec
+ (assoc ::http/routes [])
+ (http/default-interceptors)
+ (update ::http/interceptors (comp vec butlast))
+ (update ::http/interceptors conj router)))
diff --git a/examples/ring-example/project.clj b/examples/ring-example/project.clj
index 76de32bc..2849333b 100644
--- a/examples/ring-example/project.clj
+++ b/examples/ring-example/project.clj
@@ -1,6 +1,6 @@
(defproject ring-example "0.1.0-SNAPSHOT"
:description "Reitit Ring App"
:dependencies [[org.clojure/clojure "1.9.0"]
- [ring "1.6.3"]
+ [ring/ring-jetty-adapter "1.7.0-RC2"]
[metosin/reitit "0.2.1"]]
:repl-options {:init-ns example.server})
diff --git a/examples/ring-spec-swagger/project.clj b/examples/ring-spec-swagger/project.clj
index a098180c..75c21ab6 100644
--- a/examples/ring-spec-swagger/project.clj
+++ b/examples/ring-spec-swagger/project.clj
@@ -1,6 +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"]
+ [ring/ring-jetty-adapter "1.7.0-RC2"]
[metosin/reitit "0.2.1"]]
:repl-options {:init-ns example.server})
diff --git a/examples/ring-spec-swagger/src/example/server.clj b/examples/ring-spec-swagger/src/example/server.clj
index 744c5671..3cfdc946 100644
--- a/examples/ring-spec-swagger/src/example/server.clj
+++ b/examples/ring-spec-swagger/src/example/server.clj
@@ -7,7 +7,7 @@
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart]
- [ring.middleware.params :as params]
+ [reitit.ring.middleware.parameters :as parameters]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
[clojure.spec.alpha :as s]
@@ -76,7 +76,7 @@
{:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware [;; query-params & form-params
- params/wrap-params
+ parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
diff --git a/examples/ring-swagger/project.clj b/examples/ring-swagger/project.clj
index a098180c..75c21ab6 100644
--- a/examples/ring-swagger/project.clj
+++ b/examples/ring-swagger/project.clj
@@ -1,6 +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"]
+ [ring/ring-jetty-adapter "1.7.0-RC2"]
[metosin/reitit "0.2.1"]]
:repl-options {:init-ns example.server})
diff --git a/examples/ring-swagger/src/example/server.clj b/examples/ring-swagger/src/example/server.clj
index 19e36257..5e997dbb 100644
--- a/examples/ring-swagger/src/example/server.clj
+++ b/examples/ring-swagger/src/example/server.clj
@@ -7,7 +7,7 @@
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart]
- [ring.middleware.params :as params]
+ [reitit.ring.middleware.parameters :as parameters]
[ring.adapter.jetty :as jetty]
[muuntaja.core :as m]
[clojure.java.io :as io]))
@@ -17,7 +17,8 @@
(ring/router
[["/swagger.json"
{:get {:no-doc true
- :swagger {:info {:title "my-api"}}
+ :swagger {:info {:title "my-api"
+ :description "with reitit-ring"}}
:handler (swagger/create-swagger-handler)}}]
["/files"
@@ -38,8 +39,9 @@
:handler (fn [_]
{:status 200
:headers {"Content-Type" "image/png"}
- :body (io/input-stream
- (io/resource "reitit.png"))})}}]]
+ :body (-> "reitit.png"
+ (io/resource)
+ (io/input-stream))})}}]]
["/math"
{:swagger {:tags ["math"]}}
@@ -61,7 +63,7 @@
{:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware [;; query-params & form-params
- params/wrap-params
+ parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc
index c2d7e674..f45fe74c 100644
--- a/modules/reitit-core/src/reitit/interceptor.cljc
+++ b/modules/reitit-core/src/reitit/interceptor.cljc
@@ -92,7 +92,7 @@
(if-let [interceptor (into-interceptor (compile data opts) data opts)]
(map->Interceptor
(merge
- (dissoc this :create)
+ (dissoc this :compile)
(impl/strip-nils interceptor)))))))
nil
diff --git a/modules/reitit-interceptors/project.clj b/modules/reitit-interceptors/project.clj
new file mode 100644
index 00000000..1c384dce
--- /dev/null
+++ b/modules/reitit-interceptors/project.clj
@@ -0,0 +1,12 @@
+(defproject metosin/reitit-interceptors "0.2.1"
+ :description "Reitit, common interceptors bundled"
+ :url "https://github.com/metosin/reitit"
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"}
+ :scm {:name "git"
+ :url "https://github.com/metosin/reitit"}
+ :plugins [[lein-parent "0.3.2"]]
+ :parent-project {:path "../../project.clj"
+ :inherit [:deploy-repositories :managed-dependencies]}
+ :dependencies [[metosin/reitit-ring]
+ [metosin/muuntaja]])
diff --git a/modules/reitit-interceptors/src/reitit/http/interceptors/exception.clj b/modules/reitit-interceptors/src/reitit/http/interceptors/exception.clj
new file mode 100644
index 00000000..2bced558
--- /dev/null
+++ b/modules/reitit-interceptors/src/reitit/http/interceptors/exception.clj
@@ -0,0 +1,152 @@
+(ns reitit.http.interceptors.exception
+ (:require [reitit.coercion :as coercion]
+ [reitit.ring :as ring]
+ [clojure.spec.alpha :as s]
+ [clojure.string :as str])
+ (:import (java.time Instant)
+ (java.io PrintWriter)))
+
+(s/def ::handlers (s/map-of any? fn?))
+(s/def ::spec (s/keys :opt-un [::handlers]))
+
+;;
+;; helpers
+;;
+
+(defn- super-classes [^Class k]
+ (loop [sk (.getSuperclass k), ks []]
+ (if-not (= sk Object)
+ (recur (.getSuperclass sk) (conj ks sk))
+ ks)))
+
+(defn- call-error-handler [handlers error request]
+ (let [type (:type (ex-data error))
+ ex-class (class error)
+ error-handler (or (get handlers type)
+ (get handlers ex-class)
+ (some
+ (partial get handlers)
+ (descendants type))
+ (some
+ (partial get handlers)
+ (super-classes ex-class))
+ (get handlers ::default))]
+ (if-let [wrap (get handlers ::wrap)]
+ (wrap error-handler error request)
+ (error-handler error request))))
+
+(defn print! [^PrintWriter writer & more]
+ (.write writer (str (str/join " " more) "\n")))
+
+;;
+;; handlers
+;;
+
+(defn default-handler
+ "Default safe handler for any exception."
+ [^Exception e _]
+ {:status 500
+ :body {:type "exception"
+ :class (.getName (.getClass e))}})
+
+(defn create-coercion-handler
+ "Creates a coercion exception handler."
+ [status]
+ (fn [e _]
+ {:status status
+ :body (coercion/encode-error (ex-data e))}))
+
+(defn http-response-handler
+ "Reads response from Exception ex-data :response"
+ [e _]
+ (-> e ex-data :response))
+
+(defn request-parsing-handler [e _]
+ {:status 400
+ :headers {"Content-Type" "text/plain"}
+ :body (str "Malformed " (-> e ex-data :format pr-str) " request.")})
+
+(defn wrap-log-to-console [handler e {:keys [uri request-method] :as req}]
+ (print! *out* (Instant/now) request-method (pr-str uri) "=>" (.getMessage e))
+ (.printStackTrace e *out*)
+ (handler e req))
+
+;;
+;; public api
+;;
+
+(def default-handlers
+ {::default default-handler
+ ::ring/response http-response-handler
+ :muuntaja/decode request-parsing-handler
+ ::coercion/request-coercion (create-coercion-handler 400)
+ ::coercion/response-coercion (create-coercion-handler 500)})
+
+(defn exception-interceptor
+ "Creates an Interceptor that catches all exceptions. Takes a map
+ of `identifier => exception request => response` that is used to select
+ the exception handler for the thown/raised exception identifier. Exception
+ idenfier is either a `Keyword` or a Exception Class.
+
+ The following handlers special handlers are available:
+
+ | key | description
+ |------------------------|-------------
+ | `::exception/default` | a default exception handler if nothing else mathced (default [[default-handler]]).
+ | `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response`
+
+ The handler is selected from the options map by exception idenfiter
+ in the following lookup order:
+
+ 1) `:type` of exception ex-data
+ 2) Class of exception
+ 3) `:type` ancestors of exception ex-data
+ 4) Super Classes of exception
+ 5) The ::default handler
+
+ Example:
+
+ (require '[reitit.ring.interceptors.exception :as exception])
+
+ ;; type hierarchy
+ (derive ::error ::exception)
+ (derive ::failure ::exception)
+ (derive ::horror ::exception)
+
+ (defn handler [message exception request]
+ {:status 500
+ :body {:message message
+ :exception (str exception)
+ :uri (:uri request)}})
+
+ (exception/exception-interceptor
+ (merge
+ exception/default-handlers
+ {;; ex-data with :type ::error
+ ::error (partial handler \"error\")
+
+ ;; ex-data with ::exception or ::failure
+ ::exception (partial handler \"exception\")
+
+ ;; SQLException and all it's child classes
+ java.sql.SQLException (partial handler \"sql-exception\")
+
+ ;; override the default handler
+ ::exception/default (partial handler \"default\")
+
+ ;; print stack-traces for all exceptions
+ ::exception/wrap (fn [handler e request]
+ (.printStackTrace e)
+ (handler e request))}))"
+ ([]
+ (exception-interceptor default-handlers))
+ ([handlers]
+ {:name ::exception
+ :spec ::spec
+ :error (fn [ctx]
+ (let [error (:error ctx)
+ request (:request ctx)
+ response (call-error-handler handlers error request)]
+ (if (instance? Exception response)
+ (-> ctx (assoc :error response) (dissoc :response))
+ (-> ctx (assoc :response response) (dissoc :error)))))}))
diff --git a/modules/reitit-interceptors/src/reitit/http/interceptors/multipart.clj b/modules/reitit-interceptors/src/reitit/http/interceptors/multipart.clj
new file mode 100644
index 00000000..12bf779e
--- /dev/null
+++ b/modules/reitit-interceptors/src/reitit/http/interceptors/multipart.clj
@@ -0,0 +1,55 @@
+(ns reitit.http.interceptors.multipart
+ (:require [reitit.coercion :as coercion]
+ [ring.middleware.multipart-params :as multipart-params]
+ [clojure.spec.alpha :as s]
+ [spec-tools.core :as st])
+ (:import (java.io File)))
+
+(s/def ::filename string?)
+(s/def ::content-type string?)
+(s/def ::tempfile (partial instance? File))
+(s/def ::bytes bytes?)
+(s/def ::size int?)
+
+(def temp-file-part
+ "Spec for file param created by ring.middleware.multipart-params.temp-file store."
+ (st/spec
+ {:spec (s/keys :req-un [::filename ::content-type ::tempfile ::size])
+ :swagger/type "file"}))
+
+(def bytes-part
+ "Spec for file param created by ring.middleware.multipart-params.byte-array store."
+ (st/spec
+ {:spec (s/keys :req-un [::filename ::content-type ::bytes])
+ :swagger/type "file"}))
+
+(defn- coerced-request [request coercers]
+ (if-let [coerced (if coercers (coercion/coerce-request coercers request))]
+ (update request :parameters merge coerced)
+ request))
+
+;;
+;; public api
+;;
+
+(defn multipart-interceptor
+ "Creates a Interceptor to handle the multipart params, based on
+ ring.middleware.multipart-params, taking same options. Mounts only
+ if endpoint has `[:parameters :multipart]` defined. Publishes coerced
+ parameters into `[:parameters :multipart]` under request."
+ ([]
+ (multipart-interceptor nil))
+ ([options]
+ {:name ::multipart
+ :compile (fn [{:keys [parameters coercion]} opts]
+ (if-let [multipart (:multipart parameters)]
+ (let [parameter-coercion {:multipart (coercion/->ParameterCoercion
+ :multipart-params :string true true)}
+ opts (assoc opts ::coercion/parameter-coercion parameter-coercion)
+ coercers (if multipart (coercion/request-coercers coercion parameters opts))]
+ {:data {:swagger {:consumes ^:replace #{"multipart/form-data"}}}
+ :enter (fn [ctx]
+ (let [request (-> (:request ctx)
+ (multipart-params/multipart-params-request options)
+ (coerced-request coercers))]
+ (assoc ctx :request request)))})))}))
diff --git a/modules/reitit-interceptors/src/reitit/http/interceptors/muuntaja.clj b/modules/reitit-interceptors/src/reitit/http/interceptors/muuntaja.clj
new file mode 100644
index 00000000..f697bfa4
--- /dev/null
+++ b/modules/reitit-interceptors/src/reitit/http/interceptors/muuntaja.clj
@@ -0,0 +1,105 @@
+(ns reitit.http.interceptors.muuntaja
+ (:require [muuntaja.core :as m]
+ [muuntaja.interceptor]
+ [clojure.spec.alpha :as s]))
+
+(s/def ::muuntaja m/muuntaja?)
+(s/def ::spec (s/keys :opt-un [::muuntaja]))
+
+(defn- displace [x] (with-meta x {:displace true}))
+(defn- stripped [x] (select-keys x [:enter :leave :error]))
+
+(defn format-interceptor
+ "Interceptor for content-negotiation, request and response formatting.
+
+ Negotiates a request body based on `Content-Type` header and response body based on
+ `Accept`, `Accept-Charset` headers. Publishes the negotiation results as `:muuntaja/request`
+ and `:muuntaja/response` keys into the request.
+
+ Decodes the request body into `:body-params` using the `:muuntaja/request` key in request
+ if the `:body-params` doesn't already exist.
+
+ Encodes the response body using the `:muuntaja/response` key in request if the response
+ doesn't have `Content-Type` header already set.
+
+ Optionally takes a default muuntaja instance as argument.
+
+ | key | description |
+ | -------------|-------------|
+ | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set."
+ ([]
+ (format-interceptor nil))
+ ([default-muuntaja]
+ {:name ::format
+ :spec ::spec
+ :compile (fn [{:keys [muuntaja]} _]
+ (if-let [muuntaja (or muuntaja default-muuntaja)]
+ (merge
+ (stripped (muuntaja.interceptor/format-interceptor muuntaja))
+ {:data {:swagger {:produces (displace (m/encodes muuntaja))
+ :consumes (displace (m/decodes muuntaja))}}})))}))
+
+(defn format-negotiate-interceptor
+ "Interceptor for content-negotiation.
+
+ Negotiates a request body based on `Content-Type` header and response body based on
+ `Accept`, `Accept-Charset` headers. Publishes the negotiation results as `:muuntaja/request`
+ and `:muuntaja/response` keys into the request.
+
+ Optionally takes a default muuntaja instance as argument.
+
+ | key | description |
+ | -------------|-------------|
+ | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set."
+ ([]
+ (format-negotiate-interceptor nil))
+ ([default-muuntaja]
+ {:name ::format-negotiate
+ :spec ::spec
+ :compile (fn [{:keys [muuntaja]} _]
+ (if-let [muuntaja (or muuntaja default-muuntaja)]
+ (stripped (muuntaja.interceptor/format-negotiate-interceptor muuntaja))))}))
+
+(defn format-request-interceptor
+ "Interceptor for request formatting.
+
+ Decodes the request body into `:body-params` using the `:muuntaja/request` key in request
+ if the `:body-params` doesn't already exist.
+
+ Optionally takes a default muuntaja instance as argument.
+
+ | key | description |
+ | -------------|-------------|
+ | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set."
+ ([]
+ (format-request-interceptor nil))
+ ([default-muuntaja]
+ {:name ::format-request
+ :spec ::spec
+ :compile (fn [{:keys [muuntaja]} _]
+ (if-let [muuntaja (or muuntaja default-muuntaja)]
+ (merge
+ (stripped (muuntaja.interceptor/format-request-interceptor muuntaja))
+ {:data {:swagger {:consumes (displace (m/decodes muuntaja))}}})))}))
+
+(defn format-response-interceptor
+ "Interceptor for response formatting.
+
+ Encodes the response body using the `:muuntaja/response` key in request if the response
+ doesn't have `Content-Type` header already set.
+
+ Optionally takes a default muuntaja instance as argument.
+
+ | key | description |
+ | -------------|-------------|
+ | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set."
+ ([]
+ (format-response-interceptor nil))
+ ([default-muuntaja]
+ {:name ::format-response
+ :spec ::spec
+ :compile (fn [{:keys [muuntaja]} _]
+ (if-let [muuntaja (or muuntaja default-muuntaja)]
+ (merge
+ (stripped (muuntaja.interceptor/format-response-interceptor muuntaja))
+ {:data {:swagger {:produces (displace (m/encodes muuntaja))}}})))}))
diff --git a/modules/reitit-interceptors/src/reitit/http/interceptors/parameters.clj b/modules/reitit-interceptors/src/reitit/http/interceptors/parameters.clj
new file mode 100644
index 00000000..0a21ba20
--- /dev/null
+++ b/modules/reitit-interceptors/src/reitit/http/interceptors/parameters.clj
@@ -0,0 +1,16 @@
+(ns reitit.http.interceptors.parameters
+ (:require [ring.middleware.params :as params]))
+
+(defn parameters-interceptor
+ "Interceptor to parse urlencoded parameters from the query string and form
+ body (if the request is a url-encoded form). Adds the following keys to
+ the request map:
+
+ :query-params - a map of parameters from the query string
+ :form-params - a map of parameters from the body
+ :params - a merged map of all types of parameter"
+ []
+ {:name ::parameters
+ :enter (fn [ctx]
+ (let [request (:request ctx)]
+ (assoc ctx :request (params/params-request request))))})
diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/multipart.clj b/modules/reitit-middleware/src/reitit/ring/middleware/multipart.clj
index e6152e02..07a1d8c2 100644
--- a/modules/reitit-middleware/src/reitit/ring/middleware/multipart.clj
+++ b/modules/reitit-middleware/src/reitit/ring/middleware/multipart.clj
@@ -1,4 +1,4 @@
-(ns ^:no-doc reitit.ring.middleware.multipart
+(ns reitit.ring.middleware.multipart
(:refer-clojure :exclude [compile])
(:require [reitit.coercion :as coercion]
[ring.middleware.multipart-params :as multipart-params]
@@ -40,14 +40,10 @@
:wrap (fn [handler]
(fn
([request]
- (try
- (-> request
- (multipart-params/multipart-params-request options)
- (coerced-request coercers)
- (handler))
- (catch Exception e
- (.printStackTrace e)
- (throw e))))
+ (-> request
+ (multipart-params/multipart-params-request options)
+ (coerced-request coercers)
+ (handler)))
([request respond raise]
(-> request
(multipart-params/multipart-params-request options)
diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/parameters.clj b/modules/reitit-middleware/src/reitit/ring/middleware/parameters.clj
new file mode 100644
index 00000000..84355093
--- /dev/null
+++ b/modules/reitit-middleware/src/reitit/ring/middleware/parameters.clj
@@ -0,0 +1,15 @@
+(ns reitit.ring.middleware.parameters
+ (:require [ring.middleware.params :as params]))
+
+(def parameters-middleware
+ "Middleware to parse urlencoded parameters from the query string and form
+ body (if the request is a url-encoded form). Adds the following keys to
+ the request map:
+
+ :query-params - a map of parameters from the query string
+ :form-params - a map of parameters from the body
+ :params - a merged map of all types of parameter"
+ {:name ::parameters
+ :enter (fn [ctx]
+ (let [request (:request ctx)]
+ (assoc ctx :request (params/params-request request))))})
diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc
index bb92607f..8340b250 100644
--- a/modules/reitit-swagger/src/reitit/swagger.cljc
+++ b/modules/reitit-swagger/src/reitit/swagger.cljc
@@ -84,11 +84,14 @@
:x-id ids}))
accept-route (fn [route]
(-> route second :swagger :id (or ::default) ->set (set/intersection ids) seq))
- transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data middleware :middleware}]]
+ transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data
+ middleware :middleware
+ interceptors :interceptors}]]
(if (and data (not no-doc))
[method
(meta-merge
(apply meta-merge (keep (comp :swagger :data) middleware))
+ (apply meta-merge (keep (comp :swagger :data) interceptors))
(if coercion
(coercion/get-apidocs coercion :swagger data))
(select-keys data [:tags :summary :description])
diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj
index ed0c791a..f8cbff97 100644
--- a/modules/reitit/project.clj
+++ b/modules/reitit/project.clj
@@ -14,6 +14,7 @@
[metosin/reitit-ring]
[metosin/reitit-middleware]
[metosin/reitit-http]
+ [metosin/reitit-interceptors]
[metosin/reitit-swagger]
[metosin/reitit-swagger-ui]
[metosin/reitit-frontend]
diff --git a/project.clj b/project.clj
index dff9ed07..48a52000 100644
--- a/project.clj
+++ b/project.clj
@@ -17,6 +17,7 @@
[metosin/reitit-ring "0.2.1"]
[metosin/reitit-middleware "0.2.1"]
[metosin/reitit-http "0.2.1"]
+ [metosin/reitit-interceptors "0.2.1"]
[metosin/reitit-swagger "0.2.1"]
[metosin/reitit-swagger-ui "0.2.1"]
[metosin/reitit-frontend "0.2.1"]
@@ -45,6 +46,7 @@
"modules/reitit-ring/src"
"modules/reitit-http/src"
"modules/reitit-middleware/src"
+ "modules/reitit-interceptors/src"
"modules/reitit-spec/src"
"modules/reitit-schema/src"
"modules/reitit-swagger/src"
diff --git a/scripts/lein-modules b/scripts/lein-modules
index 1a95289f..35e466ff 100755
--- a/scripts/lein-modules
+++ b/scripts/lein-modules
@@ -10,6 +10,7 @@ for ext in \
reitit-ring \
reitit-middleware \
reitit-http \
+ reitit-interceptors \
reitit-swagger \
reitit-swagger-ui \
reitit-frontend \
diff --git a/test/clj/reitit/http/interceptors/exception_test.clj b/test/clj/reitit/http/interceptors/exception_test.clj
new file mode 100644
index 00000000..c50ce106
--- /dev/null
+++ b/test/clj/reitit/http/interceptors/exception_test.clj
@@ -0,0 +1,119 @@
+(ns reitit.http.interceptors.exception-test
+ (:require [clojure.test :refer [deftest testing is]]
+ [reitit.ring :as ring]
+ [reitit.http :as http]
+ [reitit.http.interceptors.exception :as exception]
+ [reitit.interceptor.sieppari :as sieppari]
+ [reitit.coercion.spec]
+ [reitit.http.coercion]
+ [muuntaja.core :as m])
+ (:import (java.sql SQLException SQLWarning)))
+
+(derive ::kikka ::kukka)
+
+(deftest exception-test
+ (letfn [(create
+ ([f]
+ (create f nil))
+ ([f wrap]
+ (http/ring-handler
+ (http/router
+ [["/defaults"
+ {:handler f}]
+ ["/coercion"
+ {:interceptors [(reitit.http.coercion/coerce-request-interceptor)
+ (reitit.http.coercion/coerce-response-interceptor)]
+ :coercion reitit.coercion.spec/coercion
+ :parameters {:query {:x int?, :y int?}}
+ :responses {200 {:body {:total pos-int?}}}
+ :handler f}]]
+ {:data {:interceptors [(exception/exception-interceptor
+ (merge
+ exception/default-handlers
+ {::kikka (constantly {:status 400, :body "kikka"})
+ SQLException (constantly {:status 400, :body "sql"})
+ ::exception/wrap wrap}))]}})
+ {:executor sieppari/executor})))]
+
+ (testing "normal calls work ok"
+ (let [response {:status 200, :body "ok"}
+ app (create (fn [_] response))]
+ (is (= response (app {:request-method :get, :uri "/defaults"})))))
+
+ (testing "unknown exception"
+ (let [app (create (fn [_] (throw (NullPointerException.))))]
+ (is (= {:status 500
+ :body {:type "exception"
+ :class "java.lang.NullPointerException"}}
+ (app {:request-method :get, :uri "/defaults"}))))
+ (let [app (create (fn [_] (throw (ex-info "fail" {:type ::invalid}))))]
+ (is (= {:status 500
+ :body {:type "exception"
+ :class "clojure.lang.ExceptionInfo"}}
+ (app {:request-method :get, :uri "/defaults"})))))
+
+ (testing "::ring/response"
+ (let [response {:status 200, :body "ok"}
+ app (create (fn [_] (throw (ex-info "fail" {:type ::ring/response, :response response}))))]
+ (is (= response (app {:request-method :get, :uri "/defaults"})))))
+
+ (testing ":muuntaja/decode"
+ (let [app (create (fn [_] (m/decode m/instance "application/json" "{:so \"invalid\"}")))]
+ (is (= {:body "Malformed \"application/json\" request."
+ :headers {"Content-Type" "text/plain"}
+ :status 400}
+ (app {:request-method :get, :uri "/defaults"}))))
+
+ (testing "::coercion/request-coercion"
+ (let [app (create (fn [{{{:keys [x y]} :query} :parameters}]
+ {:status 200, :body {:total (+ x y)}}))]
+
+ (let [{:keys [status body]} (app {:request-method :get
+ :uri "/coercion"
+ :query-params {"x" "1", "y" "2"}})]
+ (is (= 200 status))
+ (is (= {:total 3} body)))
+
+ (let [{:keys [status body]} (app {:request-method :get
+ :uri "/coercion"
+ :query-params {"x" "abba", "y" "2"}})]
+ (is (= 400 status))
+ (is (= :reitit.coercion/request-coercion (:type body))))
+
+ (let [{:keys [status body]} (app {:request-method :get
+ :uri "/coercion"
+ :query-params {"x" "-10", "y" "2"}})]
+ (is (= 500 status))
+ (is (= :reitit.coercion/response-coercion (:type body)))))))
+
+ (testing "exact :type"
+ (let [app (create (fn [_] (throw (ex-info "fail" {:type ::kikka}))))]
+ (is (= {:status 400, :body "kikka"}
+ (app {:request-method :get, :uri "/defaults"})))))
+
+ (testing "parent :type"
+ (let [app (create (fn [_] (throw (ex-info "fail" {:type ::kukka}))))]
+ (is (= {:status 400, :body "kikka"}
+ (app {:request-method :get, :uri "/defaults"})))))
+
+ (testing "exact Exception"
+ (let [app (create (fn [_] (throw (SQLException.))))]
+ (is (= {:status 400, :body "sql"}
+ (app {:request-method :get, :uri "/defaults"})))))
+
+ (testing "Exception SuperClass"
+ (let [app (create (fn [_] (throw (SQLWarning.))))]
+ (is (= {:status 400, :body "sql"}
+ (app {:request-method :get, :uri "/defaults"})))))
+
+ (testing "::exception/wrap"
+ (let [calls (atom 0)
+ app (create (fn [_] (throw (SQLWarning.)))
+ (fn [handler exception request]
+ (if (< (swap! calls inc) 2)
+ (handler exception request)
+ {:status 500, :body "too many tries"})))]
+ (is (= {:status 400, :body "sql"}
+ (app {:request-method :get, :uri "/defaults"})))
+ (is (= {:status 500, :body "too many tries"}
+ (app {:request-method :get, :uri "/defaults"})))))))
diff --git a/test/clj/reitit/http/interceptors/multipart_test.clj b/test/clj/reitit/http/interceptors/multipart_test.clj
new file mode 100644
index 00000000..79a93a7c
--- /dev/null
+++ b/test/clj/reitit/http/interceptors/multipart_test.clj
@@ -0,0 +1,3 @@
+(ns reitit.http.interceptors.multipart-test)
+
+;; TODO
diff --git a/test/clj/reitit/http/interceptors/muuntaja_test.clj b/test/clj/reitit/http/interceptors/muuntaja_test.clj
new file mode 100644
index 00000000..22c402df
--- /dev/null
+++ b/test/clj/reitit/http/interceptors/muuntaja_test.clj
@@ -0,0 +1,147 @@
+(ns reitit.http.interceptors.muuntaja-test
+ (:require [clojure.test :refer [deftest testing is]]
+ [reitit.http :as http]
+ [reitit.http.interceptors.muuntaja :as muuntaja]
+ [reitit.swagger :as swagger]
+ [reitit.interceptor.sieppari :as sieppari]
+ [muuntaja.core :as m]))
+
+(deftest muuntaja-test
+ (let [data {:kikka "kukka"}
+ app (http/ring-handler
+ (http/router
+ ["/ping" {:get (constantly {:status 200, :body data})}]
+ {:data {:muuntaja m/instance
+ :interceptors [(muuntaja/format-interceptor)]}})
+ {:executor sieppari/executor})]
+ (is (= data (->> {:request-method :get, :uri "/ping"}
+ (app)
+ :body
+ (m/decode m/instance "application/json"))))))
+
+(deftest muuntaja-swagger-test
+ (let [with-defaults m/instance
+ no-edn-decode (m/create (-> m/default-options (update-in [:formats "application/edn"] dissoc :decoder)))
+ just-edn (m/create (-> m/default-options (m/select-formats ["application/edn"])))
+ app (http/ring-handler
+ (http/router
+ [["/defaults"
+ {:get identity}]
+ ["/explicit-defaults"
+ {:muuntaja with-defaults
+ :get identity}]
+ ["/no-edn-decode"
+ {:muuntaja no-edn-decode
+ :get identity}]
+ ["/just-edn"
+ {:muuntaja just-edn
+ :get identity}]
+ ["/swagger.json"
+ {:get {:no-doc true
+ :handler (swagger/create-swagger-handler)}}]]
+ {:data {:muuntaja m/instance
+ :interceptors [(muuntaja/format-interceptor)]}})
+ {:executor sieppari/executor})
+ spec (fn [path]
+ (let [path (keyword path)]
+ (-> {:request-method :get :uri "/swagger.json"}
+ (app) :body
+ (->> (m/decode m/instance "application/json"))
+ :paths path :get)))
+ produces (comp set :produces spec)
+ consumes (comp set :consumes spec)]
+
+ (testing "with defaults"
+ (let [path "/defaults"]
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"
+ "application/edn"}
+ (produces path)
+ (consumes path)))))
+
+ (testing "with explicit muuntaja defaults"
+ (let [path "/explicit-defaults"]
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"
+ "application/edn"}
+ (produces path)
+ (consumes path)))))
+
+ (testing "without edn decode"
+ (let [path "/no-edn-decode"]
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"
+ "application/edn"}
+ (produces path)))
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"}
+ (consumes path)))))
+
+ (testing "just edn"
+ (let [path "/just-edn"]
+ (is (= #{"application/edn"}
+ (produces path)
+ (consumes path)))))))
+
+(deftest muuntaja-swagger-parts-test
+ (let [app (http/ring-handler
+ (http/router
+ [["/request"
+ {:interceptors [(muuntaja/format-negotiate-interceptor)
+ (muuntaja/format-request-interceptor)]
+ :get identity}]
+ ["/response"
+ {:interceptors [(muuntaja/format-negotiate-interceptor)
+ (muuntaja/format-response-interceptor)]
+ :get identity}]
+ ["/both"
+ {:interceptors [(muuntaja/format-negotiate-interceptor)
+ (muuntaja/format-response-interceptor)
+ (muuntaja/format-request-interceptor)]
+ :get identity}]
+
+ ["/swagger.json"
+ {:get {:no-doc true
+ :handler (swagger/create-swagger-handler)}}]]
+ {:data {:muuntaja m/instance}})
+ {:executor sieppari/executor})
+ spec (fn [path]
+ (-> {:request-method :get :uri "/swagger.json"}
+ (app) :body :paths (get path) :get))
+ produces (comp :produces spec)
+ consumes (comp :consumes spec)]
+
+ (testing "just request formatting"
+ (let [path "/request"]
+ (is (nil? (produces path)))
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"
+ "application/edn"}
+ (consumes path)))))
+
+ (testing "just response formatting"
+ (let [path "/response"]
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"
+ "application/edn"}
+ (produces path)))
+ (is (nil? (consumes path)))))
+
+ (testing "just response formatting"
+ (let [path "/both"]
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"
+ "application/edn"}
+ (produces path)))
+ (is (= #{"application/json"
+ "application/transit+msgpack"
+ "application/transit+json"
+ "application/edn"}
+ (consumes path)))))))
diff --git a/test/clj/reitit/http/interceptors/parameters_test.clj b/test/clj/reitit/http/interceptors/parameters_test.clj
new file mode 100644
index 00000000..1a1a019a
--- /dev/null
+++ b/test/clj/reitit/http/interceptors/parameters_test.clj
@@ -0,0 +1,3 @@
+(ns reitit.http.interceptors.parameters-test)
+
+;; TODO
diff --git a/test/clj/reitit/ring/middleware/parameters_test.clj b/test/clj/reitit/ring/middleware/parameters_test.clj
new file mode 100644
index 00000000..880a81d9
--- /dev/null
+++ b/test/clj/reitit/ring/middleware/parameters_test.clj
@@ -0,0 +1,3 @@
+(ns reitit.ring.middleware.parameters-test)
+
+;; TODO
diff --git a/test/cljc/reitit/interceptor_test.cljc b/test/cljc/reitit/interceptor_test.cljc
index cecc7a8a..639db51d 100644
--- a/test/cljc/reitit/interceptor_test.cljc
+++ b/test/cljc/reitit/interceptor_test.cljc
@@ -111,21 +111,21 @@
(let [app (create [[i1 :value]])]
(dotimes [_ 10]
(is (= [:data :value :ok] (app ctx)))
- (is (= 2 @calls)))))
+ (is (= 1 @calls)))))
(testing "as interceptor"
(reset! calls 0)
(let [app (create [(i1 :value)])]
(dotimes [_ 10]
(is (= [:data :value :ok] (app ctx)))
- (is (= 2 @calls)))))
+ (is (= 1 @calls)))))
(testing "deeply compiled interceptor"
(reset! calls 0)
(let [app (create [[i3 :value]])]
(dotimes [_ 10]
(is (= [:data :value :ok] (app ctx)))
- (is (= 4 @calls)))))
+ (is (= 3 @calls)))))
(testing "too deeply compiled interceptor fails"
(binding [interceptor/*max-compile-depth* 2]