diff --git a/Cargo.toml b/Cargo.toml index d2fba562b..3fc6819fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "packages/dioxus", "packages/core", "packages/core-macro", - "packages/router-core", "packages/router-macro", "packages/router", "packages/html", diff --git a/packages/router-core/Cargo.toml b/packages/router-core/Cargo.toml deleted file mode 100644 index 59767e726..000000000 --- a/packages/router-core/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "dioxus-router-core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async-lock = "2.7.0" -either = "1.8.0" -futures-channel = "0.3.25" -futures-util = "0.3.25" -gloo = { version = "0.8.0", optional = true } -log = "0.4.17" -regex = { version = "1.6.0", optional = true } -serde = { version = "1.0.147", optional = true, features = ["derive"] } -serde-wasm-bindgen = { version = "0.4.5", optional = true } -serde_urlencoded = { version = "0.7.1", optional = true } -url = "2.3.1" -urlencoding = "2.1.2" -wasm-bindgen = { version = "0.2.83", optional = true } -web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"]} -dioxus = { path = "../dioxus" } -dioxus-router-macro = { path = "../router-macro" } - -[features] -regex = ["dep:regex"] -serde = ["dep:serde", "serde_urlencoded"] -web = ["gloo", "dep:serde", "serde-wasm-bindgen", "wasm-bindgen", "web-sys"] diff --git a/packages/router-core/README.md b/packages/router-core/README.md deleted file mode 100644 index a63cad00e..000000000 --- a/packages/router-core/README.md +++ /dev/null @@ -1 +0,0 @@ -# Dioxus Router Core diff --git a/packages/router-core/src/history/memory.rs b/packages/router-core/src/history/memory.rs deleted file mode 100644 index 0267e8fe8..000000000 --- a/packages/router-core/src/history/memory.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::str::FromStr; - -use url::{ParseError, Url}; - -use super::HistoryProvider; - -const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/"; - -/// A [`HistoryProvider`] that stores all navigation information in memory. -pub struct MemoryHistory { - current: Url, - history: Vec, - future: Vec, -} - -impl MemoryHistory { - /// Create a [`MemoryHistory`] starting at `path`. - /// - /// ```rust - /// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory}; - /// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap(); - /// assert_eq!(history.current_path(), "/some/path"); - /// assert_eq!(history.can_go_back(), false); - /// ``` - pub fn with_initial_path(path: impl Into) -> Result { - let mut path = path.into(); - if path.starts_with('/') { - path.remove(0); - } - let url = Url::parse(&format!("{INITIAL_URL}{path}"))?; - - Ok(Self { - current: url, - ..Default::default() - }) - } -} - -impl Default for MemoryHistory { - fn default() -> Self { - Self { - current: Url::from_str(INITIAL_URL).unwrap(), - history: Vec::new(), - future: Vec::new(), - } - } -} - -impl HistoryProvider for MemoryHistory { - fn current_path(&self) -> String { - self.current.path().to_string() - } - - fn current_query(&self) -> Option { - self.current.query().map(|q| q.to_string()) - } - - fn can_go_back(&self) -> bool { - !self.history.is_empty() - } - - fn go_back(&mut self) { - if let Some(last) = self.history.pop() { - self.future.push(self.current.to_string()); - self.current = Url::parse(&last).unwrap(/* past URLs are always valid */); - } - } - - fn can_go_forward(&self) -> bool { - !self.future.is_empty() - } - - fn go_forward(&mut self) { - if let Some(next) = self.future.pop() { - self.history.push(self.current.to_string()); - self.current = Url::parse(&next).unwrap(/* future URLs are always valid */); - } - } - - fn push(&mut self, path: String) { - let wrong = path.starts_with("//"); - debug_assert!( - !wrong, - "cannot navigate to paths starting with \"//\": {path}" - ); - if wrong { - return; - } - - if let Ok(new) = self.current.join(&path) { - self.history.push(self.current.to_string()); - self.current = new; - self.future.clear(); - } - } - - fn replace(&mut self, path: String) { - let wrong = path.starts_with("//"); - debug_assert!( - !wrong, - "cannot navigate to paths starting with \"//\": {path}" - ); - if wrong { - return; - } - - if let Ok(new) = self.current.join(&path) { - self.current = new; - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default() { - let mem = MemoryHistory::default(); - assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); - assert_eq!(mem.history, Vec::::new()); - assert_eq!(mem.future, Vec::::new()); - } - - #[test] - fn with_initial_path() { - let mem = MemoryHistory::with_initial_path("something").unwrap(); - assert_eq!( - mem.current, - Url::parse(&format!("{INITIAL_URL}something")).unwrap() - ); - assert_eq!(mem.history, Vec::::new()); - assert_eq!(mem.future, Vec::::new()); - } - - #[test] - fn with_initial_path_with_leading_slash() { - let mem = MemoryHistory::with_initial_path("/something").unwrap(); - assert_eq!( - mem.current, - Url::parse(&format!("{INITIAL_URL}something")).unwrap() - ); - assert_eq!(mem.history, Vec::::new()); - assert_eq!(mem.future, Vec::::new()); - } - - #[test] - fn can_go_back() { - let mut mem = MemoryHistory::default(); - assert!(!mem.can_go_back()); - - mem.push(String::from("/test")); - assert!(mem.can_go_back()); - } - - #[test] - fn go_back() { - let mut mem = MemoryHistory::default(); - mem.push(String::from("/test")); - mem.go_back(); - - assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); - assert!(mem.history.is_empty()); - assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]); - } - - #[test] - fn can_go_forward() { - let mut mem = MemoryHistory::default(); - assert!(!mem.can_go_forward()); - - mem.push(String::from("/test")); - mem.go_back(); - - assert!(mem.can_go_forward()); - } - - #[test] - fn go_forward() { - let mut mem = MemoryHistory::default(); - mem.push(String::from("/test")); - mem.go_back(); - mem.go_forward(); - - assert_eq!( - mem.current, - Url::parse(&format!("{INITIAL_URL}test")).unwrap() - ); - assert_eq!(mem.history, vec![INITIAL_URL.to_string()]); - assert!(mem.future.is_empty()); - } - - #[test] - fn push() { - let mut mem = MemoryHistory::default(); - mem.push(String::from("/test")); - - assert_eq!( - mem.current, - Url::parse(&format!("{INITIAL_URL}test")).unwrap() - ); - assert_eq!(mem.history, vec![INITIAL_URL.to_string()]); - assert!(mem.future.is_empty()); - } - - #[test] - #[should_panic = r#"cannot navigate to paths starting with "//": //test"#] - #[cfg(debug_assertions)] - fn push_debug() { - let mut mem = MemoryHistory::default(); - mem.push(String::from("//test")); - } - - #[test] - #[cfg(not(debug_assertions))] - fn push_release() { - let mut mem = MemoryHistory::default(); - mem.push(String::from("//test")); - - assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); - assert!(mem.history.is_empty()) - } - - #[test] - fn replace() { - let mut mem = MemoryHistory::default(); - mem.push(String::from("/test")); - mem.push(String::from("/other")); - mem.go_back(); - mem.replace(String::from("/replace")); - - assert_eq!( - mem.current, - Url::parse(&format!("{INITIAL_URL}replace")).unwrap() - ); - assert_eq!(mem.history, vec![INITIAL_URL.to_string()]); - assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]); - } - - #[test] - #[should_panic = r#"cannot navigate to paths starting with "//": //test"#] - #[cfg(debug_assertions)] - fn replace_debug() { - let mut mem = MemoryHistory::default(); - mem.replace(String::from("//test")); - } - - #[test] - #[cfg(not(debug_assertions))] - fn replace_release() { - let mut mem = MemoryHistory::default(); - mem.replace(String::from("//test")); - - assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); - } -} diff --git a/packages/router-core/src/lib.rs b/packages/router-core/src/lib.rs deleted file mode 100644 index 94c1736fb..000000000 --- a/packages/router-core/src/lib.rs +++ /dev/null @@ -1,49 +0,0 @@ -#![doc = include_str!("../README.md")] -// #![forbid(missing_docs)] - -pub mod history; - -pub mod router; -pub use router::*; - -// pub mod navigation; - -// mod navigator; -// pub use navigator::*; - -// mod service; -// pub use service::*; - -// mod state; -// pub use state::*; - -// mod utils { -// mod sitemap; -// pub use sitemap::*; -// } - -/// A collection of useful types most applications might need. -pub mod prelude { - // pub use crate::navigation::*; - - /// An external navigation failure. - /// - /// These occur when the router tries to navigate to a [`NavigationTarget::External`] and the - /// [`HistoryProvider`](crate::history::HistoryProvider) doesn't support that. - pub struct FailureExternalNavigation; - - /// A named navigation failure. - /// - /// These occur when the router tries to navigate to a [`NavigationTarget::Named`] and the - /// specified [`Name`](crate::Name) is unknown, or a parameter is missing. - pub struct FailureNamedNavigation; - - /// A redirection limit breach. - /// - /// These occur when the router tries to navigate to any target, but encounters 25 consecutive - /// redirects. - pub struct FailureRedirectionLimit; - - /// The [`Name`](crate::Name) equivalent of `/`. - pub struct RootIndex; -} diff --git a/packages/router-core/src/navigation.rs b/packages/router-core/src/navigation.rs deleted file mode 100644 index b989dc433..000000000 --- a/packages/router-core/src/navigation.rs +++ /dev/null @@ -1,448 +0,0 @@ -//! Types pertaining to navigation. - -use std::{collections::HashMap, str::FromStr}; - -use url::{ParseError, Url}; - -/// A target for the router to navigate to. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum NavigationTarget { - /// An internal path that the router can navigate to by itself. - /// - /// ```rust - /// # use dioxus_router_core::navigation::NavigationTarget; - /// let explicit = NavigationTarget::Internal(String::from("/internal")); - /// let implicit: NavigationTarget = "/internal".into(); - /// assert_eq!(explicit, implicit); - /// ``` - Internal(R), - /// An external target that the router doesn't control. - /// - /// ```rust - /// # use dioxus_router_core::navigation::NavigationTarget; - /// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/")); - /// let implicit: NavigationTarget = "https://dioxuslabs.com/".into(); - /// assert_eq!(explicit, implicit); - /// ``` - External(String), -} - -impl NavigationTarget { - /// Create a new [`NavigationTarget::Named`] with `N` as the name. - /// - /// ```rust - /// # use dioxus_router_core::navigation::NavigationTarget; - /// let target = NavigationTarget::named::(); - /// ``` - /// - /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only - /// for brevity. It is recommend to use types with descriptive names, and create unit structs if - /// needed. - pub fn named() -> Self { - Self::Named { - name: Name::of::(), - parameters: HashMap::new(), - query: None, - } - } - - /// Add a parameter to a [`NavigationTarget::Named`]. - /// - /// ```rust - /// # use dioxus_router_core::navigation::NavigationTarget; - /// let target = NavigationTarget::named::().parameter::("some parameter"); - /// ``` - /// - /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only - /// for brevity. It is recommend to use types with descriptive names, and create unit structs if - /// needed. - /// - /// # Error Handling - /// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than - /// [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_ - /// nothing will happen. - /// 2. Parameters need to be unique within the [`NavigationTarget`]. In _debug mode_ the - /// second call with a duplicate name will panic. In _release mode_, all calls after the - /// first will be ignored. - pub fn parameter(mut self, value: impl Into) -> Self { - let n = Name::of::(); - - if let Self::Named { - name, - parameters, - query: _, - } = &mut self - { - debug_assert!( - !parameters.contains_key(&n), - "duplicate parameter: {name} - {n}", - ); - parameters.entry(n).or_insert_with(|| value.into()); - } else { - #[cfg(debug_assertions)] - panic!("parameter only available for named target: {n}"); - } - - self - } - - /// Add a parameter to a [`NavigationTarget::Named`]. - /// - /// ```rust - /// # use dioxus_router_core::navigation::NavigationTarget; - /// let target = NavigationTarget::named::().query("some=query"); - /// ``` - /// - /// # Error Handling - /// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than - /// [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_ - /// nothing will happen. - /// 2. This function may only be called once per [`NavigationTarget`]. In _debug mode_, the - /// second call will panic. In _release mode_, all calls after the first will be ignored. - pub fn query(mut self, query: impl Into) -> Self { - if let Self::Named { - name, - parameters: _, - query: q, - } = &mut self - { - debug_assert!(q.is_none(), "query cannot be changed: {name}",); - q.get_or_insert(query.into()); - } else { - #[cfg(debug_assertions)] - panic!("query only available for named target",); - } - - self - } -} - -/// Create a new [`NavigationTarget::Named`] with `N` as the name. -/// -/// ```rust -/// # use dioxus_router_core::navigation::named; -/// let target = named::(); -/// ``` -/// -/// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only -/// for brevity. It is recommend to use types with descriptive names, and create unit structs if -/// needed. -/// -/// This is a shortcut for [`NavigationTarget`]s `named` function. -pub fn named() -> NavigationTarget { - NavigationTarget::named::() -} - -impl FromStr for NavigationTarget { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match Url::parse(s) { - Ok(_) => Ok(Self::External(s.to_string())), - Err(ParseError::RelativeUrlWithoutBase) => Ok(Self::Internal(s.to_string())), - Err(e) => Err(e), - } - } -} - -impl> From for NavigationTarget { - fn from(v: T) -> Self { - let v = v.into(); - v.clone().parse().unwrap_or(Self::Internal(v)) - } -} - -/// A representation of a query string. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Query { - /// A basic query string. - /// - /// ```rust - /// # use dioxus_router_core::navigation::Query; - /// let explicit = Query::Single(String::from("some=query")); - /// let implicit: Query = "some=query".into(); - /// assert_eq!(explicit, implicit); - /// ``` - Single(String), - /// A query separated into key-value-pairs. - /// - /// ```rust - /// # use dioxus_router_core::navigation::Query; - /// let explicit = Query::List(vec![(String::from("some"), String::from("query"))]); - /// let implicit: Query = vec![("some", "query")].into(); - /// assert_eq!(explicit, implicit); - /// ``` - #[cfg(feature = "serde")] - List(Vec<(String, String)>), -} - -impl Query { - /// Create a [`Query`] from a [`Serialize`](serde::Serialize)able object. - #[cfg(feature = "serde")] - pub fn from_serde(query: impl serde::Serialize) -> Result { - serde_urlencoded::to_string(query).map(|q| Self::Single(q)) - } -} - -impl From for Query { - fn from(v: String) -> Self { - Self::Single(v) - } -} - -impl From<&str> for Query { - fn from(v: &str) -> Self { - v.to_string().into() - } -} - -#[cfg(feature = "serde")] -impl> From> for Query { - fn from(v: Vec<(T, T)>) -> Self { - Self::List(v.into_iter().map(|(a, b)| (a.into(), b.into())).collect()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn named_enum() { - assert_eq!( - NavigationTarget::named::(), - NavigationTarget::Named { - name: Name::of::(), - parameters: HashMap::new(), - query: None - } - ) - } - - #[test] - fn named_func() { - assert_eq!( - named::(), - NavigationTarget::Named { - name: Name::of::(), - parameters: HashMap::new(), - query: None - } - ) - } - - #[test] - fn parameter() { - assert_eq!( - named::() - .parameter::("integer") - .parameter::("unsigned"), - NavigationTarget::Named { - name: Name::of::(), - parameters: { - let mut r = HashMap::new(); - r.insert(Name::of::(), "integer".to_string()); - r.insert(Name::of::(), "unsigned".to_string()); - r - }, - query: None - } - ) - } - - #[test] - #[should_panic = "duplicate parameter: bool - i32"] - #[cfg(debug_assertions)] - fn parameter_duplicate_debug() { - named::() - .parameter::("integer") - .parameter::("duplicate"); - } - - #[test] - #[cfg(not(debug_assertions))] - fn parameter_duplicate_release() { - assert_eq!( - named::() - .parameter::("integer") - .parameter::("duplicate"), - NavigationTarget::Named { - name: Name::of::(), - parameters: { - let mut r = HashMap::new(); - r.insert(Name::of::(), "integer".to_string()); - r - }, - query: None - } - ); - } - - #[test] - #[should_panic = "parameter only available for named target: i32"] - #[cfg(debug_assertions)] - fn parameter_internal_debug() { - NavigationTarget::from("/test").parameter::("integer"); - } - - #[test] - #[cfg(not(debug_assertions))] - fn parameter_internal_release() { - assert_eq!( - NavigationTarget::from("/test").parameter::("integer"), - NavigationTarget::from("/test") - ); - } - - #[test] - #[should_panic = "parameter only available for named target: i32"] - #[cfg(debug_assertions)] - fn parameter_external_debug() { - NavigationTarget::from("https://dioxuslabs.com/").parameter::("integer"); - } - - #[test] - #[cfg(not(debug_assertions))] - fn parameter_external_release() { - assert_eq!( - NavigationTarget::from("https://dioxuslabs.com/").parameter::("integer"), - NavigationTarget::from("https://dioxuslabs.com/") - ); - } - - #[test] - fn query() { - assert_eq!( - named::().query("test"), - NavigationTarget::Named { - name: Name::of::(), - parameters: HashMap::new(), - query: Some(Query::Single("test".to_string())) - } - ) - } - - #[test] - #[should_panic = "query cannot be changed: bool"] - #[cfg(debug_assertions)] - fn query_multiple_debug() { - named::().query("test").query("other"); - } - - #[test] - #[cfg(not(debug_assertions))] - fn query_multiple_release() { - assert_eq!( - named::().query("test").query("other"), - NavigationTarget::Named { - name: Name::of::(), - parameters: HashMap::new(), - query: Some(Query::Single("test".to_string())) - } - ) - } - - #[test] - #[should_panic = "query only available for named target"] - #[cfg(debug_assertions)] - fn query_internal_debug() { - NavigationTarget::from("/test").query("test"); - } - - #[test] - #[cfg(not(debug_assertions))] - fn query_internal_release() { - assert_eq!( - NavigationTarget::from("/test").query("test"), - NavigationTarget::from("/test") - ); - } - - #[test] - #[should_panic = "query only available for named target"] - #[cfg(debug_assertions)] - fn query_external_debug() { - NavigationTarget::from("https://dioxuslabs.com/").query("test"); - } - - #[test] - #[cfg(not(debug_assertions))] - fn query_external_release() { - assert_eq!( - NavigationTarget::from("https://dioxuslabs.com/").query("test"), - NavigationTarget::from("https://dioxuslabs.com/") - ); - } - - #[test] - fn target_parse_internal() { - assert_eq!( - "/test".parse::(), - Ok(NavigationTarget::Internal("/test".to_string())) - ); - } - - #[test] - fn target_parse_external() { - assert_eq!( - "https://dioxuslabs.com/".parse::(), - Ok(NavigationTarget::External( - "https://dioxuslabs.com/".to_string() - )) - ) - } - - #[test] - fn target_from_str_internal() { - assert_eq!( - NavigationTarget::from("/test"), - NavigationTarget::Internal("/test".to_string()) - ); - } - - #[test] - fn target_from_str_external() { - assert_eq!( - NavigationTarget::from("https://dioxuslabs.com/"), - NavigationTarget::External("https://dioxuslabs.com/".to_string()) - ) - } - - #[test] - fn target_from_string_internal() { - assert_eq!( - NavigationTarget::from("/test".to_string()), - NavigationTarget::Internal("/test".to_string()) - ); - } - - #[test] - fn target_from_string_external() { - assert_eq!( - NavigationTarget::from("https://dioxuslabs.com/".to_string()), - NavigationTarget::External("https://dioxuslabs.com/".to_string()) - ) - } - - #[test] - fn query_from_string() { - assert_eq!( - Query::from("test".to_string()), - Query::Single("test".to_string()) - ) - } - - #[test] - fn query_from_str() { - assert_eq!(Query::from("test"), Query::Single("test".to_string())); - } - - #[test] - #[cfg(feature = "serde")] - fn query_form_vec() { - assert_eq!( - Query::from(vec![("test", "1234")]), - Query::List(vec![("test".to_string(), "1234".to_string())]) - ) - } -} diff --git a/packages/router-core/src/navigator.rs b/packages/router-core/src/navigator.rs deleted file mode 100644 index 0d6e94e94..000000000 --- a/packages/router-core/src/navigator.rs +++ /dev/null @@ -1,97 +0,0 @@ -use futures_channel::mpsc::UnboundedSender; - -use crate::{navigation::NavigationTarget, RouterMessage}; - -/// A [`Navigator`] allowing for programmatic navigation. -/// -/// The [`Navigator`] is not guaranteed to be able to trigger a navigation. When and if a navigation -/// is actually handled depends on the UI library. -pub struct Navigator { - sender: UnboundedSender>, -} - -impl Navigator { - /// Go back to the previous location. - /// - /// Will fail silently if there is no previous location to go to. - pub fn go_back(&self) { - let _ = self.sender.unbounded_send(RouterMessage::GoBack); - } - - /// Go back to the next location. - /// - /// Will fail silently if there is no next location to go to. - pub fn go_forward(&self) { - let _ = self.sender.unbounded_send(RouterMessage::GoForward); - } - - /// Push a new location. - /// - /// The previous location will be available to go back to. - pub fn push(&self, target: impl Into) { - let _ = self - .sender - .unbounded_send(RouterMessage::Push(target.into())); - } - - /// Replace the current location. - /// - /// The previous location will **not** be available to go back to. - pub fn replace(&self, target: impl Into) { - let _ = self - .sender - .unbounded_send(RouterMessage::Replace(target.into())); - } -} - -impl From>> for Navigator { - fn from(sender: UnboundedSender>) -> Self { - Self { sender } - } -} - -#[cfg(test)] -mod tests { - use futures_channel::mpsc::{unbounded, UnboundedReceiver}; - - use super::*; - - fn prepare() -> (Navigator<()>, UnboundedReceiver>) { - let (sender, receiver) = unbounded(); - (Navigator::from(sender), receiver) - } - - #[test] - fn go_back() { - let (n, mut s) = prepare(); - n.go_back(); - - assert_eq!(s.try_next().unwrap(), Some(RouterMessage::GoBack)); - } - - #[test] - fn go_forward() { - let (n, mut s) = prepare(); - n.go_forward(); - - assert_eq!(s.try_next().unwrap(), Some(RouterMessage::GoForward)); - } - - #[test] - fn push() { - let (n, mut s) = prepare(); - let target = NavigationTarget::from("https://dioxuslabs.com/"); - n.push(target.clone()); - - assert_eq!(s.try_next().unwrap(), Some(RouterMessage::Push(target))); - } - - #[test] - fn replace() { - let (n, mut s) = prepare(); - let target = NavigationTarget::from("https://dioxuslabs.com/"); - n.replace(target.clone()); - - assert_eq!(s.try_next().unwrap(), Some(RouterMessage::Replace(target))); - } -} diff --git a/packages/router-core/src/service.rs b/packages/router-core/src/service.rs deleted file mode 100644 index 9b3fcafb5..000000000 --- a/packages/router-core/src/service.rs +++ /dev/null @@ -1,1081 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - fmt::Debug, - sync::{Arc, Weak}, -}; - -use async_lock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use either::Either; -use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use futures_util::StreamExt; - -use crate::{ - history::HistoryProvider, - navigation::NavigationTarget, - prelude::{ - FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex, - }, - RouterState, -}; - -/// Messages that the [`RouterService`] can handle. -pub enum RouterMessage { - /// Subscribe to router update. - Subscribe(Arc), - /// Navigate to the specified target. - Push(NavigationTarget), - /// Replace the current location with the specified target. - Replace(NavigationTarget), - /// Trigger a routing update. - Update, - /// Navigate to the previous history entry. - GoBack, - /// Navigate to the next history entry. - GoForward, -} - -impl Debug for RouterMessage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Subscribe(arg0) => f.debug_tuple("Subscribe").field(arg0).finish(), - Self::Push(arg0) => f.debug_tuple("Push").field(arg0).finish(), - Self::Replace(arg0) => f.debug_tuple("Replace").field(arg0).finish(), - Self::Update => write!(f, "Update"), - Self::GoBack => write!(f, "GoBack"), - Self::GoForward => write!(f, "GoForward"), - } - } -} - -impl PartialEq for RouterMessage { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Subscribe(l0), Self::Subscribe(r0)) => l0 == r0, - (Self::Push(l0), Self::Push(r0)) => l0 == r0, - (Self::Replace(l0), Self::Replace(r0)) => l0 == r0, - _ => core::mem::discriminant(self) == core::mem::discriminant(other), - } - } -} - -impl Eq for RouterMessage {} - -enum NavigationFailure { - External(String), - Internal(::Err), -} - -/// A function the router will call after every routing update. -pub type RoutingCallback = Arc) -> Option>; - -/// A collection of router data that manages all routing functionality. -pub struct RouterService -where - T: Clone, - I: Clone + PartialEq + Eq, -{ - history: Box, - routes: Segment, - names: Arc, - - receiver: UnboundedReceiver>, - state: Arc>>, - - subscribers: Vec>, - subscriber_update: Arc, - routing_callback: Option>, - - failure_external_navigation: ContentAtom, - failure_named_navigation: ContentAtom, - failure_redirection_limit: ContentAtom, -} - -impl RouterService -where - T: Clone, - I: Clone + PartialEq + Eq + Send + Sync + 'static, -{ - /// Create a new [`RouterService`]. - /// - /// # Parameters - /// 1. `routes`: The root [`Segment`] the router should handle. - /// 2. `history`: A [`HistoryProvider`] to handle the navigation history. - /// 3. `subscriber_callback`: A function the rooter can call to update UI integrations. - /// 4. `failure_external_navigation`: Content to be displayed when an external navigation fails. - /// 5. `failure_named_navigation`: Content to be displayed when a named navigation fails. - /// 6. `failure_redirection_limit`: Content to be displayed when the redirection limit is - /// breached. - /// - /// # Returns - /// 1. The [`RouterService`]. - /// 2. An [`UnboundedSender`] to send [`RouterMessage`]s to the [`RouterService`]. - /// 3. Access to the [`RouterState`]. **DO NOT WRITE TO THIS!!!** Seriously, **READ ONLY!!!** - #[allow(clippy::type_complexity)] - pub fn new( - routes: Segment, - mut history: Box, - subscriber_update: Arc, - routing_callback: Option>, - failure_external_navigation: ContentAtom, - failure_named_navigation: ContentAtom, - failure_redirection_limit: ContentAtom, - ) -> ( - Self, - UnboundedSender>, - Arc>>, - ) { - // index names - let names = Arc::new(NamedSegment::from_segment(&routes)); - - // create channel - let (sender, receiver) = unbounded(); - - // initialize history - let history_sender = sender.clone(); - history.updater(Arc::new(move || { - let _ = history_sender.unbounded_send(RouterMessage::Update); - })); - let state = Arc::new(RwLock::new(RouterState { - name_map: Arc::clone(&names), - ..Default::default() - })); - - ( - Self { - history, - names, - routes, - receiver, - state: Arc::clone(&state), - subscribers: Vec::new(), - subscriber_update, - routing_callback, - failure_external_navigation, - failure_named_navigation, - failure_redirection_limit, - }, - sender, - state, - ) - } - - /// Perform the initial routing. - /// - /// Call this once, as soon as possible after creating the [`RouterService`]. Do not call this, - /// if you are going to call the `run` function. - pub fn init(&mut self) { - *self.sync_state_write_lock() = self - .update_routing() - .left_and_then(|state| { - if let Some(cb) = &self.routing_callback { - if let Some(nt) = cb(&state) { - self.replace(nt); - return self.update_routing(); - } - } - - Either::Left(state) - }) - .map_right(|err| self.handle_navigation_failure(&self.sync_state_read_lock(), err)) - .either_into(); - } - - /// Handle all messages the router has received and then return. - /// - /// Call `init` before calling this function. - pub fn run_current(&mut self) { - let mut state = None; - while let Ok(Some(msg)) = self.receiver.try_next() { - let current = match self.handle_message(msg) { - (_, Some(err)) => Either::Right(err), - (true, _) => self.update_routing(), - _ => continue, - } - .left_and_then(|state| { - if let Some(cb) = &self.routing_callback { - if let Some(nt) = cb(&state) { - self.replace(nt); - return self.update_routing(); - } - } - - Either::Left(state) - }) - .map_right(|err| self.handle_navigation_failure(&self.sync_state_read_lock(), err)) - .either_into(); - state = Some(current); - } - - if let Some(state) = state { - *self.sync_state_write_lock() = state; - } - - self.update_subscribers(); - } - - fn sync_state_read_lock(&self) -> RwLockReadGuard> { - loop { - if let Some(s) = self.state.try_read() { - return s; - } - } - } - - fn sync_state_write_lock(&mut self) -> RwLockWriteGuard> { - loop { - if let Some(s) = self.state.try_write() { - return s; - } - } - } - - /// Handle all routing messages until ended from the outside. - pub async fn run(&mut self) { - // init (unlike function with same name this is async) - { - *self.state.write().await = match self.update_routing().left_and_then(|state| { - if let Some(cb) = &self.routing_callback { - if let Some(nt) = cb(&state) { - self.replace(nt); - return self.update_routing(); - } - } - - Either::Left(state) - }) { - Either::Left(state) => state, - Either::Right(err) => { - self.handle_navigation_failure(&*self.state.read().await, err) - } - }; - } - self.update_subscribers(); - - while let Some(msg) = self.receiver.next().await { - let state = match self.handle_message(msg) { - (_, Some(err)) => Either::Right(err), - (true, _) => self.update_routing(), - _ => continue, - } - .left_and_then(|state| { - if let Some(cb) = &self.routing_callback { - if let Some(nt) = cb(&state) { - self.replace(nt); - return self.update_routing(); - } - } - - Either::Left(state) - }); - - *self.state.write().await = match state { - Either::Left(state) => state, - Either::Right(err) => { - self.handle_navigation_failure(&*self.state.read().await, err) - } - }; - - self.update_subscribers(); - } - } - - fn handle_navigation_failure( - &self, - state: &RouterState, - err: NavigationFailure, - ) -> RouterState { - match err { - NavigationFailure::External(url) => RouterState { - can_go_back: state.can_go_back, - can_go_forward: state.can_go_forward, - path: state.path.clone(), - query: state.query.clone(), - prefix: state.prefix.clone(), - names: { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }, - parameters: { - let mut r = HashMap::new(); - r.insert(Name::of::(), url); - r - }, - name_map: Arc::clone(&state.name_map), - content: vec![self.failure_external_navigation.clone()], - named_content: BTreeMap::new(), - }, - NavigationFailure::Named(n) => RouterState { - can_go_back: state.can_go_back, - can_go_forward: state.can_go_forward, - path: state.path.clone(), - query: state.query.clone(), - prefix: state.prefix.clone(), - names: { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }, - parameters: { - let mut r = HashMap::new(); - r.insert(Name::of::(), n.to_string()); - r - }, - name_map: Arc::clone(&state.name_map), - content: vec![self.failure_named_navigation.clone()], - named_content: BTreeMap::new(), - }, - } - } - - #[must_use] - fn handle_message(&mut self, msg: RouterMessage) -> (bool, Option) { - let failure = match msg { - RouterMessage::Subscribe(id) => { - self.subscribe(id); - return (false, None); - } - RouterMessage::Push(nt) => self.push(nt), - RouterMessage::Replace(nt) => self.replace(nt), - RouterMessage::Update => None, - RouterMessage::GoBack => { - self.history.go_back(); - None - } - RouterMessage::GoForward => { - self.history.go_forward(); - None - } - }; - - (true, failure) - } - - #[must_use] - fn update_routing(&mut self) -> Either, NavigationFailure> { - for _ in 0..=25 { - match self.update_routing_inner() { - Either::Left(state) => return Either::Left(state), - Either::Right(nt) => { - if let Some(err) = self.replace(nt) { - return Either::Right(err); - } - } - } - } - - #[cfg(debug_assertions)] - panic!("reached redirect limit of 25"); - #[allow(unreachable_code)] - Either::Left(RouterState { - content: vec![self.failure_redirection_limit.clone()], - can_go_back: self.history.can_go_back(), - can_go_forward: self.history.can_go_forward(), - path: self.history.current_path(), - query: self.history.current_query(), - prefix: self.history.current_prefix(), - name_map: Arc::clone(&self.names), - names: { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }, - ..Default::default() - }) - } - - #[must_use] - fn update_routing_inner(&mut self) -> Either, NavigationTarget> { - // prepare path - let mut path = self.history.current_path(); - path.remove(0); - if path.ends_with('/') { - path.pop(); - } - - let values = match path.is_empty() { - false => path.split('/').collect::>(), - true => Vec::new(), - }; - - // add root index name - let mut names = HashSet::new(); - if values.is_empty() { - names.insert(Name::of::()); - }; - - route_segment( - &self.routes, - &values, - RouterState { - can_go_back: self.history.can_go_back(), - can_go_forward: self.history.can_go_forward(), - path: self.history.current_path(), - query: self.history.current_query(), - prefix: self.history.current_prefix(), - name_map: Arc::clone(&self.names), - names, - ..Default::default() - }, - ) - } - - fn push(&mut self, target: NavigationTarget) -> Option { - match resolve_target(&self.names, &target) { - Either::Left(Either::Left(p)) => self.history.push(p), - Either::Left(Either::Right(n)) => return Some(NavigationFailure::Named(n)), - Either::Right(e) => return self.external(e), - } - - None - } - - fn replace(&mut self, target: NavigationTarget) -> Option { - match resolve_target(&self.names, &target) { - Either::Left(Either::Left(p)) => self.history.replace(p), - Either::Left(Either::Right(n)) => return Some(NavigationFailure::Named(n)), - Either::Right(e) => return self.external(e), - } - - None - } - - fn external(&mut self, external: String) -> Option { - match self.history.external(external.clone()) { - true => None, - false => Some(NavigationFailure::External(external)), - } - } - - fn subscribe(&mut self, id: Arc) { - self.subscribers.push(Arc::downgrade(&id)); - (self.subscriber_update)(id.as_ref().clone()); - } - - fn update_subscribers(&mut self) { - let mut previous = Vec::new(); - self.subscribers.retain(|id| { - if let Some(id) = id.upgrade() { - if previous.contains(&id) { - false - } else { - (self.subscriber_update)(id.as_ref().clone()); - previous.push(id); - true - } - } else { - false - } - }); - } -} - -#[cfg(test)] -mod tests { - //! The tests for [`RouterService`] test various functions that are not exposed as public. - //! However, several of those have an observable effect on the behavior of exposed functions. - //! - //! The alternative would be to send messages via the services channel and calling one of the - //! `run` functions. However, for readability and clarity, it was chosen to directly call the - //! private functions. - - use std::sync::Mutex; - - use crate::{ - history::MemoryHistory, - routes::{ParameterRoute, Route, RouteContent}, - }; - - use super::*; - - fn test_segment() -> Segment<&'static str> { - Segment::content(RouteContent::Content(ContentAtom("index"))) - .fixed( - "fixed", - Route::content(RouteContent::Content(ContentAtom("fixed"))).name::(), - ) - .fixed( - "redirect", - Route::content(RouteContent::Redirect(NavigationTarget::Internal( - String::from("fixed"), - ))), - ) - .fixed( - "redirection-loop", - Route::content(RouteContent::Redirect(NavigationTarget::Internal( - String::from("/redirection-loop"), - ))), - ) - .fixed( - "%F0%9F%8E%BA", - Route::content(RouteContent::Content(ContentAtom("🎺"))), - ) - .catch_all(ParameterRoute::empty::()) - } - - #[test] - fn new_provides_update_to_history() { - struct TestHistory {} - - impl HistoryProvider for TestHistory { - fn current_path(&self) -> String { - todo!() - } - - fn current_query(&self) -> Option { - todo!() - } - - fn go_back(&mut self) { - todo!() - } - - fn go_forward(&mut self) { - todo!() - } - - fn push(&mut self, _path: String) { - todo!() - } - - fn replace(&mut self, _path: String) { - todo!() - } - - fn updater(&mut self, callback: Arc) { - callback(); - } - } - - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(TestHistory {}), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - - assert!(matches!( - s.receiver.try_next().unwrap().unwrap(), - RouterMessage::Update - )); - } - - #[test] - fn update_routing() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - assert_eq!(s.names, s.state.try_read().unwrap().name_map); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("fixed")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/fixed")); - assert_eq!(state.query, Some(String::from("test=value"))); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - assert_eq!(s.names, state.name_map); - } - - #[test] - fn update_routing_root_index() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::::default(), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("index")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/")); - assert!(state.query.is_none()); - assert!(state.prefix.is_none()); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn update_routing_redirect() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("fixed")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/fixed")); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - #[should_panic = "reached redirect limit of 25"] - #[cfg(debug_assertions)] - fn update_routing_redirect_debug() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - } - - #[test] - #[cfg(not(debug_assertions))] - fn update_routing_redirect_release() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("redirect limit")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/redirection-loop")); - assert_eq!(state.can_go_back, false); - assert_eq!(state.can_go_forward, false); - } - - #[test] - fn update_subscribers() { - let ids = Arc::new(Mutex::new(Vec::new())); - let ids2 = Arc::clone(&ids); - - let (mut s, _, _) = RouterService::<_, u8>::new( - Segment::empty(), - Box::::default(), - Arc::new(move |id| { - ids2.lock().unwrap().push(id); - }), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - - let id0 = Arc::new(0); - s.subscribe(Arc::clone(&id0)); - - let id1 = Arc::new(1); - s.subscribe(Arc::clone(&id1)); - - let id1 = Arc::try_unwrap(id1).unwrap(); - s.update_subscribers(); - - assert_eq!(s.subscribers.len(), 1); - assert_eq!(s.subscribers[0].upgrade().unwrap(), id0); - assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]); - } - - #[test] - fn push_internal() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::::default(), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.push(NavigationTarget::Internal(String::from("/fixed"))); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("fixed")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/fixed")); - assert!(state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn push_named() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::::default(), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.push(NavigationTarget::named::()); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("fixed")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/fixed")); - assert!(state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn push_external() { - let (mut s, tx, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - tx.unbounded_send(RouterMessage::Push(NavigationTarget::External( - String::from("https://dioxuslabs.com/"), - ))) - .unwrap(); - s.run_current(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("external target")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert( - Name::of::(), - String::from("https://dioxuslabs.com/"), - ); - r - }); - assert_eq!(state.path, String::from("/fixed")); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn replace_named() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::::default(), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.replace(NavigationTarget::named::()); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("fixed")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/fixed")); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn replace_internal() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::::default(), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.replace(NavigationTarget::Internal(String::from("/fixed"))); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("fixed")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/fixed")); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn replace_external() { - let (mut s, tx, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External( - String::from("https://dioxuslabs.com/"), - ))) - .unwrap(); - s.run_current(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("external target")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert( - Name::of::(), - String::from("https://dioxuslabs.com/"), - ); - r - }); - assert_eq!(state.path, String::from("/fixed")); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn subscribe() { - let (mut s, _, _) = RouterService::<_, u8>::new( - Segment::empty(), - Box::::default(), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - - let id = Arc::new(0); - s.subscribe(Arc::clone(&id)); - - assert_eq!(s.subscribers.len(), 1); - assert_eq!(s.subscribers[0].upgrade().unwrap(), id); - } - - #[test] - fn routing_callback() { - let paths = Arc::new(Mutex::new(Vec::new())); - let paths2 = Arc::clone(&paths); - - let (mut s, c, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()), - Arc::new(|_| {}), - Some(Arc::new(move |state| { - paths2.lock().unwrap().push(state.path.clone()); - Some("/%F0%9F%8E%BA".into()) - })), - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - - assert!(paths.lock().unwrap().is_empty()); - - s.init(); - assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]); - - c.unbounded_send(RouterMessage::Update).unwrap(); - s.run_current(); - assert_eq!( - *paths.lock().unwrap(), - vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")] - ); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("🎺")]) - } - - #[test] - fn url_decoding_do() { - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - - let state = s.state.try_read().unwrap(); - assert!(state.content.is_empty()); - assert!(state.names.is_empty()); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("🥳")); - r - }); - assert_eq!(state.path, String::from("/%F0%9F%A5%B3")); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn url_decoding_do_not() { - let (mut s, c, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - c.unbounded_send(RouterMessage::Update).unwrap(); - s.run_current(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("🎺")]); - assert!(state.names.is_empty()); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/%F0%9F%8E%BA")); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } - - #[test] - fn prefix() { - struct TestHistory {} - - impl HistoryProvider for TestHistory { - fn current_path(&self) -> String { - String::from("/") - } - - fn current_query(&self) -> Option { - None - } - - fn current_prefix(&self) -> Option { - Some(String::from("/prefix")) - } - - fn can_go_back(&self) -> bool { - false - } - - fn can_go_forward(&self) -> bool { - false - } - - fn go_back(&mut self) { - todo!() - } - - fn go_forward(&mut self) { - todo!() - } - - fn push(&mut self, _path: String) { - todo!() - } - - fn replace(&mut self, _path: String) { - todo!() - } - - fn updater(&mut self, callback: Arc) { - callback(); - } - } - - let (mut s, _, _) = RouterService::<_, u8>::new( - test_segment(), - Box::new(TestHistory {}), - Arc::new(|_| {}), - None, - ContentAtom("external target"), - ContentAtom("named target"), - ContentAtom("redirect limit"), - ); - s.init(); - - let state = s.state.try_read().unwrap(); - assert_eq!(state.content, vec![ContentAtom("index")]); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/")); - assert!(state.query.is_none()); - assert_eq!(state.prefix, Some(String::from("/prefix"))); - assert!(!state.can_go_back); - assert!(!state.can_go_forward); - } -} diff --git a/packages/router-core/src/state.rs b/packages/router-core/src/state.rs deleted file mode 100644 index 51020b54f..000000000 --- a/packages/router-core/src/state.rs +++ /dev/null @@ -1,288 +0,0 @@ -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 { - /// 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, - /// The current prefix. - pub prefix: Option, - - /// The names of currently active routes. - pub names: HashSet, - /// The current path parameters. - pub parameters: HashMap, - pub(crate) name_map: Arc, - - /// The current main content. - /// - /// This should only be used by UI integration crates, and not by applications. - pub content: Vec>, - /// The current named content. - /// - /// This should only be used by UI integration crates, and not by applications. - pub named_content: BTreeMap>>, -} - -impl RouterState { - /// Get a parameter. - /// - /// ```rust - /// # use dioxus_router_core::{RouterState, Name}; - /// let mut state = RouterState::<&'static str>::default(); - /// assert_eq!(state.parameter::(), None); - /// - /// // Do not do this! For illustrative purposes only! - /// state.parameters.insert(Name::of::(), String::from("some parameter")); - /// assert_eq!(state.parameter::(), Some("some parameter".to_string())); - /// ``` - pub fn parameter(&self) -> Option { - self.parameters.get(&Name::of::()).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 Default for RouterState { - 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::()), 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::()); - } - - #[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::()), 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::(), false)); - assert!(!state.is_at(&named::(), true)); - - assert!(state.is_at(&named::(), false)); - assert!(state.is_at(&named::(), true)); - - assert!(state.is_at(&named::().parameter::("test"), false)); - assert!(state.is_at(&named::().parameter::("test"), true)); - - assert!(state.is_at(&named::().parameter::("test"), false)); - assert!(!state.is_at(&named::().parameter::("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::()); - r - }, - parameters: { - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("test")); - r - }, - ..Default::default() - } - } -} diff --git a/packages/router-core/tests/macro.rs b/packages/router-core/tests/macro.rs deleted file mode 100644 index 66f08e5a6..000000000 --- a/packages/router-core/tests/macro.rs +++ /dev/null @@ -1,245 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; -use dioxus_router_core::*; -use dioxus_router_macro::*; -use std::str::FromStr; - -#[inline_props] -fn Route1(cx: Scope, dynamic: String) -> Element { - render! { - div{ - "Route1: {dynamic}" - } - } -} - -#[inline_props] -fn Route2(cx: Scope) -> Element { - render! { - div{ - "Route2" - } - } -} - -#[inline_props] -fn Route3(cx: Scope, dynamic: u32) -> Element { - render! { - div{ - "Route3: {dynamic}" - } - } -} - -#[inline_props] -fn Route4(cx: Scope, number1: u32, number2: u32) -> Element { - render! { - div{ - "Route4: {number1} {number2}" - } - } -} - -#[inline_props] -fn Route5(cx: Scope, query: String) -> Element { - render! { - div{ - "Route5: {query}" - } - } -} - -#[inline_props] -fn Route6(cx: Scope, extra: Vec) -> Element { - render! { - div{ - "Route5: {extra:?}" - } - } -} - -#[inline_props] -fn Nested(cx: Scope, nested: String) -> Element { - render! { - div{ - "Nested: {nested:?}" - } - } -} - -#[rustfmt::skip] -#[routable] -#[derive(Clone, Debug, PartialEq)] -enum Route { - #[route("/:dynamic" Route1)] - Route1 { dynamic: String }, - #[route("/:number1/:number2" Route4)] - Route4 { number1: u32, number2: u32 }, - #[nest("/:nested" nested { nested: String } Nested)] - #[route("/" Route2)] - Route2 {}, - // #[redirect("/:dynamic/hello_world")] - #[route("/:dynamic" Route3)] - Route3 { dynamic: u32 }, - #[end_nest] - #[route("/?:query" Route5)] - Route5 { query: String }, - #[route("/:...extra" Route6)] - Route6 { extra: Vec }, -} - -#[test] -fn display_works() { - let route = Route::Route1 { - dynamic: "hello".to_string(), - }; - - assert_eq!(route.to_string(), "/hello"); - - let route = Route::Route3 { - nested: "hello_world".to_string(), - dynamic: 1234, - }; - - assert_eq!(route.to_string(), "/hello_world/1234"); - - let route = Route::Route1 { - dynamic: "hello_world2".to_string(), - }; - - assert_eq!(route.to_string(), "/hello_world2"); - - let route = Route::Route4 { - number1: 1234, - number2: 5678, - }; - - assert_eq!(route.to_string(), "/1234/5678"); - - let route = Route::Route5 { - query: "hello".to_string(), - }; - - assert_eq!(route.to_string(), "/?hello"); - - let route = Route::Route6 { - extra: vec!["hello".to_string(), "world".to_string()], - }; - - assert_eq!(route.to_string(), "/hello/world"); -} - -#[test] -fn from_string_works() { - let w = "/hello"; - assert_eq!( - Route::from_str(w), - Ok(Route::Route1 { - dynamic: "hello".to_string() - }) - ); - let w = "/hello/"; - assert_eq!( - Route::from_str(w), - Ok(Route::Route1 { - dynamic: "hello".to_string() - }) - ); - - let w = "/hello_world/1234"; - assert_eq!( - Route::from_str(w), - Ok(Route::Route3 { - nested: "hello_world".to_string(), - dynamic: 1234 - }) - ); - let w = "/hello_world/1234/"; - assert_eq!( - Route::from_str(w), - Ok(Route::Route3 { - nested: "hello_world".to_string(), - dynamic: 1234 - }) - ); - - let w = "/hello_world2"; - assert_eq!( - Route::from_str(w), - Ok(Route::Route1 { - dynamic: "hello_world2".to_string() - }) - ); - - let w = "/?x=1234&y=hello"; - assert_eq!( - Route::from_str(w), - Ok(Route::Route5 { - query: "x=1234&y=hello".to_string() - }) - ); - - let w = "/hello_world/hello_world/hello_world"; - assert_eq!( - Route::from_str(w), - Ok(Route::Route6 { - extra: vec![ - "hello_world".to_string(), - "hello_world".to_string(), - "hello_world".to_string() - ] - }) - ); -} - -#[test] -fn round_trip() { - // Route1 - let string = "hello_world2"; - let route = Route::Route1 { - dynamic: string.to_string(), - }; - assert_eq!(Route::from_str(&route.to_string()), Ok(route)); - - // Route2 - for num1 in 0..100 { - for num2 in 0..100 { - let route = Route::Route3 { - nested: format!("number{num1}"), - dynamic: num2, - }; - assert_eq!(Route::from_str(&route.to_string()), Ok(route)); - } - } - - // Route3 - for num1 in 0..100 { - for num2 in 0..100 { - let route = Route::Route4 { - number1: num1, - number2: num2, - }; - assert_eq!(Route::from_str(&route.to_string()), Ok(route)); - } - } - - // Route4 - let string = "x=1234&y=hello"; - let route = Route::Route5 { - query: string.to_string(), - }; - assert_eq!(Route::from_str(&route.to_string()), Ok(route)); - - // Route5 - let route = Route::Route6 { - extra: vec![ - "hello_world".to_string(), - "hello_world".to_string(), - "hello_world".to_string(), - ], - }; - assert_eq!(Route::from_str(&route.to_string()), Ok(route)); -} - -fn main() {} diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index 25a510f50..c60320afc 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -190,7 +190,7 @@ impl RouteEnum { } impl std::str::FromStr for #name { - type Err = RouteParseError<#error_name>; + type Err = dioxus_router::routable::RouteParseError<#error_name>; fn from_str(s: &str) -> Result { let route = s.strip_prefix('/').unwrap_or(s); @@ -200,7 +200,7 @@ impl RouteEnum { #(#tokens)* - Err(RouteParseError { + Err(dioxus_router::routable::RouteParseError { attempted_routes: errors, }) } @@ -289,7 +289,7 @@ impl RouteEnum { let index_iter = 0..layers.len(); quote! { - impl Routable for #name where Self: Clone { + impl dioxus_router::routable::Routable for #name where Self: Clone { fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> { let myself = self.clone(); match level { diff --git a/packages/router-macro/src/nest.rs b/packages/router-macro/src/nest.rs index 64b416863..6d9e43feb 100644 --- a/packages/router-macro/src/nest.rs +++ b/packages/router-macro/src/nest.rs @@ -90,7 +90,7 @@ impl Nest { } RouteSegment::Dynamic(ident, ty) => { let missing_error = segment.missing_error_name().unwrap(); - error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) }); + error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); error_variants.push(quote! { #missing_error }); display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? }); diff --git a/packages/router-macro/src/query.rs b/packages/router-macro/src/query.rs index 8306254da..8b9db9e51 100644 --- a/packages/router-macro/src/query.rs +++ b/packages/router-macro/src/query.rs @@ -14,7 +14,7 @@ impl QuerySegment { let ident = &self.ident; let ty = &self.ty; quote! { - let #ident = <#ty as dioxus_router_core::router::FromQuery>::from_query(query); + let #ident = <#ty as dioxus_router::routable::FromQuery>::from_query(query); } } diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 466d23220..91fd25916 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -203,13 +203,13 @@ impl Route { } RouteSegment::Dynamic(ident, ty) => { let missing_error = segment.missing_error_name().unwrap(); - error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) }); + error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); error_variants.push(quote! { #missing_error }); display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? }); } RouteSegment::CatchAll(ident, ty) => { - error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegments>::Err) }); + error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegments>::Err) }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index e4e786ed6..e81a4e433 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -80,7 +80,7 @@ impl RouteSegment { { let mut segments = segments.clone(); let parsed = if let Some(segment) = segments.next() { - <#ty as dioxus_router_core::router::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) + <#ty as dioxus_router::routable::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) } else { Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name)) }; @@ -101,7 +101,7 @@ impl RouteSegment { let parsed = { let mut segments = segments.clone(); let segments: Vec<_> = segments.collect(); - <#ty as dioxus_router_core::router::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) + <#ty as dioxus_router::routable::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) }; match parsed { Ok(#name) => { diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index b607b293f..c8367c1cc 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -12,19 +12,16 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] anyhow = "1.0.66" -async-lock = "2.7.0" dioxus = { path="../dioxus" } -dioxus-router-core = { path = "../router-core"} -futures-channel = "0.3.25" -futures-util = "0.3.25" +dioxus-router-macro = { path="../router-macro" } log = "0.4.17" +serde = { version = "1.0.163", features = ["derive"] } thiserror = "1.0.37" +url = "2.3.1" [features] -regex = ["dioxus-router-core/regex"] -serde = ["dioxus-router-core/serde"] wasm_test = [] -web = ["dioxus-router-core/web"] +web = [] [dev-dependencies] dioxus = { path = "../dioxus" } diff --git a/packages/router/src/components/default_errors.rs b/packages/router/src/components/default_errors.rs index 416f34589..979e448fc 100644 --- a/packages/router/src/components/default_errors.rs +++ b/packages/router/src/components/default_errors.rs @@ -1,17 +1,12 @@ -use crate::{components::Link, hooks::use_route}; +use crate::{components::Link, hooks::use_route, navigation::NavigationTarget, routable::Routable}; use dioxus::prelude::*; -use dioxus_router_core::prelude::{named, FailureExternalNavigation as FENName, RootIndex}; #[allow(non_snake_case)] -pub fn FailureExternalNavigation(cx: Scope) -> Element { - let state = use_route(cx).expect( +pub fn FailureExternalNavigation(cx: Scope) -> Element { + let href = use_route::(cx).expect( "`FailureExternalNavigation` can only be mounted by the router itself, \ since it is not exposed", ); - let href = state.parameter::().expect( - "`FailureExternalNavigation` cannot be mounted without receiving its parameter, \ - since it is not exposed", - ); render! { h1 { "External Navigation Failure!" } @@ -28,7 +23,7 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element { } #[allow(non_snake_case)] -pub fn FailureNamedNavigation(cx: Scope) -> Element { +pub fn FailureNamedNavigation(cx: Scope) -> Element { render! { h1 { "Named Navigation Failure!" } p { @@ -40,15 +35,15 @@ pub fn FailureNamedNavigation(cx: Scope) -> Element { "We are sorry for the inconvenience. The link below may help to fix the problem, but " "there is no guarantee." } - Link { - target: named::(), + Link:: { + target: NavigationTarget::External("https://google.com".into()), "Click here to try to fix the failure." } } } #[allow(non_snake_case)] -pub fn FailureRedirectionLimit(cx: Scope) -> Element { +pub fn FailureRedirectionLimit(cx: Scope) -> Element { render! { h1 { "Redirection Limit Failure!" } p { @@ -60,8 +55,8 @@ pub fn FailureRedirectionLimit(cx: Scope) -> Element { "We are sorry for the inconvenience. The link below may help to fix the problem, but " "there is no guarantee." } - Link { - target: named::(), + Link:: { + target: NavigationTarget::External("https://google.com".into()), "Click here to try to fix the failure." } } diff --git a/packages/router/src/components/history_buttons.rs b/packages/router/src/components/history_buttons.rs index 5b9fe25c4..916a24280 100644 --- a/packages/router/src/components/history_buttons.rs +++ b/packages/router/src/components/history_buttons.rs @@ -1,8 +1,7 @@ use dioxus::prelude::*; -use dioxus_router_core::RouterMessage; use log::error; -use crate::utils::use_router_internal::use_router_internal; +use crate::{routable::Routable, utils::use_router_internal::use_router_internal}; /// The properties for a [`GoBackButton`] or a [`GoForwardButton`]. #[derive(Debug, Props)] @@ -52,11 +51,11 @@ pub struct HistoryButtonProps<'a> { /// # ); /// ``` #[allow(non_snake_case)] -pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { +pub fn GoBackButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { let HistoryButtonProps { children } = cx.props; // hook up to router - let router = match use_router_internal(cx) { + let router = match use_router_internal::(cx) { Some(r) => r, #[allow(unreachable_code)] None => { @@ -67,20 +66,14 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { return None; } }; - let state = loop { - if let Some(state) = router.state.try_read() { - break state; - } - }; - let sender = router.sender.clone(); - let disabled = !state.can_go_back; + let disabled = !router.can_go_back(); render! { button { disabled: "{disabled}", prevent_default: "onclick", - onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoBack); }, + onclick: move |_| router.go_back(), children } } @@ -127,11 +120,11 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { /// # ); /// ``` #[allow(non_snake_case)] -pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { +pub fn GoForwardButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { let HistoryButtonProps { children } = cx.props; // hook up to router - let router = match use_router_internal(cx) { + let router = match use_router_internal::(cx) { Some(r) => r, #[allow(unreachable_code)] None => { @@ -142,20 +135,14 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { return None; } }; - let state = loop { - if let Some(state) = router.state.try_read() { - break state; - } - }; - let sender = router.sender.clone(); - let disabled = !state.can_go_back; + let disabled = !router.can_go_back(); render! { button { disabled: "{disabled}", prevent_default: "onclick", - onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoForward); }, + onclick: move |_| router.go_forward(), children } } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 8471d95e4..fb570855d 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -1,14 +1,15 @@ use std::fmt::Debug; use dioxus::prelude::*; -use dioxus_router_core::{navigation::NavigationTarget, RouterMessage}; use log::error; +use crate::navigation::NavigationTarget; +use crate::routable::Routable; use crate::utils::use_router_internal::use_router_internal; /// The properties for a [`Link`]. #[derive(Props)] -pub struct LinkProps<'a> { +pub struct LinkProps<'a, R: Routable> { /// A class to apply to the generate HTML anchor tag if the `target` route is active. pub active_class: Option<&'a str>, /// The children to render within the generated HTML anchor tag. @@ -18,11 +19,6 @@ pub struct LinkProps<'a> { /// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be /// appended at the end of `class`. pub class: Option<&'a str>, - /// Require the __exact__ `target` to be active, for the [`Link`] to be active. - /// - /// See [`RouterState::is_at`](dioxus_router_core::RouterState::is_at) for more details. - #[props(default)] - pub exact: bool, /// The id attribute for the generated HTML anchor tag. pub id: Option<&'a str>, /// When [`true`], the `target` route will be opened in a new tab. @@ -46,22 +42,21 @@ pub struct LinkProps<'a> { pub rel: Option<&'a str>, /// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag. #[props(into)] - pub target: NavigationTarget, + pub target: NavigationTarget, } -impl Debug for LinkProps<'_> { +impl Debug for LinkProps<'_, R> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LinkProps") .field("active_class", &self.active_class) .field("children", &self.children) .field("class", &self.class) - .field("exact", &self.exact) .field("id", &self.id) .field("new_tab", &self.new_tab) .field("onclick", &self.onclick.as_ref().map(|_| "onclick is set")) .field("onclick_only", &self.onclick_only) .field("rel", &self.rel) - .field("target", &self.target) + .field("target", &self.target.to_string()) .finish() } } @@ -130,12 +125,11 @@ impl Debug for LinkProps<'_> { /// # ); /// ``` #[allow(non_snake_case)] -pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { +pub fn Link<'a, R: Routable + Clone>(cx: Scope<'a, LinkProps<'a, R>>) -> Element { let LinkProps { active_class, children, class, - exact, id, new_tab, onclick, @@ -145,7 +139,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { } = cx.props; // hook up to router - let router = match use_router_internal(cx) { + let router = match use_router_internal::(cx) { Some(r) => r, #[allow(unreachable_code)] None => { @@ -156,19 +150,19 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { return None; } }; - let state = loop { - if let Some(state) = router.state.try_read() { - break state; - } - }; - let sender = router.sender.clone(); - let href = state.href(target); + let current_route = router.current(); + let href = current_route.to_string(); let ac = active_class - .and_then(|active_class| { - state - .is_at(target, *exact) - .then(|| format!(" {active_class}")) + .and_then(|active_class| match target { + NavigationTarget::Internal(target) => { + if href == target.to_string() { + Some(format!(" {active_class}")) + } else { + None + } + } + _ => None, }) .unwrap_or_default(); @@ -186,7 +180,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { let do_default = onclick.is_none() || !onclick_only; let action = move |event| { if do_default && is_router_nav { - let _ = sender.unbounded_send(RouterMessage::Push(target.clone())); + router.push(target.clone()); } if let Some(handler) = onclick { diff --git a/packages/router/src/components/outlet.rs b/packages/router/src/components/outlet.rs new file mode 100644 index 000000000..73b10a694 --- /dev/null +++ b/packages/router/src/components/outlet.rs @@ -0,0 +1,49 @@ +use crate::{contexts::outlet::OutletContext, routable::Routable}; +use dioxus::prelude::*; + +/// An outlet for the current content. +/// +/// Only works as descendant of a component calling [`use_router`], otherwise it will be inactive. +/// +/// The [`Outlet`] is aware of how many [`Outlet`]s it is nested within. It will render the content +/// of the active route that is __exactly as deep__. +/// +/// [`use_router`]: crate::hooks::use_router +/// +/// # Panic +/// - When the [`Outlet`] is not nested within another component calling the [`use_router`] hook, +/// but only in debug builds. +/// +/// # Example +/// ```rust +/// # use dioxus::prelude::*; +/// # use dioxus_router::prelude::*; +/// fn App(cx: Scope) -> Element { +/// use_router( +/// &cx, +/// &|| RouterConfiguration { +/// synchronous: true, // asynchronicity not needed for doc test +/// ..Default::default() +/// }, +/// &|| Segment::content(comp(Content)) +/// ); +/// +/// render! { +/// h1 { "App" } +/// Outlet { } // The content component will be rendered here +/// } +/// } +/// +/// fn Content(cx: Scope) -> Element { +/// render! { +/// p { "Content" } +/// } +/// } +/// # +/// # let mut vdom = VirtualDom::new(App); +/// # let _ = vdom.rebuild(); +/// # assert_eq!(dioxus_ssr::render(&vdom), "

App

Content

"); +/// ``` +pub fn Outlet(cx: Scope) -> Element { + OutletContext::render::(cx) +} diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs new file mode 100644 index 000000000..4e735984c --- /dev/null +++ b/packages/router/src/components/router.rs @@ -0,0 +1,60 @@ +use dioxus::prelude::*; +use log::error; +use std::{cell::RefCell, str::FromStr}; + +use crate::{ + history::HistoryProvider, + prelude::{outlet::OutletContext, RouterContext}, + routable::Routable, + router_cfg::RouterConfiguration, +}; + +/// The props for [`Router`]. +#[derive(Props)] +pub struct RouterProps { + #[props(into)] + initial_url: Option, + #[props(default, into)] + config: RefCell>>, +} + +impl PartialEq for RouterProps { + fn eq(&self, _: &Self) -> bool { + // prevent the router from re-rendering when the initial url or config changes + true + } +} + +/// A component that renders the current route. +pub fn Router + Default + 'static>( + cx: Scope>, +) -> Element +where + ::Err: std::fmt::Display, +{ + let router = use_context_provider(cx, || { + #[allow(unreachable_code, unused_variables)] + if let Some(outer) = cx.consume_context::>() { + let msg = "Router components should not be nested within each other"; + error!("{msg}, inner will be inactive and transparent"); + #[cfg(debug_assertions)] + panic!("{}", msg); + } + let router = RouterContext::new( + cx.props.config.take().unwrap_or_default(), + cx.schedule_update_any(), + ); + if let Some(initial) = cx.props.initial_url.as_ref() { + router.replace( + initial + .parse() + .unwrap_or_else(|_| panic!("failed to parse initial url")), + ); + } + router + }); + + use_context_provider(cx, || OutletContext { current_level: 1 }); + + router.current().render(cx, 0) +} diff --git a/packages/router/src/contexts/outlet.rs b/packages/router/src/contexts/outlet.rs new file mode 100644 index 000000000..e77515ca8 --- /dev/null +++ b/packages/router/src/contexts/outlet.rs @@ -0,0 +1,29 @@ +use dioxus::prelude::*; + +use crate::{hooks::use_route, routable::Routable}; + +#[derive(Clone)] +pub(crate) struct OutletContext { + pub current_level: usize, +} + +pub(crate) fn use_outlet_context(cx: &ScopeState) -> &OutletContext { + let outlet_context = use_context(cx).unwrap(); + outlet_context +} + +impl OutletContext { + pub(crate) fn render(cx: &ScopeState) -> Element<'_> { + let outlet = use_outlet_context(cx); + let current_level = outlet.current_level; + cx.provide_context({ + OutletContext { + current_level: current_level + 1, + } + }); + + use_route::(cx) + .expect("Outlet must be inside of a router") + .render(cx, current_level) + } +} diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index cd269540d..5690fc78e 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -1,12 +1,809 @@ -use std::sync::Arc; +use std::{ + collections::HashSet, + sync::{Arc, RwLock, RwLockWriteGuard}, +}; -use async_lock::RwLock; -use dioxus::{core::Component, prelude::ScopeId}; -use dioxus_router_core::{RouterMessage, RouterState}; -use futures_channel::mpsc::UnboundedSender; +use dioxus::prelude::*; -#[derive(Clone)] -pub(crate) struct RouterContext { - pub(crate) state: Arc>>, - pub(crate) sender: UnboundedSender>, +use crate::{ + history::HistoryProvider, navigation::NavigationTarget, routable::Routable, + router_cfg::RouterConfiguration, +}; + +/// An error that can occur when navigating. +pub enum NavigationFailure { + /// The router failed to navigate to an external URL. + External(String), + /// The router failed to navigate to an internal URL. + Internal(::Err), } + +/// A function the router will call after every routing update. +pub type RoutingCallback = Arc) -> Option>>; + +struct MutableRouterState +where + R: Routable, +{ + /// 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. + 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. + can_go_forward: bool, + + /// The current prefix. + prefix: Option, + + history: Box>, +} + +/// A collection of router data that manages all routing functionality. +pub struct RouterContext +where + R: Routable, +{ + state: Arc>>, + + subscribers: Arc>>, + subscriber_update: Arc, + routing_callback: Option>, + + failure_external_navigation: fn(Scope) -> Element, + failure_named_navigation: fn(Scope) -> Element, + failure_redirection_limit: fn(Scope) -> Element, +} + +impl Clone for RouterContext { + fn clone(&self) -> Self { + Self { + state: self.state.clone(), + subscribers: self.subscribers.clone(), + subscriber_update: self.subscriber_update.clone(), + routing_callback: self.routing_callback.clone(), + failure_external_navigation: self.failure_external_navigation, + failure_named_navigation: self.failure_named_navigation, + failure_redirection_limit: self.failure_redirection_limit, + } + } +} + +impl RouterContext +where + R: Routable, +{ + pub(crate) fn new(cfg: RouterConfiguration, mark_dirty: Arc) -> Self + where + R: Clone, + { + let state = Arc::new(RwLock::new(MutableRouterState { + can_go_back: false, + can_go_forward: false, + prefix: Default::default(), + history: cfg.history, + })); + + Self { + state, + subscribers: Arc::new(RwLock::new(HashSet::new())), + subscriber_update: mark_dirty, + + routing_callback: cfg.on_update, + + failure_external_navigation: cfg.failure_external_navigation, + failure_named_navigation: cfg.failure_named_navigation, + failure_redirection_limit: cfg.failure_redirection_limit, + } + } + + /// Check whether there is a previous page to navigate back to. + #[must_use] + pub fn can_go_back(&self) -> bool { + self.state.read().unwrap().can_go_back + } + + /// Check whether there is a future page to navigate forward to. + #[must_use] + pub fn can_go_forward(&self) -> bool { + self.state.read().unwrap().can_go_forward + } + + /// Go back to the previous location. + /// + /// Will fail silently if there is no previous location to go to. + pub fn go_back(&self) { + self.state.write().unwrap().history.go_back(); + self.update_subscribers(); + } + + /// Go back to the next location. + /// + /// Will fail silently if there is no next location to go to. + pub fn go_forward(&self) { + self.state.write().unwrap().history.go_forward(); + self.update_subscribers(); + } + + /// Push a new location. + /// + /// The previous location will be available to go back to. + pub fn push(&self, target: NavigationTarget) -> Option> { + let mut state = self.state_mut(); + match target { + NavigationTarget::Internal(p) => state.history.push(p), + NavigationTarget::External(e) => return self.external(e), + } + + self.update_subscribers(); + None + } + + /// Replace the current location. + /// + /// The previous location will **not** be available to go back to. + pub fn replace(&self, target: NavigationTarget) -> Option> { + let mut state = self.state_mut(); + match target { + NavigationTarget::Internal(p) => state.history.replace(p), + NavigationTarget::External(e) => return self.external(e), + } + + self.update_subscribers(); + None + } + + /// The route that is currently active. + pub fn current(&self) -> R + where + R: Clone, + { + self.state.read().unwrap().history.current_route().clone() + } + + /// The prefix that is currently active. + pub fn prefix(&self) -> Option { + self.state.read().unwrap().prefix.clone() + } + + fn external(&self, external: String) -> Option> { + let mut state = self.state_mut(); + match state.history.external(external.clone()) { + true => None, + false => Some(NavigationFailure::External(external)), + } + } + + fn state_mut(&self) -> RwLockWriteGuard> { + self.state.write().unwrap() + } + + /// Manually subscribe to the current route + pub fn subscribe(&self, id: ScopeId) { + self.subscribers.write().unwrap().insert(id); + } + + /// Manually unsubscribe from the current route + pub fn unsubscribe(&self, id: ScopeId) { + self.subscribers.write().unwrap().remove(&id); + } + + fn update_subscribers(&self) { + for &id in self.subscribers.read().unwrap().iter() { + (self.subscriber_update)(id); + } + } +} + +// #[cfg(test)] +// mod tests { +// //! The tests for [`RouterContext`] test various functions that are not exposed as public. +// //! However, several of those have an observable effect on the behavior of exposed functions. +// //! +// //! The alternative would be to send messages via the services channel and calling one of the +// //! `run` functions. However, for readability and clarity, it was chosen to directly call the +// //! private functions. + +// use std::sync::Mutex; + +// use crate::{ +// history::MemoryHistory, +// routes::{ParameterRoute, Route, RouteContent}, +// }; + +// use super::*; + +// fn test_segment() -> Segment<&'static str> { +// Segment::content(RouteContent::Content(ContentAtom("index"))) +// .fixed( +// "fixed", +// Route::content(RouteContent::Content(ContentAtom("fixed"))).name::(), +// ) +// .fixed( +// "redirect", +// Route::content(RouteContent::Redirect(NavigationTarget::Internal( +// String::from("fixed"), +// ))), +// ) +// .fixed( +// "redirection-loop", +// Route::content(RouteContent::Redirect(NavigationTarget::Internal( +// String::from("/redirection-loop"), +// ))), +// ) +// .fixed( +// "%F0%9F%8E%BA", +// Route::content(RouteContent::Content(ContentAtom("🎺"))), +// ) +// .catch_all(ParameterRoute::empty::()) +// } + +// #[test] +// fn new_provides_update_to_history() { +// struct TestHistory {} + +// impl HistoryProvider for TestHistory { +// fn current_path(&self) -> String { +// todo!() +// } + +// fn current_query(&self) -> Option { +// todo!() +// } + +// fn go_back(&mut self) { +// todo!() +// } + +// fn go_forward(&mut self) { +// todo!() +// } + +// fn push(&mut self, _path: String) { +// todo!() +// } + +// fn replace(&mut self, _path: String) { +// todo!() +// } + +// fn updater(&mut self, callback: Arc) { +// callback(); +// } +// } + +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(TestHistory {}), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); + +// assert!(matches!( +// s.receiver.try_next().unwrap().unwrap(), +// RouterMessage::Update +// )); +// } + +// #[test] +// fn update_routing() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// assert_eq!(s.names, s.state.try_read().unwrap().name_map); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("fixed")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/fixed")); +// assert_eq!(state.query, Some(String::from("test=value"))); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// assert_eq!(s.names, state.name_map); +// } + +// #[test] +// fn update_routing_root_index() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::::default(), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("index")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/")); +// assert!(state.query.is_none()); +// assert!(state.prefix.is_none()); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn update_routing_redirect() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("fixed")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/fixed")); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// #[should_panic = "reached redirect limit of 25"] +// #[cfg(debug_assertions)] +// fn update_routing_redirect_debug() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); +// } + +// #[test] +// #[cfg(not(debug_assertions))] +// fn update_routing_redirect_release() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("redirect limit")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/redirection-loop")); +// assert_eq!(state.can_go_back, false); +// assert_eq!(state.can_go_forward, false); +// } + +// #[test] +// fn update_subscribers() { +// let ids = Arc::new(Mutex::new(Vec::new())); +// let ids2 = Arc::clone(&ids); + +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// Segment::empty(), +// Box::::default(), +// Arc::new(move |id| { +// ids2.lock().unwrap().push(id); +// }), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); + +// let id0 = Arc::new(0); +// s.subscribe(Arc::clone(&id0)); + +// let id1 = Arc::new(1); +// s.subscribe(Arc::clone(&id1)); + +// let id1 = Arc::try_unwrap(id1).unwrap(); +// s.update_subscribers(); + +// assert_eq!(s.subscribers.len(), 1); +// assert_eq!(s.subscribers[0].upgrade().unwrap(), id0); +// assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]); +// } + +// #[test] +// fn push_internal() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::::default(), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.push(NavigationTarget::Internal(String::from("/fixed"))); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("fixed")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/fixed")); +// assert!(state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn push_named() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::::default(), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.push(NavigationTarget::named::()); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("fixed")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/fixed")); +// assert!(state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn push_external() { +// let (mut s, tx, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); +// tx.unbounded_send(RouterMessage::Push(NavigationTarget::External( +// String::from("https://dioxuslabs.com/"), +// ))) +// .unwrap(); +// s.run_current(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("external target")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert_eq!(state.parameters, { +// let mut r = HashMap::new(); +// r.insert( +// Name::of::(), +// String::from("https://dioxuslabs.com/"), +// ); +// r +// }); +// assert_eq!(state.path, String::from("/fixed")); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn replace_named() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::::default(), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.replace(NavigationTarget::named::()); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("fixed")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/fixed")); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn replace_internal() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::::default(), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.replace(NavigationTarget::Internal(String::from("/fixed"))); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("fixed")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/fixed")); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn replace_external() { +// let (mut s, tx, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); +// tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External( +// String::from("https://dioxuslabs.com/"), +// ))) +// .unwrap(); +// s.run_current(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("external target")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert_eq!(state.parameters, { +// let mut r = HashMap::new(); +// r.insert( +// Name::of::(), +// String::from("https://dioxuslabs.com/"), +// ); +// r +// }); +// assert_eq!(state.path, String::from("/fixed")); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn subscribe() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// Segment::empty(), +// Box::::default(), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); + +// let id = Arc::new(0); +// s.subscribe(Arc::clone(&id)); + +// assert_eq!(s.subscribers.len(), 1); +// assert_eq!(s.subscribers[0].upgrade().unwrap(), id); +// } + +// #[test] +// fn routing_callback() { +// let paths = Arc::new(Mutex::new(Vec::new())); +// let paths2 = Arc::clone(&paths); + +// let (mut s, c, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()), +// Arc::new(|_| {}), +// Some(Arc::new(move |state| { +// paths2.lock().unwrap().push(state.path.clone()); +// Some("/%F0%9F%8E%BA".into()) +// })), +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); + +// assert!(paths.lock().unwrap().is_empty()); + +// s.init(); +// assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]); + +// c.unbounded_send(RouterMessage::Update).unwrap(); +// s.run_current(); +// assert_eq!( +// *paths.lock().unwrap(), +// vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")] +// ); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("🎺")]) +// } + +// #[test] +// fn url_decoding_do() { +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert!(state.content.is_empty()); +// assert!(state.names.is_empty()); +// assert_eq!(state.parameters, { +// let mut r = HashMap::new(); +// r.insert(Name::of::(), String::from("🥳")); +// r +// }); +// assert_eq!(state.path, String::from("/%F0%9F%A5%B3")); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn url_decoding_do_not() { +// let (mut s, c, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); +// c.unbounded_send(RouterMessage::Update).unwrap(); +// s.run_current(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("🎺")]); +// assert!(state.names.is_empty()); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/%F0%9F%8E%BA")); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } + +// #[test] +// fn prefix() { +// struct TestHistory {} + +// impl HistoryProvider for TestHistory { +// fn current_path(&self) -> String { +// String::from("/") +// } + +// fn current_query(&self) -> Option { +// None +// } + +// fn current_prefix(&self) -> Option { +// Some(String::from("/prefix")) +// } + +// fn can_go_back(&self) -> bool { +// false +// } + +// fn can_go_forward(&self) -> bool { +// false +// } + +// fn go_back(&mut self) { +// todo!() +// } + +// fn go_forward(&mut self) { +// todo!() +// } + +// fn push(&mut self, _path: String) { +// todo!() +// } + +// fn replace(&mut self, _path: String) { +// todo!() +// } + +// fn updater(&mut self, callback: Arc) { +// callback(); +// } +// } + +// let (mut s, _, _) = RouterContext::<_, u8>::new( +// test_segment(), +// Box::new(TestHistory {}), +// Arc::new(|_| {}), +// None, +// ContentAtom("external target"), +// ContentAtom("named target"), +// ContentAtom("redirect limit"), +// ); +// s.init(); + +// let state = s.state.try_read().unwrap(); +// assert_eq!(state.content, vec![ContentAtom("index")]); +// assert_eq!(state.names, { +// let mut r = HashSet::new(); +// r.insert(Name::of::()); +// r +// }); +// assert!(state.parameters.is_empty()); +// assert_eq!(state.path, String::from("/")); +// assert!(state.query.is_none()); +// assert_eq!(state.prefix, Some(String::from("/prefix"))); +// assert!(!state.can_go_back); +// assert!(!state.can_go_forward); +// } +// } diff --git a/packages/router/src/history/memory.rs b/packages/router/src/history/memory.rs new file mode 100644 index 000000000..116b733fa --- /dev/null +++ b/packages/router/src/history/memory.rs @@ -0,0 +1,230 @@ +use std::str::FromStr; + +use crate::routable::Routable; + +use super::HistoryProvider; + +/// A [`HistoryProvider`] that stores all navigation information in memory. +pub struct MemoryHistory { + current: Option, + history: Vec, + future: Vec, +} + +impl MemoryHistory { + /// Create a [`MemoryHistory`] starting at `path`. + /// + /// ```rust + /// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory}; + /// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap(); + /// assert_eq!(history.current_path(), "/some/path"); + /// assert_eq!(history.can_go_back(), false); + /// ``` + pub fn with_initial_path(path: impl Into) -> Result::Err> { + let path = path.into(); + + Ok(Self { + current: Some(R::from_str(&path)?), + ..Default::default() + }) + } +} + +impl Default for MemoryHistory { + fn default() -> Self { + Self { + current: None, + history: Vec::new(), + future: Vec::new(), + } + } +} + +impl HistoryProvider for MemoryHistory { + fn current_route(&self) -> &R { + self.current.as_ref().expect("current route is not set") + } + + fn can_go_back(&self) -> bool { + !self.history.is_empty() + } + + fn go_back(&mut self) { + if let Some(last) = self.history.pop() { + let new = self.current.replace(last); + if let Some(new) = new { + self.future.push(new); + } + } + } + + fn can_go_forward(&self) -> bool { + !self.future.is_empty() + } + + fn go_forward(&mut self) { + if let Some(next) = self.future.pop() { + let old = self.current.replace(next); + if let Some(old) = old { + self.history.push(old); + } + } + } + + fn push(&mut self, new: R) { + if let Some(current) = self.current.take() { + self.history.push(current); + } + self.current = Some(new); + self.future.clear(); + } + + fn replace(&mut self, path: R) { + self.current = Some(path); + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn default() { +// let mem = MemoryHistory::default(); +// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); +// assert_eq!(mem.history, Vec::::new()); +// assert_eq!(mem.future, Vec::::new()); +// } + +// #[test] +// fn with_initial_path() { +// let mem = MemoryHistory::with_initial_path("something").unwrap(); +// assert_eq!( +// mem.current, +// Url::parse(&format!("{INITIAL_URL}something")).unwrap() +// ); +// assert_eq!(mem.history, Vec::::new()); +// assert_eq!(mem.future, Vec::::new()); +// } + +// #[test] +// fn with_initial_path_with_leading_slash() { +// let mem = MemoryHistory::with_initial_path("/something").unwrap(); +// assert_eq!( +// mem.current, +// Url::parse(&format!("{INITIAL_URL}something")).unwrap() +// ); +// assert_eq!(mem.history, Vec::::new()); +// assert_eq!(mem.future, Vec::::new()); +// } + +// #[test] +// fn can_go_back() { +// let mut mem = MemoryHistory::default(); +// assert!(!mem.can_go_back()); + +// mem.push(String::from("/test")); +// assert!(mem.can_go_back()); +// } + +// #[test] +// fn go_back() { +// let mut mem = MemoryHistory::default(); +// mem.push(String::from("/test")); +// mem.go_back(); + +// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); +// assert!(mem.history.is_empty()); +// assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]); +// } + +// #[test] +// fn can_go_forward() { +// let mut mem = MemoryHistory::default(); +// assert!(!mem.can_go_forward()); + +// mem.push(String::from("/test")); +// mem.go_back(); + +// assert!(mem.can_go_forward()); +// } + +// #[test] +// fn go_forward() { +// let mut mem = MemoryHistory::default(); +// mem.push(String::from("/test")); +// mem.go_back(); +// mem.go_forward(); + +// assert_eq!( +// mem.current, +// Url::parse(&format!("{INITIAL_URL}test")).unwrap() +// ); +// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]); +// assert!(mem.future.is_empty()); +// } + +// #[test] +// fn push() { +// let mut mem = MemoryHistory::default(); +// mem.push(String::from("/test")); + +// assert_eq!( +// mem.current, +// Url::parse(&format!("{INITIAL_URL}test")).unwrap() +// ); +// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]); +// assert!(mem.future.is_empty()); +// } + +// #[test] +// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#] +// #[cfg(debug_assertions)] +// fn push_debug() { +// let mut mem = MemoryHistory::default(); +// mem.push(String::from("//test")); +// } + +// #[test] +// #[cfg(not(debug_assertions))] +// fn push_release() { +// let mut mem = MemoryHistory::default(); +// mem.push(String::from("//test")); + +// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); +// assert!(mem.history.is_empty()) +// } + +// #[test] +// fn replace() { +// let mut mem = MemoryHistory::default(); +// mem.push(String::from("/test")); +// mem.push(String::from("/other")); +// mem.go_back(); +// mem.replace(String::from("/replace")); + +// assert_eq!( +// mem.current, +// Url::parse(&format!("{INITIAL_URL}replace")).unwrap() +// ); +// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]); +// assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]); +// } + +// #[test] +// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#] +// #[cfg(debug_assertions)] +// fn replace_debug() { +// let mut mem = MemoryHistory::default(); +// mem.replace(String::from("//test")); +// } + +// #[test] +// #[cfg(not(debug_assertions))] +// fn replace_release() { +// let mut mem = MemoryHistory::default(); +// mem.replace(String::from("//test")); + +// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap()); +// } +// } diff --git a/packages/router-core/src/history/mod.rs b/packages/router/src/history/mod.rs similarity index 91% rename from packages/router-core/src/history/mod.rs rename to packages/router/src/history/mod.rs index 327513ec0..4c58457b6 100644 --- a/packages/router-core/src/history/mod.rs +++ b/packages/router/src/history/mod.rs @@ -21,6 +21,8 @@ mod web_hash; #[cfg(feature = "web")] pub use web_hash::*; +use crate::routable::Routable; + #[cfg(feature = "web")] pub(crate) mod web_scroll; @@ -32,7 +34,7 @@ pub(crate) mod web_scroll; /// However, you should document all deviations. Also, make sure the navigation is user-friendly. /// The described behaviors are designed to mimic a web browser, which most users should already /// know. Deviations might confuse them. -pub trait HistoryProvider { +pub trait HistoryProvider { /// Get the path of the current URL. /// /// **Must start** with `/`. **Must _not_ contain** the prefix. @@ -46,20 +48,8 @@ pub trait HistoryProvider { /// assert_eq!(history.current_path(), "/path"); /// ``` #[must_use] - fn current_path(&self) -> String; - /// Get the query string of the current URL. - /// - /// **Must _not_** start with `?`. - /// - /// ```rust - /// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory}; - /// let mut history = MemoryHistory::default(); - /// assert_eq!(history.current_query(), None); - /// - /// history.push(String::from("?some=value")); - /// assert_eq!(history.current_query(), Some("some=value".to_string())); - /// ``` - fn current_query(&self) -> Option; + fn current_route(&self) -> &R; + /// Get the current path prefix of the URL. /// /// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a @@ -87,6 +77,7 @@ pub trait HistoryProvider { fn can_go_back(&self) -> bool { true } + /// Go back to a previous page. /// /// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method @@ -127,6 +118,7 @@ pub trait HistoryProvider { fn can_go_forward(&self) -> bool { true } + /// Go forward to a future page. /// /// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method @@ -162,7 +154,8 @@ pub trait HistoryProvider { /// assert_eq!(history.current_path(), "/some-other-page"); /// assert!(history.can_go_back()); /// ``` - fn push(&mut self, path: String); + fn push(&mut self, route: R); + /// Replace the current page with another one. /// /// This should merge the current URL with the `path` parameter (which may also include a query @@ -178,7 +171,7 @@ pub trait HistoryProvider { /// assert_eq!(history.current_path(), "/some-other-page"); /// assert!(!history.can_go_back()); /// ``` - fn replace(&mut self, path: String); + fn replace(&mut self, path: R); /// Navigate to an external URL. /// diff --git a/packages/router-core/src/history/web.rs b/packages/router/src/history/web.rs similarity index 98% rename from packages/router-core/src/history/web.rs rename to packages/router/src/history/web.rs index 3154a6be7..1594de0c8 100644 --- a/packages/router-core/src/history/web.rs +++ b/packages/router/src/history/web.rs @@ -23,7 +23,7 @@ use super::{ /// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added. /// /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API -pub struct WebHistory { +pub struct WebHistory { do_scroll_restoration: bool, history: History, listener_navigation: Option, @@ -72,7 +72,7 @@ impl WebHistory { } impl HistoryProvider for WebHistory { - fn current_path(&self) -> String { + fn current_route(&self) -> String { let path = self .window .location() diff --git a/packages/router-core/src/history/web_hash.rs b/packages/router/src/history/web_hash.rs similarity index 98% rename from packages/router-core/src/history/web_hash.rs rename to packages/router/src/history/web_hash.rs index edfb885b4..37af06e9b 100644 --- a/packages/router-core/src/history/web_hash.rs +++ b/packages/router/src/history/web_hash.rs @@ -21,7 +21,7 @@ const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/"; /// was not, because no browser supports WebAssembly without the [History API]. /// /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API -pub struct WebHashHistory { +pub struct WebHashHistory { do_scroll_restoration: bool, history: History, listener_navigation: Option, @@ -121,7 +121,7 @@ impl WebHashHistory { } impl HistoryProvider for WebHashHistory { - fn current_path(&self) -> String { + fn current_route(&self) -> String { self.url() .map(|url| url.path().to_string()) .unwrap_or(String::from("/")) diff --git a/packages/router-core/src/history/web_scroll.rs b/packages/router/src/history/web_scroll.rs similarity index 100% rename from packages/router-core/src/history/web_scroll.rs rename to packages/router/src/history/web_scroll.rs diff --git a/packages/router/src/hooks/use_navigate.rs b/packages/router/src/hooks/use_navigate.rs deleted file mode 100644 index 29f8989cb..000000000 --- a/packages/router/src/hooks/use_navigate.rs +++ /dev/null @@ -1,71 +0,0 @@ -use dioxus::prelude::{ScopeId, ScopeState}; -use dioxus_router_core::Navigator; - -use crate::utils::use_router_internal::use_router_internal; - -/// A hook that allows for programmatic navigation. -/// -/// # Return values -/// - [`RouterError::NotInsideRouter`], when the calling component is not nested within another -/// component calling the [`use_router`] hook. -/// - Otherwise [`Ok`]. -/// -/// [`use_router`]: crate::hooks::use_router -/// -/// # Panic -/// - When the calling component is not nested within another component calling the [`use_router`] -/// hook, but only in debug builds. -/// -/// # Example -/// ```rust -/// # use dioxus::prelude::*; -/// # use dioxus_router::prelude::*; -/// fn App(cx: Scope) -> Element { -/// let (state, _) = use_router( -/// &cx, -/// &|| RouterConfiguration { -/// synchronous: true, // asynchronicity not needed for doc test -/// ..Default::default() -/// }, -/// &|| Segment::content(comp(Redirect)).fixed("content", comp(Content)) -/// ); -/// -/// render! { -/// h1 { "App" } -/// Outlet { } -/// } -/// } -/// -/// fn Redirect(cx: Scope) -> Element { -/// let nav = use_navigate(&cx)?; -/// nav.push("/content"); -/// render! { () } -/// } -/// -/// fn Content(cx: Scope) -> Element { -/// render! { -/// p { "Content" } -/// } -/// } -/// # -/// # let mut vdom = VirtualDom::new(App); -/// # -/// # // first render with Redirect component -/// # let _ = vdom.rebuild(); -/// # assert_eq!(dioxus_ssr::render(&vdom), "

App

"); -/// # -/// # // second render with Content component -/// # let _ = vdom.rebuild(); -/// # assert_eq!(dioxus_ssr::render(&vdom), "

App

Content

"); -/// ``` -pub fn use_navigate(cx: &ScopeState) -> Option> { - match use_router_internal(cx) { - Some(r) => Some(r.sender.clone().into()), - None => { - #[cfg(debug_assertions)] - panic!("`use_navigate` must have access to a parent router"); - #[allow(unreachable_code)] - None - } - } -} diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 5f13e71a2..b6a1f0bf5 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,8 +1,6 @@ -use async_lock::RwLockReadGuard; -use dioxus::{core::Component, prelude::ScopeState}; -use dioxus_router_core::RouterState; +use dioxus::prelude::ScopeState; -use crate::utils::use_router_internal::use_router_internal; +use crate::{routable::Routable, utils::use_router_internal::use_router_internal}; /// A hook that provides access to information about the current routing location. /// @@ -56,13 +54,9 @@ use crate::utils::use_router_internal::use_router_internal; /// ``` /// /// [`use_router`]: crate::hooks::use_router -pub fn use_route(cx: &ScopeState) -> Option>> { +pub fn use_route(cx: &ScopeState) -> Option { match use_router_internal(cx) { - Some(r) => loop { - if let Some(s) = r.state.try_read() { - break Some(s); - } - }, + Some(r) => Some(r.current()), None => { #[cfg(debug_assertions)] panic!("`use_route` must have access to a parent router"); diff --git a/packages/router/src/hooks/use_router.rs b/packages/router/src/hooks/use_router.rs index 73b89352b..4e87054b2 100644 --- a/packages/router/src/hooks/use_router.rs +++ b/packages/router/src/hooks/use_router.rs @@ -1,200 +1,12 @@ -use async_lock::RwLockReadGuard; -use dioxus::{core::Component, prelude::*}; -use dioxus_router_core::{ - history::{HistoryProvider, MemoryHistory}, - routes::{ContentAtom, Segment}, - Navigator, RouterService, RouterState, RoutingCallback, -}; -use log::error; +use dioxus::prelude::ScopeState; use crate::{ - contexts::router::RouterContext, - prelude::{ - comp, - default_errors::{ - FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, - }, - }, + prelude::RouterContext, routable::Routable, utils::use_router_internal::use_router_internal, }; -/// The basic building block required for all other router components and hooks. -/// -/// This manages a [`dioxus_router_core::RouterService`], which in turn is required for basically -/// all router functionality. All other components and hooks provided by [`dioxus_router`](crate) -/// will only work as/in components nested within a component calling [`use_router`]. -/// -/// Components calling [`use_router`] should not be nested within each other. -/// -/// # Return values -/// This hook returns the current router state and a navigator. For more information about the -/// state, see the [`use_route`](crate::hooks::use_route) hook. For more information about the -/// [`Navigator`], see its own documentation and the [`use_navigate`](crate::hooks::use_navigate) -/// hook. -/// -/// # Panic -/// - When used within a component, that is nested inside another component calling [`use_router`], -/// but only in debug builds. -/// -/// # Example -/// ```rust -/// # use dioxus::prelude::*; -/// # use dioxus_router::prelude::*; -/// fn App(cx: Scope) -> Element { -/// let (_, _) = use_router( -/// &cx, -/// &|| RouterConfiguration { -/// synchronous: true, // asynchronicity not needed for doc test -/// ..Default::default() -/// }, -/// &|| Segment::content(comp(Content)) -/// ); -/// -/// render! { -/// h1 { "App" } -/// Outlet { } -/// } -/// } -/// -/// fn Content(cx: Scope) -> Element { -/// render! { -/// p { "Some content" } -/// } -/// } -/// # let mut vdom = VirtualDom::new(App); -/// # let _ = vdom.rebuild(); -/// # assert_eq!(dioxus_ssr::render(&vdom), "

App

Some content

"); -/// ``` -pub fn use_router<'a>( - cx: &'a ScopeState, - cfg: &dyn Fn() -> RouterConfiguration, - content: &dyn Fn() -> Segment, -) -> ( - RwLockReadGuard<'a, RouterState>, - Navigator, -) { - let (service, state, sender) = cx.use_hook(|| { - #[allow(unreachable_code, unused_variables)] - if let Some(outer) = cx.consume_context::() { - let msg = "components using `use_router` should not be nested within each other"; - error!("{msg}, inner will be inactive and transparent"); - #[cfg(debug_assertions)] - panic!("{}", msg); - return (None, outer.state, outer.sender); - } - - let cfg = cfg(); - let content = content(); - - let (mut service, sender, state) = RouterService::new( - content, - cfg.history, - cx.schedule_update_any(), - cfg.on_update, - cfg.failure_external_navigation, - cfg.failure_named_navigation, - cfg.failure_redirection_limit, - ); - - cx.provide_context(RouterContext { - state: state.clone(), - sender: sender.clone(), - }); - - ( - if cfg.synchronous { - service.init(); - Some(service) - } else { - cx.spawn(async move { service.run().await }); - None - }, - state, - sender, - ) - }); - - if let Some(service) = service { - service.run_current(); - } - - ( - loop { - if let Some(state) = state.try_read() { - break state; - } - }, - sender.clone().into(), - ) -} - -/// Global configuration options for the router. -/// -/// This implements [`Default`], so you can use it like this: -/// ```rust,no_run -/// # use dioxus_router::prelude::RouterConfiguration; -/// let cfg = RouterConfiguration { -/// synchronous: false, -/// ..Default::default() -/// }; -/// ``` -pub struct RouterConfiguration { - /// A component to render when an external navigation fails. - /// - /// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part - /// of the public API. Do not confuse it with - /// [`dioxus_router_core::prelude::FailureExternalNavigation`]. - pub failure_external_navigation: ContentAtom, - /// A component to render when a named navigation fails. - /// - /// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of - /// the public API. Do not confuse it with - /// [`dioxus_router_core::prelude::FailureNamedNavigation`]. - pub failure_named_navigation: ContentAtom, - /// A component to render when the redirect limit is reached. - /// - /// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of - /// the public API. Do not confuse it with - /// [`dioxus_router_core::prelude::FailureRedirectionLimit`]. - pub failure_redirection_limit: ContentAtom, - /// The [`HistoryProvider`] the router should use. - /// - /// Defaults to a default [`MemoryHistory`]. - pub history: Box, - /// A function to be called whenever the routing is updated. - /// - /// The callback is invoked after the routing is updated, but before components and hooks are - /// updated. - /// - /// If the callback returns a [`NavigationTarget`] the router will replace the current location - /// with it. If no navigation failure was triggered, the router will then updated dependent - /// components and hooks. - /// - /// The callback is called no more than once per rerouting. It will not be called if a - /// navigation failure occurs. - /// - /// Defaults to [`None`]. - /// - /// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget - pub on_update: Option>, - /// Whether the router should run in synchronous mode. - /// - /// If [`true`], the router will only update its state whenever the component using the - /// [`use_router`] hook rerenders. If [`false`], an asynchronous task is launched and the router - /// will update whenever it receives new input. - /// - /// Defaults to [`false`]. - pub synchronous: bool, -} - -impl Default for RouterConfiguration { - fn default() -> Self { - Self { - failure_external_navigation: comp(FailureExternalNavigation), - failure_named_navigation: comp(FailureNamedNavigation), - failure_redirection_limit: comp(FailureRedirectionLimit), - history: Box::::default(), - on_update: None, - synchronous: false, - } - } +/// A hook that provides access to information about the router +pub fn use_router(cx: &ScopeState) -> &RouterContext { + use_router_internal(cx) + .as_ref() + .expect("use_route must have access to a router") } diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 0d563e7fd..eaf81e9e5 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -1,6 +1,10 @@ #![doc = include_str!("../README.md")] // cannot use forbid, because props derive macro generates #[allow(missing_docs)] #![deny(missing_docs)] +#![allow(non_snake_case)] + +pub mod navigation; +pub mod routable; /// Components interacting with the router. pub mod components { @@ -14,19 +18,23 @@ pub mod components { mod outlet; pub use outlet::*; + + mod router; + pub use router::*; } mod contexts { + pub(crate) mod outlet; pub(crate) mod router; + pub use router::*; } -pub use dioxus_router_core::history; +mod router_cfg; + +pub mod history; /// Hooks for interacting with the router in components. pub mod hooks { - mod use_navigate; - pub use use_navigate::*; - mod use_router; pub use use_router::*; @@ -36,18 +44,10 @@ pub mod hooks { /// A collection of useful items most applications might need. pub mod prelude { - pub use dioxus_router_core::prelude::*; - pub use crate::components::*; + pub use crate::contexts::*; pub use crate::hooks::*; - - /// Wrap a [`Component`](dioxus::core::Component) inside a [`ContentAtom`]. - /// - /// This is purely a convenience function. - #[must_use] - pub fn comp(component: dioxus::core::Component) -> ContentAtom { - ContentAtom(component) - } + pub use dioxus_router_macro::routable; } mod utils { diff --git a/packages/router/src/navigation.rs b/packages/router/src/navigation.rs new file mode 100644 index 000000000..263059a87 --- /dev/null +++ b/packages/router/src/navigation.rs @@ -0,0 +1,69 @@ +//! Types pertaining to navigation. + +use std::{fmt::Display, str::FromStr}; + +use url::{ParseError, Url}; + +use crate::routable::Routable; + +/// A target for the router to navigate to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NavigationTarget { + /// An internal path that the router can navigate to by itself. + /// + /// ```rust + /// # use dioxus_router_core::navigation::NavigationTarget; + /// let explicit = NavigationTarget::Internal(String::from("/internal")); + /// let implicit: NavigationTarget = "/internal".into(); + /// assert_eq!(explicit, implicit); + /// ``` + Internal(R), + /// An external target that the router doesn't control. + /// + /// ```rust + /// # use dioxus_router_core::navigation::NavigationTarget; + /// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/")); + /// let implicit: NavigationTarget = "https://dioxuslabs.com/".into(); + /// assert_eq!(explicit, implicit); + /// ``` + External(String), +} + +impl From for NavigationTarget { + fn from(value: R) -> Self { + Self::Internal(value) + } +} + +impl Display for NavigationTarget { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NavigationTarget::Internal(r) => write!(f, "{}", r), + NavigationTarget::External(s) => write!(f, "{}", s), + } + } +} + +/// An error that can occur when parsing a [`NavigationTarget`]. +pub enum NavigationTargetParseError { + /// A URL that is not valid. + InvalidUrl(ParseError), + /// An internal URL that is not valid. + InvalidInternalURL(::Err), +} + +impl FromStr for NavigationTarget { + type Err = NavigationTargetParseError; + + fn from_str(s: &str) -> Result { + match Url::parse(s) { + Ok(_) => Ok(Self::External(s.to_string())), + Err(ParseError::RelativeUrlWithoutBase) => { + Ok(Self::Internal(R::from_str(s).map_err(|e| { + NavigationTargetParseError::InvalidInternalURL(e) + })?)) + } + Err(e) => Err(NavigationTargetParseError::InvalidUrl(e)), + } + } +} diff --git a/packages/router-core/src/router.rs b/packages/router/src/routable.rs similarity index 51% rename from packages/router-core/src/router.rs rename to packages/router/src/routable.rs index 6f8f5bbb8..3cfcdbcec 100644 --- a/packages/router-core/src/router.rs +++ b/packages/router/src/routable.rs @@ -1,11 +1,14 @@ +//! # Routable + #![allow(non_snake_case)] -use crate::history::HistoryProvider; use dioxus::prelude::*; -use std::{cell::RefCell, rc::Rc, str::FromStr, sync::Arc}; +use std::str::FromStr; +/// An error that occurs when parsing a route #[derive(Debug, PartialEq)] pub struct RouteParseError { + /// The attempted routes that failed to match pub attempted_routes: Vec, } @@ -19,39 +22,9 @@ impl std::fmt::Display for RouteParseError { } } -#[derive(Clone)] -pub struct Router { - subscribers: Rc>>, - update_any: Arc, - history: Rc, - route: Rc>>>, -} - -impl Router { - fn set_route(&self, route: R) - where - R::Err: std::fmt::Display, - { - *self.route.borrow_mut() = Some(Rc::new(route)); - for subscriber in self.subscribers.borrow().iter() { - (self.update_any)(*subscriber); - } - } -} - -fn use_router(cx: &ScopeState) -> &Router { - use_context(cx).unwrap() -} - -fn use_route(cx: &ScopeState) -> Rc { - let router = use_router(cx); - cx.use_hook(|| { - router.subscribers.borrow_mut().push(cx.scope_id()); - }); - router.route.borrow().clone().unwrap() -} - +/// Something that can be created from a query string pub trait FromQuery { + /// Create an instance of `Self` from a query string fn from_query(query: &str) -> Self; } @@ -61,9 +34,12 @@ impl From<&'a str>> FromQuery for T { } } +/// Something that can be created from a route segment pub trait FromRouteSegment: Sized { + /// The error that can occur when parsing a route segment type Err; + /// Create an instance of `Self` from a route segment fn from_route_segment(route: &str) -> Result; } @@ -78,7 +54,9 @@ where } } +/// Something that can be converted to route segments pub trait ToRouteSegments { + /// Display the route segments fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; } @@ -95,9 +73,12 @@ where } } +/// Something that can be created from route segments pub trait FromRouteSegments: Sized { + /// The error that can occur when parsing route segments type Err; + /// Create an instance of `Self` from route segments fn from_route_segments(segments: &[&str]) -> Result; } @@ -112,14 +93,9 @@ impl> FromRouteSegments for I { } } -#[derive(Props, PartialEq)] -pub struct RouterProps { - pub current_route: String, -} -pub trait Routable: std::fmt::Display + std::str::FromStr + 'static -where - ::Err: std::fmt::Display, -{ +/// Something that can be routed to +pub trait Routable: std::fmt::Display + std::str::FromStr + 'static { + /// Render the route at the given level fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>; } @@ -148,63 +124,3 @@ where self.render(cx, level) } } - -#[derive(Clone)] -struct OutletContext { - current_level: usize, -} - -fn use_outlet_context(cx: &ScopeState) -> &OutletContext { - let outlet_context = use_context(cx).unwrap(); - outlet_context -} - -impl OutletContext { - fn render(cx: &ScopeState) -> Element<'_> { - let outlet = use_outlet_context(cx); - let current_level = outlet.current_level; - cx.provide_context({ - OutletContext { - current_level: current_level + 1, - } - }); - - use_route(cx).render(cx, current_level) - } -} - -pub fn Outlet(cx: Scope) -> Element { - OutletContext::render(cx) -} - -pub fn Router( - cx: Scope, -) -> Element -where - ::Err: std::fmt::Display, -{ - let current_route = R::from_str(&cx.props.current_route); - let router = use_context_provider(cx, || Router { - subscribers: Rc::default(), - update_any: cx.schedule_update_any(), - history: Rc::::default(), - route: Rc::new(RefCell::new(None)), - }); - - use_context_provider(cx, || OutletContext { current_level: 1 }); - - match current_route { - Ok(current_route) => { - router.set_route(current_route); - - router.route.borrow().as_ref().unwrap().render(cx, 0) - } - Err(err) => { - render! { - pre { - "{err}" - } - } - } - } -} diff --git a/packages/router/src/router_cfg.rs b/packages/router/src/router_cfg.rs new file mode 100644 index 000000000..1706f5b14 --- /dev/null +++ b/packages/router/src/router_cfg.rs @@ -0,0 +1,71 @@ +use crate::contexts::router::RoutingCallback; +use crate::history::{HistoryProvider, MemoryHistory}; +use crate::routable::Routable; +use dioxus::prelude::*; + +use crate::prelude::default_errors::{ + FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, +}; + +/// Global configuration options for the router. +/// +/// This implements [`Default`], so you can use it like this: +/// ```rust,no_run +/// # use dioxus_router::prelude::RouterConfiguration; +/// let cfg = RouterConfiguration { +/// synchronous: false, +/// ..Default::default() +/// }; +/// ``` +pub struct RouterConfiguration { + /// A component to render when an external navigation fails. + /// + /// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part + /// of the public API. Do not confuse it with + /// [`dioxus_router_core::prelude::FailureExternalNavigation`]. + pub failure_external_navigation: fn(Scope) -> Element, + /// A component to render when a named navigation fails. + /// + /// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of + /// the public API. Do not confuse it with + /// [`dioxus_router_core::prelude::FailureNamedNavigation`]. + pub failure_named_navigation: fn(Scope) -> Element, + /// A component to render when the redirect limit is reached. + /// + /// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of + /// the public API. Do not confuse it with + /// [`dioxus_router_core::prelude::FailureRedirectionLimit`]. + pub failure_redirection_limit: fn(Scope) -> Element, + /// The [`HistoryProvider`] the router should use. + /// + /// Defaults to a default [`MemoryHistory`]. + pub history: Box>, + /// A function to be called whenever the routing is updated. + /// + /// The callback is invoked after the routing is updated, but before components and hooks are + /// updated. + /// + /// If the callback returns a [`NavigationTarget`] the router will replace the current location + /// with it. If no navigation failure was triggered, the router will then updated dependent + /// components and hooks. + /// + /// The callback is called no more than once per rerouting. It will not be called if a + /// navigation failure occurs. + /// + /// Defaults to [`None`]. + /// + /// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget + pub on_update: Option>, +} + +impl Default for RouterConfiguration { + fn default() -> Self { + Self { + failure_external_navigation: FailureExternalNavigation::, + failure_named_navigation: FailureNamedNavigation::, + failure_redirection_limit: FailureRedirectionLimit::, + history: Box::>::default(), + on_update: None, + } + } +} diff --git a/packages/router-core/src/utils/sitemap.rs b/packages/router/src/utils/sitemap.rs similarity index 100% rename from packages/router-core/src/utils/sitemap.rs rename to packages/router/src/utils/sitemap.rs diff --git a/packages/router/src/utils/use_router_internal.rs b/packages/router/src/utils/use_router_internal.rs index 0bee3406d..ab1a8e37c 100644 --- a/packages/router/src/utils/use_router_internal.rs +++ b/packages/router/src/utils/use_router_internal.rs @@ -1,9 +1,6 @@ -use std::sync::Arc; +use dioxus::prelude::{ScopeId, ScopeState}; -use dioxus::prelude::ScopeState; -use dioxus_router_core::RouterMessage; - -use crate::contexts::router::RouterContext; +use crate::{contexts::router::RouterContext, routable::Routable}; /// A private hook to subscribe to the router. /// @@ -15,16 +12,25 @@ use crate::contexts::router::RouterContext; /// - Otherwise [`Some`]. /// /// [`use_router`]: crate::hooks::use_router -pub(crate) fn use_router_internal(cx: &ScopeState) -> &mut Option { - let id = cx.use_hook(|| Arc::new(cx.scope_id())); +pub(crate) fn use_router_internal(cx: &ScopeState) -> &Option> { + let inner = cx.use_hook(|| { + let router = cx.consume_context::>()?; - cx.use_hook(|| { - let router = cx.consume_context::()?; + let id = cx.scope_id(); + router.subscribe(id); - let _ = router - .sender - .unbounded_send(RouterMessage::Subscribe(id.clone())); + Some(Subscription { router, id }) + }); + cx.use_hook(|| inner.as_ref().map(|s| s.router.clone())) +} - Some(router) - }) +struct Subscription { + router: RouterContext, + id: ScopeId, +} + +impl Drop for Subscription { + fn drop(&mut self) { + self.router.unsubscribe(self.id); + } }