diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index 8145404a1..75eb2d6d1 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -1,5 +1,6 @@ (ns reitit.frontend (:require [clojure.set :as set] + [clojure.string :as str] [reitit.coercion :as coercion] [reitit.coercion :as rc] [reitit.core :as r]) @@ -14,12 +15,38 @@ (map (juxt keyword #(.get q %))) (into {})))) +(defn trailing-slash-router [parent method] + (if method + ^{:type ::r/router} + (reify r/Router + (router-name [_] + :trailing-slash-handler) + (routes [_] + (r/routes parent)) + (compiled-routes [_] + (r/compiled-routes parent)) + (options [_] + (r/options parent)) + (route-names [_] + (r/route-names parent)) + (match-by-path [_ path] + (or (r/match-by-path parent path) + (if (str/ends-with? path "/") + (if (not= method :add) + (r/match-by-path parent (subs path 0 (dec (count path))))) + (if (not= method :remove) + (r/match-by-path parent (str path "/")))))) + (match-by-name [_ name] + (r/match-by-name parent name))) + parent)) + (defn match-by-path "Given routing tree and current path, return match with possibly - coerced parameters. Return nil if no match found." + coerced parameters. Returns nil if no match found." [router path] (let [uri (.parse Uri path)] (if-let [match (r/match-by-path router (.getPath uri))] + ;; User can update browser location in on-navigate call using replace-state (let [q (query-params uri) match (assoc match :query-params q) ;; Return uncoerced values if coercion is not enabled - so @@ -40,11 +67,19 @@ (defn router "Create a `reitit.core.router` from raw route data and optionally an options map. - Enables request coercion. See [[reitit.core/router]] for details on options." + Enables request coercion. See [[reitit.core/router]] for details on options. + + Additional options: + + | key | description | + | -------------|-------------| + | :trailing-slash-handling | TODO | + " ([raw-routes] (router raw-routes {})) ([raw-routes opts] - (r/router raw-routes (merge {:compile rc/compile-request-coercers} opts)))) + (-> (r/router raw-routes (merge {:compile rc/compile-request-coercers} opts)) + (trailing-slash-router (:trailing-slash-handling opts))))) (defn match-by-name! "Logs problems using console.warn" diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 200090883..ffeccf317 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -12,10 +12,11 @@ (deftest match-by-path-test (testing "simple" - (let [router (r/router ["/" - ["" ::frontpage] - ["foo" ::foo] - ["bar" ::bar]])] + (let [router (rf/router + ["/" + ["" ::frontpage] + ["foo" ::foo] + ["bar" ::bar]])] (is (= (r/map->Match {:template "/" :data {:name ::frontpage} @@ -51,10 +52,11 @@ (rf/match-by-name! router ::asd))))))))) (testing "schema coercion" - (let [router (r/router ["/" - [":id" {:name ::foo - :parameters {:path {:id s/Int} - :query {(s/optional-key :mode) s/Keyword}}}]] + (let [router (rf/router + ["/" + [":id" {:name ::foo + :parameters {:path {:id s/Int} + :query {(s/optional-key :mode) s/Keyword}}}]] {:compile rc/compile-request-coercers :data {:coercion rsc/coercion}})] @@ -104,3 +106,92 @@ (capture-console (fn [] (rf/match-by-name! router ::foo {})))))))))) + + +(deftest trailing-slash-handling-test + (testing ":both" + (let [router (rf/router + ["/" + ["" ::frontpage] + ["foo" ::foo] + ["bar/" ::bar]] + {:trailing-slash-handling :both})] + (is (= (r/map->Match + {:template "/foo" + :data {:name ::foo} + :path-params {} + :query-params {} + :path "/foo" + :parameters {:query {} + :path {}}}) + (rf/match-by-path router "/foo/") + (rf/match-by-path router "/foo"))) + + (is (= (r/map->Match + {:template "/bar/" + :data {:name ::bar} + :path-params {} + :query-params {} + :path "/bar/" + :parameters {:query {} + :path {}}}) + (rf/match-by-path router "/bar/") + (rf/match-by-path router "/bar"))) )) + + (testing ":add" + (let [router (rf/router + ["/" + ["" ::frontpage] + ["foo" ::foo] + ["bar/" ::bar]] + {:trailing-slash-handling :add})] + (is (= (r/map->Match + {:template "/foo" + :data {:name ::foo} + :path-params {} + :query-params {} + :path "/foo" + :parameters {:query {} + :path {}}}) + (rf/match-by-path router "/foo"))) + (is (nil? (rf/match-by-path router "/foo/"))) + + (is (= (r/map->Match + {:template "/bar/" + :data {:name ::bar} + :path-params {} + :query-params {} + :path "/bar/" + :parameters {:query {} + :path {}}}) + (rf/match-by-path router "/bar/") + (rf/match-by-path router "/bar"))))) + + (testing ":remove" + (let [router (rf/router + ["/" + ["" ::frontpage] + ["foo" ::foo] + ["bar/" ::bar]] + {:trailing-slash-handling :remove})] + (is (= (r/map->Match + {:template "/foo" + :data {:name ::foo} + :path-params {} + :query-params {} + :path "/foo" + :parameters {:query {} + :path {}}}) + (rf/match-by-path router "/foo/") + (rf/match-by-path router "/foo"))) + + (is (= (r/map->Match + {:template "/bar/" + :data {:name ::bar} + :path-params {} + :query-params {} + :path "/bar/" + :parameters {:query {} + :path {}}}) + (rf/match-by-path router "/bar/"))) + (is (nil? (rf/match-by-path router "/bar"))))))