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..bd1aa9da 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -30,7 +30,7 @@ (expand [_ _])) (defn walk [data {:keys [path meta routes expand] - :or {path "", meta [], routes [], expand expand} + :or {meta [], routes [], expand expand} :as opts}] (letfn [(walk-many [p m r] @@ -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]) @@ -242,7 +245,7 @@ | key | description | | -------------|-------------| - | `:path` | Base-path for routes (default `\"\"`) + | `:path` | Base-path for routes | `:routes` | Initial resolved routes (default `[]`) | `:meta` | Initial route meta (default `{}`) | `:expand` | Function of `arg opts => meta` to expand route arg to route meta-data (default `reitit.core/expand`) diff --git a/src/reitit/spec.cljc b/src/reitit/spec.cljc new file mode 100644 index 00000000..8953f3f7 --- /dev/null +++ b/src/reitit/spec.cljc @@ -0,0 +1,63 @@ +(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/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 ::path) +(s/def :reitit.router/routes ::routes) +(s/def :reitit.router/meta ::meta) +(s/def :reitit.router/expand fn?) +(s/def :reitit.router/coerce fn?) +(s/def :reitit.router/compile fn?) +(s/def :reitit.router/conflicts fn?) +(s/def :reitit.router/router fn?) + +(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..cb3f18a6 --- /dev/null +++ b/test/cljc/reitit/spec_test.cljc @@ -0,0 +1,74 @@ +(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 {}} + {:expand (fn [_ _] {})} + {:coerce (fn [route _] route)} + {:compile (fn [_ _])} + {:conflicts (fn [_])} + {:router reitit/linear-router}) + + (are [opts] + (is (thrown-with-msg? + ExceptionInfo + #"Call to #'reitit.core/router did not conform to spec" + (reitit/router + ["/api"] opts))) + + {:path ""} + {:path nil} + {:meta nil} + {:expand nil} + {:coerce nil} + {:compile nil} + {:conflicts nil} + {:router nil}))))