segment.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. use std::collections::BTreeMap;
  2. use crate::{
  3. utils::{gen_parameter_sitemap, gen_sitemap},
  4. Name,
  5. };
  6. use super::{Matcher, ParameterRoute, Route, RouteContent};
  7. /// A segment, representing a segment of the URLs path part (i.e. the stuff between two slashes).
  8. #[derive(Debug)]
  9. pub struct Segment<T: Clone> {
  10. pub(crate) index: Option<RouteContent<T>>,
  11. pub(crate) fallback: Option<RouteContent<T>>,
  12. pub(crate) clear_fallback: Option<bool>,
  13. pub(crate) fixed: BTreeMap<String, Route<T>>,
  14. pub(crate) matching: Vec<(Box<dyn Matcher>, ParameterRoute<T>)>,
  15. pub(crate) catch_all: Option<Box<ParameterRoute<T>>>,
  16. }
  17. impl<T: Clone> Segment<T> {
  18. /// Create a new [`Segment`] without index content.
  19. ///
  20. /// ```rust
  21. /// # use dioxus_router_core::routes::Segment;
  22. /// let seg: Segment<&'static str> = Segment::empty();
  23. /// ```
  24. pub fn empty() -> Self {
  25. Default::default()
  26. }
  27. /// Create a new [`Segment`] with some index `content`.
  28. ///
  29. /// ```rust
  30. /// # use dioxus_router_core::routes::{ContentAtom, Segment};
  31. /// let seg = Segment::content(ContentAtom("some content"));
  32. /// ```
  33. pub fn content(content: impl Into<RouteContent<T>>) -> Self {
  34. Self {
  35. index: Some(content.into()),
  36. ..Default::default()
  37. }
  38. }
  39. /// Create a new [`Segment`], possibly with some index `content`.
  40. ///
  41. /// ```rust
  42. /// # use dioxus_router_core::routes::{ContentAtom, Segment};
  43. /// let seg = Segment::new(Some(ContentAtom("some content")));
  44. /// ```
  45. pub fn new(content: Option<impl Into<RouteContent<T>>>) -> Self {
  46. match content {
  47. Some(content) => Self::content(content),
  48. None => Self::empty(),
  49. }
  50. }
  51. /// Add fallback content to a [`Segment`].
  52. ///
  53. /// ```rust
  54. /// # use dioxus_router_core::routes::{ContentAtom, Segment};
  55. /// Segment::content(ContentAtom("some content")).fallback(ContentAtom("fallback content"));
  56. /// ```
  57. ///
  58. /// The fallback content of the innermost matched [`Segment`] is used, if the router cannot find
  59. /// a full matching route.
  60. ///
  61. /// # Error Handling
  62. /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will
  63. /// panic. In _release mode_, all calls after the first will be ignored.
  64. pub fn fallback(mut self, content: impl Into<RouteContent<T>>) -> Self {
  65. debug_assert!(
  66. self.fallback.is_none(),
  67. "fallback content cannot be changed"
  68. );
  69. self.fallback.get_or_insert(content.into());
  70. self
  71. }
  72. /// Set whether to clear matched content when using the fallback.
  73. ///
  74. /// ```rust
  75. /// # use dioxus_router_core::routes::{ContentAtom, Segment};
  76. /// Segment::content(ContentAtom("some content"))
  77. /// .fallback(ContentAtom("fallback content"))
  78. /// .clear_fallback(true);
  79. /// ```
  80. ///
  81. /// When this is [`true`], the router will remove all content it previously found when falling
  82. /// back to this [`Segment`]s fallback content. If not set, a [`Segment`] will inherit this
  83. /// value from its parent segment. For the root [`Segment`], this defaults to [`false`].
  84. ///
  85. /// # Error Handling
  86. /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will
  87. /// panic. In _release mode_, all calls after the first will be ignored.
  88. pub fn clear_fallback(mut self, clear: bool) -> Self {
  89. debug_assert!(
  90. self.clear_fallback.is_none(),
  91. "fallback clearing cannot be changed"
  92. );
  93. self.clear_fallback.get_or_insert(clear);
  94. self
  95. }
  96. /// Add a fixed [`Route`] to the [`Segment`].
  97. ///
  98. /// ```rust
  99. /// # use dioxus_router_core::routes::{ContentAtom, Segment};
  100. /// Segment::empty().fixed("path", ContentAtom("fixed route content"));
  101. /// ```
  102. ///
  103. /// A fixed route is active only when the corresponding URL segment is exactly the same as its
  104. /// path.
  105. ///
  106. /// # Error Handling
  107. /// An error occurs if multiple fixed routes on the same [`Segment`] have the same `path`. In
  108. /// _debug mode_, the second call with panic. In _release mode_, the later routes will be
  109. /// ignored and the initial preserved.
  110. pub fn fixed(mut self, path: impl Into<String>, content: impl Into<Route<T>>) -> Self {
  111. let path = path.into();
  112. debug_assert!(
  113. !self.fixed.contains_key(&path),
  114. "duplicate fixed route: {path}"
  115. );
  116. self.fixed.entry(path).or_insert_with(|| content.into());
  117. self
  118. }
  119. /// Add a matching [`ParameterRoute`] to the [`Segment`].
  120. ///
  121. /// ```rust,ignore
  122. /// # use dioxus_router_core::routes::Segment;
  123. /// Segment::empty().matching("some matcher", (true, ContentAtom("matching route content")));
  124. /// ```
  125. ///
  126. /// A matching route is active only when the corresponding URL segment is accepted by its
  127. /// [`Matcher`], and no previously added matching route is.
  128. ///
  129. /// The example above is not checked by the compiler. This is because dioxus-router-core doesn't ship any
  130. /// [`Matcher`]s by default. However, you can implement your own, or turn on the `regex` feature
  131. /// to enable a regex implementation.
  132. pub fn matching(
  133. mut self,
  134. matcher: impl Matcher + 'static,
  135. content: impl Into<ParameterRoute<T>>,
  136. ) -> Self {
  137. self.matching.push((Box::new(matcher), content.into()));
  138. self
  139. }
  140. /// Add a catch all [`ParameterRoute`] to the [`Segment`].
  141. ///
  142. /// ```rust
  143. /// # use dioxus_router_core::routes::{ContentAtom, Segment};
  144. /// Segment::empty().catch_all((ContentAtom("catch all route content"), true));
  145. /// ```
  146. ///
  147. /// A catch all route is active only if no fixed or matching route is.
  148. ///
  149. /// # Error Handling
  150. /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will
  151. /// panic. In _release mode_, all calls after the first will be ignored.
  152. pub fn catch_all(mut self, content: impl Into<ParameterRoute<T>>) -> Self {
  153. debug_assert!(self.catch_all.is_none(), "duplicate catch all route");
  154. self.catch_all.get_or_insert(Box::new(content.into()));
  155. self
  156. }
  157. /// Generate a site map.
  158. ///
  159. /// ```rust
  160. /// # use std::collections::BTreeMap;
  161. /// # use dioxus_router_core::{Name, routes::Segment};
  162. /// let seg = Segment::<u8>::empty().fixed("fixed", "").catch_all(("", true));
  163. /// let sitemap = seg.gen_sitemap();
  164. /// assert_eq!(sitemap, vec!["/", "/fixed", "/\\bool"]);
  165. /// ```
  166. ///
  167. /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a
  168. /// path. Fixed routes are passed in as is, while matching and catch all routes are represented
  169. /// by their key, marked with a leading `\`. Since the otherwise all paths should be valid in
  170. /// URLs, and `\` is not, this doesn't cause a conflict.
  171. pub fn gen_sitemap(&self) -> Vec<String> {
  172. let mut res = Vec::new();
  173. res.push(String::from("/"));
  174. gen_sitemap(self, "", &mut res);
  175. res
  176. }
  177. /// Generate a site map with parameters filled in.
  178. ///
  179. /// ```rust
  180. /// # use std::collections::BTreeMap;
  181. /// # use dioxus_router_core::{Name, routes::Segment};
  182. /// let seg = Segment::<u8>::empty().fixed("fixed", "").catch_all(("", true));
  183. /// let mut parameters = BTreeMap::new();
  184. /// parameters.insert(Name::of::<bool>(), vec![String::from("1"), String::from("2")]);
  185. ///
  186. /// let sitemap = seg.gen_parameter_sitemap(&parameters);
  187. /// assert_eq!(sitemap, vec!["/", "/fixed", "/1", "/2"]);
  188. /// ```
  189. ///
  190. /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a
  191. /// path. Fixed routes are passed in as is, while matching and catch all will be represented
  192. /// with all `parameters` provided for their key. Matching routes will also filter out all
  193. /// invalid parameters.
  194. pub fn gen_parameter_sitemap(&self, parameters: &BTreeMap<Name, Vec<String>>) -> Vec<String> {
  195. let mut res = Vec::new();
  196. res.push(String::from("/"));
  197. gen_parameter_sitemap(self, parameters, "", &mut res);
  198. res
  199. }
  200. }
  201. impl<T: Clone> Default for Segment<T> {
  202. fn default() -> Self {
  203. Self {
  204. index: None,
  205. fallback: None,
  206. clear_fallback: None,
  207. fixed: BTreeMap::new(),
  208. matching: Vec::new(),
  209. catch_all: None,
  210. }
  211. }
  212. }
  213. #[cfg(test)]
  214. mod tests {
  215. use crate::routes::{content::test_content, ContentAtom};
  216. use super::*;
  217. #[test]
  218. fn default() {
  219. let seg: Segment<&str> = Default::default();
  220. assert!(seg.index.is_none());
  221. assert!(seg.fallback.is_none());
  222. assert!(seg.clear_fallback.is_none());
  223. assert!(seg.fixed.is_empty());
  224. assert!(seg.matching.is_empty());
  225. assert!(seg.catch_all.is_none());
  226. }
  227. #[test]
  228. fn empty() {
  229. let seg = Segment::<&str>::empty();
  230. assert!(seg.index.is_none());
  231. assert!(seg.fallback.is_none());
  232. assert!(seg.clear_fallback.is_none());
  233. assert!(seg.fixed.is_empty());
  234. assert!(seg.matching.is_empty());
  235. assert!(seg.catch_all.is_none());
  236. }
  237. #[test]
  238. fn content() {
  239. let seg = Segment::content(test_content());
  240. assert_eq!(seg.index, Some(test_content()));
  241. assert!(seg.fallback.is_none());
  242. assert!(seg.clear_fallback.is_none());
  243. assert!(seg.fixed.is_empty());
  244. assert!(seg.matching.is_empty());
  245. assert!(seg.catch_all.is_none());
  246. }
  247. #[test]
  248. fn new_empty() {
  249. let seg = Segment::<&str>::new(None::<String>);
  250. assert!(seg.index.is_none());
  251. assert!(seg.fallback.is_none());
  252. assert!(seg.clear_fallback.is_none());
  253. assert!(seg.fixed.is_empty());
  254. assert!(seg.matching.is_empty());
  255. assert!(seg.catch_all.is_none());
  256. }
  257. #[test]
  258. fn new_content() {
  259. let seg = Segment::new(Some(test_content()));
  260. assert_eq!(seg.index, Some(test_content()));
  261. assert!(seg.fallback.is_none());
  262. assert!(seg.clear_fallback.is_none());
  263. assert!(seg.fixed.is_empty());
  264. assert!(seg.matching.is_empty());
  265. assert!(seg.catch_all.is_none());
  266. }
  267. #[test]
  268. fn fallback_initial() {
  269. let seg = Segment::empty().fallback(test_content());
  270. assert_eq!(seg.fallback, Some(test_content()));
  271. }
  272. #[test]
  273. #[should_panic = "fallback content cannot be changed"]
  274. #[cfg(debug_assertions)]
  275. fn fallback_debug() {
  276. Segment::empty()
  277. .fallback(test_content())
  278. .fallback(test_content());
  279. }
  280. #[test]
  281. #[cfg(not(debug_assertions))]
  282. fn fallback_release() {
  283. let seg = Segment::empty()
  284. .fallback(test_content())
  285. .fallback(RouteContent::Content(ContentAtom("invalid")));
  286. assert_eq!(seg.fallback, Some(test_content()));
  287. }
  288. #[test]
  289. fn clear_fallback() {
  290. let mut seg = Segment::<&str>::empty();
  291. assert!(seg.clear_fallback.is_none());
  292. seg = seg.clear_fallback(true);
  293. assert_eq!(seg.clear_fallback, Some(true));
  294. }
  295. #[test]
  296. #[should_panic = "fallback clearing cannot be changed"]
  297. #[cfg(debug_assertions)]
  298. fn clear_fallback_debug() {
  299. Segment::<&str>::empty()
  300. .clear_fallback(true)
  301. .clear_fallback(false);
  302. }
  303. #[test]
  304. #[cfg(not(debug_assertions))]
  305. fn clear_fallback_release() {
  306. let seg = Segment::<&str>::empty()
  307. .clear_fallback(true)
  308. .clear_fallback(false);
  309. assert_eq!(seg.clear_fallback, Some(true));
  310. }
  311. #[test]
  312. fn fixed() {
  313. let test = RouteContent::Content(ContentAtom("test"));
  314. let other = RouteContent::Content(ContentAtom("other"));
  315. let seg = Segment::empty()
  316. .fixed("test", Route::content(test.clone()))
  317. .fixed("other", Route::content(other.clone()));
  318. assert_eq!(seg.fixed.len(), 2);
  319. assert_eq!(seg.fixed["test"].content, Some(test));
  320. assert_eq!(seg.fixed["other"].content, Some(other));
  321. }
  322. #[test]
  323. #[should_panic = "duplicate fixed route: test"]
  324. #[cfg(debug_assertions)]
  325. fn fixed_debug() {
  326. Segment::empty()
  327. .fixed(
  328. "test",
  329. Route::content(RouteContent::Content(ContentAtom("test"))),
  330. )
  331. .fixed(
  332. "test",
  333. Route::content(RouteContent::Content(ContentAtom("other"))),
  334. );
  335. }
  336. #[test]
  337. #[cfg(not(debug_assertions))]
  338. fn fixed_release() {
  339. let test = RouteContent::Content(ContentAtom("test"));
  340. let other = RouteContent::Content(ContentAtom("other"));
  341. let seg = Segment::empty()
  342. .fixed("test", Route::content(test.clone()))
  343. .fixed("test", Route::content(other.clone()));
  344. assert_eq!(seg.fixed.len(), 1);
  345. assert_eq!(seg.fixed["test"].content, Some(test));
  346. }
  347. #[test]
  348. fn matching() {
  349. let test = RouteContent::Content(ContentAtom("test"));
  350. let other = RouteContent::Content(ContentAtom("other"));
  351. let seg = Segment::empty()
  352. .matching(
  353. String::from("test"),
  354. ParameterRoute::content::<String>(test.clone()),
  355. )
  356. .matching(
  357. String::from("other"),
  358. ParameterRoute::content::<String>(other.clone()),
  359. );
  360. assert_eq!(seg.matching.len(), 2);
  361. assert_eq!(seg.matching[0].1.content, Some(test));
  362. assert_eq!(seg.matching[1].1.content, Some(other));
  363. }
  364. #[test]
  365. fn catch_all_initial() {
  366. let seg = Segment::empty().catch_all(ParameterRoute::content::<String>(test_content()));
  367. assert!(seg.catch_all.is_some());
  368. assert_eq!(seg.catch_all.unwrap().content, Some(test_content()));
  369. }
  370. #[test]
  371. #[should_panic = "duplicate catch all route"]
  372. #[cfg(debug_assertions)]
  373. fn catch_all_debug() {
  374. Segment::empty()
  375. .catch_all(ParameterRoute::content::<String>(test_content()))
  376. .catch_all(ParameterRoute::content::<String>(test_content()));
  377. }
  378. #[test]
  379. #[cfg(not(debug_assertions))]
  380. fn catch_all_release() {
  381. let seg = Segment::empty()
  382. .catch_all(ParameterRoute::content::<String>(test_content()))
  383. .catch_all(ParameterRoute::empty::<bool>());
  384. assert!(seg.catch_all.is_some());
  385. assert_eq!(seg.catch_all.unwrap().content, Some(test_content()));
  386. }
  387. // Check whether the returned sitemap includes "/". More elaborate tests are located alongside
  388. // the internal `gen_sitemap` function.
  389. #[test]
  390. fn gen_sitemap() {
  391. assert_eq!(Segment::<&'static str>::empty().gen_sitemap(), vec!["/"]);
  392. }
  393. // Check whether the returned sitemap includes "/". More elaborate tests are located alongside
  394. // the internal `gen_parameter_sitemap` function.
  395. #[test]
  396. fn gen_parameter_sitemap() {
  397. assert_eq!(
  398. Segment::<&'static str>::empty().gen_parameter_sitemap(&BTreeMap::new()),
  399. vec!["/"]
  400. );
  401. }
  402. }