123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- use std::{
- collections::{BTreeMap, HashMap, HashSet},
- sync::Arc,
- };
- use either::Either;
- use crate::{
- navigation::NavigationTarget, routes::ContentAtom, segments::NameMap, utils::resolve_target,
- Name,
- };
- /// The current state of the router.
- #[derive(Debug)]
- pub struct RouterState<T: Clone> {
- /// Whether there is a previous page to navigate back to.
- ///
- /// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
- /// safe to tell the router to go back.
- pub can_go_back: bool,
- /// Whether there is a future page to navigate forward to.
- ///
- /// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
- /// to tell the router to go forward.
- pub can_go_forward: bool,
- /// The current path.
- pub path: String,
- /// The current query.
- pub query: Option<String>,
- /// The current prefix.
- pub prefix: Option<String>,
- /// The names of currently active routes.
- pub names: HashSet<Name>,
- /// The current path parameters.
- pub parameters: HashMap<Name, String>,
- pub(crate) name_map: Arc<NameMap>,
- /// The current main content.
- ///
- /// This should only be used by UI integration crates, and not by applications.
- pub content: Vec<ContentAtom<T>>,
- /// The current named content.
- ///
- /// This should only be used by UI integration crates, and not by applications.
- pub named_content: BTreeMap<Name, Vec<ContentAtom<T>>>,
- }
- impl<T: Clone> RouterState<T> {
- /// Get a parameter.
- ///
- /// ```rust
- /// # use dioxus_router_core::{RouterState, Name};
- /// let mut state = RouterState::<&'static str>::default();
- /// assert_eq!(state.parameter::<bool>(), None);
- ///
- /// // Do not do this! For illustrative purposes only!
- /// state.parameters.insert(Name::of::<bool>(), String::from("some parameter"));
- /// assert_eq!(state.parameter::<bool>(), Some("some parameter".to_string()));
- /// ```
- pub fn parameter<N: 'static>(&self) -> Option<String> {
- self.parameters.get(&Name::of::<N>()).cloned()
- }
- /// Get the `href` for the `target`.
- pub fn href(&self, target: &NavigationTarget) -> String {
- match resolve_target(&self.name_map, target) {
- Either::Left(Either::Left(i)) => match &self.prefix {
- Some(p) => format!("{p}{i}"),
- None => i,
- },
- Either::Left(Either::Right(n)) => {
- // the following assert currently cannot trigger, as resolve_target (or more
- // precisely resolve_name, which is called by resolve_targe) will panic in debug
- debug_assert!(false, "requested href for unknown name or parameter: {n}");
- String::new()
- }
- Either::Right(e) => e,
- }
- }
- /// Check whether the `target` is currently active.
- ///
- /// # Normal mode
- /// 1. For internal targets wrapping an absolute path, the current path has to start with it.
- /// 2. For internal targets wrapping a relative path, it has to match the last current segment
- /// exactly.
- /// 3. For named targets, the provided name needs to be active.
- /// 4. For external targets [`false`].
- ///
- /// # Exact mode
- /// 1. For internal targets, the current path must match the wrapped path exactly.
- /// 2. For named targets, the provided name needs to be active and all parameters need to match
- /// exactly.
- /// 3. For external targets [`false`].
- pub fn is_at(&self, target: &NavigationTarget, exact: bool) -> bool {
- match target {
- NavigationTarget::Internal(i) => {
- if exact {
- i == &self.path
- } else if i.starts_with('/') {
- self.path.starts_with(i)
- } else if let Some((_, s)) = self.path.rsplit_once('/') {
- s == i
- } else {
- false
- }
- }
- NavigationTarget::Named {
- name,
- parameters,
- query: _,
- } => {
- if !self.names.contains(name) {
- false
- } else if exact {
- for (k, v) in parameters {
- match self.parameters.get(k) {
- Some(p) if p != v => return false,
- None => return false,
- _ => {}
- }
- }
- true
- } else {
- true
- }
- }
- NavigationTarget::External(_) => false,
- }
- }
- }
- // manual impl required because derive macro requires default for T unnecessarily
- impl<T: Clone> Default for RouterState<T> {
- fn default() -> Self {
- Self {
- can_go_back: Default::default(),
- can_go_forward: Default::default(),
- path: Default::default(),
- query: Default::default(),
- prefix: Default::default(),
- names: Default::default(),
- parameters: Default::default(),
- name_map: Default::default(),
- content: Default::default(),
- named_content: Default::default(),
- }
- }
- }
- #[cfg(test)]
- mod tests {
- use crate::{navigation::named, prelude::RootIndex};
- use super::*;
- #[test]
- fn href_internal() {
- let state = RouterState::<&str> {
- prefix: Some(String::from("/prefix")),
- ..Default::default()
- };
- assert_eq!(state.href(&"/test".into()), String::from("/prefix/test"))
- }
- #[test]
- fn href_named() {
- let state = RouterState::<&str> {
- name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
- prefix: Some(String::from("/prefix")),
- ..Default::default()
- };
- assert_eq!(state.href(&named::<RootIndex>()), String::from("/prefix/"))
- }
- #[test]
- #[should_panic = "named navigation to unknown name: bool"]
- #[cfg(debug_assertions)]
- fn href_named_debug() {
- let state = RouterState::<&str> {
- name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
- prefix: Some(String::from("/prefix")),
- ..Default::default()
- };
- state.href(&named::<bool>());
- }
- #[test]
- #[cfg(not(debug_assertions))]
- fn href_named_release() {
- let state = RouterState::<&str> {
- name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
- prefix: Some(String::from("/prefix")),
- ..Default::default()
- };
- assert_eq!(state.href(&named::<bool>()), String::new())
- }
- #[test]
- fn href_external() {
- let state = RouterState::<&str> {
- prefix: Some(String::from("/prefix")),
- ..Default::default()
- };
- assert_eq!(
- state.href(&"https://dioxuslabs.com/".into()),
- String::from("https://dioxuslabs.com/")
- )
- }
- #[test]
- fn is_at_internal_absolute() {
- let state = test_state();
- assert!(!state.is_at(&"/levels".into(), false));
- assert!(!state.is_at(&"/levels".into(), true));
- assert!(state.is_at(&"/test".into(), false));
- assert!(!state.is_at(&"/test".into(), true));
- assert!(state.is_at(&"/test/with/some/nested/levels".into(), false));
- assert!(state.is_at(&"/test/with/some/nested/levels".into(), true));
- }
- #[test]
- fn is_at_internal_relative() {
- let state = test_state();
- assert!(state.is_at(&"levels".into(), false));
- assert!(!state.is_at(&"levels".into(), true));
- assert!(!state.is_at(&"test".into(), false));
- assert!(!state.is_at(&"test".into(), true));
- assert!(!state.is_at(&"test/with/some/nested/levels".into(), false));
- assert!(!state.is_at(&"test/with/some/nested/levels".into(), true));
- }
- #[test]
- fn is_at_named() {
- let state = test_state();
- assert!(!state.is_at(&named::<RootIndex>(), false));
- assert!(!state.is_at(&named::<RootIndex>(), true));
- assert!(state.is_at(&named::<bool>(), false));
- assert!(state.is_at(&named::<bool>(), true));
- assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), false));
- assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), true));
- assert!(state.is_at(&named::<bool>().parameter::<i8>("test"), false));
- assert!(!state.is_at(&named::<bool>().parameter::<i8>("test"), true));
- }
- #[test]
- fn is_at_external() {
- let state = test_state();
- assert!(!state.is_at(&"https://dioxuslabs.com/".into(), false));
- assert!(!state.is_at(&"https://dioxuslabs.com/".into(), true));
- }
- fn test_state() -> RouterState<&'static str> {
- RouterState {
- path: String::from("/test/with/some/nested/levels"),
- names: {
- let mut r = HashSet::new();
- r.insert(Name::of::<bool>());
- r
- },
- parameters: {
- let mut r = HashMap::new();
- r.insert(Name::of::<bool>(), String::from("test"));
- r
- },
- ..Default::default()
- }
- }
- }
|