mirror of
https://github.com/metosin/reitit.git
synced 2026-02-20 01:26:03 +00:00
Compare commits
7 commits
a429067a69
...
c3e3d77eb4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3e3d77eb4 | ||
|
|
7c1544c3ce | ||
|
|
0724c0c5a0 | ||
|
|
7051c99e99 | ||
|
|
4c8a69c616 | ||
|
|
97bfafa907 | ||
|
|
7a707f042b |
8 changed files with 482 additions and 19 deletions
|
|
@ -12,6 +12,12 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
|||
|
||||
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
|
||||
|
||||
## UNRELEASED
|
||||
|
||||
* **FIX** redirect-trailing-slash-handler won't make external redirects. [#776](https://github.com/metosin/reitit/pull/776)
|
||||
* Allow colons in bracket parameter syntax. [#770](https://github.com/metosin/reitit/pull/770)
|
||||
* Add `url-encode?` option to `match-by-name`. [#778](https://github.com/metosin/reitit/pull/778)
|
||||
|
||||
## 0.10.0 (2026-01-09)
|
||||
|
||||
* Improve & document how response schemas get picked in per-content-type coercion. See [docs](./doc/ring/coercion.md#per-content-type-coercion). [#745](https://github.com/metosin/reitit/issues/745).
|
||||
|
|
|
|||
|
|
@ -75,6 +75,17 @@ Path-parameters are automatically coerced into strings, with the help of (curren
|
|||
; :path-params {:id "1"}}
|
||||
```
|
||||
|
||||
In case you want to do something like generate a template path for documentation, you can disable url-encoding:
|
||||
|
||||
```clj
|
||||
(r/match-by-name router ::user {:id "<id goes here>"} {:url-encode? false})
|
||||
; #reitit.core.Match{:template "/api/user/:id"
|
||||
; :data {:name :user/user}
|
||||
; :path "/api/user/<id goes here>"
|
||||
; :result nil
|
||||
; :path-params {:id "<id goes here>"}}
|
||||
```
|
||||
|
||||
There is also an exception throwing version:
|
||||
|
||||
```clj
|
||||
|
|
@ -97,5 +108,5 @@ It can take an optional map of query-parameters too:
|
|||
(-> router
|
||||
(r/match-by-name ::user {:id 1})
|
||||
(r/match->path {:iso "möly"}))
|
||||
; "/api/user/1?iso=m%C3%B6ly"
|
||||
; "/api/user/1?iso=m%C3%B6ly"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
(options [this])
|
||||
(route-names [this])
|
||||
(match-by-path [this path])
|
||||
(match-by-name [this name] [this name path-params]))
|
||||
(match-by-name [this name] [this name path-params] [this name path-params opts]))
|
||||
|
||||
(defn router? [x]
|
||||
(satisfies? Router x))
|
||||
|
|
@ -122,9 +122,11 @@
|
|||
(match-by-name [_ name]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
(match nil)))
|
||||
(match-by-name [_ name path-params]
|
||||
(match-by-name [r name path-params]
|
||||
(match-by-name r name path-params nil))
|
||||
(match-by-name [_ name path-params opts]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
(match (impl/path-params path-params))))))))
|
||||
(match (impl/path-params path-params opts))))))))
|
||||
|
||||
(defn lookup-router
|
||||
"Creates a lookup-router from resolved routes and optional
|
||||
|
|
@ -161,9 +163,11 @@
|
|||
(match-by-name [_ name]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
(match nil)))
|
||||
(match-by-name [_ name path-params]
|
||||
(match-by-name [r name path-params]
|
||||
(match-by-name r name path-params nil))
|
||||
(match-by-name [_ name path-params opts]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
(match (impl/path-params path-params))))))))
|
||||
(match (impl/path-params path-params opts))))))))
|
||||
|
||||
(defn trie-router
|
||||
"Creates a special prefix-tree router from resolved routes and optional
|
||||
|
|
@ -208,9 +212,11 @@
|
|||
(match-by-name [_ name]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
(match nil)))
|
||||
(match-by-name [_ name path-params]
|
||||
(match-by-name [r name path-params]
|
||||
(match-by-name r name path-params nil))
|
||||
(match-by-name [_ name path-params opts]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
(match (impl/path-params path-params))))))))
|
||||
(match (impl/path-params path-params opts))))))))
|
||||
|
||||
(defn single-static-path-router
|
||||
"Creates a fast router of 1 static route(s) and optional
|
||||
|
|
@ -238,8 +244,10 @@
|
|||
(if (#?(:clj .equals :cljs =) p path) match))
|
||||
(match-by-name [_ name]
|
||||
(if (= n name) match))
|
||||
(match-by-name [_ name path-params]
|
||||
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params))))))))
|
||||
(match-by-name [r name path-params]
|
||||
(match-by-name r name path-params nil))
|
||||
(match-by-name [_ name path-params opts]
|
||||
(if (= n name) (impl/fast-assoc match :path-params (impl/path-params path-params opts))))))))
|
||||
|
||||
(defn mixed-router
|
||||
"Creates two routers: [[lookup-router]] or [[single-static-path-router]] for
|
||||
|
|
@ -268,9 +276,11 @@
|
|||
(match-by-name [_ name]
|
||||
(or (match-by-name static-router name)
|
||||
(match-by-name wildcard-router name)))
|
||||
(match-by-name [_ name path-params]
|
||||
(or (match-by-name static-router name path-params)
|
||||
(match-by-name wildcard-router name path-params)))))))
|
||||
(match-by-name [r name path-params]
|
||||
(match-by-name r name path-params nil))
|
||||
(match-by-name [_ name path-params opts]
|
||||
(or (match-by-name static-router name path-params opts)
|
||||
(match-by-name wildcard-router name path-params opts)))))))
|
||||
|
||||
(defn quarantine-router
|
||||
"Creates two routers: [[mixed-router]] for non-conflicting routes
|
||||
|
|
@ -299,9 +309,11 @@
|
|||
(match-by-name [_ name]
|
||||
(or (match-by-name mixed-router name)
|
||||
(match-by-name linear-router name)))
|
||||
(match-by-name [_ name path-params]
|
||||
(or (match-by-name mixed-router name path-params)
|
||||
(match-by-name linear-router name path-params)))))))
|
||||
(match-by-name [r name path-params]
|
||||
(match-by-name r name path-params nil))
|
||||
(match-by-name [_ name path-params opts]
|
||||
(or (match-by-name mixed-router name path-params opts)
|
||||
(match-by-name linear-router name path-params opts)))))))
|
||||
|
||||
;;
|
||||
;; Creating Routers
|
||||
|
|
|
|||
|
|
@ -297,8 +297,11 @@
|
|||
|
||||
(defn path-params
|
||||
"Convert parameters' values into URL-encoded strings, suitable for URL paths"
|
||||
[params]
|
||||
(maybe-map-values #(url-encode (into-string %)) params))
|
||||
([params] (path-params params nil))
|
||||
([params {:keys [url-encode?] :or {url-encode? true}}]
|
||||
(if url-encode?
|
||||
(maybe-map-values #(url-encode (into-string %)) params)
|
||||
(maybe-map-values #(into-string %) params))))
|
||||
|
||||
(defn- query-parameter [k v]
|
||||
(str (form-encode (into-string k))
|
||||
|
|
|
|||
121
modules/reitit-core/src/reitit/regex.cljc
Normal file
121
modules/reitit-core/src/reitit/regex.cljc
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
(ns reitit.regex
|
||||
(:require [clojure.set :as set]
|
||||
[clojure.string :as str]
|
||||
[reitit.core :as r]))
|
||||
|
||||
(defn compile-regex-route
|
||||
"Given a route vector [path route-data], returns a map with:
|
||||
- :pattern: a compiled regex pattern built from the path segments,
|
||||
- :group-keys: vector of parameter keys in order,
|
||||
- :route-data: the provided route data,
|
||||
- :original-segments: original path segments for path generation,
|
||||
- :template: the original path template for Match objects."
|
||||
[[path route-data]]
|
||||
(let [;; Normalize route-data to ensure it's a map with :name
|
||||
route-data (if (keyword? route-data)
|
||||
{:name route-data}
|
||||
route-data)
|
||||
|
||||
;; Store the original path template for Match objects
|
||||
template (if (str/starts-with? path "/")
|
||||
path
|
||||
(str "/" path))
|
||||
|
||||
;; Handle paths with or without leading slashes
|
||||
normalized-path (cond-> path
|
||||
(str/starts-with? path "/") (subs 1))
|
||||
|
||||
;; Split into segments, handling empty paths
|
||||
segments (if (empty? normalized-path)
|
||||
[]
|
||||
(str/split normalized-path #"/"))
|
||||
|
||||
;; Store original segments for path generation
|
||||
original-segments segments
|
||||
|
||||
compiled-segments
|
||||
(map (fn [seg]
|
||||
(if (str/starts-with? seg ":")
|
||||
(let [param-key (keyword (subs seg 1))
|
||||
param-regex (get-in route-data [:parameters :path param-key])]
|
||||
(if (and param-regex (instance? java.util.regex.Pattern param-regex))
|
||||
(str "(" (.pattern ^java.util.regex.Pattern param-regex) ")")
|
||||
;; Fallback: match any non-slash characters.
|
||||
"([^/]+)"))
|
||||
(java.util.regex.Pattern/quote seg)))
|
||||
segments)
|
||||
|
||||
;; Create the pattern string, handling special case for root path
|
||||
pattern-str (if (empty? segments)
|
||||
"^/?$" ;; Match root path with optional trailing slash
|
||||
(str "^/" (str/join "/" compiled-segments) "$"))
|
||||
|
||||
group-keys (->> segments
|
||||
(filter #(str/starts-with? % ":"))
|
||||
(map #(keyword (subs % 1)))
|
||||
(vec))]
|
||||
|
||||
{:pattern (re-pattern pattern-str)
|
||||
:group-keys group-keys
|
||||
:route-data route-data
|
||||
:original-segments original-segments
|
||||
:template template}))
|
||||
|
||||
(defn- generate-path
|
||||
"Generate a path from a route and path parameters."
|
||||
[route path-params]
|
||||
(if (empty? (:original-segments route))
|
||||
"/"
|
||||
(str "/" (str/join "/"
|
||||
(map (fn [segment]
|
||||
(if (str/starts-with? segment ":")
|
||||
(let [param-key (keyword (subs segment 1))]
|
||||
(get path-params param-key ""))
|
||||
segment))
|
||||
(:original-segments route))))))
|
||||
|
||||
(defrecord RegexRouter [compiled-routes]
|
||||
r/Router
|
||||
(router-name [_] :regex-router)
|
||||
|
||||
(routes [_]
|
||||
(mapv (fn [{:keys [route-data original-segments]}]
|
||||
[(str "/" (str/join "/" original-segments)) route-data])
|
||||
compiled-routes))
|
||||
|
||||
(compiled-routes [_] compiled-routes)
|
||||
|
||||
(options [_] {})
|
||||
|
||||
(route-names [_]
|
||||
(keep (comp :name :route-data) compiled-routes))
|
||||
|
||||
(match-by-path [_ path]
|
||||
(some (fn [{:keys [pattern group-keys route-data template]}]
|
||||
(when-let [matches (re-matches pattern path)]
|
||||
(let [params (zipmap group-keys (rest matches))]
|
||||
(r/->Match template route-data nil params path))))
|
||||
compiled-routes))
|
||||
|
||||
(match-by-name [this name]
|
||||
(r/match-by-name this name {}))
|
||||
|
||||
(match-by-name [router name path-params]
|
||||
(when-let [{:keys [group-keys route-data template] :as route}
|
||||
(first (filter #(= name (get-in % [:route-data :name])) (r/compiled-routes router)))]
|
||||
;; Check if all required params are provided
|
||||
(let [required-params (set group-keys)
|
||||
provided-params (set (keys path-params))]
|
||||
(if (every? #(contains? provided-params %) required-params)
|
||||
;; All required params provided, return a Match
|
||||
(let [path (generate-path route path-params)]
|
||||
(r/->Match template route-data nil path-params path))
|
||||
;; Some required params missing, return a PartialMatch
|
||||
(let [missing (set/difference required-params provided-params)]
|
||||
(r/->PartialMatch template route-data nil path-params missing)))))))
|
||||
|
||||
(defn create-regex-router
|
||||
"Create a RegexRouter from a vector of routes.
|
||||
Each route should be a vector [path route-data]."
|
||||
[routes]
|
||||
(->RegexRouter (mapv compile-regex-route routes)))
|
||||
|
|
@ -33,6 +33,12 @@
|
|||
:path "/api/ipa/large"
|
||||
:path-params {:size "large"}})
|
||||
(r/match-by-name router ::beer {:size "large"})))
|
||||
(is (= (r/map->Match
|
||||
{:template "/api/ipa/:size"
|
||||
:data {:name ::beer}
|
||||
:path "/api/ipa/:large"
|
||||
:path-params {:size ":large"}})
|
||||
(r/match-by-name router ::beer {:size ":large"} {:url-encode? false})))
|
||||
(is (= (r/map->Match
|
||||
{:template "/api/ipa/:size"
|
||||
:data {:name ::beer}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,33 @@
|
|||
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
|
||||
:k :kikka
|
||||
:qk ::kikka
|
||||
:nil nil}))))
|
||||
:nil nil})))
|
||||
(is (= {:n "1"
|
||||
:n1 "-1"
|
||||
:n2 "1"
|
||||
:n3 "1"
|
||||
:n4 "1"
|
||||
:n5 "1"
|
||||
:d "2.2"
|
||||
:b "true"
|
||||
:s "kikka"
|
||||
:u "c2541900-17a7-4353-9024-db8ac258ba4e"
|
||||
:k "kikka"
|
||||
:qk "reitit.impl-test/kikka"
|
||||
:nil nil}
|
||||
(impl/path-params {:n 1
|
||||
:n1 -1
|
||||
:n2 (long 1)
|
||||
:n3 (int 1)
|
||||
:n4 (short 1)
|
||||
:n5 (byte 1)
|
||||
:d 2.2
|
||||
:b true
|
||||
:s "kikka"
|
||||
:u #uuid "c2541900-17a7-4353-9024-db8ac258ba4e"
|
||||
:k :kikka
|
||||
:qk ::kikka
|
||||
:nil nil} {:url-encode? false}))))
|
||||
|
||||
(deftest query-params-test
|
||||
(are [x y]
|
||||
|
|
|
|||
278
test/cljc/reitit/regex_test.cljc
Normal file
278
test/cljc/reitit/regex_test.cljc
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
(ns reitit.regex-test
|
||||
(:require [clojure.test :refer [deftest is testing]]
|
||||
[reitit.core :as r]
|
||||
[reitit.regex :as rt.regex]))
|
||||
|
||||
(defn re-=
|
||||
"A custom equality function that handles regex patterns specially.
|
||||
Returns true if a and b are equal, with special handling for regex patterns.
|
||||
Also handles comparing records by their map representation."
|
||||
[a b]
|
||||
(cond
|
||||
;; Handle record comparison by using their map representation
|
||||
(and (instance? clojure.lang.IRecord a)
|
||||
(instance? clojure.lang.IRecord b))
|
||||
(re-= (into {} a) (into {} b))
|
||||
|
||||
;; If both are regex patterns, compare their string representations
|
||||
(and (instance? java.util.regex.Pattern a)
|
||||
(instance? java.util.regex.Pattern b))
|
||||
(= (str a) (str b))
|
||||
|
||||
;; If one is a regex and the other isn't, they're not equal
|
||||
(or (instance? java.util.regex.Pattern a)
|
||||
(instance? java.util.regex.Pattern b))
|
||||
false
|
||||
|
||||
;; For maps, compare each key-value pair using regex-aware-equals
|
||||
(and (map? a) (map? b))
|
||||
(and (= (set (keys a)) (set (keys b)))
|
||||
(every? #(re-= (get a %) (get b %)) (keys a)))
|
||||
|
||||
;; For sequences, compare each element using regex-aware-equals
|
||||
(and (sequential? a) (sequential? b))
|
||||
(and (= (count a) (count b))
|
||||
(every? identity (map re-= a b)))
|
||||
|
||||
;; For sets, convert to sequences and compare
|
||||
(and (set? a) (set? b))
|
||||
(re-= (seq a) (seq b))
|
||||
|
||||
;; For everything else, use regular equality
|
||||
:else
|
||||
(= a b)))
|
||||
|
||||
(def routes
|
||||
(rt.regex/create-regex-router
|
||||
[["" ::home]
|
||||
[":item-id" {:name ::item
|
||||
:parameters {:path {:item-id #"[a-z]{16,20}"}}}]
|
||||
["inbox" ::inbox]
|
||||
["teams" ::teams]
|
||||
["teams/:team-id-b58/members" {:name ::->members
|
||||
:parameters {:path {:team-id-b58 #"[a-z]"}}}]]))
|
||||
|
||||
(deftest regex-match-by-path-test
|
||||
(testing "Basic path matching"
|
||||
(is (= (r/map->Match {:path "/"
|
||||
:path-params {}
|
||||
:data {:name ::home}
|
||||
:template "/"
|
||||
:result nil})
|
||||
(r/match-by-path routes "/")))
|
||||
|
||||
(is (= (r/map->Match {:path "/inbox"
|
||||
:path-params {}
|
||||
:data {:name ::inbox}
|
||||
:template "/inbox"
|
||||
:result nil})
|
||||
(r/match-by-path routes "/inbox")))
|
||||
|
||||
(is (= (r/map->Match {:path "/teams"
|
||||
:path-params {}
|
||||
:data {:name ::teams}
|
||||
:template "/teams"
|
||||
:result nil})
|
||||
(r/match-by-path routes "/teams"))))
|
||||
|
||||
(testing "Path with regex parameter"
|
||||
(let [valid-id "abcdefghijklmnopq"] ; 17 lowercase letters
|
||||
(is (re-= (r/map->Match {:path (str "/" valid-id)
|
||||
:path-params {:item-id valid-id}
|
||||
:data {:name ::item
|
||||
:parameters {:path {:item-id #"[a-z]{16,20}"}}},
|
||||
:template "/:item-id"
|
||||
:result nil})
|
||||
(r/match-by-path routes (str "/" valid-id)))))
|
||||
|
||||
;; Invalid parameter cases
|
||||
(is (nil? (r/match-by-path routes "/abcdefg")) "Too short")
|
||||
(is (nil? (r/match-by-path routes "/abcdefghijklmnopqRST")) "Contains uppercase")
|
||||
(is (nil? (r/match-by-path routes "/abcdefghijklmn1234")) "Contains digits"))
|
||||
|
||||
(testing "Nested path with parameter"
|
||||
(is (re-= (r/map->Match {:path "/teams/a/members"
|
||||
:path-params {:team-id-b58 "a"}
|
||||
:data {:name ::->members
|
||||
:parameters {:path {:team-id-b58 #"[a-z]"}}}
|
||||
:template "/teams/:team-id-b58/members"
|
||||
:result nil})
|
||||
(r/match-by-path routes "/teams/a/members")))
|
||||
|
||||
(is (nil? (r/match-by-path routes "/teams/abc/members")) "Multiple characters")
|
||||
(is (nil? (r/match-by-path routes "/teams/1/members")) "Digit instead of letter"))
|
||||
|
||||
(testing "Non-matching paths"
|
||||
(is (nil? (r/match-by-path routes "/unknown")))
|
||||
(is (nil? (r/match-by-path routes "/team"))) ; 'team' not 'teams'
|
||||
(is (nil? (r/match-by-path routes "/teams/extra/segments/here")))))
|
||||
|
||||
(deftest regex-match-by-name-test
|
||||
(testing "Basic match-by-name functionality"
|
||||
;; Root path
|
||||
(is (re-= (r/map->Match {:path "/"
|
||||
:path-params {}
|
||||
:data {:name ::home}
|
||||
:template "/"
|
||||
:result nil})
|
||||
(r/match-by-name routes ::home)))
|
||||
|
||||
;; Static paths
|
||||
(is (re-= (r/map->Match {:path "/inbox"
|
||||
:path-params {}
|
||||
:data {:name ::inbox}
|
||||
:template "/inbox"
|
||||
:result nil})
|
||||
(r/match-by-name routes ::inbox)))
|
||||
|
||||
(is (re-= (r/map->Match {:path "/teams"
|
||||
:path-params {}
|
||||
:data {:name ::teams}
|
||||
:template "/teams"
|
||||
:result nil})
|
||||
(r/match-by-name routes ::teams)))
|
||||
|
||||
;; Path with parameter
|
||||
(let [valid-id "abcdefghijklmnopq"]
|
||||
(is (re-= (r/map->Match {:path (str "/" valid-id)
|
||||
:path-params {:item-id valid-id}
|
||||
:data {:name ::item
|
||||
:parameters {:path {:item-id #"[a-z]{16,20}"}}}
|
||||
:template "/:item-id"
|
||||
:result nil})
|
||||
(r/match-by-name routes ::item {:item-id valid-id}))))
|
||||
|
||||
;; Nested path with parameter
|
||||
(is (re-= (r/map->Match {:path "/teams/a/members"
|
||||
:path-params {:team-id-b58 "a"}
|
||||
:data {:name ::->members
|
||||
:parameters {:path {:team-id-b58 #"[a-z]"}}}
|
||||
:template "/teams/:team-id-b58/members"
|
||||
:result nil})
|
||||
(r/match-by-name routes ::->members {:team-id-b58 "a"}))))
|
||||
|
||||
(testing "Path round-trip matching"
|
||||
;; Test that paths generated by match-by-name can be successfully matched by match-by-path
|
||||
(let [valid-id "abcdefghijklmnopq"
|
||||
match (r/match-by-name routes ::item {:item-id valid-id})
|
||||
path (:path match)]
|
||||
|
||||
(is (some? path) "Should generate a valid path")
|
||||
(is (re-= match (r/match-by-path routes path))
|
||||
"match-by-path should find the same route that generated the path"))
|
||||
|
||||
(let [match (r/match-by-name routes ::->members {:team-id-b58 "a"})
|
||||
path (:path match)]
|
||||
|
||||
(is (some? path) "Should generate a valid path")
|
||||
(is (re-= match (r/match-by-path routes path))
|
||||
"match-by-path should find the same route that generated the path")))
|
||||
|
||||
(testing "Partial match with missing parameters"
|
||||
;; Test that routes with missing parameters return PartialMatch
|
||||
(let [partial-match (r/match-by-name routes ::item {})]
|
||||
(is (instance? reitit.core.PartialMatch partial-match)
|
||||
"Should return a PartialMatch when params are missing")
|
||||
(is (= #{:item-id} (:required partial-match))
|
||||
"PartialMatch should indicate the required parameters")
|
||||
(is (re-= (r/map->PartialMatch {:template "/:item-id"
|
||||
:data {:name ::item
|
||||
:parameters {:path {:item-id #"[a-z]{16,20}"}}}
|
||||
:path-params {}
|
||||
:required #{:item-id}
|
||||
:result nil})
|
||||
partial-match)))
|
||||
|
||||
;; Test for a nested path with missing parameters
|
||||
(let [partial-match (r/match-by-name routes ::->members {})]
|
||||
(is (instance? reitit.core.PartialMatch partial-match)
|
||||
"Should return a PartialMatch for nested paths too")
|
||||
(is (= #{:team-id-b58} (:required partial-match))
|
||||
"PartialMatch should indicate the required parameters")))
|
||||
|
||||
(testing "Match with invalid parameters"
|
||||
;; Invalid parameters (that don't match the regex) still produce a Match
|
||||
(let [match (r/match-by-name routes ::item {:item-id "too-short"})
|
||||
path (:path match)]
|
||||
|
||||
(is (instance? reitit.core.Match match)
|
||||
"Should produce a Match even with invalid parameters")
|
||||
(is (= "/too-short" path)
|
||||
"Path should contain the provided parameter value")
|
||||
(is (nil? (r/match-by-path routes path))
|
||||
"Path with invalid parameter shouldn't be matchable by match-by-path")))
|
||||
|
||||
(testing "Non-existent routes"
|
||||
(is (nil? (r/match-by-name routes ::non-existent))
|
||||
"Should return nil for non-existent routes")))
|
||||
|
||||
(deftest regex-router-edge-cases-test
|
||||
(testing "Empty router"
|
||||
(let [empty-router (rt.regex/create-regex-router [])]
|
||||
(is (nil? (r/match-by-path empty-router "/any/path")))))
|
||||
|
||||
(testing "Handling trailing slashes"
|
||||
(is (nil? (r/match-by-path routes "/inbox/")))
|
||||
|
||||
(let [router-with-trailing-slash (rt.regex/create-regex-router [["inbox/" ::inbox-with-slash]])]
|
||||
(is (nil? (r/match-by-path router-with-trailing-slash "/inbox/")))
|
||||
(is (some? (r/match-by-path router-with-trailing-slash "/inbox")))))
|
||||
|
||||
(testing "Complex path patterns"
|
||||
(let [complex-router (rt.regex/create-regex-router
|
||||
[["articles/:year/:month/:slug"
|
||||
{:name ::article
|
||||
:parameters {:path {:year #"\d{4}"
|
||||
:month #"\d{2}"
|
||||
:slug #"[a-z0-9\-]+"}}}]
|
||||
["files/:path*"
|
||||
{:name ::file-path}]])]
|
||||
|
||||
;; Test article route with valid params
|
||||
(let [match (r/match-by-name complex-router ::article
|
||||
{:year "2023" :month "02" :slug "test-article"})]
|
||||
(is (instance? reitit.core.Match match)
|
||||
"Should return a Match for complex routes with valid params")
|
||||
(is (= "/articles/2023/02/test-article" (:path match))
|
||||
"Path should be constructed correctly"))
|
||||
|
||||
;; Test match-by-path with the generated path
|
||||
(let [match (r/match-by-path complex-router "/articles/2023/02/test-article")]
|
||||
(is (some? match)
|
||||
"Should match a valid article path")
|
||||
(is (= {:year "2023", :month "02", :slug "test-article"}
|
||||
(:path-params match))
|
||||
"Should extract all parameters correctly"))
|
||||
|
||||
;; Test invalid path
|
||||
(is (nil? (r/match-by-path complex-router "/articles/202/02/test-article"))
|
||||
"Should not match an invalid year (3 digits)")
|
||||
|
||||
;; Test partial params
|
||||
(let [partial-match (r/match-by-name complex-router ::article {:year "2023"})]
|
||||
(is (instance? reitit.core.PartialMatch partial-match)
|
||||
"Should return PartialMatch when some params are missing")
|
||||
(is (= #{:month :slug} (:required partial-match))
|
||||
"Should indicate which params are missing")))))
|
||||
|
||||
(deftest custom-router-features-test
|
||||
(testing "Router information access"
|
||||
;; Test that router information methods work properly
|
||||
(is (= :regex-router (r/router-name routes))
|
||||
"Should return the correct router name")
|
||||
|
||||
(is (seq (r/routes routes))
|
||||
"Should return the list of routes")
|
||||
|
||||
(is (= (set [::home ::item ::inbox ::teams ::->members])
|
||||
(set (r/route-names routes)))
|
||||
"Should return all route names"))
|
||||
|
||||
(testing "Compiled routes access"
|
||||
(let [compiled (r/compiled-routes routes)]
|
||||
(is (seq compiled)
|
||||
"Should return compiled routes")
|
||||
(is (every? :pattern compiled)
|
||||
"Every compiled route should have a pattern")
|
||||
(is (every? :route-data compiled)
|
||||
"Every compiled route should have route data"))))
|
||||
Loading…
Reference in a new issue