mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 08:21:11 +00:00
Merge pull request #114 from metosin/reitit-middleware
Reitit default middleware
This commit is contained in:
commit
b7302a236a
22 changed files with 852 additions and 74 deletions
|
|
@ -28,6 +28,7 @@
|
||||||
* [Dynamic Extensions](ring/dynamic_extensions.md)
|
* [Dynamic Extensions](ring/dynamic_extensions.md)
|
||||||
* [Data-driven Middleware](ring/data_driven_middleware.md)
|
* [Data-driven Middleware](ring/data_driven_middleware.md)
|
||||||
* [Middleware Registry](ring/middleware_registry.md)
|
* [Middleware Registry](ring/middleware_registry.md)
|
||||||
|
* [Default Middleware](ring/default_middleware.md)
|
||||||
* [Pluggable Coercion](ring/coercion.md)
|
* [Pluggable Coercion](ring/coercion.md)
|
||||||
* [Route Data Validation](ring/route_data_validation.md)
|
* [Route Data Validation](ring/route_data_validation.md)
|
||||||
* [Compiling Middleware](ring/compiling_middleware.md)
|
* [Compiling Middleware](ring/compiling_middleware.md)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
* [Dynamic Extensions](dynamic_extensions.md)
|
* [Dynamic Extensions](dynamic_extensions.md)
|
||||||
* [Data-driven Middleware](data_driven_middleware.md)
|
* [Data-driven Middleware](data_driven_middleware.md)
|
||||||
* [Middleware Registry](middleware_registry.md)
|
* [Middleware Registry](middleware_registry.md)
|
||||||
|
* [Default Middleware](default_middleware.md)
|
||||||
* [Pluggable Coercion](coercion.md)
|
* [Pluggable Coercion](coercion.md)
|
||||||
* [Route Data Validation](route_data_validation.md)
|
* [Route Data Validation](route_data_validation.md)
|
||||||
* [Compiling Middleware](compiling_middleware.md)
|
* [Compiling Middleware](compiling_middleware.md)
|
||||||
|
|
|
||||||
131
doc/ring/default_middleware.md
Normal file
131
doc/ring/default_middleware.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
[ring.middleware.params]
|
[ring.middleware.params]
|
||||||
[muuntaja.middleware]
|
[muuntaja.middleware]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as coercion]
|
||||||
[example.dspec]
|
[example.dspec]
|
||||||
[example.schema]
|
[example.schema]
|
||||||
[example.spec]))
|
[example.spec]))
|
||||||
|
|
@ -18,9 +18,9 @@
|
||||||
example.spec/routes]
|
example.spec/routes]
|
||||||
{:data {:middleware [ring.middleware.params/wrap-params
|
{:data {:middleware [ring.middleware.params/wrap-params
|
||||||
muuntaja.middleware/wrap-format
|
muuntaja.middleware/wrap-format
|
||||||
rrc/coerce-exceptions-middleware
|
coercion/coerce-exceptions-middleware
|
||||||
rrc/coerce-request-middleware
|
coercion/coerce-request-middleware
|
||||||
rrc/coerce-response-middleware]}})))
|
coercion/coerce-response-middleware]}})))
|
||||||
|
|
||||||
(defn restart []
|
(defn restart []
|
||||||
(swap! server (fn [x]
|
(swap! server (fn [x]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,5 @@
|
||||||
:description "Reitit Ring App with Swagger"
|
:description "Reitit Ring App with Swagger"
|
||||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||||
[ring "1.6.3"]
|
[ring "1.6.3"]
|
||||||
[metosin/muuntaja "0.5.0"]
|
|
||||||
[metosin/reitit "0.2.0-SNAPSHOT"]]
|
[metosin/reitit "0.2.0-SNAPSHOT"]]
|
||||||
:repl-options {:init-ns example.server})
|
:repl-options {:init-ns example.server})
|
||||||
|
|
|
||||||
BIN
examples/ring-swagger/resources/reitit.png
Normal file
BIN
examples/ring-swagger/resources/reitit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
|
|
@ -2,28 +2,45 @@
|
||||||
(:require [reitit.ring :as ring]
|
(:require [reitit.ring :as ring]
|
||||||
[reitit.swagger :as swagger]
|
[reitit.swagger :as swagger]
|
||||||
[reitit.swagger-ui :as swagger-ui]
|
[reitit.swagger-ui :as swagger-ui]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as coercion]
|
||||||
[reitit.coercion.spec :as spec]
|
[reitit.coercion.spec]
|
||||||
[reitit.coercion.schema :as schema]
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
[schema.core :refer [Int]]
|
[reitit.ring.middleware.exception :as exception]
|
||||||
|
[reitit.ring.middleware.multipart :as multipart]
|
||||||
|
[ring.middleware.params :as params]
|
||||||
[ring.adapter.jetty :as jetty]
|
[ring.adapter.jetty :as jetty]
|
||||||
[ring.middleware.params]
|
[muuntaja.core :as m]
|
||||||
[muuntaja.middleware]))
|
[clojure.java.io :as io]))
|
||||||
|
|
||||||
(def app
|
(def app
|
||||||
(ring/ring-handler
|
(ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
["/api"
|
[["/swagger.json"
|
||||||
|
|
||||||
["/swagger.json"
|
|
||||||
{:get {:no-doc true
|
{:get {:no-doc true
|
||||||
:swagger {:info {:title "my-api"}}
|
:swagger {:info {:title "my-api"}}
|
||||||
:handler (swagger/create-swagger-handler)}}]
|
:handler (swagger/create-swagger-handler)}}]
|
||||||
|
|
||||||
["/spec"
|
["/files"
|
||||||
{:coercion spec/coercion
|
{:swagger {:tags ["files"]}}
|
||||||
:swagger {:tags ["spec"]}}
|
|
||||||
|
["/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"
|
["/plus"
|
||||||
{:get {:summary "plus with spec query parameters"
|
{:get {:summary "plus with spec query parameters"
|
||||||
|
|
@ -35,43 +52,30 @@
|
||||||
:post {:summary "plus with spec body parameters"
|
:post {:summary "plus with spec body parameters"
|
||||||
:parameters {:body {:x int?, :y int?}}
|
:parameters {:body {:x int?, :y int?}}
|
||||||
:responses {200 {:body {:total 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}]
|
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:total (+ x y)}})}}]]]
|
:body {:total (+ x y)}})}}]]]
|
||||||
|
|
||||||
{:data {:middleware [ring.middleware.params/wrap-params
|
{:data {:coercion reitit.coercion.spec/coercion
|
||||||
muuntaja.middleware/wrap-format
|
:muuntaja m/instance
|
||||||
swagger/swagger-feature
|
:middleware [;; query-params & form-params
|
||||||
rrc/coerce-exceptions-middleware
|
params/wrap-params
|
||||||
rrc/coerce-request-middleware
|
;; content-negotiation
|
||||||
rrc/coerce-response-middleware]
|
muuntaja/format-negotiate-middleware
|
||||||
:swagger {:produces #{"application/json"
|
;; encoding response body
|
||||||
"application/edn"
|
muuntaja/format-response-middleware
|
||||||
"application/transit+json"}
|
;; exception handling
|
||||||
:consumes #{"application/json"
|
exception/exception-middleware
|
||||||
"application/edn"
|
;; decoding request body
|
||||||
"application/transit+json"}}}})
|
muuntaja/format-request-middleware
|
||||||
|
;; coercing response bodys
|
||||||
|
coercion/coerce-response-middleware
|
||||||
|
;; coercing request parameters
|
||||||
|
coercion/coerce-request-middleware
|
||||||
|
;; multipart
|
||||||
|
multipart/multipart-middleware]}})
|
||||||
(ring/routes
|
(ring/routes
|
||||||
(swagger-ui/create-swagger-ui-handler
|
(swagger-ui/create-swagger-ui-handler {:path "/"})
|
||||||
{:path "/", :url "/api/swagger.json"})
|
|
||||||
(ring/create-default-handler))))
|
(ring/create-default-handler))))
|
||||||
|
|
||||||
(defn start []
|
(defn start []
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@
|
||||||
:or {extract-request-format extract-request-format-default
|
:or {extract-request-format extract-request-format-default
|
||||||
parameter-coercion default-parameter-coercion}}]
|
parameter-coercion default-parameter-coercion}}]
|
||||||
(if coercion
|
(if coercion
|
||||||
(let [{:keys [keywordize? open? in style]} (parameter-coercion type)
|
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
|
||||||
transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
|
||||||
model (if open? (-open-model coercion model) model)
|
model (if open? (-open-model coercion model) model)
|
||||||
coercer (-request-coercer coercion style model)]
|
coercer (-request-coercer coercion style model)]
|
||||||
(fn [request]
|
(fn [request]
|
||||||
|
|
@ -84,9 +84,9 @@
|
||||||
result (coercer value format)]
|
result (coercer value format)]
|
||||||
(if (error? result)
|
(if (error? result)
|
||||||
(request-coercion-failed! result coercion value in request)
|
(request-coercion-failed! result coercion value in request)
|
||||||
result))))))
|
result)))))))
|
||||||
|
|
||||||
(defn extract-response-format-default [request response]
|
(defn extract-response-format-default [request _]
|
||||||
(-> request :muuntaja/response :format))
|
(-> request :muuntaja/response :format))
|
||||||
|
|
||||||
(defn response-coercer [coercion body {:keys [extract-response-format]
|
(defn response-coercer [coercion body {:keys [extract-response-format]
|
||||||
|
|
@ -124,6 +124,7 @@
|
||||||
(->> (for [[k v] parameters
|
(->> (for [[k v] parameters
|
||||||
:when v]
|
:when v]
|
||||||
[k (request-coercer coercion k v opts)])
|
[k (request-coercer coercion k v opts)])
|
||||||
|
(filter second)
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
(defn response-coercers [coercion responses opts]
|
(defn response-coercers [coercion responses opts]
|
||||||
|
|
@ -140,6 +141,28 @@
|
||||||
"{:compile reitit.coercion/compile-request-coercers}\n")
|
"{:compile reitit.coercion/compile-request-coercers}\n")
|
||||||
{:match match})))
|
{: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
|
;; integration
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
10
modules/reitit-middleware/project.clj
Normal file
10
modules/reitit-middleware/project.clj
Normal file
|
|
@ -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]])
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
(ns reitit.ring.middleware.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- on-exception [handlers e request respond raise]
|
||||||
|
(try
|
||||||
|
(respond (call-error-handler handlers e request))
|
||||||
|
(catch Exception e
|
||||||
|
(raise e))))
|
||||||
|
|
||||||
|
(defn- wrap [handlers]
|
||||||
|
(fn [handler]
|
||||||
|
(fn
|
||||||
|
([request]
|
||||||
|
(try
|
||||||
|
(handler request)
|
||||||
|
(catch Throwable e
|
||||||
|
(on-exception handlers e request identity #(throw %)))))
|
||||||
|
([request respond raise]
|
||||||
|
(try
|
||||||
|
(handler request respond (fn [e] (on-exception handlers e request respond raise)))
|
||||||
|
(catch Throwable e
|
||||||
|
(on-exception handlers e request respond raise)))))))
|
||||||
|
|
||||||
|
(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 wrap-exception
|
||||||
|
([handler]
|
||||||
|
(handler default-handlers))
|
||||||
|
([handler options]
|
||||||
|
(-> options wrap handler)))
|
||||||
|
|
||||||
|
(def exception-middleware
|
||||||
|
{:name ::exception
|
||||||
|
:spec ::spec
|
||||||
|
:wrap (wrap default-handlers)})
|
||||||
|
|
||||||
|
(defn create-exception-middleware
|
||||||
|
"Creates a reitit middleware 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
|
||||||
|
|--------------|-------------
|
||||||
|
| `::default` | a default exception handler if nothing else mathced (default [[default-handler]]).
|
||||||
|
| `::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.middleware.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/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]
|
||||||
|
(.printStackTrace e)
|
||||||
|
(handler e request))}))"
|
||||||
|
([]
|
||||||
|
(create-exception-middleware default-handlers))
|
||||||
|
([handlers]
|
||||||
|
{:name ::exception
|
||||||
|
:spec ::spec
|
||||||
|
:wrap (wrap handlers)}))
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
(ns ^:no-doc reitit.ring.middleware.multipart
|
||||||
|
(:refer-clojure :exclude [compile])
|
||||||
|
(: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))
|
||||||
|
|
||||||
|
(defn- compile [options]
|
||||||
|
(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"}}}
|
||||||
|
: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 respond raise]
|
||||||
|
(-> request
|
||||||
|
(multipart-params/multipart-params-request options)
|
||||||
|
(coerced-request coercers)
|
||||||
|
(handler respond raise)))))}))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; public api
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn create-multipart-middleware
|
||||||
|
"Creates a Middleware 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."
|
||||||
|
([]
|
||||||
|
(create-multipart-middleware nil))
|
||||||
|
([options]
|
||||||
|
{:name ::multipart
|
||||||
|
:compile (compile options)}))
|
||||||
|
|
||||||
|
(def multipart-middleware
|
||||||
|
"Middleware 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."
|
||||||
|
(create-multipart-middleware))
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
(ns reitit.ring.middleware.muuntaja
|
||||||
|
(:require [muuntaja.core :as m]
|
||||||
|
[muuntaja.middleware]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
(s/def ::muuntaja (partial instance? m/Muuntaja))
|
||||||
|
(s/def ::spec (s/keys :opt-un [::muuntaja]))
|
||||||
|
|
||||||
|
(defn- displace [x] (with-meta x {:displace true}))
|
||||||
|
|
||||||
|
(def format-middleware
|
||||||
|
{:name ::format
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:data {:swagger {:produces (displace (m/encodes muuntaja))
|
||||||
|
:consumes (displace (m/decodes muuntaja))}}
|
||||||
|
:wrap #(muuntaja.middleware/wrap-format % muuntaja)}))})
|
||||||
|
|
||||||
|
(def format-negotiate-middleware
|
||||||
|
{:name ::format-negotiate
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:wrap #(muuntaja.middleware/wrap-format-negotiate % muuntaja)}))})
|
||||||
|
|
||||||
|
(def format-request-middleware
|
||||||
|
{:name ::format-request
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:data {:swagger {:consumes (displace (m/decodes muuntaja))}}
|
||||||
|
:wrap #(muuntaja.middleware/wrap-format-request % muuntaja)}))})
|
||||||
|
|
||||||
|
(def format-response-middleware
|
||||||
|
{:name ::format-response
|
||||||
|
:spec ::spec
|
||||||
|
:compile (fn [{:keys [muuntaja]} _]
|
||||||
|
(if muuntaja
|
||||||
|
{:data {:swagger {:produces (displace (m/encodes muuntaja))}}
|
||||||
|
:wrap #(muuntaja.middleware/wrap-format-response % muuntaja)}))})
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
(-get-options [_] opts)
|
(-get-options [_] opts)
|
||||||
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
||||||
;; TODO: this looks identical to spec, refactor when schema is done.
|
;; TODO: this looks identical to spec, refactor when schema is done.
|
||||||
(condp = spesification
|
(case spesification
|
||||||
:swagger (swagger/swagger-spec
|
:swagger (swagger/swagger-spec
|
||||||
(merge
|
(merge
|
||||||
(if parameters
|
(if parameters
|
||||||
|
|
|
||||||
30
modules/reitit-schema/src/reitit/ring/schema.cljc
Normal file
30
modules/reitit-schema/src/reitit/ring/schema.cljc
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
(ns reitit.ring.schema
|
||||||
|
(:require [schema.core :as s]
|
||||||
|
[schema-tools.swagger.core :as swagger])
|
||||||
|
#?(:clj (:import (java.io File))))
|
||||||
|
|
||||||
|
(defrecord Upload [m]
|
||||||
|
s/Schema
|
||||||
|
(spec [_]
|
||||||
|
(s/spec m))
|
||||||
|
(explain [_]
|
||||||
|
(cons 'file m))
|
||||||
|
|
||||||
|
swagger/SwaggerSchema
|
||||||
|
(-transform [_ _]
|
||||||
|
{:type "file"}))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(def TempFilePart
|
||||||
|
"Schema for file param created by ring.middleware.multipart-params.temp-file store."
|
||||||
|
(->Upload {:filename s/Str
|
||||||
|
:content-type s/Str
|
||||||
|
:size s/Int
|
||||||
|
:tempfile File})))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(def BytesPart
|
||||||
|
"Schema for file param created by ring.middleware.multipart-params.byte-array store."
|
||||||
|
(->Upload {:filename s/Str
|
||||||
|
:content-type s/Str
|
||||||
|
:bytes s/Any})))
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
(-get-name [_] :spec)
|
(-get-name [_] :spec)
|
||||||
(-get-options [_] opts)
|
(-get-options [_] opts)
|
||||||
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
(-get-apidocs [this spesification {:keys [parameters responses]}]
|
||||||
(condp = spesification
|
(case spesification
|
||||||
:swagger (swagger/swagger-spec
|
:swagger (swagger/swagger-spec
|
||||||
(merge
|
(merge
|
||||||
(if parameters
|
(if parameters
|
||||||
|
|
|
||||||
|
|
@ -77,18 +77,22 @@
|
||||||
(let [{:keys [id] :or {id ::default} :as swagger} (-> match :result request-method :data :swagger)
|
(let [{:keys [id] :or {id ::default} :as swagger} (-> match :result request-method :data :swagger)
|
||||||
->set (fn [x] (if (or (set? x) (sequential? x)) (set x) (conj #{} x)))
|
->set (fn [x] (if (or (set? x) (sequential? x)) (set x) (conj #{} x)))
|
||||||
ids (->set id)
|
ids (->set id)
|
||||||
swagger (->> (dissoc swagger :id)
|
strip-top-level-keys #(dissoc % :id :info :host :basePath :definitions :securityDefinitions)
|
||||||
|
strip-endpoint-keys #(dissoc % :id :parameters :responses :summary :description)
|
||||||
|
swagger (->> (strip-endpoint-keys swagger)
|
||||||
(merge {:swagger "2.0"
|
(merge {:swagger "2.0"
|
||||||
:x-id ids}))
|
:x-id ids}))
|
||||||
accept-route #(-> % second :swagger :id (or ::default) ->set (set/intersection ids) seq)
|
accept-route (fn [route]
|
||||||
transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data}]]
|
(-> 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}]]
|
||||||
(if (and data (not no-doc))
|
(if (and data (not no-doc))
|
||||||
[method
|
[method
|
||||||
(meta-merge
|
(meta-merge
|
||||||
|
(apply meta-merge (keep (comp :swagger :data) middleware))
|
||||||
(if coercion
|
(if coercion
|
||||||
(coercion/-get-apidocs coercion :swagger data))
|
(coercion/get-apidocs coercion :swagger data))
|
||||||
(select-keys data [:tags :summary :description])
|
(select-keys data [:tags :summary :description])
|
||||||
(dissoc swagger :id))]))
|
(strip-top-level-keys swagger))]))
|
||||||
transform-path (fn [[p _ c]]
|
transform-path (fn [[p _ c]]
|
||||||
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
|
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
|
||||||
[(path->template p) endpoint]))]
|
[(path->template p) endpoint]))]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
:inherit [:deploy-repositories :managed-dependencies]}
|
:inherit [:deploy-repositories :managed-dependencies]}
|
||||||
:dependencies [[metosin/reitit-core]
|
:dependencies [[metosin/reitit-core]
|
||||||
[metosin/reitit-ring]
|
[metosin/reitit-ring]
|
||||||
|
[metosin/reitit-middleware]
|
||||||
[metosin/reitit-spec]
|
[metosin/reitit-spec]
|
||||||
[metosin/reitit-schema]
|
[metosin/reitit-schema]
|
||||||
[metosin/reitit-swagger]
|
[metosin/reitit-swagger]
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,18 @@
|
||||||
:managed-dependencies [[metosin/reitit "0.2.0-SNAPSHOT"]
|
:managed-dependencies [[metosin/reitit "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
[metosin/reitit-core "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
[metosin/reitit-ring "0.2.0-SNAPSHOT"]
|
||||||
|
[metosin/reitit-middleware "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
[metosin/reitit-spec "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
[metosin/reitit-schema "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
[metosin/reitit-swagger "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
[metosin/reitit-swagger-ui "0.2.0-SNAPSHOT"]
|
||||||
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
[metosin/reitit-frontend "0.2.0-SNAPSHOT"]
|
||||||
|
|
||||||
[meta-merge "1.0.0"]
|
[meta-merge "1.0.0"]
|
||||||
[ring/ring-core "1.6.3"]
|
[ring/ring-core "1.6.3"]
|
||||||
[metosin/spec-tools "0.7.1"]
|
[metosin/spec-tools "0.7.1"]
|
||||||
[metosin/schema-tools "0.10.3"]
|
[metosin/schema-tools "0.10.3"]
|
||||||
[metosin/ring-swagger-ui "2.2.10"]
|
[metosin/ring-swagger-ui "2.2.10"]
|
||||||
|
[metosin/muuntaja "0.6.0-alpha1"]
|
||||||
[metosin/jsonista "0.2.1"]]
|
[metosin/jsonista "0.2.1"]]
|
||||||
|
|
||||||
:plugins [[jonase/eastwood "0.2.6"]
|
:plugins [[jonase/eastwood "0.2.6"]
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
:source-paths ["modules/reitit/src"
|
:source-paths ["modules/reitit/src"
|
||||||
"modules/reitit-core/src"
|
"modules/reitit-core/src"
|
||||||
"modules/reitit-ring/src"
|
"modules/reitit-ring/src"
|
||||||
|
"modules/reitit-middleware/src"
|
||||||
"modules/reitit-spec/src"
|
"modules/reitit-spec/src"
|
||||||
"modules/reitit-schema/src"
|
"modules/reitit-schema/src"
|
||||||
"modules/reitit-swagger/src"
|
"modules/reitit-swagger/src"
|
||||||
|
|
@ -55,7 +57,7 @@
|
||||||
|
|
||||||
[ring "1.6.3"]
|
[ring "1.6.3"]
|
||||||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||||
[metosin/muuntaja "0.6.0-SNAPSHOT"]
|
[metosin/muuntaja "0.6.0-alpha1"]
|
||||||
[metosin/ring-swagger-ui "2.2.10"]
|
[metosin/ring-swagger-ui "2.2.10"]
|
||||||
[metosin/jsonista "0.2.1"]
|
[metosin/jsonista "0.2.1"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Modules
|
# Modules
|
||||||
for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit-frontend reitit; do
|
for ext in reitit-core reitit-ring reitit-middleware reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit-frontend reitit; do
|
||||||
cd modules/$ext; lein "$@"; cd ../..;
|
cd modules/$ext; lein "$@"; cd ../..;
|
||||||
done
|
done
|
||||||
|
|
|
||||||
116
test/clj/reitit/ring/middleware/exception_test.clj
Normal file
116
test/clj/reitit/ring/middleware/exception_test.clj
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
(ns reitit.ring.middleware.exception-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[reitit.ring.middleware.exception :as exception]
|
||||||
|
[reitit.coercion.spec]
|
||||||
|
[reitit.ring.coercion]
|
||||||
|
[muuntaja.core :as m])
|
||||||
|
(:import (java.sql SQLException SQLWarning)))
|
||||||
|
|
||||||
|
(derive ::kikka ::kukka)
|
||||||
|
|
||||||
|
(deftest exception-test
|
||||||
|
(letfn [(create
|
||||||
|
([f]
|
||||||
|
(create f nil))
|
||||||
|
([f wrap]
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/defaults"
|
||||||
|
{:handler f}]
|
||||||
|
["/coercion"
|
||||||
|
{:middleware [reitit.ring.coercion/coerce-request-middleware
|
||||||
|
reitit.ring.coercion/coerce-response-middleware]
|
||||||
|
:coercion reitit.coercion.spec/coercion
|
||||||
|
:parameters {:query {:x int?, :y int?}}
|
||||||
|
:responses {200 {:body {:total pos-int?}}}
|
||||||
|
:handler f}]]
|
||||||
|
{:data {:middleware [(exception/create-exception-middleware
|
||||||
|
(merge
|
||||||
|
exception/default-handlers
|
||||||
|
{::kikka (constantly {:status 400, :body "kikka"})
|
||||||
|
SQLException (constantly {:status 400, :body "sql"})
|
||||||
|
::exception/wrap wrap}))]}}))))]
|
||||||
|
|
||||||
|
(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"})))))))
|
||||||
143
test/clj/reitit/ring/middleware/muuntaja_test.clj
Normal file
143
test/clj/reitit/ring/middleware/muuntaja_test.clj
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
(ns reitit.ring.middleware.muuntaja-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
|
[reitit.swagger :as swagger]
|
||||||
|
[muuntaja.core :as m]))
|
||||||
|
|
||||||
|
(deftest muuntaja-test
|
||||||
|
(let [data {:kikka "kukka"}
|
||||||
|
app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/ping" {:get (constantly {:status 200, :body data})}]
|
||||||
|
{:data {:muuntaja m/instance
|
||||||
|
:middleware [muuntaja/format-middleware]}}))]
|
||||||
|
(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 (ring/ring-handler
|
||||||
|
(ring/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
|
||||||
|
:middleware [muuntaja/format-middleware]}}))
|
||||||
|
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 (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/request"
|
||||||
|
{:middleware [muuntaja/format-negotiate-middleware
|
||||||
|
muuntaja/format-request-middleware]
|
||||||
|
:get identity}]
|
||||||
|
["/response"
|
||||||
|
{:middleware [muuntaja/format-negotiate-middleware
|
||||||
|
muuntaja/format-response-middleware]
|
||||||
|
:get identity}]
|
||||||
|
["/both"
|
||||||
|
{:middleware [muuntaja/format-negotiate-middleware
|
||||||
|
muuntaja/format-response-middleware
|
||||||
|
muuntaja/format-request-middleware]
|
||||||
|
:get identity}]
|
||||||
|
|
||||||
|
["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (swagger/create-swagger-handler)}}]]
|
||||||
|
{:data {:muuntaja m/instance}}))
|
||||||
|
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)))))))
|
||||||
|
|
@ -182,3 +182,21 @@
|
||||||
(is (= #{::swagger/default}
|
(is (= #{::swagger/default}
|
||||||
(-> {:request-method :get :uri "/swagger.json"}
|
(-> {:request-method :get :uri "/swagger.json"}
|
||||||
(app) :body :x-id)))))
|
(app) :body :x-id)))))
|
||||||
|
|
||||||
|
(deftest all-parameter-types-test
|
||||||
|
(let [app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/parameters"
|
||||||
|
{:post {:coercion spec/coercion
|
||||||
|
:parameters {:query {:q string?}
|
||||||
|
:body {:b string?}
|
||||||
|
:form {:f string?}
|
||||||
|
:header {:h string?}
|
||||||
|
:path {:p string?}}
|
||||||
|
:handler identity}}]
|
||||||
|
["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (swagger/create-swagger-handler)}}]]))
|
||||||
|
spec (:body (app {:request-method :get, :uri "/swagger.json"}))]
|
||||||
|
(is (= ["query" "body" "formData" "header" "path"]
|
||||||
|
(map :in (get-in spec [:paths "/parameters" :post :parameters]))))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue