diff --git a/modules/reitit-core/src/reitit/middleware.cljc b/modules/reitit-core/src/reitit/middleware.cljc index d135843b..591a9c97 100644 --- a/modules/reitit-core/src/reitit/middleware.cljc +++ b/modules/reitit-core/src/reitit/middleware.cljc @@ -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) diff --git a/modules/reitit-ring/src/reitit/ring/spec.cljc b/modules/reitit-ring/src/reitit/ring/spec.cljc index 96c407fa..3f8c6020 100644 --- a/modules/reitit-ring/src/reitit/ring/spec.cljc +++ b/modules/reitit-ring/src/reitit/ring/spec.cljc @@ -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 (partial 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))) diff --git a/test/cljc/reitit/ring_spec_test.cljc b/test/cljc/reitit/ring_spec_test.cljc index 8d063d79..726d8e0e 100644 --- a/test/cljc/reitit/ring_spec_test.cljc +++ b/test/cljc/reitit/ring_spec_test.cljc @@ -2,11 +2,14 @@ (:require [clojure.test :refer [deftest testing is]] [reitit.ring :as ring] [reitit.ring.spec :as rrs] + [clojure.spec.alpha :as s] [reitit.core :as r] - [reitit.spec :as rs]) + [expound.alpha :as e]) #?(: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 +30,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 +44,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)))}]}})))))