Skip to content

Commit

Permalink
Add support for catch all route. Bump version.
Browse files Browse the repository at this point in the history
  • Loading branch information
retro committed Dec 8, 2019
1 parent a10793b commit bf8bfd3
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 30 deletions.
2 changes: 1 addition & 1 deletion project.clj
@@ -1,4 +1,4 @@
(defproject keechma/router "0.1.3"
(defproject keechma/router "0.1.4"
:description "Router - Pure functional router for ClojureScript applications."
:url "http://keechma.com/"
:license {:name "MIT"}
Expand Down
67 changes: 38 additions & 29 deletions src/router/core.cljs
Expand Up @@ -52,7 +52,7 @@
:re-match re-match}))

(defn ^:private route-regex [parts]
(let [base-regex (clojure.string/join "/" (map (fn [p] (:re-match p)) parts))
(let [base-regex (str/join "/" (map (fn [p] (:re-match p)) parts))
full-regex (str "^" base-regex "$")]
(re-pattern full-regex)))

Expand All @@ -62,25 +62,34 @@
(remove nil?)))

(defn ^:private add-default-params [route]
(if (string? route) [route {}] route))
(if (vector? route) route [route {}]))

(defn ^:private strip-slashes
([route]
(clojure.string/replace (clojure.string/trim (or route "")) #"^/+|/+$" ""))
(str/replace (str/trim (str route)) #"^/+|/+$" ""))
([side route]
(case side
:left (clojure.string/replace (clojure.string/trim (or route "")) #"^/+" "")
:right (clojure.string/replace (clojure.string/trim (or route "")) #"/+$" "")
:left (str/replace (str/trim (or route "")) #"^/+" "")
:right (str/replace (str/trim (or route "")) #"/+$" "")
route)))

(defn ^:private process-route [[route defaults]]
(let [parts (clojure.string/split route #"/")
processed-parts (map (partial process-route-part (set (keys defaults))) parts)]
{:parts processed-parts
:regex (route-regex processed-parts)
:placeholders (set (route-placeholders processed-parts))
(if (= :* route)
{:parts []
:regex #".*"
:placeholders #{}
:route route
:defaults (or defaults {})}))
:defaults (or defaults {})
:type ::catch-all}
(let [parts (str/split route #"/")
processed-parts (map (partial process-route-part (set (keys defaults))) parts)
placeholders (set (route-placeholders processed-parts))]
{:parts processed-parts
:regex (route-regex processed-parts)
:placeholders placeholders
:route route
:defaults (or defaults {})
:type (if (empty? placeholders) ::exact ::pattern)})))

(defn ^:private remove-empty-matches [matches]
(->> matches
Expand All @@ -92,7 +101,7 @@
(into {})))

(defn ^:private expand-route [route]
(let [strip-slashes (fn [[route defaults]] [(strip-slashes route) defaults])]
(let [strip-slashes (fn [[route defaults]] [(if (string? route) (strip-slashes route) route) defaults])]
(-> route
add-default-params
strip-slashes
Expand Down Expand Up @@ -146,20 +155,14 @@
(when-not (nil? matches)
(zipmap (:placeholders route) (rest matches)))))

(defn ^:private match-path [expanded-routes path]
(let [route-count (count expanded-routes)
max-index (dec route-count)]
(when (pos? route-count)
(loop [index 0]
(let [route (get expanded-routes index)
matches (match-path-with-route route path)
end? (= max-index index)]
(cond
matches {:route (:route route)
:data (merge (:defaults route)
(remove-empty-matches matches))}
end? nil
:else (recur (inc index))))))))
(defn ^:private match-path [expanded-routes path]
(reduce
(fn [result route]
(when-let [matches (match-path-with-route route path)]
(reduced {:route (:route route)
:data (merge (:defaults route) (remove-empty-matches matches))})))
nil
expanded-routes))

;; Public API

Expand Down Expand Up @@ -187,7 +190,7 @@
```
"
[expanded-routes url]
(let [[u q] (clojure.string/split url #"\?")
(let [[u q] (str/split url #"\?")
path (if (= u "/") u (strip-slashes :left u))
query (remove-empty-matches (keywordize-url-keys (decode-query-params q)))
matched-path (match-path expanded-routes path)]
Expand Down Expand Up @@ -248,12 +251,18 @@
```
"
[routes]
(let [expanded-routes (map expand-route routes)
(let [expanded-routes (group-by :type (map expand-route routes))


without-placeholders (filter #(not (seq (:placeholders %))) expanded-routes)
with-placeholders (filter #(seq (:placeholders %)) expanded-routes)]
;; We put routes without placeholders at the start of the list, so they would
;; be matched first - exact matches have precedence over matches with placeholders
;;
;; Routes that have placeholders are sorted so that the routes with the most
;; placeholders come first, because these have more specificity
(vec (concat without-placeholders (sort-by #(- (count (:placeholders %))) with-placeholders)))))
;;
;; At the end of the list we put the catch-all route (if any exist)
(vec (concat (expanded-routes ::exact)
(sort-by #(- (count (:placeholders %))) (expanded-routes ::pattern))
(expanded-routes ::catch-all)))))
9 changes: 9 additions & 0 deletions test/router/router.cljs
Expand Up @@ -161,3 +161,12 @@
:foo.bar.baz.qux/foo {:bar.baz/qux "foo"}
:page "homepage"}
(:data (router/url->map routes "?foo$bar=1&foo.bar$baz=2&foo.bar.baz$qux[qux$foo]=bar&foo.bar.baz.qux$foo[bar.baz$qux]=foo"))))))


(deftest catch-all-route
(let [routes (router/expand-routes [["" {:page "homepage"}]
":page"
[:* {:page "not-found"}]])]
(is (= {:page "homepage"} (:data (router/url->map routes ""))))
(is (= {:page "foo"} (:data (router/url->map routes "foo"))))
(is (= {:page "not-found"} (:data (router/url->map routes "foo/bar/baz"))))))

0 comments on commit bf8bfd3

Please sign in to comment.