mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11:11 +00:00
commit
48504bf71e
6 changed files with 288 additions and 84 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
(ns example.server
|
(ns example.server
|
||||||
(:require [io.pedestal.http]
|
(:require [io.pedestal.http]
|
||||||
[clojure.core.async :as a]
|
[clojure.core.async :as a]
|
||||||
[reitit.pedestal :as pedestal]
|
[reitit.interceptor.pedestal :as pedestal]
|
||||||
[reitit.http :as http]
|
[reitit.http :as http]
|
||||||
[reitit.ring :as ring]))
|
[reitit.ring :as ring]))
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
(http/router
|
(http/router
|
||||||
["/api"
|
["/api"
|
||||||
{:interceptors [[interceptor :api]
|
{:interceptors [[interceptor :api]
|
||||||
[interceptor :apa]]}
|
[interceptor :ipa]]}
|
||||||
|
|
||||||
["/sync"
|
["/sync"
|
||||||
{:interceptors [[interceptor :sync]]
|
{:interceptors [[interceptor :sync]]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(ns reitit.pedestal
|
(ns reitit.interceptor.pedestal
|
||||||
(:require [io.pedestal.interceptor.chain :as chain]
|
(:require [io.pedestal.interceptor.chain :as chain]
|
||||||
[io.pedestal.interceptor :as interceptor]
|
[io.pedestal.interceptor :as interceptor]
|
||||||
[io.pedestal.http :as http]
|
[io.pedestal.http :as http]
|
||||||
|
|
@ -89,60 +89,63 @@
|
||||||
context))}))
|
context))}))
|
||||||
|
|
||||||
(defn ring-handler
|
(defn ring-handler
|
||||||
"Creates a ring-handler out of a http-router,
|
"Creates a ring-handler out of a http-router, optional default ring-handler
|
||||||
a default ring-handler and options map, with the following keys:
|
and options map, with the following keys:
|
||||||
|
|
||||||
| key | description |
|
| key | description |
|
||||||
| ----------------|-------------|
|
| ----------------|-------------|
|
||||||
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
||||||
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler"
|
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler"
|
||||||
[router default-handler {:keys [executor interceptors]}]
|
([router opts]
|
||||||
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
(ring-handler router nil opts))
|
||||||
default-queue (->> [default-handler]
|
([router default-handler {:keys [executor interceptors]}]
|
||||||
(concat interceptors)
|
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
||||||
(map #(interceptor/into-interceptor % nil (r/options router)))
|
default-queue (->> [default-handler]
|
||||||
(interceptor/queue executor))
|
(concat interceptors)
|
||||||
router-opts (-> (r/options router)
|
(map #(interceptor/into-interceptor % nil (r/options router)))
|
||||||
(assoc ::interceptor/queue (partial interceptor/queue executor))
|
(interceptor/queue executor))
|
||||||
(cond-> (seq interceptors)
|
router-opts (-> (r/options router)
|
||||||
(update-in [:data :interceptors] (partial into (vec interceptors)))))
|
(assoc ::interceptor/queue (partial interceptor/queue executor))
|
||||||
router (reitit.http/router (r/routes router) router-opts)]
|
(dissoc :data) ; data is already merged into routes
|
||||||
(with-meta
|
(cond-> (seq interceptors)
|
||||||
(fn
|
(update-in [:data :interceptors] (partial into (vec interceptors)))))
|
||||||
([request]
|
router (reitit.http/router (r/routes router) router-opts)]
|
||||||
(if-let [match (r/match-by-path router (:uri request))]
|
(with-meta
|
||||||
(let [method (:request-method request)
|
(fn
|
||||||
path-params (:path-params match)
|
([request]
|
||||||
endpoint (-> match :result method)
|
(if-let [match (r/match-by-path router (:uri request))]
|
||||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
(let [method (:request-method request)
|
||||||
request (-> request
|
path-params (:path-params match)
|
||||||
(impl/fast-assoc :path-params path-params)
|
endpoint (-> match :result method)
|
||||||
(impl/fast-assoc ::r/match match)
|
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||||
(impl/fast-assoc ::r/router router))]
|
request (-> request
|
||||||
(or (interceptor/execute executor interceptors request)
|
(impl/fast-assoc :path-params path-params)
|
||||||
(interceptor/execute executor default-queue request)))
|
(impl/fast-assoc ::r/match match)
|
||||||
(interceptor/execute executor default-queue request)))
|
(impl/fast-assoc ::r/router router))]
|
||||||
([request respond raise]
|
(or (interceptor/execute executor interceptors request)
|
||||||
(let [default #(interceptor/execute executor default-queue % respond raise)]
|
(interceptor/execute executor default-queue request)))
|
||||||
(if-let [match (r/match-by-path router (:uri request))]
|
(interceptor/execute executor default-queue request)))
|
||||||
(let [method (:request-method request)
|
([request respond raise]
|
||||||
path-params (:path-params match)
|
(let [default #(interceptor/execute executor default-queue % respond raise)]
|
||||||
endpoint (-> match :result method)
|
(if-let [match (r/match-by-path router (:uri request))]
|
||||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
(let [method (:request-method request)
|
||||||
request (-> request
|
path-params (:path-params match)
|
||||||
(impl/fast-assoc :path-params path-params)
|
endpoint (-> match :result method)
|
||||||
(impl/fast-assoc ::r/match match)
|
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||||
(impl/fast-assoc ::r/router router))
|
request (-> request
|
||||||
respond' (fn [response]
|
(impl/fast-assoc :path-params path-params)
|
||||||
(if response
|
(impl/fast-assoc ::r/match match)
|
||||||
(respond response)
|
(impl/fast-assoc ::r/router router))
|
||||||
(default request)))]
|
respond' (fn [response]
|
||||||
(if interceptors
|
(if response
|
||||||
(interceptor/execute executor interceptors request respond' raise)
|
(respond response)
|
||||||
(default request)))
|
(default request)))]
|
||||||
(default request)))
|
(if interceptors
|
||||||
nil))
|
(interceptor/execute executor interceptors request respond' raise)
|
||||||
{::r/router router})))
|
(default request)))
|
||||||
|
(default request)))
|
||||||
|
nil))
|
||||||
|
{::r/router router}))))
|
||||||
|
|
||||||
(defn get-router [handler]
|
(defn get-router [handler]
|
||||||
(-> handler meta ::r/router))
|
(-> handler meta ::r/router))
|
||||||
|
|
|
||||||
|
|
@ -3,51 +3,54 @@
|
||||||
[reitit.spec :as rs]
|
[reitit.spec :as rs]
|
||||||
[reitit.impl :as impl]))
|
[reitit.impl :as impl]))
|
||||||
|
|
||||||
(def coerce-request-interceptor
|
(defn coerce-request-interceptor
|
||||||
"Interceptor for pluggable request coercion.
|
"Interceptor for pluggable request coercion.
|
||||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||||
and :parameters from route data, otherwise does not mount."
|
and :parameters from route data, otherwise does not mount."
|
||||||
|
[]
|
||||||
{:name ::coerce-request
|
{:name ::coerce-request
|
||||||
:spec ::rs/parameters
|
:spec ::rs/parameters
|
||||||
:compile (fn [{:keys [coercion parameters]} opts]
|
:compile (fn [{:keys [coercion parameters]} opts]
|
||||||
(if (and coercion parameters)
|
(if (and coercion parameters)
|
||||||
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
(let [coercers (coercion/request-coercers coercion parameters opts)]
|
||||||
{:enter
|
{:enter (fn [ctx]
|
||||||
(fn [ctx]
|
(let [request (:request ctx)
|
||||||
(let [request (:request ctx)
|
coerced (coercion/coerce-request coercers request)
|
||||||
coerced (coercion/coerce-request coercers request)
|
request (impl/fast-assoc request :parameters coerced)]
|
||||||
request (impl/fast-assoc request :parameters coerced)]
|
(assoc ctx :request request)))})))})
|
||||||
(assoc ctx :request request)))})))})
|
|
||||||
|
|
||||||
(def coerce-response-interceptor
|
(defn coerce-response-interceptor
|
||||||
"Interceptor for pluggable response coercion.
|
"Interceptor for pluggable response coercion.
|
||||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||||
and :responses from route data, otherwise does not mount."
|
and :responses from route data, otherwise does not mount."
|
||||||
|
[]
|
||||||
{:name ::coerce-response
|
{:name ::coerce-response
|
||||||
:spec ::rs/responses
|
:spec ::rs/responses
|
||||||
:compile (fn [{:keys [coercion responses]} opts]
|
:compile (fn [{:keys [coercion responses]} opts]
|
||||||
(if (and coercion responses)
|
(if (and coercion responses)
|
||||||
(let [coercers (coercion/response-coercers coercion responses opts)]
|
(let [coercers (coercion/response-coercers coercion responses opts)]
|
||||||
{:leave
|
{:leave (fn [ctx]
|
||||||
(fn [ctx]
|
(let [request (:request ctx)
|
||||||
(let [response (coercion/coerce-response coercers (:request ctx) (:response ctx))]
|
response (:response ctx)]
|
||||||
(assoc ctx :response response)))})))})
|
(let [response (coercion/coerce-response coercers request response)]
|
||||||
|
(assoc ctx :response response))))})))})
|
||||||
|
|
||||||
(def coerce-exceptions-interceptor
|
(defn coerce-exceptions-interceptor
|
||||||
"Interceptor for handling coercion exceptions.
|
"Interceptor for handling coercion exceptions.
|
||||||
Expects a :coercion of type `reitit.coercion/Coercion`
|
Expects a :coercion of type `reitit.coercion/Coercion`
|
||||||
and :parameters or :responses from route data, otherwise does not mount."
|
and :parameters or :responses from route data, otherwise does not mount."
|
||||||
{:name ::coerce-exceptions
|
[]
|
||||||
:compile (fn [{:keys [coercion parameters responses]} _]
|
{:name ::coerce-exceptions
|
||||||
(if (and coercion (or parameters responses))
|
:compile (fn [{:keys [coercion parameters responses]} _]
|
||||||
{:error (fn [ctx]
|
(if (and coercion (or parameters responses))
|
||||||
(let [data (ex-data (:error ctx))]
|
{:error (fn [ctx]
|
||||||
(if-let [status (case (:type data)
|
(let [data (ex-data (:error ctx))]
|
||||||
::coercion/request-coercion 400
|
(if-let [status (case (:type data)
|
||||||
::coercion/response-coercion 500
|
::coercion/request-coercion 400
|
||||||
nil)]
|
::coercion/response-coercion 500
|
||||||
(let [response {:status status, :body (coercion/encode-error data)}]
|
nil)]
|
||||||
(-> ctx
|
(let [response {:status status, :body (coercion/encode-error data)}]
|
||||||
(assoc :response response)
|
(-> ctx
|
||||||
(assoc :error nil)))
|
(assoc :response response)
|
||||||
ctx)))}))})
|
(assoc :error nil)))
|
||||||
|
ctx)))}))})
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
[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-alpha4"]
|
[metosin/muuntaja "0.6.0-alpha5"]
|
||||||
[metosin/jsonista "0.2.1"]
|
[metosin/jsonista "0.2.1"]
|
||||||
[metosin/sieppari "0.0.0-alpha4"]]
|
[metosin/sieppari "0.0.0-alpha4"]]
|
||||||
|
|
||||||
|
|
@ -62,7 +62,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-alpha4"]
|
[metosin/muuntaja "0.6.0-alpha5"]
|
||||||
[metosin/ring-swagger-ui "2.2.10"]
|
[metosin/ring-swagger-ui "2.2.10"]
|
||||||
[metosin/sieppari "0.0.0-alpha4"]
|
[metosin/sieppari "0.0.0-alpha4"]
|
||||||
[metosin/jsonista "0.2.1"]
|
[metosin/jsonista "0.2.1"]
|
||||||
|
|
|
||||||
198
test/clj/reitit/http_coercion_test.clj
Normal file
198
test/clj/reitit/http_coercion_test.clj
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
(ns reitit.http-coercion-test
|
||||||
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
|
[schema.core :as s]
|
||||||
|
[reitit.http :as http]
|
||||||
|
[reitit.http.coercion :as rrc]
|
||||||
|
[reitit.coercion.spec :as spec]
|
||||||
|
[reitit.coercion.schema :as schema]
|
||||||
|
[muuntaja.interceptor]
|
||||||
|
[jsonista.core :as j]
|
||||||
|
[reitit.interceptor.sieppari :as sieppari])
|
||||||
|
(:import (clojure.lang ExceptionInfo)
|
||||||
|
(java.io ByteArrayInputStream)))
|
||||||
|
|
||||||
|
(defn handler [{{{:keys [a]} :query
|
||||||
|
{:keys [b]} :body
|
||||||
|
{:keys [c]} :form
|
||||||
|
{:keys [d]} :header
|
||||||
|
{:keys [e]} :path} :parameters}]
|
||||||
|
(if (= 666 a)
|
||||||
|
{:status 500
|
||||||
|
:body {:evil true}}
|
||||||
|
{:status 200
|
||||||
|
:body {:total (+ a b c d e)}}))
|
||||||
|
|
||||||
|
(def valid-request
|
||||||
|
{:uri "/api/plus/5"
|
||||||
|
:request-method :get
|
||||||
|
:query-params {"a" "1"}
|
||||||
|
:body-params {:b 2}
|
||||||
|
:form-params {:c 3}
|
||||||
|
:headers {"d" "4"}})
|
||||||
|
|
||||||
|
(def invalid-request
|
||||||
|
{:uri "/api/plus/5"
|
||||||
|
:request-method :get})
|
||||||
|
|
||||||
|
(def invalid-request2
|
||||||
|
{:uri "/api/plus/5"
|
||||||
|
:request-method :get
|
||||||
|
:query-params {"a" "1"}
|
||||||
|
:body-params {:b 2}
|
||||||
|
:form-params {:c 3}
|
||||||
|
:headers {"d" "-40"}})
|
||||||
|
|
||||||
|
(deftest spec-coercion-test
|
||||||
|
(let [create (fn [interceptors]
|
||||||
|
(http/ring-handler
|
||||||
|
(http/router
|
||||||
|
["/api"
|
||||||
|
["/plus/:e"
|
||||||
|
{:get {:parameters {:query {:a int?}
|
||||||
|
:body {:b int?}
|
||||||
|
:form {:c int?}
|
||||||
|
:header {:d int?}
|
||||||
|
:path {:e int?}}
|
||||||
|
:responses {200 {:body {:total pos-int?}}
|
||||||
|
500 {:description "fail"}}
|
||||||
|
:handler handler}}]]
|
||||||
|
{:data {:interceptors interceptors
|
||||||
|
:coercion spec/coercion}})
|
||||||
|
{:executor sieppari/executor}))]
|
||||||
|
|
||||||
|
(testing "without exception handling"
|
||||||
|
(let [app (create [(rrc/coerce-request-interceptor)
|
||||||
|
(rrc/coerce-response-interceptor)])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request)))
|
||||||
|
(is (= {:status 500
|
||||||
|
:body {:evil true}}
|
||||||
|
(app (assoc-in valid-request [:query-params "a"] "666")))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Request coercion failed"
|
||||||
|
(app invalid-request))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Response coercion failed"
|
||||||
|
(app invalid-request2))))))
|
||||||
|
|
||||||
|
(testing "with exception handling"
|
||||||
|
(let [app (create [(rrc/coerce-exceptions-interceptor)
|
||||||
|
(rrc/coerce-request-interceptor)
|
||||||
|
(rrc/coerce-response-interceptor)])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(let [{:keys [status]} (app invalid-request)]
|
||||||
|
(is (= 400 status))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(let [{:keys [status]} (app invalid-request2)]
|
||||||
|
(is (= 500 status))))))))
|
||||||
|
|
||||||
|
(deftest schema-coercion-test
|
||||||
|
(let [create (fn [middleware]
|
||||||
|
(http/ring-handler
|
||||||
|
(http/router
|
||||||
|
["/api"
|
||||||
|
["/plus/:e"
|
||||||
|
{:get {:parameters {:query {:a s/Int}
|
||||||
|
:body {:b s/Int}
|
||||||
|
:form {:c s/Int}
|
||||||
|
:header {:d s/Int}
|
||||||
|
:path {:e s/Int}}
|
||||||
|
:responses {200 {:body {:total (s/constrained s/Int pos? 'positive)}}
|
||||||
|
500 {:description "fail"}}
|
||||||
|
:handler handler}}]]
|
||||||
|
{:data {:interceptors middleware
|
||||||
|
:coercion schema/coercion}})
|
||||||
|
{:executor sieppari/executor}))]
|
||||||
|
|
||||||
|
(testing "withut exception handling"
|
||||||
|
(let [app (create [(rrc/coerce-request-interceptor)
|
||||||
|
(rrc/coerce-response-interceptor)])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request)))
|
||||||
|
(is (= {:status 500
|
||||||
|
:body {:evil true}}
|
||||||
|
(app (assoc-in valid-request [:query-params "a"] "666")))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Request coercion failed"
|
||||||
|
(app invalid-request))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Response coercion failed"
|
||||||
|
(app invalid-request2))))
|
||||||
|
|
||||||
|
(testing "with exception handling"
|
||||||
|
(let [app (create [(rrc/coerce-exceptions-interceptor)
|
||||||
|
(rrc/coerce-request-interceptor)
|
||||||
|
(rrc/coerce-response-interceptor)])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(let [{:keys [status]} (app invalid-request)]
|
||||||
|
(is (= 400 status))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(let [{:keys [status]} (app invalid-request2)]
|
||||||
|
(is (= 500 status))))))))))
|
||||||
|
|
||||||
|
(deftest muuntaja-test
|
||||||
|
(let [app (http/ring-handler
|
||||||
|
(http/router
|
||||||
|
["/api"
|
||||||
|
["/plus"
|
||||||
|
{:post {:parameters {:body {:int int?, :keyword keyword?}}
|
||||||
|
:responses {200 {:body {:int int?, :keyword keyword?}}}
|
||||||
|
:handler (fn [{{:keys [body]} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body body})}}]]
|
||||||
|
{:data {:interceptors [(muuntaja.interceptor/format-interceptor)
|
||||||
|
(rrc/coerce-response-interceptor)
|
||||||
|
(rrc/coerce-request-interceptor)]
|
||||||
|
:coercion spec/coercion}})
|
||||||
|
{:executor sieppari/executor})
|
||||||
|
request (fn [content-type body]
|
||||||
|
(-> {:request-method :post
|
||||||
|
:headers {"content-type" content-type, "accept" content-type}
|
||||||
|
:uri "/api/plus"
|
||||||
|
:body body}))
|
||||||
|
data-edn {:int 1 :keyword :kikka}
|
||||||
|
data-json {:int 1 :keyword "kikka"}]
|
||||||
|
|
||||||
|
(testing "json coercion"
|
||||||
|
(let [e2e #(-> (request "application/json" (ByteArrayInputStream. (j/write-value-as-bytes %)))
|
||||||
|
(app) :body (slurp) (j/read-value (j/object-mapper {:decode-key-fn true})))]
|
||||||
|
(is (= data-json (e2e data-edn)))
|
||||||
|
(is (= data-json (e2e data-json)))))
|
||||||
|
|
||||||
|
(testing "edn coercion"
|
||||||
|
(let [e2e #(-> (request "application/edn" (pr-str %))
|
||||||
|
(app) :body slurp (read-string))]
|
||||||
|
(is (= data-edn (e2e data-edn)))
|
||||||
|
(is (thrown? ExceptionInfo (e2e data-json)))))))
|
||||||
Loading…
Reference in a new issue