use std::collections::BTreeMap; use crate::{ utils::{gen_parameter_sitemap, gen_sitemap}, Name, }; use super::{Matcher, ParameterRoute, Route, RouteContent}; /// A segment, representing a segment of the URLs path part (i.e. the stuff between two slashes). #[derive(Debug)] pub struct Segment { pub(crate) index: Option>, pub(crate) fallback: Option>, pub(crate) clear_fallback: Option, pub(crate) fixed: BTreeMap>, pub(crate) matching: Vec<(Box, ParameterRoute)>, pub(crate) catch_all: Option>>, } impl Segment { /// Create a new [`Segment`] without index content. /// /// ```rust /// # use dioxus_router_core::routes::Segment; /// let seg: Segment<&'static str> = Segment::empty(); /// ``` pub fn empty() -> Self { Default::default() } /// Create a new [`Segment`] with some index `content`. /// /// ```rust /// # use dioxus_router_core::routes::{ContentAtom, Segment}; /// let seg = Segment::content(ContentAtom("some content")); /// ``` pub fn content(content: impl Into>) -> Self { Self { index: Some(content.into()), ..Default::default() } } /// Create a new [`Segment`], possibly with some index `content`. /// /// ```rust /// # use dioxus_router_core::routes::{ContentAtom, Segment}; /// let seg = Segment::new(Some(ContentAtom("some content"))); /// ``` pub fn new(content: Option>>) -> Self { match content { Some(content) => Self::content(content), None => Self::empty(), } } /// Add fallback content to a [`Segment`]. /// /// ```rust /// # use dioxus_router_core::routes::{ContentAtom, Segment}; /// Segment::content(ContentAtom("some content")).fallback(ContentAtom("fallback content")); /// ``` /// /// The fallback content of the innermost matched [`Segment`] is used, if the router cannot find /// a full matching route. /// /// # Error Handling /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will /// panic. In _release mode_, all calls after the first will be ignored. pub fn fallback(mut self, content: impl Into>) -> Self { debug_assert!( self.fallback.is_none(), "fallback content cannot be changed" ); self.fallback.get_or_insert(content.into()); self } /// Set whether to clear matched content when using the fallback. /// /// ```rust /// # use dioxus_router_core::routes::{ContentAtom, Segment}; /// Segment::content(ContentAtom("some content")) /// .fallback(ContentAtom("fallback content")) /// .clear_fallback(true); /// ``` /// /// When this is [`true`], the router will remove all content it previously found when falling /// back to this [`Segment`]s fallback content. If not set, a [`Segment`] will inherit this /// value from its parent segment. For the root [`Segment`], this defaults to [`false`]. /// /// # Error Handling /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will /// panic. In _release mode_, all calls after the first will be ignored. pub fn clear_fallback(mut self, clear: bool) -> Self { debug_assert!( self.clear_fallback.is_none(), "fallback clearing cannot be changed" ); self.clear_fallback.get_or_insert(clear); self } /// Add a fixed [`Route`] to the [`Segment`]. /// /// ```rust /// # use dioxus_router_core::routes::{ContentAtom, Segment}; /// Segment::empty().fixed("path", ContentAtom("fixed route content")); /// ``` /// /// A fixed route is active only when the corresponding URL segment is exactly the same as its /// path. /// /// # Error Handling /// An error occurs if multiple fixed routes on the same [`Segment`] have the same `path`. In /// _debug mode_, the second call with panic. In _release mode_, the later routes will be /// ignored and the initial preserved. pub fn fixed(mut self, path: impl Into, content: impl Into>) -> Self { let path = path.into(); debug_assert!( !self.fixed.contains_key(&path), "duplicate fixed route: {path}" ); self.fixed.entry(path).or_insert_with(|| content.into()); self } /// Add a matching [`ParameterRoute`] to the [`Segment`]. /// /// ```rust,ignore /// # use dioxus_router_core::routes::Segment; /// Segment::empty().matching("some matcher", (true, ContentAtom("matching route content"))); /// ``` /// /// A matching route is active only when the corresponding URL segment is accepted by its /// [`Matcher`], and no previously added matching route is. /// /// The example above is not checked by the compiler. This is because dioxus-router-core doesn't ship any /// [`Matcher`]s by default. However, you can implement your own, or turn on the `regex` feature /// to enable a regex implementation. pub fn matching( mut self, matcher: impl Matcher + 'static, content: impl Into>, ) -> Self { self.matching.push((Box::new(matcher), content.into())); self } /// Add a catch all [`ParameterRoute`] to the [`Segment`]. /// /// ```rust /// # use dioxus_router_core::routes::{ContentAtom, Segment}; /// Segment::empty().catch_all((ContentAtom("catch all route content"), true)); /// ``` /// /// A catch all route is active only if no fixed or matching route is. /// /// # Error Handling /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will /// panic. In _release mode_, all calls after the first will be ignored. pub fn catch_all(mut self, content: impl Into>) -> Self { debug_assert!(self.catch_all.is_none(), "duplicate catch all route"); self.catch_all.get_or_insert(Box::new(content.into())); self } /// Generate a site map. /// /// ```rust /// # use std::collections::BTreeMap; /// # use dioxus_router_core::{Name, routes::Segment}; /// let seg = Segment::::empty().fixed("fixed", "").catch_all(("", true)); /// let sitemap = seg.gen_sitemap(); /// assert_eq!(sitemap, vec!["/", "/fixed", "/\\bool"]); /// ``` /// /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a /// path. Fixed routes are passed in as is, while matching and catch all routes are represented /// by their key, marked with a leading `\`. Since the otherwise all paths should be valid in /// URLs, and `\` is not, this doesn't cause a conflict. pub fn gen_sitemap(&self) -> Vec { let mut res = Vec::new(); res.push(String::from("/")); gen_sitemap(self, "", &mut res); res } /// Generate a site map with parameters filled in. /// /// ```rust /// # use std::collections::BTreeMap; /// # use dioxus_router_core::{Name, routes::Segment}; /// let seg = Segment::::empty().fixed("fixed", "").catch_all(("", true)); /// let mut parameters = BTreeMap::new(); /// parameters.insert(Name::of::(), vec![String::from("1"), String::from("2")]); /// /// let sitemap = seg.gen_parameter_sitemap(¶meters); /// assert_eq!(sitemap, vec!["/", "/fixed", "/1", "/2"]); /// ``` /// /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a /// path. Fixed routes are passed in as is, while matching and catch all will be represented /// with all `parameters` provided for their key. Matching routes will also filter out all /// invalid parameters. pub fn gen_parameter_sitemap(&self, parameters: &BTreeMap>) -> Vec { let mut res = Vec::new(); res.push(String::from("/")); gen_parameter_sitemap(self, parameters, "", &mut res); res } } impl Default for Segment { fn default() -> Self { Self { index: None, fallback: None, clear_fallback: None, fixed: BTreeMap::new(), matching: Vec::new(), catch_all: None, } } } #[cfg(test)] mod tests { use crate::routes::{content::test_content, ContentAtom}; use super::*; #[test] fn default() { let seg: Segment<&str> = Default::default(); assert!(seg.index.is_none()); assert!(seg.fallback.is_none()); assert!(seg.clear_fallback.is_none()); assert!(seg.fixed.is_empty()); assert!(seg.matching.is_empty()); assert!(seg.catch_all.is_none()); } #[test] fn empty() { let seg = Segment::<&str>::empty(); assert!(seg.index.is_none()); assert!(seg.fallback.is_none()); assert!(seg.clear_fallback.is_none()); assert!(seg.fixed.is_empty()); assert!(seg.matching.is_empty()); assert!(seg.catch_all.is_none()); } #[test] fn content() { let seg = Segment::content(test_content()); assert_eq!(seg.index, Some(test_content())); assert!(seg.fallback.is_none()); assert!(seg.clear_fallback.is_none()); assert!(seg.fixed.is_empty()); assert!(seg.matching.is_empty()); assert!(seg.catch_all.is_none()); } #[test] fn new_empty() { let seg = Segment::<&str>::new(None::); assert!(seg.index.is_none()); assert!(seg.fallback.is_none()); assert!(seg.clear_fallback.is_none()); assert!(seg.fixed.is_empty()); assert!(seg.matching.is_empty()); assert!(seg.catch_all.is_none()); } #[test] fn new_content() { let seg = Segment::new(Some(test_content())); assert_eq!(seg.index, Some(test_content())); assert!(seg.fallback.is_none()); assert!(seg.clear_fallback.is_none()); assert!(seg.fixed.is_empty()); assert!(seg.matching.is_empty()); assert!(seg.catch_all.is_none()); } #[test] fn fallback_initial() { let seg = Segment::empty().fallback(test_content()); assert_eq!(seg.fallback, Some(test_content())); } #[test] #[should_panic = "fallback content cannot be changed"] #[cfg(debug_assertions)] fn fallback_debug() { Segment::empty() .fallback(test_content()) .fallback(test_content()); } #[test] #[cfg(not(debug_assertions))] fn fallback_release() { let seg = Segment::empty() .fallback(test_content()) .fallback(RouteContent::Content(ContentAtom("invalid"))); assert_eq!(seg.fallback, Some(test_content())); } #[test] fn clear_fallback() { let mut seg = Segment::<&str>::empty(); assert!(seg.clear_fallback.is_none()); seg = seg.clear_fallback(true); assert_eq!(seg.clear_fallback, Some(true)); } #[test] #[should_panic = "fallback clearing cannot be changed"] #[cfg(debug_assertions)] fn clear_fallback_debug() { Segment::<&str>::empty() .clear_fallback(true) .clear_fallback(false); } #[test] #[cfg(not(debug_assertions))] fn clear_fallback_release() { let seg = Segment::<&str>::empty() .clear_fallback(true) .clear_fallback(false); assert_eq!(seg.clear_fallback, Some(true)); } #[test] fn fixed() { let test = RouteContent::Content(ContentAtom("test")); let other = RouteContent::Content(ContentAtom("other")); let seg = Segment::empty() .fixed("test", Route::content(test.clone())) .fixed("other", Route::content(other.clone())); assert_eq!(seg.fixed.len(), 2); assert_eq!(seg.fixed["test"].content, Some(test)); assert_eq!(seg.fixed["other"].content, Some(other)); } #[test] #[should_panic = "duplicate fixed route: test"] #[cfg(debug_assertions)] fn fixed_debug() { Segment::empty() .fixed( "test", Route::content(RouteContent::Content(ContentAtom("test"))), ) .fixed( "test", Route::content(RouteContent::Content(ContentAtom("other"))), ); } #[test] #[cfg(not(debug_assertions))] fn fixed_release() { let test = RouteContent::Content(ContentAtom("test")); let other = RouteContent::Content(ContentAtom("other")); let seg = Segment::empty() .fixed("test", Route::content(test.clone())) .fixed("test", Route::content(other.clone())); assert_eq!(seg.fixed.len(), 1); assert_eq!(seg.fixed["test"].content, Some(test)); } #[test] fn matching() { let test = RouteContent::Content(ContentAtom("test")); let other = RouteContent::Content(ContentAtom("other")); let seg = Segment::empty() .matching( String::from("test"), ParameterRoute::content::(test.clone()), ) .matching( String::from("other"), ParameterRoute::content::(other.clone()), ); assert_eq!(seg.matching.len(), 2); assert_eq!(seg.matching[0].1.content, Some(test)); assert_eq!(seg.matching[1].1.content, Some(other)); } #[test] fn catch_all_initial() { let seg = Segment::empty().catch_all(ParameterRoute::content::(test_content())); assert!(seg.catch_all.is_some()); assert_eq!(seg.catch_all.unwrap().content, Some(test_content())); } #[test] #[should_panic = "duplicate catch all route"] #[cfg(debug_assertions)] fn catch_all_debug() { Segment::empty() .catch_all(ParameterRoute::content::(test_content())) .catch_all(ParameterRoute::content::(test_content())); } #[test] #[cfg(not(debug_assertions))] fn catch_all_release() { let seg = Segment::empty() .catch_all(ParameterRoute::content::(test_content())) .catch_all(ParameterRoute::empty::()); assert!(seg.catch_all.is_some()); assert_eq!(seg.catch_all.unwrap().content, Some(test_content())); } // Check whether the returned sitemap includes "/". More elaborate tests are located alongside // the internal `gen_sitemap` function. #[test] fn gen_sitemap() { assert_eq!(Segment::<&'static str>::empty().gen_sitemap(), vec!["/"]); } // Check whether the returned sitemap includes "/". More elaborate tests are located alongside // the internal `gen_parameter_sitemap` function. #[test] fn gen_parameter_sitemap() { assert_eq!( Segment::<&'static str>::empty().gen_parameter_sitemap(&BTreeMap::new()), vec!["/"] ); } }