Merge pull request #114 from metosin/reitit-middleware

Reitit default middleware
This commit is contained in:
Tommi Reiman 2018-08-03 10:09:20 +03:00 committed by GitHub
commit b7302a236a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 852 additions and 74 deletions

View file

@ -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)

View file

@ -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)

View 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.

View file

@ -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]

View file

@ -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})

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

View file

@ -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 []

View file

@ -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
;; ;;

View 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]])

View file

@ -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)}))

View file

@ -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))

View file

@ -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)}))})

View file

@ -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

View 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})))

View file

@ -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

View file

@ -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]))]

View file

@ -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]

View file

@ -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"]

View file

@ -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

View 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"})))))))

View 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)))))))

View file

@ -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]))))))