mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 08:21:11 +00:00
commit
3890e68100
49 changed files with 1164 additions and 63 deletions
15
CHANGELOG.md
15
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`
|
||||
|
|
|
|||
10
README.md
10
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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Controllers (WIP)
|
||||
# Controllers
|
||||
|
||||
* https://github.com/metosin/reitit/tree/master/examples/frontend-controllers
|
||||
|
||||
|
|
|
|||
26
doc/http/default_interceptors.md
Normal file
26
doc/http/default_interceptors.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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/
|
||||
|
|
|
|||
25
doc/http/pedestal.md
Normal file
25
doc/http/pedestal.md
Normal file
|
|
@ -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
|
||||
71
doc/http/sieppari.md
Normal file
71
doc/http/sieppari.md
Normal file
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
examples/http-swagger/.gitignore
vendored
Normal file
11
examples/http-swagger/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
.hgignore
|
||||
.hg/
|
||||
23
examples/http-swagger/README.md
Normal file
23
examples/http-swagger/README.md
Normal file
|
|
@ -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
|
||||
```
|
||||
|
||||
<img src="https://raw.githubusercontent.com/metosin/reitit/master/examples/ring-swagger/swagger.png" />
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2018 Metosin Oy
|
||||
6
examples/http-swagger/project.clj
Normal file
6
examples/http-swagger/project.clj
Normal file
|
|
@ -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})
|
||||
BIN
examples/http-swagger/resources/reitit.png
Normal file
BIN
examples/http-swagger/resources/reitit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
94
examples/http-swagger/src/example/server.clj
Normal file
94
examples/http-swagger/src/example/server.clj
Normal file
|
|
@ -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))
|
||||
BIN
examples/http-swagger/swagger.png
Normal file
BIN
examples/http-swagger/swagger.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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"]])
|
||||
|
|
|
|||
11
examples/pedestal-swagger/.gitignore
vendored
Normal file
11
examples/pedestal-swagger/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
.hgignore
|
||||
.hg/
|
||||
23
examples/pedestal-swagger/README.md
Normal file
23
examples/pedestal-swagger/README.md
Normal file
|
|
@ -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
|
||||
```
|
||||
|
||||
<img src="https://raw.githubusercontent.com/metosin/reitit/master/examples/pedestal-swagger/swagger.png" />
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2018 Metosin Oy
|
||||
7
examples/pedestal-swagger/project.clj
Normal file
7
examples/pedestal-swagger/project.clj
Normal file
|
|
@ -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})
|
||||
BIN
examples/pedestal-swagger/resources/reitit.png
Normal file
BIN
examples/pedestal-swagger/resources/reitit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
112
examples/pedestal-swagger/src/example/server.clj
Normal file
112
examples/pedestal-swagger/src/example/server.clj
Normal file
|
|
@ -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))
|
||||
|
|
@ -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)))
|
||||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
modules/reitit-interceptors/project.clj
Normal file
12
modules/reitit-interceptors/project.clj
Normal file
|
|
@ -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]])
|
||||
|
|
@ -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)))))}))
|
||||
|
|
@ -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)))})))}))
|
||||
|
|
@ -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))}}})))}))
|
||||
|
|
@ -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))))})
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))))})
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ for ext in \
|
|||
reitit-ring \
|
||||
reitit-middleware \
|
||||
reitit-http \
|
||||
reitit-interceptors \
|
||||
reitit-swagger \
|
||||
reitit-swagger-ui \
|
||||
reitit-frontend \
|
||||
|
|
|
|||
119
test/clj/reitit/http/interceptors/exception_test.clj
Normal file
119
test/clj/reitit/http/interceptors/exception_test.clj
Normal file
|
|
@ -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"})))))))
|
||||
3
test/clj/reitit/http/interceptors/multipart_test.clj
Normal file
3
test/clj/reitit/http/interceptors/multipart_test.clj
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
(ns reitit.http.interceptors.multipart-test)
|
||||
|
||||
;; TODO
|
||||
147
test/clj/reitit/http/interceptors/muuntaja_test.clj
Normal file
147
test/clj/reitit/http/interceptors/muuntaja_test.clj
Normal file
|
|
@ -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)))))))
|
||||
3
test/clj/reitit/http/interceptors/parameters_test.clj
Normal file
3
test/clj/reitit/http/interceptors/parameters_test.clj
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
(ns reitit.http.interceptors.parameters-test)
|
||||
|
||||
;; TODO
|
||||
3
test/clj/reitit/ring/middleware/parameters_test.clj
Normal file
3
test/clj/reitit/ring/middleware/parameters_test.clj
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
(ns reitit.ring.middleware.parameters-test)
|
||||
|
||||
;; TODO
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue