state.rs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. use std::{
  2. collections::{BTreeMap, HashMap, HashSet},
  3. sync::Arc,
  4. };
  5. use either::Either;
  6. use crate::{
  7. navigation::NavigationTarget, routes::ContentAtom, segments::NameMap, utils::resolve_target,
  8. Name,
  9. };
  10. /// The current state of the router.
  11. #[derive(Debug)]
  12. pub struct RouterState<T: Clone> {
  13. /// Whether there is a previous page to navigate back to.
  14. ///
  15. /// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
  16. /// safe to tell the router to go back.
  17. pub can_go_back: bool,
  18. /// Whether there is a future page to navigate forward to.
  19. ///
  20. /// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
  21. /// to tell the router to go forward.
  22. pub can_go_forward: bool,
  23. /// The current path.
  24. pub path: String,
  25. /// The current query.
  26. pub query: Option<String>,
  27. /// The current prefix.
  28. pub prefix: Option<String>,
  29. /// The names of currently active routes.
  30. pub names: HashSet<Name>,
  31. /// The current path parameters.
  32. pub parameters: HashMap<Name, String>,
  33. pub(crate) name_map: Arc<NameMap>,
  34. /// The current main content.
  35. ///
  36. /// This should only be used by UI integration crates, and not by applications.
  37. pub content: Vec<ContentAtom<T>>,
  38. /// The current named content.
  39. ///
  40. /// This should only be used by UI integration crates, and not by applications.
  41. pub named_content: BTreeMap<Name, Vec<ContentAtom<T>>>,
  42. }
  43. impl<T: Clone> RouterState<T> {
  44. /// Get a parameter.
  45. ///
  46. /// ```rust
  47. /// # use dioxus_router_core::{RouterState, Name};
  48. /// let mut state = RouterState::<&'static str>::default();
  49. /// assert_eq!(state.parameter::<bool>(), None);
  50. ///
  51. /// // Do not do this! For illustrative purposes only!
  52. /// state.parameters.insert(Name::of::<bool>(), String::from("some parameter"));
  53. /// assert_eq!(state.parameter::<bool>(), Some("some parameter".to_string()));
  54. /// ```
  55. pub fn parameter<N: 'static>(&self) -> Option<String> {
  56. self.parameters.get(&Name::of::<N>()).cloned()
  57. }
  58. /// Get the `href` for the `target`.
  59. pub fn href(&self, target: &NavigationTarget) -> String {
  60. match resolve_target(&self.name_map, target) {
  61. Either::Left(Either::Left(i)) => match &self.prefix {
  62. Some(p) => format!("{p}{i}"),
  63. None => i,
  64. },
  65. Either::Left(Either::Right(n)) => {
  66. // the following assert currently cannot trigger, as resolve_target (or more
  67. // precisely resolve_name, which is called by resolve_targe) will panic in debug
  68. debug_assert!(false, "requested href for unknown name or parameter: {n}");
  69. String::new()
  70. }
  71. Either::Right(e) => e,
  72. }
  73. }
  74. /// Check whether the `target` is currently active.
  75. ///
  76. /// # Normal mode
  77. /// 1. For internal targets wrapping an absolute path, the current path has to start with it.
  78. /// 2. For internal targets wrapping a relative path, it has to match the last current segment
  79. /// exactly.
  80. /// 3. For named targets, the provided name needs to be active.
  81. /// 4. For external targets [`false`].
  82. ///
  83. /// # Exact mode
  84. /// 1. For internal targets, the current path must match the wrapped path exactly.
  85. /// 2. For named targets, the provided name needs to be active and all parameters need to match
  86. /// exactly.
  87. /// 3. For external targets [`false`].
  88. pub fn is_at(&self, target: &NavigationTarget, exact: bool) -> bool {
  89. match target {
  90. NavigationTarget::Internal(i) => {
  91. if exact {
  92. i == &self.path
  93. } else if i.starts_with('/') {
  94. self.path.starts_with(i)
  95. } else if let Some((_, s)) = self.path.rsplit_once('/') {
  96. s == i
  97. } else {
  98. false
  99. }
  100. }
  101. NavigationTarget::Named {
  102. name,
  103. parameters,
  104. query: _,
  105. } => {
  106. if !self.names.contains(name) {
  107. false
  108. } else if exact {
  109. for (k, v) in parameters {
  110. match self.parameters.get(k) {
  111. Some(p) if p != v => return false,
  112. None => return false,
  113. _ => {}
  114. }
  115. }
  116. true
  117. } else {
  118. true
  119. }
  120. }
  121. NavigationTarget::External(_) => false,
  122. }
  123. }
  124. }
  125. // manual impl required because derive macro requires default for T unnecessarily
  126. impl<T: Clone> Default for RouterState<T> {
  127. fn default() -> Self {
  128. Self {
  129. can_go_back: Default::default(),
  130. can_go_forward: Default::default(),
  131. path: Default::default(),
  132. query: Default::default(),
  133. prefix: Default::default(),
  134. names: Default::default(),
  135. parameters: Default::default(),
  136. name_map: Default::default(),
  137. content: Default::default(),
  138. named_content: Default::default(),
  139. }
  140. }
  141. }
  142. #[cfg(test)]
  143. mod tests {
  144. use crate::{navigation::named, prelude::RootIndex};
  145. use super::*;
  146. #[test]
  147. fn href_internal() {
  148. let state = RouterState::<&str> {
  149. prefix: Some(String::from("/prefix")),
  150. ..Default::default()
  151. };
  152. assert_eq!(state.href(&"/test".into()), String::from("/prefix/test"))
  153. }
  154. #[test]
  155. fn href_named() {
  156. let state = RouterState::<&str> {
  157. name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
  158. prefix: Some(String::from("/prefix")),
  159. ..Default::default()
  160. };
  161. assert_eq!(state.href(&named::<RootIndex>()), String::from("/prefix/"))
  162. }
  163. #[test]
  164. #[should_panic = "named navigation to unknown name: bool"]
  165. #[cfg(debug_assertions)]
  166. fn href_named_debug() {
  167. let state = RouterState::<&str> {
  168. name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
  169. prefix: Some(String::from("/prefix")),
  170. ..Default::default()
  171. };
  172. state.href(&named::<bool>());
  173. }
  174. #[test]
  175. #[cfg(not(debug_assertions))]
  176. fn href_named_release() {
  177. let state = RouterState::<&str> {
  178. name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
  179. prefix: Some(String::from("/prefix")),
  180. ..Default::default()
  181. };
  182. assert_eq!(state.href(&named::<bool>()), String::new())
  183. }
  184. #[test]
  185. fn href_external() {
  186. let state = RouterState::<&str> {
  187. prefix: Some(String::from("/prefix")),
  188. ..Default::default()
  189. };
  190. assert_eq!(
  191. state.href(&"https://dioxuslabs.com/".into()),
  192. String::from("https://dioxuslabs.com/")
  193. )
  194. }
  195. #[test]
  196. fn is_at_internal_absolute() {
  197. let state = test_state();
  198. assert!(!state.is_at(&"/levels".into(), false));
  199. assert!(!state.is_at(&"/levels".into(), true));
  200. assert!(state.is_at(&"/test".into(), false));
  201. assert!(!state.is_at(&"/test".into(), true));
  202. assert!(state.is_at(&"/test/with/some/nested/levels".into(), false));
  203. assert!(state.is_at(&"/test/with/some/nested/levels".into(), true));
  204. }
  205. #[test]
  206. fn is_at_internal_relative() {
  207. let state = test_state();
  208. assert!(state.is_at(&"levels".into(), false));
  209. assert!(!state.is_at(&"levels".into(), true));
  210. assert!(!state.is_at(&"test".into(), false));
  211. assert!(!state.is_at(&"test".into(), true));
  212. assert!(!state.is_at(&"test/with/some/nested/levels".into(), false));
  213. assert!(!state.is_at(&"test/with/some/nested/levels".into(), true));
  214. }
  215. #[test]
  216. fn is_at_named() {
  217. let state = test_state();
  218. assert!(!state.is_at(&named::<RootIndex>(), false));
  219. assert!(!state.is_at(&named::<RootIndex>(), true));
  220. assert!(state.is_at(&named::<bool>(), false));
  221. assert!(state.is_at(&named::<bool>(), true));
  222. assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), false));
  223. assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), true));
  224. assert!(state.is_at(&named::<bool>().parameter::<i8>("test"), false));
  225. assert!(!state.is_at(&named::<bool>().parameter::<i8>("test"), true));
  226. }
  227. #[test]
  228. fn is_at_external() {
  229. let state = test_state();
  230. assert!(!state.is_at(&"https://dioxuslabs.com/".into(), false));
  231. assert!(!state.is_at(&"https://dioxuslabs.com/".into(), true));
  232. }
  233. fn test_state() -> RouterState<&'static str> {
  234. RouterState {
  235. path: String::from("/test/with/some/nested/levels"),
  236. names: {
  237. let mut r = HashSet::new();
  238. r.insert(Name::of::<bool>());
  239. r
  240. },
  241. parameters: {
  242. let mut r = HashMap::new();
  243. r.insert(Name::of::<bool>(), String::from("test"));
  244. r
  245. },
  246. ..Default::default()
  247. }
  248. }
  249. }