Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reitit default middleware #114

Merged
merged 25 commits into from
Aug 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* [Dynamic Extensions](ring/dynamic_extensions.md)
* [Data-driven Middleware](ring/data_driven_middleware.md)
* [Middleware Registry](ring/middleware_registry.md)
* [Default Middleware](ring/default_middleware.md)
* [Pluggable Coercion](ring/coercion.md)
* [Route Data Validation](ring/route_data_validation.md)
* [Compiling Middleware](ring/compiling_middleware.md)
Expand Down
1 change: 1 addition & 0 deletions doc/ring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [Dynamic Extensions](dynamic_extensions.md)
* [Data-driven Middleware](data_driven_middleware.md)
* [Middleware Registry](middleware_registry.md)
* [Default Middleware](default_middleware.md)
* [Pluggable Coercion](coercion.md)
* [Route Data Validation](route_data_validation.md)
* [Compiling Middleware](compiling_middleware.md)
Expand Down
131 changes: 131 additions & 0 deletions doc/ring/default_middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Default Middleware

```clj
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
```

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.

* [Exception handling](#exception-handling)
* [Content negotiation](#content-negotiation)
* [Multipart request handling](#multipart-request-handling)

## Exception handling

A polished version of [compojure-api](https://github.com/metosin/compojure-api) exception handling. Catches all exceptions and invokes configured exception handler.

```clj
(require '[reitit.ring.middleware.exception :as exception])
```

### `exception/exception-middleware`

A preconfigured middleware using `exception/default-handlers`. Catches:

* Request & response [Coercion](coercion.md) exceptions
* [Muuntaja](https://github.com/metosin/muuntaja) decode exceptions
* Exceptions with `:type` of `:reitit.ring/response`, returning `:response` key from `ex-data`.
* Safely all other exceptions

```clj
(require '[reitit.ring :as ring])

(def app
(ring/ring-handler
(ring/router
["/fail" (fn [_] (throw (Exception. "fail")))]
{:data {:middleware [exception/exception-middleware]}})))

(app {:request-method :get, :uri "/fail"})
;{:status 500
; :body {:type "exception"
; :class "java.lang.Exception"}}
```

### `exception/create-exception-middleware`

Creates the exception-middleware with custom options. 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 keys are available:

| key | description
|--------------|-------------
| `::default` | a default exception handler if nothing else mathced (default `exception/default-handler`).
| `::wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` (no default).

The handler is selected from the options map by exception idenfitifier 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

```clj
;; type hierarchy
(derive ::error ::exception)
(derive ::failure ::exception)
(derive ::horror ::exception)

(defn handler [message exception request]
{:status 500
:body {:message message
:exception (.getClass exception)
:data (ex-data exception)
:uri (:uri request)}})

(def exception-middleware
(exception/create-exception-middleware
(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]
(println "ERROR" (pr-str (:uri request)))
(handler e request))})))

(def app
(ring/ring-handler
(ring/router
["/fail" (fn [_] (throw (ex-info "fail" {:type ::failue})))]
{:data {:middleware [exception-middleware]}})))

(app {:request-method :get, :uri "/fail"})
; ERROR "/fail"
; => {:status 500,
; :body {:message "default"
; :exception clojure.lang.ExceptionInfo
; :data {:type :user/failue}
; :uri "/fail"}}
```

## Content Negotiation

Wrapper for [Muuntaja](https://github.com/metosin/muuntaja) middleware for content-negotiation, request decoding and response encoding. Reads configuration from route data and emit's [swagger](swagger.md) `:produces` and `:consumes` definitions automatically.

```clj
(require '[reitit.ring.middleware.muuntaja :as muuntaja])
```

## Multipart request handling

Wrapper for [Ring Multipart Middleware](https://github.com/ring-clojure/ring/blob/master/ring-core/src/ring/middleware/multipart_params.clj). Conditionally mounts to an endpoint only if it has `:multipart` params defined. Emits swagger `:consumes` definitions automatically.

```clj
(require '[reitit.ring.middleware.multipart :as multipart])
```

## Example app

See an example app with the default middleware in action: https://github.com/metosin/reitit/blob/master/examples/ring-swagger/src/example/server.clj.
8 changes: 4 additions & 4 deletions examples/ring-example/src/example/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[ring.middleware.params]
[muuntaja.middleware]
[reitit.ring :as ring]
[reitit.ring.coercion :as rrc]
[reitit.ring.coercion :as coercion]
[example.dspec]
[example.schema]
[example.spec]))
Expand All @@ -18,9 +18,9 @@
example.spec/routes]
{:data {:middleware [ring.middleware.params/wrap-params
muuntaja.middleware/wrap-format
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})))
coercion/coerce-exceptions-middleware
coercion/coerce-request-middleware
coercion/coerce-response-middleware]}})))

(defn restart []
(swap! server (fn [x]
Expand Down
1 change: 0 additions & 1 deletion examples/ring-swagger/project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
:description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.9.0"]
[ring "1.6.3"]
[metosin/muuntaja "0.5.0"]
[metosin/reitit "0.2.0-SNAPSHOT"]]
:repl-options {:init-ns example.server})
Binary file added examples/ring-swagger/resources/reitit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 49 additions & 45 deletions examples/ring-swagger/src/example/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,45 @@
(:require [reitit.ring :as ring]
[reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
[reitit.ring.coercion :as rrc]
[reitit.coercion.spec :as spec]
[reitit.coercion.schema :as schema]
[schema.core :refer [Int]]

[reitit.ring.coercion :as coercion]
[reitit.coercion.spec]
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.multipart :as multipart]
[ring.middleware.params :as params]
[ring.adapter.jetty :as jetty]
[ring.middleware.params]
[muuntaja.middleware]))
[muuntaja.core :as m]
[clojure.java.io :as io]))

(def app
(ring/ring-handler
(ring/router
["/api"

["/swagger.json"
[["/swagger.json"
{:get {:no-doc true
:swagger {:info {:title "my-api"}}
:handler (swagger/create-swagger-handler)}}]

["/spec"
{:coercion spec/coercion
:swagger {:tags ["spec"]}}
["/files"
{:swagger {:tags ["files"]}}

["/upload"
{:post {:summary "upload a file"
:parameters {:multipart {:file multipart/temp-file-part}}
:responses {200 {:body {:file multipart/temp-file-part}}}
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
{:status 200
:body {:file 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"
Expand All @@ -35,43 +52,30 @@
: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)}})}}]]

["/schema"
{:coercion schema/coercion
:swagger {:tags ["schema"]}}

["/plus"
{:get {:summary "plus with schema 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 schema 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 {:middleware [ring.middleware.params/wrap-params
muuntaja.middleware/wrap-format
swagger/swagger-feature
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:swagger {:produces #{"application/json"
"application/edn"
"application/transit+json"}
:consumes #{"application/json"
"application/edn"
"application/transit+json"}}}})
{:data {:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware [;; query-params & form-params
params/wrap-params
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
exception/exception-middleware
;; decoding request body
muuntaja/format-request-middleware
;; coercing response bodys
coercion/coerce-response-middleware
;; coercing request parameters
coercion/coerce-request-middleware
;; multipart
multipart/multipart-middleware]}})
(ring/routes
(swagger-ui/create-swagger-ui-handler
{:path "/", :url "/api/swagger.json"})
(swagger-ui/create-swagger-ui-handler {:path "/"})
(ring/create-default-handler))))

(defn start []
Expand Down
49 changes: 36 additions & 13 deletions modules/reitit-core/src/reitit/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,19 @@
:or {extract-request-format extract-request-format-default
parameter-coercion default-parameter-coercion}}]
(if coercion
(let [{:keys [keywordize? open? in style]} (parameter-coercion type)
transform (comp (if keywordize? walk/keywordize-keys identity) in)
model (if open? (-open-model coercion model) model)
coercer (-request-coercer coercion style model)]
(fn [request]
(let [value (transform request)
format (extract-request-format request)
result (coercer value format)]
(if (error? result)
(request-coercion-failed! result coercion value in request)
result))))))

(defn extract-response-format-default [request response]
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
model (if open? (-open-model coercion model) model)
coercer (-request-coercer coercion style model)]
(fn [request]
(let [value (transform request)
format (extract-request-format request)
result (coercer value format)]
(if (error? result)
(request-coercion-failed! result coercion value in request)
result)))))))

(defn extract-response-format-default [request _]
(-> request :muuntaja/response :format))

(defn response-coercer [coercion body {:keys [extract-response-format]
Expand Down Expand Up @@ -124,6 +124,7 @@
(->> (for [[k v] parameters
:when v]
[k (request-coercer coercion k v opts)])
(filter second)
(into {})))

(defn response-coercers [coercion responses opts]
Expand All @@ -140,6 +141,28 @@
"{:compile reitit.coercion/compile-request-coercers}\n")
{:match match})))

;;
;; api-docs
;;

(defn get-apidocs [this spesification data]
(let [swagger-parameter {:query :query
:body :body
:form :formData
:header :header
:path :path
:multipart :formData}]
(case spesification
:swagger (->> (update
data
:parameters
(fn [parameters]
(->> parameters
(map (fn [[k v]] [(swagger-parameter k) v]))
(filter first)
(into {}))))
(-get-apidocs this spesification)))))

;;
;; integration
;;
Expand Down
10 changes: 10 additions & 0 deletions modules/reitit-middleware/project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
(defproject metosin/reitit-middleware "0.2.0-SNAPSHOT"
:description "Reitit, common middleware bundled"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:plugins [[lein-parent "0.3.2"]]
:parent-project {:path "../../project.clj"
:inherit [:deploy-repositories :managed-dependencies]}
:dependencies [[metosin/reitit-ring]
[metosin/muuntaja]])
Loading