diff --git a/advanced/configuring_routers.html b/advanced/configuring_routers.html index bf3f5550..7876d5ca 100644 --- a/advanced/configuring_routers.html +++ b/advanced/configuring_routers.html @@ -310,12 +310,12 @@ -
Reitit is a small Clojure(Script) library for data-driven routing.
To use Reitit, add the following dependecy to your project:
@@ -442,6 +456,7 @@[metosin/reitit-core "0.1.0-SNAPSHOT"] ; just the router
[metosin/reitit-ring "0.1.0-SNAPSHOT"] ; ring-router
[metosin/reitit-spec "0.1.0-SNAPSHOT"] ; spec-coercion
+[metosin/reitit-schema "0.1.0-SNAPSHOT"] ; schema coercion
For discussions, there is a #reitit channel in Clojurians slack.
Reitit provides pluggable parameter coercion via reitit.ring.coercion.protocol/Coercion protocol, originally introduced in compojure-api.
Reitit ships with the following coercion modules:
+reitit.ring.coercion.schema/SchemaCoercion for plumatic schema.reitit.ring.coercion.spec/SpecCoercion for both clojure.spec and data-specs.To use Coercion with Ring, one needs to do the following:
:parameters map, with submaps for different parameters: :query, :body, :form, :header and :path. Parameters are defined in the format understood by the Coercion.:responses map, with response status codes as keys (or :default for "everything else") with maps with :schema and optionally :description as values.Coercion implementation to route data under :coercionreitit.ring.coercion/gen-wrap-coerce-parametersreitit.ring.coercion/gen-wrap-coerce-responseIf the request coercion succeeds, the coerced parameters are injected into request under :parameters.
If either request or response coercion fails, an descriptive error is thrown. To turn the exceptions into http responses, one can also mount the reitit.ring.coercion/gen-wrap-coerce-exceptions middleware
(require '[reitit.ring :as ring])
+(require '[reitit.ring.coercion :as coercion])
+(require '[reitit.ring.coercion.schema :as schema])
+(require '[schema.core :as s])
+
+(def app
+ (ring/ring-handler
+ (ring/router
+ ["/api"
+ ["/ping" {:parameters {:body {:x s/Int, :y s/Int}}
+ :responses {200 {:schema {:total (s/constrained s/Int pos?}}}
+ :get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]
+ {:data {:middleware [coercion/gen-wrap-coerce-exceptions
+ coercion/gen-wrap-coerce-parameters
+ coercion/gen-wrap-coerce-response]
+ :coercion schema/coercion}})))
+
+Valid request:
+(app
+ {:request-method :get
+ :uri "/api/ping"
+ :body-params {:x 1, :y 2}})
+; {:status 200
+; :body {:total 3}}
+
+Invalid request:
+(app
+ {:request-method :get
+ :uri "/api/ping"
+ :body-params {:x 1, :y "2"}})
+; {:status 400,
+; :body {:type :reitit.ring.coercion/request-coercion
+; :coercion :schema
+; :in [:request :body-params]
+; :value {:x 1, :y "2"}
+; :schema {:x "Int", :y "Int"}
+; :errors {:y "(not (integer? \"2\"))"}}}
+
+(require '[reitit.ring :as ring])
+(require '[reitit.ring.coercion :as coercion])
+(require '[reitit.ring.coercion.spec :as spec])
+
+(def app
+ (ring/ring-handler
+ (ring/router
+ ["/api"
+ ["/ping" {:parameters {:body {:x int?, :y int?}}
+ :responses {200 {:schema {:total pos-int?}}}
+ :get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]
+ {:data {:middleware [coercion/gen-wrap-coerce-exceptions
+ coercion/gen-wrap-coerce-parameters
+ coercion/gen-wrap-coerce-response]
+ :coercion spec/coercion}})))
+
+Valid request:
+(app
+ {:request-method :get
+ :uri "/api/ping"
+ :body-params {:x 1, :y 2}})
+; {:status 200
+; :body {:total 3}}
+
+Invalid request:
+(app
+ {:request-method :get
+ :uri "/api/ping"
+ :body-params {:x 1, :y "2"}})
+; {:status 400,
+; :body {:type ::coercion/request-coercion
+; :coercion :spec
+; :in [:request :body-params]
+; :value {:x 1, :y "2"}
+; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:$spec37747/x :$spec37747/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})"
+; :problems [{:path [:y]
+; :pred "clojure.core/int?"
+; :val "2"
+; :via [:$spec37747/y]
+; :in [:y]}]}}
+
+Currently, clojure.spec doesn't support runtime transformations via conforming, so one needs to wrap all specs with spec-tools.core/spec.
(require '[reitit.ring :as ring])
+(require '[reitit.ring.coercion :as coercion])
+(require '[reitit.ring.coercion.spec :as spec])
+(require '[clojure.spec.alpha :as s])
+(require '[spec-tools.core :as st])
+
+(s/def ::x (st/spec int?))
+(s/def ::y (st/spec int?))
+(s/def ::total int?)
+(s/def ::request (s/keys :req-un [::x ::y]))
+(s/def ::response (s/keys :req-un [::total]))
+
+(def app
+ (ring/ring-handler
+ (ring/router
+ ["/api"
+ ["/ping" {:parameters {:body ::request}
+ :responses {200 {:schema ::response}}
+ :get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
+ {:status 200
+ :body {:total (+ x y)}})}}]]
+ {:data {:middleware [coercion/gen-wrap-coerce-exceptions
+ coercion/gen-wrap-coerce-parameters
+ coercion/gen-wrap-coerce-response]
+ :coercion spec/coercion}})))
+
+Valid request:
+(app
+ {:request-method :get
+ :uri "/api/ping"
+ :body-params {:x 1, :y 2}})
+; {:status 200
+; :body {:total 3}}
+
+Invalid request:
+(app
+ {:request-method :get
+ :uri "/api/ping"
+ :body-params {:x 1, :y "2"}})
+; {:status 400,
+; :body {:type ::coercion/request-coercion
+; :coercion :spec
+; :in [:request :body-params]
+; :value {:x 1, :y "2"}
+; :spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:reitit.coercion-test/x :reitit.coercion-test/y]), :type :map, :keys #{:y :x}, :keys/req #{:y :x}})"
+; :problems [{:path [:y]
+; :pred "clojure.core/int?"
+; :val "2"
+; :via [::request ::y]
+; :in [:y]}]}}
+
+Both Schema and Spec Coercion can be configured via options, see the source code for details.
+To plug in new validation engine, see the
+reitit.ring.coercion.protocol/Coercion protocol.
(defprotocol Coercion
+ "Pluggable coercion protocol"
+ (get-name [this] "Keyword name for the coercion")
+ (compile [this model name] "Compiles a coercion model")
+ (get-apidocs [this model data] "???")
+ (make-open [this model] "Returns a new map model which doesn't fail on extra keys")
+ (encode-error [this error] "Converts error in to a serializable format")
+ (request-coercer [this type model] "Returns a `value format => value` request coercion function")
+ (response-coercer [this model] "Returns a `value format => value` response coercion function"))
+
+
+
+ The dynamic extensions is a easy way to extend the system. To enable fast lookups into route data, we can compile them into any shape (records, functions etc.) we want, enabling fast access at request-time.
-Still, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware/interceptor at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding much faster runtime processing. It can also decide not to mount itself by returning nil. Why mount a wrap-enforce-roles middleware for a route if there are no roles required for it?
But, we can do much better. As we know the exact route that middleware/interceptor is linked to, we can pass the (compiled) route information into the middleware/interceptor at creation-time. It can do local reasoning: extract and transform relevant data just for it and pass it into the actual request-handler via a closure - yielding much faster runtime processing. It can also decide not to mount itself by returning nil. Why mount a wrap-enforce-roles middleware for a route if there are no roles required for it?
To enable this we use middleware records :gen-wrap key instead of the normal :wrap. :gen-wrap expects a function of route-data router-opts => ?wrap.
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with :gen-wrap. Actual codes can be found in reitit.ring.coercion:
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with :gen-wrap.
(require '[reitit.ring.middleware :as middleware])
(def gen-wrap-coerce-response
- "Generator for pluggable response coercion middleware.
+ "Middleware for pluggable response coercion.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responses from route data, otherwise does not mount."
(middleware/create
{:name ::coerce-response
- :gen-wrap (fn [{:keys [responses coercion opts]} _]
- (if (and coercion responses)
- (let [coercers (response-coercers coercion responses opts)]
- (fn [handler]
- (fn
- ([request]
- (coerce-response coercers request (handler request)))
- ([request respond raise]
- (handler request #(respond (coerce-response coercers request %)) raise)))))))}))
+ :gen-wrap (fn [{:keys [coercion responses opts]} _]
+ (if (and coercion responses)
+ (let [coercers (response-coercers coercion responses opts)]
+ (fn [handler]
+ (fn
+ ([request]
+ (coerce-response coercers request (handler request)))
+ ([request respond raise]
+ (handler request #(respond (coerce-response coercers request %)) raise)))))))}))
The latter has 50% less code, is easier to reason about and is much faster.
@@ -514,7 +527,7 @@ - + @@ -530,7 +543,7 @@ diff --git a/ring/data_driven_middleware.html b/ring/data_driven_middleware.html index f0575187..71c4ef95 100644 --- a/ring/data_driven_middleware.html +++ b/ring/data_driven_middleware.html @@ -57,7 +57,7 @@ - + @@ -310,12 +310,12 @@