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]