diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 5447ef6b..0dc5d6a7 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -2,6 +2,7 @@ (:require [meta-merge.core :refer [meta-merge]] [clojure.string :as str] [reitit.trie :as trie] + [reitit.segment :as segment] [reitit.impl :as impl #?@(:cljs [:refer [Route]])]) #?(:clj (:import (reitit.impl Route)))) @@ -260,6 +261,47 @@ (if-let [match (impl/fast-get lookup name)] (match params))))))) +(defn segment-router + "Creates a special prefix-tree style segment router from resolved routes and optional + expanded options. See [[router]] for available options" + ([routes] + (segment-router routes {})) + ([routes opts] + (let [compiled (compile-routes routes opts) + names (find-names routes opts) + [pl nl] (reduce + (fn [[pl nl] [p {:keys [name] :as data} result]] + (let [{:keys [params] :as route} (impl/create [p data result]) + f #(if-let [path (impl/path-for route %)] + (->Match p data result % path) + (->PartialMatch p data result % params))] + [(segment/insert pl p (->Match p data result nil nil)) + (if name (assoc nl name f) nl)])) + [nil {}] compiled) + lookup (impl/fast-map nl)] + ^{:type ::router} + (reify + Router + (router-name [_] + :segment-router) + (routes [_] + compiled) + (options [_] + opts) + (route-names [_] + names) + (match-by-path [_ path] + (if-let [match (segment/lookup pl path)] + (-> (:data match) + (assoc :params (:params match)) + (assoc :path path)))) + (match-by-name [_ name] + (if-let [match (impl/fast-get lookup name)] + (match nil))) + (match-by-name [_ name params] + (if-let [match (impl/fast-get lookup name)] + (match params))))))) + (defn single-static-path-router "Creates a fast router of 1 static route(s) and optional expanded options. See [[router]] for available options" diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index a418afe0..dc84b06c 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -39,11 +39,14 @@ (some #(-lookup (impl/fast-get children' %) ps (assoc params % p)) wilds) (-catch-all catch-all data params p ps)))))))) +(defn insert [root path data] + (-insert (or root (segment)) (impl/segments path) (map->Match {:data data}))) + (defn create [paths] (reduce (fn [segment [p data]] - (-insert segment (impl/segments p) (map->Match {:data data}))) - (segment) paths)) + (insert segment p data)) + nil paths)) (defn lookup [segment ^String path] (-lookup segment (.split path "/") {})) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index b9442546..9f684faa 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -9,41 +9,57 @@ (testing "routers handling wildcard paths" (are [r name] - (let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]] {:router r})] - (is (= name (r/router-name router))) - (is (= [["/api/ipa/:size" {:name ::beer} nil]] - (r/routes router))) - (is (= true (map? (r/options router)))) - (is (= (r/map->Match - {:template "/api/ipa/:size" - :data {:name ::beer} - :path "/api/ipa/large" - :params {:size "large"}}) - (r/match-by-path router "/api/ipa/large"))) - (is (= (r/map->Match - {:template "/api/ipa/:size" - :data {:name ::beer} - :path "/api/ipa/large" - :params {:size "large"}}) - (r/match-by-name router ::beer {:size "large"}))) - (is (= nil (r/match-by-name router "ILLEGAL"))) - (is (= [::beer] (r/route-names router))) + (testing "wild" - (testing "name-based routing with missing parameters" - (is (= (r/map->PartialMatch - {:template "/api/ipa/:size" - :data {:name ::beer} - :required #{:size} - :params nil}) - (r/match-by-name router ::beer))) - (is (= true (r/partial-match? (r/match-by-name router ::beer)))) - (is (thrown-with-msg? - ExceptionInfo - #"^missing path-params for route /api/ipa/:size -> \#\{:size\}$" - (r/match-by-name! router ::beer))))) + (testing "simple" + (let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]] {:router r})] + (is (= name (r/router-name router))) + (is (= [["/api/ipa/:size" {:name ::beer} nil]] + (r/routes router))) + (is (= true (map? (r/options router)))) + (is (= (r/map->Match + {:template "/api/ipa/:size" + :data {:name ::beer} + :path "/api/ipa/large" + :params {:size "large"}}) + (r/match-by-path router "/api/ipa/large"))) + (is (= (r/map->Match + {:template "/api/ipa/:size" + :data {:name ::beer} + :path "/api/ipa/large" + :params {:size "large"}}) + (r/match-by-name router ::beer {:size "large"}))) + (is (= nil (r/match-by-name router "ILLEGAL"))) + (is (= [::beer] (r/route-names router))) + + (testing "name-based routing with missing parameters" + (is (= (r/map->PartialMatch + {:template "/api/ipa/:size" + :data {:name ::beer} + :required #{:size} + :params nil}) + (r/match-by-name router ::beer))) + (is (= true (r/partial-match? (r/match-by-name router ::beer)))) + (is (thrown-with-msg? + ExceptionInfo + #"^missing path-params for route /api/ipa/:size -> \#\{:size\}$" + (r/match-by-name! router ::beer)))))) + + (testing "complex" + (let [router (r/router + [["/:abba" ::abba] + ["/abba/1" ::abba1] + ["/:abba/:dabba/doo" ::doo] + ["/abba/:dabba/boo" ::boo]] {:router r}) + matches #(-> router (r/match-by-path %) :data :name)] + (is (= ::abba (matches "/abba"))) + (is (= ::abba1 (matches "/abba/1"))) + (is (= ::doo (matches "/abba/1/doo"))) + (is (= ::boo (matches "/abba/1/boo")))))) r/linear-router :linear-router r/prefix-tree-router :prefix-tree-router + r/segment-router :segment-router r/mixed-router :mixed-router)) (testing "routers handling static paths" @@ -80,6 +96,7 @@ r/single-static-path-router :single-static-path-router r/linear-router :linear-router r/prefix-tree-router :prefix-tree-router + r/segment-router :segment-router r/mixed-router :mixed-router)) (testing "route coercion & compilation"