mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11:11 +00:00
commit
ae9fbe08e0
9 changed files with 354 additions and 30 deletions
|
|
@ -23,7 +23,8 @@
|
|||
* [Ring-router](ring/ring.md)
|
||||
* [Dynamic Extensions](ring/dynamic_extensions.md)
|
||||
* [Data-driven Middleware](ring/data_driven_middleware.md)
|
||||
* [Ring Coercion](ring/coercion.md)
|
||||
* [Pluggable Coercion](ring/coercion.md)
|
||||
* [Route Data Validation](ring/route_data_validation.md)
|
||||
* [Compiling Middleware](ring/compiling_middleware.md)
|
||||
* [Performance](performance.md)
|
||||
* [FAQ](faq.md)
|
||||
|
|
|
|||
|
|
@ -1 +1,6 @@
|
|||
# Frequently Asked Questions
|
||||
|
||||
### Why yet another routing library?
|
||||
|
||||
There are many great routing libs for Clojure, but we felt that none was perfect. We picked
|
||||
best parts of existing libs, added things that were missing (like first-class route data, spec-coercion and full route conflict resolution) trying to make a library it both both fun to use and really, really fast.
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
* [Ring-router](ring.md)
|
||||
* [Dynamic Extensions](dynamic_extensions.md)
|
||||
* [Data-driven Middleware](data_driven_middleware.md)
|
||||
* [Ring Coercion](coercion.md)
|
||||
* [Pluggable Coercion](coercion.md)
|
||||
* [Route Data Validation](route_data_validation.md)
|
||||
* [Compiling Middleware](compiling_middleware.md)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Ring Coercion
|
||||
# Pluggable Coercion
|
||||
|
||||
Coercion is explained in detail [in the Coercion Guide](../coercion/coercion.md). Both request parameters (`:query`, `:body`, `:form`, `:header` and `:path`) and response `:body` can be coerced.
|
||||
Basic coercion is explained in detail [in the Coercion Guide](../coercion/coercion.md). With Ring, both request parameters (`:query`, `:body`, `:form`, `:header` and `:path`) and response `:body` can be coerced.
|
||||
|
||||
To enable coercion, the following things need to be done:
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ Records can have arbitrary keys, but the following keys have a special purpose:
|
|||
|
||||
| key | description |
|
||||
| ---------------|-------------|
|
||||
| `:name` | Name of the middleware as a qualified keyword (optional)
|
||||
| `:name` | Name of the middleware as a qualified keyword
|
||||
| `:spec` | `clojure.spec` definition for the route data, see [route data validation](route_data_validation.md) (optional)
|
||||
| `:wrap` | The actual middleware function of `handler & args => request => response`
|
||||
| `:compile` | Middleware compilation function, see [compiling middleware](compiling_middleware.md).
|
||||
|
||||
|
|
@ -135,11 +136,5 @@ Some things bubblin' under:
|
|||
* Support Middleware dependency resolution with new keys `:requires` and `:provides`. Values are set of top-level keys of the request. e.g.
|
||||
* `InjectUserIntoRequestMiddleware` requires `#{:session}` and provides `#{:user}`
|
||||
* `AuthorizationMiddleware` requires `#{:user}`
|
||||
* Support partial `s/keys` route data specs with Middleware (and Router). Merged together to define sound spec for the route data and/or route data for a given route.
|
||||
* e.g. `AuthrorizationMiddleware` has a spec defining `:roles` key (a set of keywords)
|
||||
* Documentation for the route data
|
||||
* Route data is validated against the spec:
|
||||
* Complain of keywords that are not handled by anything
|
||||
* Propose fixes for typos (Figwheel-style)
|
||||
|
||||
Ideas welcome & see [issues](https://github.com/metosin/reitit/issues) for details.
|
||||
|
|
|
|||
286
doc/ring/route_data_validation.md
Normal file
286
doc/ring/route_data_validation.md
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
# Route Data Validation
|
||||
|
||||
Ring route validation works [just like with core router](../basics/route_data_validation.md), with few differences:
|
||||
|
||||
* `reitit.ring.spec/validate-spec!` should be used instead of `reitit.spec/validate-spec!` - to support validating all endpoints (`:get`, `:post` etc.)
|
||||
* With `clojure.spec` validation, Middleware can contribute to route spec via `:specs` key. The effective route data spec is router spec merged with middleware specs.
|
||||
|
||||
## Example
|
||||
|
||||
Let's build a ring app with with both explicit (via middleware) and implicit (fully-qualified keys) spec validation.
|
||||
|
||||
A simple app with spec-validation turned on:
|
||||
|
||||
```clj
|
||||
(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-spec!
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
||||
All good:
|
||||
|
||||
```clj
|
||||
(app {:request-method :get
|
||||
:uri "/api/internal/users"})
|
||||
; {:status 200, :body "ok"}
|
||||
```
|
||||
|
||||
### Explicit specs via middleware
|
||||
|
||||
Middleware that requires `:zone` to be present in route data:
|
||||
|
||||
```clj
|
||||
(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:
|
||||
|
||||
```clj
|
||||
(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-spec!
|
||||
::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:
|
||||
|
||||
```clj
|
||||
(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-spec!
|
||||
::rs/explain e/expound-str})))
|
||||
|
||||
(app {:request-method :get
|
||||
:uri "/api/internal/users"})
|
||||
; in zone :internal
|
||||
; => {:status 200, :body "ok"}
|
||||
```
|
||||
|
||||
### Implicit specs
|
||||
|
||||
By design, clojure.spec validates all fully-qualified keys with `s/keys` specs even if they are not defined in that keyset. Validation in implicit but powerful.
|
||||
|
||||
Let's reuse the `wrap-enforce-roles` from [Dynamic extensions](dynamic_extensions.md) and define specs for the data:
|
||||
|
||||
```clj
|
||||
(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:
|
||||
|
||||
```clj
|
||||
(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-spec!
|
||||
::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:
|
||||
|
||||
```clj
|
||||
(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-spec!
|
||||
::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`
|
||||
```
|
||||
|
||||
### Pushing the data to the endpoints
|
||||
|
||||
Ability to define (and reuse) route-data in sub-paths is a powerful feature, but having data scattered all around might be harder to reason about. There is always an option to push all data to the endpoints.
|
||||
|
||||
```clj
|
||||
(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-spec!
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
||||
Or even flatten the routes:
|
||||
|
||||
```clj
|
||||
(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-spec!
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
||||
The common Middleware can also be pushed to the router, here cleanly separing behavior and data:
|
||||
|
||||
```clj
|
||||
(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}}}]]
|
||||
{:middleware [zone-middleware wrap-enforce-roles]
|
||||
:validate rrs/validate-spec!
|
||||
::rs/explain e/expound-str})))
|
||||
```
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
(defprotocol IntoMiddleware
|
||||
(into-middleware [this data opts]))
|
||||
|
||||
(defrecord Middleware [name wrap])
|
||||
(defrecord Middleware [name wrap spec])
|
||||
(defrecord Endpoint [data handler middleware])
|
||||
|
||||
(def ^:dynamic *max-compile-depth* 10)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
(ns reitit.ring.spec
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[reitit.middleware #?@(:cljs [:refer [Middleware]])]
|
||||
[reitit.spec :as rs])
|
||||
#?(:clj
|
||||
(:import (reitit.middleware Middleware))))
|
||||
[reitit.middleware :as middleware]
|
||||
[reitit.spec :as rs]))
|
||||
|
||||
;;
|
||||
;; Specs
|
||||
;;
|
||||
|
||||
(s/def ::middleware (s/coll-of (partial instance? Middleware)))
|
||||
(s/def ::middleware (s/coll-of #(satisfies? middleware/IntoMiddleware %)))
|
||||
|
||||
(s/def ::data
|
||||
(s/keys :req-un [::rs/handler]
|
||||
|
|
@ -19,10 +17,22 @@
|
|||
;; Validator
|
||||
;;
|
||||
|
||||
(defn merge-specs [specs]
|
||||
(when-let [non-specs (seq (remove #(or (s/spec? %) (s/get-spec %)) specs))]
|
||||
(throw
|
||||
(ex-info
|
||||
(str "Not all specs satisfy the Spec protocol: " non-specs)
|
||||
{:specs specs
|
||||
:non-specs non-specs})))
|
||||
(s/merge-spec-impl (vec specs) (vec specs) nil))
|
||||
|
||||
(defn- validate-ring-route-data [routes spec]
|
||||
(->> (for [[p _ c] routes
|
||||
[method {:keys [data] :as endpoint}] c
|
||||
:when endpoint]
|
||||
[method {:keys [data middleware] :as endpoint}] c
|
||||
:when endpoint
|
||||
:let [mw-specs (seq (keep :spec middleware))
|
||||
specs (keep identity (into [spec] mw-specs))
|
||||
spec (merge-specs specs)]]
|
||||
(when-let [problems (and spec (s/explain-data spec data))]
|
||||
(rs/->Problem p method data spec problems)))
|
||||
(keep identity) (seq)))
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.ring.spec :as rrs]
|
||||
[reitit.core :as r]
|
||||
[reitit.spec :as rs])
|
||||
[clojure.spec.alpha :as s]
|
||||
[reitit.core :as r])
|
||||
#?(:clj
|
||||
(:import (clojure.lang ExceptionInfo))))
|
||||
|
||||
(s/def ::role #{:admin :user})
|
||||
(s/def ::roles (s/and (s/coll-of ::role :into #{}) set?))
|
||||
|
||||
(deftest route-data-validation-test
|
||||
(testing "validation is turned off by default"
|
||||
|
|
@ -27,13 +29,6 @@
|
|||
(ring/router
|
||||
["/api" {:handler identity
|
||||
:name "kikka"}]
|
||||
{:validate rrs/validate-spec!})))
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
(ring/router
|
||||
["/api" {:handler identity
|
||||
:middleware [{}]}]
|
||||
{:validate rrs/validate-spec!}))))
|
||||
|
||||
(testing "all endpoints are validated"
|
||||
|
|
@ -48,5 +43,36 @@
|
|||
(is (true? (r/router?
|
||||
(ring/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:spec any?
|
||||
:validate rrs/validate-spec!}))))))
|
||||
{:spec (s/spec any?)
|
||||
:validate rrs/validate-spec!}))))
|
||||
|
||||
(testing "predicates are not allowed"
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Not all specs satisfy the Spec protocol"
|
||||
(ring/router
|
||||
["/api" {:handler "identity"}]
|
||||
{:spec any?
|
||||
:validate rrs/validate-spec!})))))
|
||||
|
||||
(testing "middleware can contribute to specs"
|
||||
(is (true? (r/router?
|
||||
(ring/router
|
||||
["/api" {:get {:handler identity
|
||||
:roles #{:admin}}}]
|
||||
{:validate rrs/validate-spec!
|
||||
:data {:middleware [{:spec (s/keys :opt-un [::roles])
|
||||
:wrap (fn [handler]
|
||||
(fn [request]
|
||||
(handler request)))}]}}))))
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo
|
||||
#"Invalid route data"
|
||||
(ring/router
|
||||
["/api" {:get {:handler identity
|
||||
:roles #{:adminz}}}]
|
||||
{:validate rrs/validate-spec!
|
||||
:data {:middleware [{:spec (s/keys :opt-un [::roles])
|
||||
:wrap (fn [handler]
|
||||
(fn [request]
|
||||
(handler request)))}]}})))))
|
||||
|
|
|
|||
Loading…
Reference in a new issue