diff --git a/project.clj b/project.clj index 6c4926db..f843d35e 100644 --- a/project.clj +++ b/project.clj @@ -26,6 +26,9 @@ [metosin/spec-tools "0.3.3"] [org.clojure/spec.alpha "0.1.123"] + [expound "0.2.1"] + [orchestra "2017.08.13"] + [criterium "0.4.4"] [org.clojure/test.check "0.9.0"] [org.clojure/tools.namespace "0.2.11"] diff --git a/src/reitit/core.cljc b/src/reitit/core.cljc index ae1282fb..cdc3e85b 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -100,6 +100,9 @@ (match-by-path [this path]) (match-by-name [this name] [this name params])) +(defn router? [x] + (satisfies? Router x)) + (defrecord Match [template meta result params path]) (defrecord PartialMatch [template meta result params required]) diff --git a/src/reitit/spec.cljc b/src/reitit/spec.cljc new file mode 100644 index 00000000..5673ecb5 --- /dev/null +++ b/src/reitit/spec.cljc @@ -0,0 +1,81 @@ +(ns reitit.spec + (:require [clojure.spec.alpha :as s] + [clojure.spec.gen.alpha :as gen] + [clojure.string :as str] + [reitit.core :as reitit])) + +;; +;; routes +;; + +(s/def ::path (s/with-gen (s/and string? #(str/starts-with? % "/")) + #(gen/fmap (fn [s] (str "/" s)) (s/gen string?)))) + +(s/def ::arg (s/and any? (complement vector?))) +(s/def ::meta (s/map-of keyword? any?)) +(s/def ::result any?) + +(s/def ::raw-route + (s/cat :path ::path + :arg (s/? ::arg) + :childs (s/* (s/spec (s/and ::raw-route))))) + +(s/def ::raw-routes + (s/or :route ::raw-route + :routes (s/coll-of ::raw-route :into []))) + +(s/def ::route + (s/cat :path ::path + :meta ::meta)) + +(s/def ::routes + (s/or :route ::route + :routes (s/coll-of ::route :into []))) + +;; +;; router +;; + +(s/def ::router reitit/router?) + +(s/def :reitit.router/path (s/or :empty #{""} :path ::path)) + +(s/def :reitit.router/routes ::routes) + +(s/def :reitit.router/meta ::meta) + +(s/def :reitit.router/expand + (s/fspec :args (s/cat :arg ::arg, :opts ::opts) + :ret ::route)) + +(s/def :reitit.router/coerce + (s/fspec :args (s/cat :route (s/spec ::route), :opts ::opts) + :ret ::route)) + +(s/def :reitit.router/compile + (s/fspec :args (s/cat :route (s/spec ::route), :opts ::opts) + :ret ::result)) + +(s/def :reitit.router/conflicts + (s/fspec :args (s/cat :conflicts (s/map-of ::route (s/coll-of ::route :into #{}))))) + +(s/def :reitit.router/router + (s/fspec :args (s/cat :routes ::routes, :opts ::opts) + :ret ::router)) + +;; TODO: fspecs fail.. +(s/def ::opts + (s/nilable + (s/keys :opt-un [:reitit.router/path + :reitit.router/routes + :reitit.router/meta + #_:reitit.router/expand + #_:reitit.router/coerce + #_:reitit.router/compile + #_:reitit.router/conflicts + #_:reitit.router/router]))) + +(s/fdef reitit/router + :args (s/or :1arity (s/cat :data (s/spec ::raw-routes)) + :2arity (s/cat :data (s/spec ::raw-routes), :opts ::opts)) + :ret ::router) diff --git a/test/cljc/reitit/spec_test.cljc b/test/cljc/reitit/spec_test.cljc new file mode 100644 index 00000000..2221a966 --- /dev/null +++ b/test/cljc/reitit/spec_test.cljc @@ -0,0 +1,67 @@ +(ns reitit.spec-test + (:require [clojure.test :refer [deftest testing is are]] + [clojure.spec.test.alpha :as stest] + [clojure.spec.alpha :as s] + [reitit.core :as reitit] + [reitit.spec :as spec]) + #?(:clj + (:import (clojure.lang ExceptionInfo)))) + +(stest/instrument `reitit/router) + +(deftest router-spec-test + + (testing "router" + + (testing "route-data" + (are [data] + (is (= true (reitit/router? (reitit/router data)))) + + ["/api" {}] + + [["/api" {}]] + + ["/api" + ["/ipa" ::ipa] + ["/tea" + ["/room"]]]) + + (testing "with invalid routes" + (are [data] + (is (thrown-with-msg? + ExceptionInfo + #"Call to #'reitit.core/router did not conform to spec" + (reitit/router + data))) + + ;; missing slash + ["invalid" {}] + + ;; path + [:invalid {}] + + ;; vector meta + ["/api" [] + ["/ipa"]]))) + + (testing "options" + + (are [opts] + (is (= true (reitit/router? (reitit/router ["/api"] opts)))) + + {:path "/"} + + {:meta {}} + + #_{:coerce (fn [_ _] ["/"])} + ) + + + (are [opts] + (is (thrown-with-msg? + ExceptionInfo + #"Call to #'reitit.core/router did not conform to spec" + (reitit/router + ["/api"] opts))) + + {:meta 1}))))