Ring route validation works just like with core router, with few differences:
reitit.ring.spec/validate
should be used instead of reitit.spec/validate
- to support validating all endpoints (:get
, :post
etc.)clojure.spec
validation, Middleware can contribute to route spec via :specs
key. The effective route data spec is router spec merged with middleware specs.A simple app with spec-validation turned on:
(require '[clojure.spec.alpha :as s])
(require '[reitit.ring :as ring])
(require '[reitit.ring.spec :as rrs])
(require '[reitit.spec :as rs])
(require '[expound.alpha :as e])
(defn handler [_]
{:status 200, :body "ok"})
(def app
(ring/ring-handler
(ring/router
["/api"
["/public"
["/ping" {:get handler}]]
["/internal"
["/users" {:get {:handler handler}
:delete {:handler handler}}]]]
{:validate rrs/validate
::rs/explain e/expound-str})))
All good:
(app {:request-method :get
:uri "/api/internal/users"})
; {:status 200, :body "ok"}
Middleware that requires :zone
to be present in route data:
(s/def ::zone #{:public :internal})
(def zone-middleware
{:name ::zone-middleware
:spec (s/keys :req-un [::zone])
:wrap (fn [handler]
(fn [request]
(let [zone (-> request (ring/get-match) :data :zone)]
(println zone)
(handler request))))})
Missing route data fails fast at router creation:
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [zone-middleware]} ;; <--- added
["/public"
["/ping" {:get handler}]]
["/internal"
["/users" {:get {:handler handler}
:delete {:handler handler}}]]]
{:validate rrs/validate
::rs/explain e/expound-str})))
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
;
; -- On route -----------------------
;
; "/api/public/ping" :get
;
; -- Spec failed --------------------
;
; {:middleware ...,
; :handler ...}
;
; should contain key: `:zone`
;
; | key | spec |
; |-------+-------|
; | :zone | :zone |
;
;
; -- On route -----------------------
;
; "/api/internal/users" :get
;
; -- Spec failed --------------------
;
; {:middleware ...,
; :handler ...}
;
; should contain key: `:zone`
;
; | key | spec |
; |-------+-------|
; | :zone | :zone |
;
;
; -- On route -----------------------
;
; "/api/internal/users" :delete
;
; -- Spec failed --------------------
;
; {:middleware ...,
; :handler ...}
;
; should contain key: `:zone`
;
; | key | spec |
; |-------+-------|
; | :zone | :zone |
Adding the :zone
to route data fixes the problem:
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [zone-middleware]}
["/public" {:zone :public} ;; <--- added
["/ping" {:get handler}]]
["/internal" {:zone :internal} ;; <--- added
["/users" {:get {:handler handler}
:delete {:handler handler}}]]]
{:validate rrs/validate
::rs/explain e/expound-str})))
(app {:request-method :get
:uri "/api/internal/users"})
; in zone :internal
; => {:status 200, :body "ok"}
By design, clojure.spec validates all fully-qualified keys with s/keys
specs even if they are not defined in that keyset. Validation is implicit but powerful.
Let's reuse the wrap-enforce-roles
from Dynamic extensions and define specs for the data:
(require '[clojure.set :as set])
(s/def ::role #{:admin :manager})
(s/def ::roles (s/coll-of ::role :into #{}))
(defn wrap-enforce-roles [handler]
(fn [{:keys [::roles] :as request}]
(let [required (some-> request (ring/get-match) :data ::roles)]
(if (and (seq required) (not (set/subset? required roles)))
{:status 403, :body "forbidden"}
(handler request)))))
wrap-enforce-roles
silently ignores if the ::roles
is not present:
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [zone-middleware
wrap-enforce-roles]} ;; <--- added
["/public" {:zone :public}
["/ping" {:get handler}]]
["/internal" {:zone :internal}
["/users" {:get {:handler handler}
:delete {:handler handler}}]]]
{:validate rrs/validate
::rs/explain e/expound-str})))
(app {:request-method :get
:uri "/api/zones/admin/ping"})
; in zone :internal
; => {:status 200, :body "ok"}
But fails if they are present and invalid:
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [zone-middleware
wrap-enforce-roles]}
["/public" {:zone :public}
["/ping" {:get handler}]]
["/internal" {:zone :internal}
["/users" {:get {:handler handler
::roles #{:manager} ;; <--- added
:delete {:handler handler
::roles #{:adminz}}}]]] ;; <--- added
{:validate rrs/validate
::rs/explain e/expound-str})))
; CompilerException clojure.lang.ExceptionInfo: Invalid route data:
;
; -- On route -----------------------
;
; "/api/internal/users" :delete
;
; -- Spec failed --------------------
;
; {:middleware ...,
; :zone ...,
; :handler ...,
; :user/roles #{:adminz}}
; ^^^^^^^
;
; should be one of: `:admin`,`:manager`
Ability to define (and reuse) route-data in mid-paths is a powerful feature, but having data defined all around might be harder to reason about. There is always an option to define all data at the endpoints.
(def app
(ring/ring-handler
(ring/router
["/api"
["/public"
["/ping" {:zone :public
:get handler
:middleware [zone-middleware
wrap-enforce-roles]}]]
["/internal"
["/users" {:zone :internal
:middleware [zone-middleware
wrap-enforce-roles]
:get {:handler handler
::roles #{:manager}}
:delete {:handler handler
::roles #{:admin}}}]]]
{:validate rrs/validate
::rs/explain e/expound-str})))
Or even flatten the routes:
(def app
(ring/ring-handler
(ring/router
[["/api/public/ping" {:zone :public
:get handler
:middleware [zone-middleware
wrap-enforce-roles]}]
["/api/internal/users" {:zone :internal
:middleware [zone-middleware
wrap-enforce-roles]
:get {:handler handler
::roles #{:manager}}
:delete {:handler handler
::roles #{:admin}}}]]
{:validate rrs/validate
::rs/explain e/expound-str})))
The common Middleware can also be pushed to the router, here cleanly separating behavior and data:
(def app
(ring/ring-handler
(ring/router
[["/api/public/ping" {:zone :public
:get handler}]
["/api/internal/users" {:zone :internal
:get {:handler handler
::roles #{:manager}}
:delete {:handler handler
::roles #{:admin}}}]]
{:data {:middleware [zone-middleware wrap-enforce-roles]}
:validate rrs/validate
::rs/explain e/expound-str})))
Can you improve this documentation? These fine people already did:
Tommi Reiman, Marcus Spiegel & tjalkaneEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close