diff --git a/README.md b/README.md index c73eba40..59a6f0c6 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,13 @@ The expanded routes: ; ["/api/user/:id" {:name :user/user}]] ``` +Route names: + +```clj +(reitit/route-names router) +; [:user/ping :user/user] +``` + Path-based routing: ```clj @@ -114,8 +121,19 @@ Name-based (reverse) routing: ```clj (reitit/match-by-name router ::user) -; ExceptionInfo missing path-params for route '/api/user/:id': #{:id} +; #PartialMatch{:template "/api/user/:id", +; :meta {:name :user/user}, +; :handler nil, +; :params nil, +; :required #{:id}} +(reitit/partial-match? (reitit/match-by-name router ::user)) +; true +``` + +Only a partial match. Let's provide path-parameters: + +```clj (reitit/match-by-name router ::user {:id "1"}) ; #Match{:template "/api/user/:id" ; :meta {:name :user/user} @@ -124,6 +142,13 @@ Name-based (reverse) routing: ; :params {:id "1"}} ``` +There is also a exception throwing version: + +``` +(reitit/match-by-name! router ::user) +; ExceptionInfo missing path-params for route /api/user/:id: #{:id} +``` + ## Route meta-data Routes can have arbitrary meta-data. For nested routes, the meta-data is accumulated from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge). diff --git a/src/reitit/core.cljc b/src/reitit/core.cljc index cdd36186..9bc35368 100644 --- a/src/reitit/core.cljc +++ b/src/reitit/core.cljc @@ -60,30 +60,54 @@ (cond->> (->> (walk data opts) (map-meta merge-meta)) coerce (into [] (keep #(coerce % opts))))) +(defn name-lookup [[_ {:keys [name]}] opts] + (if name #{name})) + +(defn find-names [routes opts] + (into [] (keep #(-> % second :name) routes))) + (defn compile-route [[p m :as route] {:keys [compile] :as opts}] [p m (if compile (compile route opts))]) (defprotocol Routing (routes [this]) + (route-names [this]) (match-by-path [this path]) - (match-by-name [this name] [this name parameters])) + (match-by-name [this name] [this name params])) -(defrecord Match [template meta path handler params]) +(defrecord Match [template meta handler params path]) +(defrecord PartialMatch [template meta handler params required]) + +(defn partial-match? [x] + (instance? PartialMatch x)) + +(defn match-by-name! + ([this name] + (match-by-name! this name nil)) + ([this name params] + (if-let [match (match-by-name this name params)] + (if-not (partial-match? match) + match + (impl/throw-on-missing-path-params + (:template match) (:required match) params))))) (def default-router-options - {:expand expand + {:lookup name-lookup + :expand expand :coerce (fn [route _] route) :compile (fn [[_ {:keys [handler]}] _] handler)}) -(defrecord LinearRouter [routes data lookup] +(defrecord LinearRouter [routes names data lookup] Routing (routes [_] routes) + (route-names [_] + names) (match-by-path [_ path] (reduce (fn [acc ^Route route] (if-let [params ((:matcher route) path)] - (reduced (->Match (:path route) (:meta route) path (:handler route) params)))) + (reduced (->Match (:path route) (:meta route) (:handler route) params path)))) nil data)) (match-by-name [_ name] (if-let [match (lookup name)] @@ -99,19 +123,24 @@ (linear-router routes {})) ([routes opts] (let [compiled (map #(compile-route % opts) routes) + names (find-names routes opts) [data lookup] (reduce (fn [[data lookup] [p {:keys [name] :as meta} handler]] - (let [route (impl/create [p meta handler])] + (let [{:keys [params] :as route} (impl/create [p meta handler]) + f #(if-let [path (impl/path-for route %)] + (->Match p meta handler % path) + (->PartialMatch p meta handler % params))] [(conj data route) - (if name - (assoc lookup name #(->Match p meta (impl/path-for route %) handler %)) - lookup)])) [[] {}] compiled)] - (->LinearRouter routes data lookup)))) + (if name (assoc lookup name f) lookup)])) + [[] {}] compiled)] + (->LinearRouter routes names data lookup)))) -(defrecord LookupRouter [routes data lookup] +(defrecord LookupRouter [routes names data lookup] Routing (routes [_] routes) + (route-names [_] + names) (match-by-path [_ path] (data path)) (match-by-name [_ name] @@ -134,13 +163,14 @@ {:route route :routes routes}))) (let [compiled (map #(compile-route % opts) routes) + names (find-names routes opts) [data lookup] (reduce (fn [[data lookup] [p {:keys [name] :as meta} handler]] - [(assoc data p (->Match p meta p handler {})) + [(assoc data p (->Match p meta handler {} p)) (if name - (assoc lookup name #(->Match p meta p handler %)) + (assoc lookup name #(->Match p meta handler % p)) lookup)]) [{} {}] compiled)] - (->LookupRouter routes data lookup)))) + (->LookupRouter routes names data lookup)))) (defn router "Create a [[Router]] from raw route data and optionally an options map. diff --git a/src/reitit/impl.cljc b/src/reitit/impl.cljc index 38fb5577..cd6b5d69 100644 --- a/src/reitit/impl.cljc +++ b/src/reitit/impl.cljc @@ -122,12 +122,16 @@ :handler handler}))) (defn path-for [^Route route params] - (when-not (every? #(contains? params %) (:params route)) + (if-let [required (:params route)] + (if (every? #(contains? params %) required) + (str "/" (str/join \/ (map #(get (or params {}) % %) (:parts route))))) + (:path route))) + +(defn throw-on-missing-path-params [template required params] + (when-not (every? #(contains? params %) required) (let [defined (-> params keys set) - required (:params route) missing (clojure.set/difference required defined)] (throw (ex-info - (str "missing path-params for route '" (:path route) "': " missing) - {:params params, :required required})))) - (str "/" (str/join \/ (map #(get params % %) (:parts route))))) + (str "missing path-params for route " template ": " missing) + {:params params, :required required}))))) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 6e28e101..77a2e7fd 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -25,11 +25,19 @@ :params {:size "large"}}) (reitit/match-by-name router ::beer {:size "large"}))) (is (= nil (reitit/match-by-name router "ILLEGAL"))) - (testing "name-based routing at runtime for missing parameters" + (is (= [::beer] (reitit/route-names router))) + (testing "name-based routing with missing parameters" + (is (= (reitit/map->PartialMatch + {:template "/api/ipa/:size" + :meta {:name ::beer} + :required #{:size} + :params nil}) + (reitit/match-by-name router ::beer))) + (is (= true (reitit/partial-match? (reitit/match-by-name router ::beer)))) (is (thrown-with-msg? ExceptionInfo - #"^missing path-params for route '/api/ipa/:size': \#\{:size\}$" - (reitit/match-by-name router ::beer)))))) + #"^missing path-params for route /api/ipa/:size: \#\{:size\}$" + (reitit/match-by-name! router ::beer)))))) (testing "lookup router" (let [router (reitit/router ["/api" ["/ipa" ["/large" ::beer]]])] @@ -49,6 +57,7 @@ :params {:size "large"}}) (reitit/match-by-name router ::beer {:size "large"}))) (is (= nil (reitit/match-by-name router "ILLEGAL"))) + (is (= [::beer] (reitit/route-names router))) (testing "can't be created with wildcard routes" (is (thrown-with-msg? ExceptionInfo