From e26ff6a5c05fcea9e8e68558be432a972fefba6e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 12 May 2023 09:18:55 -0500 Subject: [PATCH] delete unneeded files --- packages/router-core/src/lib.rs | 39 +- packages/router-core/src/name/mod.rs | 76 --- packages/router-core/src/name/segments.rs | 133 ----- packages/router-core/src/navigation.rs | 2 - packages/router-core/src/outlet.rs | 201 -------- packages/router-core/src/router.rs | 163 ++++++ packages/router-core/src/routes/atom.rs | 29 -- packages/router-core/src/routes/content.rs | 306 ----------- packages/router-core/src/routes/matcher.rs | 25 - .../router-core/src/routes/parameter_route.rs | 275 ---------- packages/router-core/src/routes/route.rs | 247 --------- packages/router-core/src/routes/segment.rs | 458 ----------------- packages/router-core/src/utils/name.rs | 144 ------ packages/router-core/src/utils/route.rs | 483 ------------------ packages/router-core/src/utils/sitemap.rs | 1 - packages/router-core/src/utils/target.rs | 120 ----- packages/router-macro/Cargo.toml | 23 + packages/router-macro/README.md | 0 packages/router-macro/src/lib.rs | 206 ++++++++ packages/router-macro/src/route.rs | 338 ++++++++++++ packages/router-macro/src/route_tree.rs | 258 ++++++++++ 21 files changed, 990 insertions(+), 2537 deletions(-) delete mode 100644 packages/router-core/src/name/mod.rs delete mode 100644 packages/router-core/src/name/segments.rs delete mode 100644 packages/router-core/src/outlet.rs create mode 100644 packages/router-core/src/router.rs delete mode 100644 packages/router-core/src/routes/atom.rs delete mode 100644 packages/router-core/src/routes/content.rs delete mode 100644 packages/router-core/src/routes/matcher.rs delete mode 100644 packages/router-core/src/routes/parameter_route.rs delete mode 100644 packages/router-core/src/routes/route.rs delete mode 100644 packages/router-core/src/routes/segment.rs delete mode 100644 packages/router-core/src/utils/name.rs delete mode 100644 packages/router-core/src/utils/route.rs delete mode 100644 packages/router-core/src/utils/target.rs create mode 100644 packages/router-macro/Cargo.toml create mode 100644 packages/router-macro/README.md create mode 100644 packages/router-macro/src/lib.rs create mode 100644 packages/router-macro/src/route.rs create mode 100644 packages/router-macro/src/route_tree.rs diff --git a/packages/router-core/src/lib.rs b/packages/router-core/src/lib.rs index 0ae87a2ec..da75864b5 100644 --- a/packages/router-core/src/lib.rs +++ b/packages/router-core/src/lib.rs @@ -3,38 +3,14 @@ pub mod history; -mod name; -pub use name::*; +mod router; +pub use router::*; pub mod navigation; mod navigator; pub use navigator::*; -mod outlet; -pub use outlet::*; - -/// Types for defining the available routes. -pub mod routes { - mod atom; - pub use atom::*; - - mod content; - pub use content::*; - - mod matcher; - pub use matcher::*; - - mod route; - pub use route::*; - - mod segment; - pub use segment::*; - - mod parameter_route; - pub use parameter_route::*; -} - mod service; pub use service::*; @@ -42,24 +18,13 @@ mod state; pub use state::*; mod utils { - mod name; - pub use name::*; - - mod route; - pub use route::*; - mod sitemap; pub use sitemap::*; - - mod target; - pub use target::*; } /// A collection of useful types most applications might need. pub mod prelude { - pub use crate::name::*; pub use crate::navigation::*; - pub use crate::routes::*; /// An external navigation failure. /// diff --git a/packages/router-core/src/name/mod.rs b/packages/router-core/src/name/mod.rs deleted file mode 100644 index a00a71ad9..000000000 --- a/packages/router-core/src/name/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::{ - any::{type_name, TypeId}, - fmt::Display, - hash::Hash, -}; - -pub(crate) mod segments; - -/// A combination of a types [`TypeId`] and its name. -/// -/// This is used inside the router wherever a name is needed. This has the advantage that typos will -/// be caught by the compiler. -/// -/// **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, -/// like this. -/// -/// ```rust -/// # use dioxus_router_core::Name; -/// struct SomeName; -/// let name = Name::of::(); -/// ``` -#[derive(Clone, Debug)] -pub struct Name { - id: TypeId, - name: &'static str, -} - -impl Name { - /// Get the [`Name`] of `T`. - /// - /// ```rust - /// # use dioxus_router_core::Name; - /// struct SomeName; - /// let name = Name::of::(); - /// ``` - #[must_use] - pub fn of() -> Self { - Self { - id: TypeId::of::(), - name: type_name::(), - } - } -} - -impl PartialEq for Name { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for Name {} - -impl PartialOrd for Name { - fn partial_cmp(&self, other: &Self) -> Option { - self.id.partial_cmp(&other.id) - } -} - -impl Ord for Name { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.id.cmp(&other.id) - } -} - -impl Display for Name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Hash for Name { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} diff --git a/packages/router-core/src/name/segments.rs b/packages/router-core/src/name/segments.rs deleted file mode 100644 index 6acdb1d2f..000000000 --- a/packages/router-core/src/name/segments.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::collections::BTreeMap; - -use crate::{ - prelude::RootIndex, - routes::{ParameterRoute, Route, Segment}, - Name, -}; - -pub type NameMap = BTreeMap>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum NamedSegment { - Fixed(String), - Parameter(Name), -} - -impl NamedSegment { - pub fn from_segment(segment: &Segment) -> NameMap { - let mut res = BTreeMap::new(); - res.insert(Name::of::(), vec![]); - Self::from_segment_inner(Vec::new(), segment, &mut res); - res - } - - fn from_segment_inner( - current: Vec, - segment: &Segment, - result: &mut NameMap, - ) { - for (p, r) in &segment.fixed { - Self::from_route(current.clone(), p, r, result); - } - - for (_, r) in &segment.matching { - Self::from_parameter_route(current.clone(), r, result); - } - - if let Some(r) = &segment.catch_all { - Self::from_parameter_route(current, r, result); - } - } - - fn from_route( - mut current: Vec, - path: &str, - route: &Route, - result: &mut NameMap, - ) { - current.push(Self::Fixed(path.to_string())); - - if let Some(n) = &route.name { - debug_assert!(!result.contains_key(n), "duplicate name: {n}"); - result.entry(n.clone()).or_insert_with(|| current.clone()); - } - - if let Some(n) = &route.nested { - Self::from_segment_inner(current, n, result); - } - } - - fn from_parameter_route( - mut current: Vec, - route: &ParameterRoute, - result: &mut NameMap, - ) { - current.push(Self::Parameter(route.key.clone())); - - if let Some(n) = &route.name { - debug_assert!(!result.contains_key(n), "duplicate name: {n}"); - result.entry(n.clone()).or_insert_with(|| current.clone()); - } - - if let Some(n) = &route.nested { - Self::from_segment_inner(current, n, result); - } - } -} - -#[cfg(test)] -mod tests { - use super::{NamedSegment::*, *}; - - #[test] - fn create_map() { - assert_eq!( - NamedSegment::from_segment( - &Segment::<&'static str>::empty() - .fixed( - "fixed", - Route::empty() - .name::() - .nested(Segment::empty().fixed("nested", Route::empty().name::())) - ) - .matching( - String::from(""), - ParameterRoute::empty::().name::().nested( - Segment::empty().matching( - String::from(""), - ParameterRoute::empty::().name::() - ) - ) - ) - .catch_all(ParameterRoute::empty::().name::().nested( - Segment::empty().catch_all(ParameterRoute::empty::().name::()) - )) - ), - { - let mut r = BTreeMap::new(); - r.insert(Name::of::(), vec![]); - - r.insert(Name::of::(), vec![Fixed(String::from("fixed"))]); - r.insert( - Name::of::(), - vec![Fixed(String::from("fixed")), Fixed(String::from("nested"))], - ); - - r.insert(Name::of::(), vec![Parameter(Name::of::())]); - r.insert( - Name::of::(), - vec![Parameter(Name::of::()), Parameter(Name::of::())], - ); - - r.insert(Name::of::(), vec![Parameter(Name::of::())]); - r.insert( - Name::of::(), - vec![Parameter(Name::of::()), Parameter(Name::of::())], - ); - - r - } - ) - } -} diff --git a/packages/router-core/src/navigation.rs b/packages/router-core/src/navigation.rs index 65559812f..e6bd8bc71 100644 --- a/packages/router-core/src/navigation.rs +++ b/packages/router-core/src/navigation.rs @@ -4,8 +4,6 @@ use std::{collections::HashMap, str::FromStr}; use url::{ParseError, Url}; -use crate::Name; - /// A target for the router to navigate to. #[derive(Clone, Debug, PartialEq, Eq)] pub enum NavigationTarget { diff --git a/packages/router-core/src/outlet.rs b/packages/router-core/src/outlet.rs deleted file mode 100644 index 2008fdefc..000000000 --- a/packages/router-core/src/outlet.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::collections::BTreeMap; - -use crate::Name; - -/// Information outlets can use to find out what to render. -/// -/// Outlets (which must be implemented by crates tying dioxus-router-core to UI crates) can use this -/// information to find out how deeply nested they are within other outlets, and communicate the -/// same to outlets nested inside them. -#[derive(Debug, Default, Clone)] -pub struct OutletData { - main: Option, - named: BTreeMap, -} - -impl OutletData { - /// Create some [`OutletData`] nested one level deeper and get the current depth. - /// - /// ```rust - /// # use dioxus_router_core::{Name, OutletData}; - /// let mut d = OutletData::default(); - /// let (m, a, n); - /// (m, d) = d.next(&None); - /// (a, d) = d.next(&None); - /// (n, d) = d.next(&Some(Name::of::())); - /// - /// assert_eq!(m, 0); - /// assert_eq!(a, 1); - /// assert_eq!(n, 0); - /// ``` - pub fn next(&self, name: &Option) -> (usize, Self) { - let mut next = self.clone(); - - let depth = next.depth(name).map(|d| d + 1).unwrap_or(0); - - next.set_depth(name, depth); - - (depth, next) - } - - /// Get the current depth for `name`. - /// - /// ```rust - /// # use dioxus_router_core::OutletData; - /// let mut d = OutletData::default(); - /// let b = d.depth(&None); - /// d.set_depth(&None, 18); - /// let a = d.depth(&None); - /// - /// assert_eq!(b, None); - /// assert_eq!(a, Some(18)); - /// ``` - pub fn depth(&self, name: &Option) -> Option { - match name { - None => self.main, - Some(n) => self.named.get(n).copied(), - } - } - - /// Set the depth for `name`. - /// - /// ```rust - /// # use dioxus_router_core::OutletData; - /// let mut d = OutletData::default(); - /// let b = d.depth(&None); - /// d.set_depth(&None, 18); - /// let a = d.depth(&None); - /// - /// assert_eq!(b, None); - /// assert_eq!(a, Some(18)); - /// ``` - pub fn set_depth(&mut self, name: &Option, depth: usize) { - match name { - None => self.main = Some(depth), - Some(n) => _ = self.named.insert(n.clone(), depth), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn test_data() -> OutletData { - let mut named = BTreeMap::new(); - named.insert(Name::of::(), 0); - named.insert(Name::of::(), 8); - named.insert(Name::of::(), 16); - named.insert(Name::of::(), 32); - named.insert(Name::of::(), 64); - - OutletData { - main: Some(18), - named, - } - } - - #[test] - fn default() { - let d = OutletData::default(); - - assert!(d.main.is_none()); - assert!(d.named.is_empty()); - } - - #[test] - fn depth() { - let td = test_data(); - - assert_eq!(td.depth(&None), Some(18)); - assert_eq!(td.depth(&Some(Name::of::())), Some(0)); - - assert_eq!(td.depth(&Some(Name::of::())), Some(8)); - assert_eq!(td.depth(&Some(Name::of::())), Some(16)); - assert_eq!(td.depth(&Some(Name::of::())), Some(32)); - assert_eq!(td.depth(&Some(Name::of::())), Some(64)); - - assert_eq!(td.depth(&Some(Name::of::())), None); - assert_eq!(td.depth(&Some(Name::of::())), None); - assert_eq!(td.depth(&Some(Name::of::())), None); - assert_eq!(td.depth(&Some(Name::of::())), None); - } - - #[test] - fn set_depth() { - let mut td = test_data(); - - // set - td.set_depth(&None, 0); - td.set_depth(&Some(Name::of::()), 1); - - td.set_depth(&Some(Name::of::()), 2); - td.set_depth(&Some(Name::of::()), 4); - td.set_depth(&Some(Name::of::()), 8); - td.set_depth(&Some(Name::of::()), 16); - - td.set_depth(&Some(Name::of::()), 32); - td.set_depth(&Some(Name::of::()), 64); - td.set_depth(&Some(Name::of::()), 128); - td.set_depth(&Some(Name::of::()), 256); - - // check - assert_eq!(td.depth(&None), Some(0)); - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 1); - - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 2); - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 4); - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 8); - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 16); - - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 32); - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 64); - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 128); - assert_eq!(*td.named.get(&Name::of::()).unwrap(), 256); - } - - #[test] - fn next() { - let td = test_data(); - - let (current, next) = td.next(&None); - assert_eq!(current, 19); - assert_eq!(next.depth(&None), Some(19)); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 1); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 1); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 9); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 9); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 17); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 17); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 33); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 33); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 65); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 65); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 0); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 0); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 0); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 0); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 0); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 0); - - let (current, next) = td.next(&Some(Name::of::())); - assert_eq!(current, 0); - assert_eq!(*next.named.get(&Name::of::()).unwrap(), 0); - } -} diff --git a/packages/router-core/src/router.rs b/packages/router-core/src/router.rs new file mode 100644 index 000000000..003b1563f --- /dev/null +++ b/packages/router-core/src/router.rs @@ -0,0 +1,163 @@ +use crate::history::HistoryProvider; +use std::str::FromStr; + +#[derive(Debug, PartialEq)] +struct RouteParseError { + attempted_routes: Vec, +} + +impl std::fmt::Display for RouteParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Route did not match:\nAttempted Matches:\n")?; + for (i, route) in self.attempted_routes.iter().enumerate() { + writeln!(f, "{}) {route}", i + 1)?; + } + Ok(()) + } +} + +struct Router +where + ::Err: std::fmt::Display, +{ + history: H, + route: R, +} + +impl Router +where + ::Err: std::fmt::Display, +{ + fn new(history: H) -> Result { + let path = history.current_path(); + Ok(Self { + history, + route: R::from_str(path.as_str())?, + }) + } +} + +// #[derive(Props, PartialEq)] +// struct RouterProps { +// current_route: String, +// } + +trait Routable: FromStr + std::fmt::Display + Clone +where + ::Err: std::fmt::Display, +{ +// fn render(self, cx: &ScopeState) -> Element; + +// fn comp(cx: Scope) -> Element +// where +// Self: 'static, +// { +// let router = Self::from_str(&cx.props.current_route); +// match router { +// Ok(router) => router.render(cx), +// Err(err) => { +// render! {pre { +// "{err}" +// }} +// } +// } +// } +} + +#[derive(Routable, Clone, Debug, PartialEq)] +enum Route { + #[route("/(dynamic)")] + Route1 { dynamic: String }, + #[route("/hello_world")] + Route2 {}, + #[redirect("/(dynamic)/hello_world")] + #[route("/hello_world/(dynamic)")] + Route3 { dynamic: u32 }, + #[route("/(number1)/(number2)")] + Route4 { number1: u32, number2: u32 }, + #[route("/")] + Route5 {}, +} + +#[test] +fn display_works() { + let route = Route::Route1 { + dynamic: "hello".to_string(), + }; + + assert_eq!(route.to_string(), "/hello"); + + let route = Route::Route3 { 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"); +} + +#[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 { dynamic: 1234 })); + let w = "/hello_world/1234/"; + assert_eq!(Route::from_str(w), Ok(Route::Route3 { dynamic: 1234 })); + + let w = "/hello_world2"; + assert_eq!( + Route::from_str(w), + Ok(Route::Route1 { + dynamic: "hello_world2".to_string() + }) + ); + + let w = "/hello_world/-1"; + match Route::from_str(w) { + Ok(r) => panic!("should not parse {r:?}"), + Err(err) => println!("{err}"), + } +} + +#[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 num in 0..100 { + let route = Route::Route3 { dynamic: num }; + 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)); + } + } +} diff --git a/packages/router-core/src/routes/atom.rs b/packages/router-core/src/routes/atom.rs deleted file mode 100644 index 20ddd5f1e..000000000 --- a/packages/router-core/src/routes/atom.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::fmt::Debug; - -/// The basic content type of dioxus-router-core. -/// -/// For actual route definitions this type is basically useless. However, it allows the router to -/// support convenience [`From`] implementations which can tell content and redirects apart. -/// -/// ```rust -/// # use dioxus_router_core::routes::ContentAtom; -/// let content = ContentAtom("some content"); -/// ``` -#[derive(Clone)] -pub struct ContentAtom(pub T) -where - T: Clone; - -impl Debug for ContentAtom { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("ContentAtom").field(&self.0).finish() - } -} - -impl PartialEq for ContentAtom { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for ContentAtom {} diff --git a/packages/router-core/src/routes/content.rs b/packages/router-core/src/routes/content.rs deleted file mode 100644 index 6082f5c32..000000000 --- a/packages/router-core/src/routes/content.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::{collections::BTreeMap, fmt::Debug}; - -use crate::{navigation::NavigationTarget, Name}; - -use super::ContentAtom; - -/// The content of a route. -#[derive(Clone)] -pub enum RouteContent { - /// Some actual content. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, RouteContent}; - /// let explicit = RouteContent::Content(ContentAtom("content")); - /// let implicit: RouteContent<_> = ContentAtom("content").into(); - /// assert_eq!(explicit, implicit); - /// ``` - Content(ContentAtom), - /// A redirect to another location. - /// - /// ```rust - /// # use dioxus_router_core::routes::RouteContent; - /// let explicit = RouteContent::<&'static str>::Redirect("/some_path".into()); - /// let implicit: RouteContent<&'static str> = "/some_path".into(); - /// assert_eq!(explicit, implicit); - /// ``` - Redirect(NavigationTarget), - /// Multiple content. - /// - /// This may contain some main content, and named content. - /// - /// ```rust - /// # use std::collections::BTreeMap; - /// # use dioxus_router_core::{Name, routes::{ContentAtom, multi, RouteContent}}; - /// let explicit = RouteContent::MultiContent{ - /// main: Some(ContentAtom("main")), - /// named: { - /// let mut r = BTreeMap::new(); - /// r.insert(Name::of::(), ContentAtom("first")); - /// r.insert(Name::of::(), ContentAtom("second")); - /// r - /// } - /// }; - /// let implicit = multi(Some(ContentAtom("main"))) - /// .add_named::(ContentAtom("first")) - /// .add_named::(ContentAtom("second")); - /// assert_eq!(explicit, implicit); - /// ``` - MultiContent { - /// The main content. - main: Option>, - /// Named content. - named: BTreeMap>, - }, -} - -impl RouteContent { - /// Create a new [`RouteContent::MultiContent`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, multi, RouteContent}; - /// let content = multi(Some(ContentAtom("main"))) - /// .add_named::(ContentAtom("first")) - /// .add_named::(ContentAtom("second")); - /// ``` - pub fn multi(main: Option>) -> Self { - Self::MultiContent { - main, - named: BTreeMap::new(), - } - } - - /// Add some named content to a [`RouteContent::MultiContent`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, multi, RouteContent}; - /// let content = multi(Some(ContentAtom("main"))) - /// .add_named::(ContentAtom("first")) - /// .add_named::(ContentAtom("second")); - /// ``` - /// - /// **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, like this. - /// - /// # Error Handling - /// An error occurs if `self` is any other [`RouteContent`] variant then - /// [`RouteContent::MultiContent`]. In _debug mode_, this will trigger a panic. In _release - /// mode_ nothing will happen. - pub fn add_named(mut self, content: ContentAtom) -> Self { - debug_assert!( - matches!(self, Self::MultiContent { main: _, named: _ }), - "add_named only available for MultiContent" - ); - - if let Self::MultiContent { main: _, named } = &mut self { - let name = Name::of::(); - debug_assert!( - !named.contains_key(&name), - "name not unique within MultiContent: {name}" - ); - named.entry(name).or_insert(content); - } - - self - } -} - -/// Create a new [`RouteContent::MultiContent`]. -/// -/// ```rust -/// # use dioxus_router_core::routes::{ContentAtom, multi, RouteContent}; -/// let content = multi(Some(ContentAtom("main"))) -/// .add_named::(ContentAtom("first")) -/// .add_named::(ContentAtom("second")); -/// ``` -/// -/// This is a shortcut for [`RouteContent`]s `multi` method. -pub fn multi(main: Option>) -> RouteContent { - RouteContent::multi(main) -} - -#[cfg(test)] -pub(crate) fn test_content() -> RouteContent<&'static str> { - RouteContent::Content(ContentAtom("test content")) -} - -impl From> for RouteContent { - fn from(c: ContentAtom) -> Self { - Self::Content(c) - } -} - -impl> From for RouteContent { - fn from(nt: N) -> Self { - Self::Redirect(nt.into()) - } -} - -impl Debug for RouteContent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Content(_) => f.debug_tuple("Content").finish(), - Self::Redirect(nt) => f.debug_tuple("Target").field(nt).finish(), - Self::MultiContent { main: _, named } => { - f.debug_tuple("MultiContent").field(&named.keys()).finish() - } - } - } -} - -impl PartialEq for RouteContent { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Content(l0), Self::Content(r0)) => l0 == r0, - (Self::Redirect(l), Self::Redirect(r)) => l == r, - ( - Self::MultiContent { - main: lm, - named: ln, - }, - Self::MultiContent { - main: rm, - named: rn, - }, - ) => lm == rm && ln == rn, - _ => false, - } - } -} - -impl Eq for RouteContent {} - -#[cfg(test)] -mod tests { - use crate::{navigation::named, Name}; - - use super::*; - - #[test] - fn content_from_content() { - assert_eq!( - Into::>::into(ContentAtom("test content")), - test_content() - ) - } - - #[test] - fn content_from_target() { - assert_eq!( - Into::>::into(named::()), - RouteContent::<&str>::Redirect(NavigationTarget::Named { - name: Name::of::(), - parameters: Default::default(), - query: None - }) - ) - } - - #[test] - fn content_from_string() { - let internal = "/test"; - assert_eq!( - Into::>::into(internal.to_string()), - RouteContent::Redirect(internal.into()) - ); - - let external = "https://dioxuslabs.com/"; - assert_eq!( - Into::>::into(external.to_string()), - RouteContent::Redirect(external.into()) - ) - } - - #[test] - fn content_from_str() { - let internal = "/test"; - assert_eq!( - Into::>::into(internal), - RouteContent::Redirect(internal.into()) - ); - - let external = "https://dioxuslabs.com/"; - assert_eq!( - Into::>::into(external), - RouteContent::Redirect(external.into()) - ) - } - - #[test] - fn multi() { - let c = RouteContent::multi(Some(ContentAtom("test"))); - match c { - RouteContent::MultiContent { main, named } => { - assert_eq!(main, Some(ContentAtom("test"))); - assert!(named.is_empty()); - } - _ => panic!("wrong kind"), - }; - } - - #[test] - fn multi_add() { - let c = RouteContent::multi(None) - .add_named::(ContentAtom("1")) - .add_named::(ContentAtom("2")); - - match c { - RouteContent::MultiContent { main, named } => { - assert!(main.is_none()); - assert_eq!(named, { - let mut r = BTreeMap::new(); - r.insert(Name::of::(), ContentAtom("1")); - r.insert(Name::of::(), ContentAtom("2")); - r - }); - } - _ => panic!("wrong kind"), - }; - } - - #[test] - #[should_panic = "add_named only available for MultiContent"] - #[cfg(debug_assertions)] - fn multi_add_wrong_kind_debug() { - RouteContent::Content(ContentAtom("1")).add_named::(ContentAtom("2")); - } - - #[test] - #[cfg(not(debug_assertions))] - fn multi_add_wrong_kind_release() { - assert_eq!( - RouteContent::Content(ContentAtom("1")).add_named::(ContentAtom("2")), - RouteContent::Content(ContentAtom("1")) - ); - } - - #[test] - #[should_panic = "name not unique within MultiContent: u8"] - #[cfg(debug_assertions)] - fn multi_add_duplicate_debug() { - RouteContent::multi(None) - .add_named::(ContentAtom("1")) - .add_named::(ContentAtom("2")); - } - - #[test] - #[cfg(not(debug_assertions))] - fn multi_add_duplicate_release() { - let c = RouteContent::multi(None) - .add_named::(ContentAtom("1")) - .add_named::(ContentAtom("2")); - - match c { - RouteContent::MultiContent { main, named } => { - assert!(main.is_none()); - assert_eq!(named, { - let mut r = BTreeMap::new(); - r.insert(Name::of::(), ContentAtom("1")); - r - }); - } - _ => panic!("wrong kind"), - }; - } -} diff --git a/packages/router-core/src/routes/matcher.rs b/packages/router-core/src/routes/matcher.rs deleted file mode 100644 index 3ea695045..000000000 --- a/packages/router-core/src/routes/matcher.rs +++ /dev/null @@ -1,25 +0,0 @@ -/// Something that can check whether a string meets a condition. -/// -/// This is used by matching routes (see the [`Segment`](super::Segment) `matching` function for -/// more details) to see if they are active. -pub trait Matcher: std::fmt::Debug { - /// Check whether `segment_value` fulfills the [`Matcher`]s requirement. - fn matches(&self, segment_value: &str) -> bool; -} - -// The following implementation is for test purposes only. It could later be replaced by an -// implementation providing wildcard syntax or something similar. - -#[cfg(test)] -impl Matcher for String { - fn matches(&self, segment_value: &str) -> bool { - self == segment_value - } -} - -#[cfg(feature = "regex")] -impl Matcher for regex::Regex { - fn matches(&self, segment_value: &str) -> bool { - self.is_match(segment_value) - } -} diff --git a/packages/router-core/src/routes/parameter_route.rs b/packages/router-core/src/routes/parameter_route.rs deleted file mode 100644 index cd0d9d5b7..000000000 --- a/packages/router-core/src/routes/parameter_route.rs +++ /dev/null @@ -1,275 +0,0 @@ -use super::{RouteContent, Segment}; -use crate::{ - prelude::{ - FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex, - }, - Name, -}; - -/// A parameter route. -#[derive(Debug)] -pub struct ParameterRoute { - pub(crate) content: Option>, - pub(crate) name: Option, - pub(crate) nested: Option>, - pub(crate) key: Name, -} - -impl ParameterRoute { - /// Create a new [`ParameterRoute`] with `N` as the key. - /// - /// ```rust - /// # use dioxus_router_core::routes::ParameterRoute; - /// let route: ParameterRoute<&'static str> = ParameterRoute::empty::(); - /// ``` - /// - /// **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 keys, and create unit structs if - /// needed. - pub fn empty() -> Self { - Self { - content: None, - name: None, - nested: None, - key: Name::of::(), - } - } - - /// Create a new [`ParameterRoute`] with `N` as the key and some `content`. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute}; - /// let route = ParameterRoute::content::(ContentAtom("some content")); - /// ``` - /// - /// **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 content(content: impl Into>) -> Self { - Self { - content: Some(content.into()), - name: None, - nested: None, - key: Name::of::(), - } - } - - /// Create a new [`ParameterRoute`] with `N` as the key and possibly some `content`. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute}; - /// let route = ParameterRoute::new::(Some(ContentAtom("some content"))); - /// ``` - /// - /// **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 new(content: Option>>) -> Self { - match content { - Some(c) => Self::content::(c), - None => Self::empty::(), - } - } - - /// Add a name to a [`ParameterRoute`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute}; - /// ParameterRoute::content::(ContentAtom("some content")).name::(); - /// ``` - /// - /// Names must be unique within their top level [`Segment`]. [`RootIndex`], - /// [`FailureExternalNavigation`], [`FailureNamedNavigation`] and [`FailureRedirectionLimit`] - /// are reserved and **may not** be used. - /// - /// **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. This function may only be called once per [`ParameterRoute`]. In _debug mode_, the second - /// call will panic. In _release mode_, all calls after the first will be ignored. - /// 2. If one of the forbidden names (see above) is used, this function will panic, even in - /// _release mode_. - pub fn name(mut self) -> Self { - let new = Name::of::(); - - debug_assert!( - self.name.is_none(), - "name cannot be changed: {} to {new}", - self.name.as_ref().unwrap(), - ); - assert_ne!(new, Name::of::(), "forbidden name: {new}"); - assert_ne!( - new, - Name::of::(), - "forbidden name: {new}" - ); - assert_ne!( - new, - Name::of::(), - "forbidden name: {new}" - ); - assert_ne!( - new, - Name::of::(), - "forbidden name: {new}" - ); - - self.name.get_or_insert(new); - self - } - - /// Add a nested [`Segment`] to the [`ParameterRoute`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute, Segment}; - /// ParameterRoute::content::(ContentAtom("some content")).nested(Segment::empty()); - /// ``` - /// - /// # Error Handling - /// This function may only be called once per [`ParameterRoute`]. In _debug mode_, the second - /// call will panic. In _release mode_, all calls after the first will be ignored. - pub fn nested(mut self, nested: impl Into>) -> Self { - debug_assert!(self.nested.is_none(), "nested segment cannot be changed"); - self.nested.get_or_insert(nested.into()); - self - } -} - -impl>, N: 'static> From<(C, N)> for ParameterRoute { - fn from((c, _): (C, N)) -> Self { - Self::content::(c) - } -} - -#[cfg(test)] -mod tests { - use crate::routes::{test_content, ContentAtom}; - - use super::*; - - #[test] - fn empty() { - let p = ParameterRoute::<&str>::empty::(); - - assert!(p.content.is_none()); - assert!(p.name.is_none()); - assert!(p.nested.is_none()); - assert_eq!(p.key, Name::of::()); - } - - #[test] - fn content() { - let p = ParameterRoute::content::(test_content()); - - assert_eq!(p.content, Some(test_content())); - assert!(p.name.is_none()); - assert!(p.nested.is_none()); - assert_eq!(p.key, Name::of::()); - } - - #[test] - fn new_empty() { - let p = ParameterRoute::<&str>::new::(None::); - - assert!(p.content.is_none()); - assert!(p.name.is_none()); - assert!(p.nested.is_none()); - assert_eq!(p.key, Name::of::()); - } - - #[test] - fn new_content() { - let p = ParameterRoute::new::(Some(test_content())); - - assert_eq!(p.content, Some(test_content())); - assert!(p.name.is_none()); - assert!(p.nested.is_none()); - assert_eq!(p.key, Name::of::()); - } - - #[test] - fn name_initial() { - let route = ParameterRoute::<&str>::empty::().name::<&str>(); - - assert_eq!(route.name, Some(Name::of::<&str>())) - } - - #[test] - #[should_panic = "name cannot be changed: alloc::string::String to &str"] - #[cfg(debug_assertions)] - fn name_debug() { - ParameterRoute::<&str>::empty::() - .name::() - .name::<&str>(); - } - - #[test] - #[cfg(not(debug_assertions))] - fn name_release() { - let route = ParameterRoute::<&str>::empty::() - .name::() - .name::<&str>(); - - assert_eq!(route.name, Some(Name::of::())); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::RootIndex"] - fn name_root_index() { - ParameterRoute::<&str>::empty::<&str>().name::(); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureExternalNavigation"] - fn name_external_navigation() { - ParameterRoute::<&str>::empty::<&str>().name::(); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureNamedNavigation"] - fn name_named_navigation() { - ParameterRoute::<&str>::empty::<&str>().name::(); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureRedirectionLimit"] - fn name_redirection_limit() { - ParameterRoute::<&str>::empty::<&str>().name::(); - } - - #[test] - fn nested_initial() { - let route = ParameterRoute::empty::().nested(nested_segment()); - assert!(route.nested.is_some()); - - let n = route.nested.unwrap(); - assert_eq!(n.index, nested_segment().index); - } - - #[test] - #[should_panic = "nested segment cannot be changed"] - #[cfg(debug_assertions)] - fn nested_debug() { - ParameterRoute::empty::() - .nested(nested_segment()) - .nested(nested_segment()); - } - - #[test] - #[cfg(not(debug_assertions))] - fn nested_release() { - let route = ParameterRoute::empty::() - .nested(nested_segment()) - .nested(Segment::empty()); - assert!(route.nested.is_some()); - - let n = route.nested.unwrap(); - assert_eq!(n.index, nested_segment().index); - } - - fn nested_segment() -> Segment<&'static str> { - Segment::content(RouteContent::Content(ContentAtom("nested"))) - } -} diff --git a/packages/router-core/src/routes/route.rs b/packages/router-core/src/routes/route.rs deleted file mode 100644 index 994dc1955..000000000 --- a/packages/router-core/src/routes/route.rs +++ /dev/null @@ -1,247 +0,0 @@ -use super::{RouteContent, Segment}; -use crate::{ - prelude::{ - FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex, - }, - Name, -}; - -/// A fixed route. -#[derive(Debug)] -pub struct Route { - pub(crate) content: Option>, - pub(crate) name: Option, - pub(crate) nested: Option>, -} - -impl Route { - /// Create a new [`Route`] without content. - /// - /// ```rust - /// # use dioxus_router_core::routes::Route; - /// let r: Route<&'static str> = Route::empty(); - /// ``` - #[must_use] - pub fn empty() -> Self { - Self { - content: None, - name: None, - nested: None, - } - } - - /// Create a new [`Route`] with some `content`. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Route}; - /// let r = Route::content(ContentAtom("some content")); - /// ``` - #[must_use] - pub fn content(content: impl Into>) -> Self { - Self { - content: Some(content.into()), - name: None, - nested: None, - } - } - - /// Create a new [`Route`], possible with some `content`. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Route}; - /// let r = Route::new(Some(ContentAtom("some content"))); - /// ``` - #[must_use] - pub fn new(content: Option>>) -> Self { - match content { - Some(c) => Self::content(c), - None => Self::empty(), - } - } - - /// Add a name to a [`Route`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Route}; - /// Route::content(ContentAtom("some content")).name::(); - /// ``` - /// - /// Names must be unique within their top level [`Segment`]. [`RootIndex`], - /// [`FailureExternalNavigation`], [`FailureNamedNavigation`] and [`FailureRedirectionLimit`] - /// are reserved and **may not** be used. - /// - /// **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. This function may only be called once per [`Route`]. In _debug mode_, the second call - /// will panic. In _release mode_, all calls after the first will be ignored. - /// 2. If one of the forbidden names (see above) is used, this function will panic, even in - /// _release mode_. - pub fn name(mut self) -> Self { - let new = Name::of::(); - - debug_assert!( - self.name.is_none(), - "name cannot be changed: {} to {new}", - self.name.as_ref().unwrap(), - ); - if new == Name::of::() - || new == Name::of::() - || new == Name::of::() - || new == Name::of::() - { - panic!("forbidden name: {new}"); - } - - self.name.get_or_insert(new); - self - } - - /// Add a nested [`Segment`] to the [`Route`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Route, Segment}; - /// Route::content(ContentAtom("some content")).nested(Segment::empty()); - /// ``` - /// - /// # Error Handling - /// This function may only be called once per [`Route`]. In _debug mode_, the second call will - /// panic. In _release mode_, all calls after the first will be ignored. - pub fn nested(mut self, nested: impl Into>) -> Self { - debug_assert!(self.nested.is_none(), "nested segment cannot be changed"); - self.nested.get_or_insert(nested.into()); - self - } -} - -impl>> From for Route { - fn from(c: C) -> Self { - Self::content(c) - } -} - -#[cfg(test)] -mod tests { - use crate::routes::{test_content, ContentAtom}; - - use super::*; - - #[test] - fn empty() { - let route = Route::<&str>::empty(); - - assert!(route.content.is_none()); - assert!(route.name.is_none()); - assert!(route.nested.is_none()); - } - - #[test] - fn content() { - let route = Route::content(test_content()); - - assert_eq!(route.content, Some(test_content())); - assert!(route.name.is_none()); - assert!(route.nested.is_none()); - } - - #[test] - fn new_empty() { - let route = Route::<&str>::new(None::<&str>); - - assert!(route.content.is_none()); - assert!(route.name.is_none()); - assert!(route.nested.is_none()); - } - - #[test] - fn new_content() { - let route = Route::new(Some(test_content())); - - assert_eq!(route.content, Some(test_content())); - assert!(route.name.is_none()); - assert!(route.nested.is_none()); - } - - #[test] - fn name_initial() { - let route = Route::<&str>::empty().name::<&str>(); - - assert_eq!(route.name, Some(Name::of::<&str>())) - } - - #[test] - #[should_panic = "name cannot be changed: alloc::string::String to &str"] - #[cfg(debug_assertions)] - fn name_debug() { - Route::<&str>::empty().name::().name::<&str>(); - } - - #[test] - #[cfg(not(debug_assertions))] - fn name_release() { - let route = Route::<&str>::empty().name::().name::<&str>(); - - assert_eq!(route.name, Some(Name::of::())); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::RootIndex"] - fn name_root_index() { - Route::<&str>::empty().name::(); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureExternalNavigation"] - fn name_external_navigation() { - Route::<&str>::empty().name::(); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureNamedNavigation"] - fn name_named_navigation() { - Route::<&str>::empty().name::(); - } - - #[test] - #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureRedirectionLimit"] - fn name_redirection_limit() { - Route::<&str>::empty().name::(); - } - - #[test] - fn nested_initial() { - let route = Route::empty().nested(nested_segment()); - assert!(route.nested.is_some()); - - let n = route.nested.unwrap(); - assert_eq!(n.index, nested_segment().index); - } - - #[test] - #[should_panic = "nested segment cannot be changed"] - #[cfg(debug_assertions)] - fn nested_debug() { - Route::empty() - .nested(nested_segment()) - .nested(nested_segment()); - } - - #[test] - #[cfg(not(debug_assertions))] - fn nested_release() { - let route = Route::empty() - .nested(nested_segment()) - .nested(Segment::empty()); - - assert!(route.nested.is_some()); - - let n = route.nested.unwrap(); - assert_eq!(n.index, nested_segment().index); - } - - fn nested_segment() -> Segment<&'static str> { - Segment::content(RouteContent::Content(ContentAtom("nested"))) - } -} diff --git a/packages/router-core/src/routes/segment.rs b/packages/router-core/src/routes/segment.rs deleted file mode 100644 index 846f70495..000000000 --- a/packages/router-core/src/routes/segment.rs +++ /dev/null @@ -1,458 +0,0 @@ -use std::collections::BTreeMap; - -use crate::{ - utils::{gen_parameter_sitemap, gen_sitemap}, - Name, -}; - -use super::{Matcher, ParameterRoute, Route, RouteContent}; - -/// A segment, representing a segment of the URLs path part (i.e. the stuff between two slashes). -#[derive(Debug)] -pub struct Segment { - pub(crate) index: Option>, - - pub(crate) fallback: Option>, - pub(crate) clear_fallback: Option, - - pub(crate) fixed: BTreeMap>, - pub(crate) matching: Vec<(Box, ParameterRoute)>, - pub(crate) catch_all: Option>>, -} - -impl Segment { - /// Create a new [`Segment`] without index content. - /// - /// ```rust - /// # use dioxus_router_core::routes::Segment; - /// let seg: Segment<&'static str> = Segment::empty(); - /// ``` - pub fn empty() -> Self { - Default::default() - } - - /// Create a new [`Segment`] with some index `content`. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Segment}; - /// let seg = Segment::content(ContentAtom("some content")); - /// ``` - pub fn content(content: impl Into>) -> Self { - Self { - index: Some(content.into()), - ..Default::default() - } - } - - /// Create a new [`Segment`], possibly with some index `content`. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Segment}; - /// let seg = Segment::new(Some(ContentAtom("some content"))); - /// ``` - pub fn new(content: Option>>) -> Self { - match content { - Some(content) => Self::content(content), - None => Self::empty(), - } - } - - /// Add fallback content to a [`Segment`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Segment}; - /// Segment::content(ContentAtom("some content")).fallback(ContentAtom("fallback content")); - /// ``` - /// - /// The fallback content of the innermost matched [`Segment`] is used, if the router cannot find - /// a full matching route. - /// - /// # Error Handling - /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will - /// panic. In _release mode_, all calls after the first will be ignored. - pub fn fallback(mut self, content: impl Into>) -> Self { - debug_assert!( - self.fallback.is_none(), - "fallback content cannot be changed" - ); - self.fallback.get_or_insert(content.into()); - - self - } - - /// Set whether to clear matched content when using the fallback. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Segment}; - /// Segment::content(ContentAtom("some content")) - /// .fallback(ContentAtom("fallback content")) - /// .clear_fallback(true); - /// ``` - /// - /// When this is [`true`], the router will remove all content it previously found when falling - /// back to this [`Segment`]s fallback content. If not set, a [`Segment`] will inherit this - /// value from its parent segment. For the root [`Segment`], this defaults to [`false`]. - /// - /// # Error Handling - /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will - /// panic. In _release mode_, all calls after the first will be ignored. - pub fn clear_fallback(mut self, clear: bool) -> Self { - debug_assert!( - self.clear_fallback.is_none(), - "fallback clearing cannot be changed" - ); - self.clear_fallback.get_or_insert(clear); - - self - } - - /// Add a fixed [`Route`] to the [`Segment`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Segment}; - /// Segment::empty().fixed("path", ContentAtom("fixed route content")); - /// ``` - /// - /// A fixed route is active only when the corresponding URL segment is exactly the same as its - /// path. - /// - /// # Error Handling - /// An error occurs if multiple fixed routes on the same [`Segment`] have the same `path`. In - /// _debug mode_, the second call with panic. In _release mode_, the later routes will be - /// ignored and the initial preserved. - pub fn fixed(mut self, path: impl Into, content: impl Into>) -> Self { - let path = path.into(); - - debug_assert!( - !self.fixed.contains_key(&path), - "duplicate fixed route: {path}" - ); - self.fixed.entry(path).or_insert_with(|| content.into()); - - self - } - - /// Add a matching [`ParameterRoute`] to the [`Segment`]. - /// - /// ```rust,ignore - /// # // Test is ignored because there are scenarios where no matcher is available. - /// # use dioxus_router_core::routes::Segment; - /// Segment::empty().matching("some matcher", (true, ContentAtom("matching route content"))); - /// ``` - /// - /// A matching route is active only when the corresponding URL segment is accepted by its - /// [`Matcher`], and no previously added matching route is. - /// - /// The example above is not checked by the compiler. This is because dioxus-router-core doesn't ship any - /// [`Matcher`]s by default. However, you can implement your own, or turn on the `regex` feature - /// to enable a regex implementation. - pub fn matching( - mut self, - matcher: impl Matcher + 'static, - content: impl Into>, - ) -> Self { - self.matching.push((Box::new(matcher), content.into())); - self - } - - /// Add a catch all [`ParameterRoute`] to the [`Segment`]. - /// - /// ```rust - /// # use dioxus_router_core::routes::{ContentAtom, Segment}; - /// Segment::empty().catch_all((ContentAtom("catch all route content"), true)); - /// ``` - /// - /// A catch all route is active only if no fixed or matching route is. - /// - /// # Error Handling - /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will - /// panic. In _release mode_, all calls after the first will be ignored. - pub fn catch_all(mut self, content: impl Into>) -> Self { - debug_assert!(self.catch_all.is_none(), "duplicate catch all route"); - self.catch_all.get_or_insert(Box::new(content.into())); - self - } - - /// Generate a site map. - /// - /// ```rust - /// # use std::collections::BTreeMap; - /// # use dioxus_router_core::{Name, routes::Segment}; - /// let seg = Segment::::empty().fixed("fixed", "").catch_all(("", true)); - /// let sitemap = seg.gen_sitemap(); - /// assert_eq!(sitemap, vec!["/", "/fixed", "/\\bool"]); - /// ``` - /// - /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a - /// path. Fixed routes are passed in as is, while matching and catch all routes are represented - /// by their key, marked with a leading `\`. Since the otherwise all paths should be valid in - /// URLs, and `\` is not, this doesn't cause a conflict. - pub fn gen_sitemap(&self) -> Vec { - let mut res = Vec::new(); - res.push(String::from("/")); - gen_sitemap(self, "", &mut res); - res - } - - /// Generate a site map with parameters filled in. - /// - /// ```rust - /// # use std::collections::BTreeMap; - /// # use dioxus_router_core::{Name, routes::Segment}; - /// let seg = Segment::::empty().fixed("fixed", "").catch_all(("", true)); - /// let mut parameters = BTreeMap::new(); - /// parameters.insert(Name::of::(), vec![String::from("1"), String::from("2")]); - /// - /// let sitemap = seg.gen_parameter_sitemap(¶meters); - /// assert_eq!(sitemap, vec!["/", "/fixed", "/1", "/2"]); - /// ``` - /// - /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a - /// path. Fixed routes are passed in as is, while matching and catch all will be represented - /// with all `parameters` provided for their key. Matching routes will also filter out all - /// invalid parameters. - pub fn gen_parameter_sitemap(&self, parameters: &BTreeMap>) -> Vec { - let mut res = Vec::new(); - res.push(String::from("/")); - gen_parameter_sitemap(self, parameters, "", &mut res); - res - } -} - -impl Default for Segment { - fn default() -> Self { - Self { - index: None, - fallback: None, - clear_fallback: None, - fixed: BTreeMap::new(), - matching: Vec::new(), - catch_all: None, - } - } -} - -#[cfg(test)] -mod tests { - use crate::routes::{content::test_content, ContentAtom}; - - use super::*; - - #[test] - fn default() { - let seg: Segment<&str> = Default::default(); - - assert!(seg.index.is_none()); - assert!(seg.fallback.is_none()); - assert!(seg.clear_fallback.is_none()); - assert!(seg.fixed.is_empty()); - assert!(seg.matching.is_empty()); - assert!(seg.catch_all.is_none()); - } - - #[test] - fn empty() { - let seg = Segment::<&str>::empty(); - - assert!(seg.index.is_none()); - assert!(seg.fallback.is_none()); - assert!(seg.clear_fallback.is_none()); - assert!(seg.fixed.is_empty()); - assert!(seg.matching.is_empty()); - assert!(seg.catch_all.is_none()); - } - - #[test] - fn content() { - let seg = Segment::content(test_content()); - - assert_eq!(seg.index, Some(test_content())); - assert!(seg.fallback.is_none()); - assert!(seg.clear_fallback.is_none()); - assert!(seg.fixed.is_empty()); - assert!(seg.matching.is_empty()); - assert!(seg.catch_all.is_none()); - } - - #[test] - fn new_empty() { - let seg = Segment::<&str>::new(None::); - - assert!(seg.index.is_none()); - assert!(seg.fallback.is_none()); - assert!(seg.clear_fallback.is_none()); - assert!(seg.fixed.is_empty()); - assert!(seg.matching.is_empty()); - assert!(seg.catch_all.is_none()); - } - - #[test] - fn new_content() { - let seg = Segment::new(Some(test_content())); - - assert_eq!(seg.index, Some(test_content())); - assert!(seg.fallback.is_none()); - assert!(seg.clear_fallback.is_none()); - assert!(seg.fixed.is_empty()); - assert!(seg.matching.is_empty()); - assert!(seg.catch_all.is_none()); - } - - #[test] - fn fallback_initial() { - let seg = Segment::empty().fallback(test_content()); - - assert_eq!(seg.fallback, Some(test_content())); - } - - #[test] - #[should_panic = "fallback content cannot be changed"] - #[cfg(debug_assertions)] - fn fallback_debug() { - Segment::empty() - .fallback(test_content()) - .fallback(test_content()); - } - - #[test] - #[cfg(not(debug_assertions))] - fn fallback_release() { - let seg = Segment::empty() - .fallback(test_content()) - .fallback(RouteContent::Content(ContentAtom("invalid"))); - - assert_eq!(seg.fallback, Some(test_content())); - } - - #[test] - fn clear_fallback() { - let mut seg = Segment::<&str>::empty(); - assert!(seg.clear_fallback.is_none()); - - seg = seg.clear_fallback(true); - assert_eq!(seg.clear_fallback, Some(true)); - } - - #[test] - #[should_panic = "fallback clearing cannot be changed"] - #[cfg(debug_assertions)] - fn clear_fallback_debug() { - Segment::<&str>::empty() - .clear_fallback(true) - .clear_fallback(false); - } - - #[test] - #[cfg(not(debug_assertions))] - fn clear_fallback_release() { - let seg = Segment::<&str>::empty() - .clear_fallback(true) - .clear_fallback(false); - assert_eq!(seg.clear_fallback, Some(true)); - } - - #[test] - fn fixed() { - let test = RouteContent::Content(ContentAtom("test")); - let other = RouteContent::Content(ContentAtom("other")); - let seg = Segment::empty() - .fixed("test", Route::content(test.clone())) - .fixed("other", Route::content(other.clone())); - - assert_eq!(seg.fixed.len(), 2); - assert_eq!(seg.fixed["test"].content, Some(test)); - assert_eq!(seg.fixed["other"].content, Some(other)); - } - - #[test] - #[should_panic = "duplicate fixed route: test"] - #[cfg(debug_assertions)] - fn fixed_debug() { - Segment::empty() - .fixed( - "test", - Route::content(RouteContent::Content(ContentAtom("test"))), - ) - .fixed( - "test", - Route::content(RouteContent::Content(ContentAtom("other"))), - ); - } - - #[test] - #[cfg(not(debug_assertions))] - fn fixed_release() { - let test = RouteContent::Content(ContentAtom("test")); - let other = RouteContent::Content(ContentAtom("other")); - let seg = Segment::empty() - .fixed("test", Route::content(test.clone())) - .fixed("test", Route::content(other.clone())); - - assert_eq!(seg.fixed.len(), 1); - assert_eq!(seg.fixed["test"].content, Some(test)); - } - - #[test] - fn matching() { - let test = RouteContent::Content(ContentAtom("test")); - let other = RouteContent::Content(ContentAtom("other")); - let seg = Segment::empty() - .matching( - String::from("test"), - ParameterRoute::content::(test.clone()), - ) - .matching( - String::from("other"), - ParameterRoute::content::(other.clone()), - ); - - assert_eq!(seg.matching.len(), 2); - assert_eq!(seg.matching[0].1.content, Some(test)); - assert_eq!(seg.matching[1].1.content, Some(other)); - } - - #[test] - fn catch_all_initial() { - let seg = Segment::empty().catch_all(ParameterRoute::content::(test_content())); - - assert!(seg.catch_all.is_some()); - assert_eq!(seg.catch_all.unwrap().content, Some(test_content())); - } - - #[test] - #[should_panic = "duplicate catch all route"] - #[cfg(debug_assertions)] - fn catch_all_debug() { - Segment::empty() - .catch_all(ParameterRoute::content::(test_content())) - .catch_all(ParameterRoute::content::(test_content())); - } - - #[test] - #[cfg(not(debug_assertions))] - fn catch_all_release() { - let seg = Segment::empty() - .catch_all(ParameterRoute::content::(test_content())) - .catch_all(ParameterRoute::empty::()); - - assert!(seg.catch_all.is_some()); - assert_eq!(seg.catch_all.unwrap().content, Some(test_content())); - } - - // Check whether the returned sitemap includes "/". More elaborate tests are located alongside - // the internal `gen_sitemap` function. - #[test] - fn gen_sitemap() { - assert_eq!(Segment::<&'static str>::empty().gen_sitemap(), vec!["/"]); - } - - // Check whether the returned sitemap includes "/". More elaborate tests are located alongside - // the internal `gen_parameter_sitemap` function. - #[test] - fn gen_parameter_sitemap() { - assert_eq!( - Segment::<&'static str>::empty().gen_parameter_sitemap(&BTreeMap::new()), - vec!["/"] - ); - } -} diff --git a/packages/router-core/src/utils/name.rs b/packages/router-core/src/utils/name.rs deleted file mode 100644 index 3aa43b368..000000000 --- a/packages/router-core/src/utils/name.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::collections::HashMap; - -use urlencoding::encode; - -use crate::{ - segments::{NameMap, NamedSegment}, - Name, -}; - -pub fn resolve_name( - map: &NameMap, - name: &Name, - parameters: &HashMap, -) -> Option { - debug_assert!( - map.contains_key(name), - "named navigation to unknown name: {name}" - ); - let target = map.get(name)?; - - let mut res = String::new(); - for t in target { - res += "/"; - match t { - NamedSegment::Fixed(f) => res += f, - NamedSegment::Parameter(p) => { - debug_assert!( - parameters.contains_key(p), - "named navigation is missing parameter: target {name} parameter {p}" - ); - let val = parameters.get(p)?; - - res += &encode(val); - } - } - } - - if res.is_empty() { - res += "/"; - } - - Some(res) -} - -#[cfg(test)] -mod tests { - use crate::{ - prelude::RootIndex, - routes::{ParameterRoute, Route, Segment}, - }; - - use super::*; - - fn test_map() -> NameMap { - NamedSegment::from_segment( - &Segment::<&str>::empty() - .fixed( - "fixed", - Route::empty().name::().nested( - Segment::empty().catch_all(ParameterRoute::empty::().name::()), - ), - ) - .catch_all(ParameterRoute::empty::().name::()), - ) - } - - #[test] - fn root_index() { - assert_eq!( - resolve_name(&test_map(), &Name::of::(), &HashMap::new()), - Some(String::from("/")) - ) - } - - #[test] - fn fixed() { - assert_eq!( - resolve_name(&test_map(), &Name::of::(), &HashMap::new()), - Some(String::from("/fixed")) - ) - } - - #[test] - fn matching() { - assert_eq!( - resolve_name(&test_map(), &Name::of::(), &{ - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("test")); - r - }), - Some(String::from("/test")) - ); - } - - #[test] - fn nested() { - assert_eq!( - resolve_name(&test_map(), &Name::of::(), &{ - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("nested")); - r - }), - Some(String::from("/fixed/nested")) - ); - } - - #[test] - #[should_panic = "named navigation to unknown name: bool"] - #[cfg(debug_assertions)] - fn missing_name_debug() { - resolve_name(&test_map(), &Name::of::(), &HashMap::new()); - } - - #[test] - #[cfg(not(debug_assertions))] - fn missing_name_release() { - assert!(resolve_name(&test_map(), &Name::of::(), &HashMap::new()).is_none()); - } - - #[test] - #[should_panic = "named navigation is missing parameter: target u32 parameter u16"] - #[cfg(debug_assertions)] - fn missing_parameter_debug() { - resolve_name(&test_map(), &Name::of::(), &HashMap::new()); - } - - #[test] - #[cfg(not(debug_assertions))] - fn missing_parameter_release() { - assert!(resolve_name(&test_map(), &Name::of::(), &HashMap::new()).is_none()); - } - - #[test] - fn url_encoding() { - assert_eq!( - resolve_name(&test_map(), &Name::of::(), &{ - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("🥳")); - r - }), - Some(String::from("/fixed/%F0%9F%A5%B3")) - ); - } -} diff --git a/packages/router-core/src/utils/route.rs b/packages/router-core/src/utils/route.rs deleted file mode 100644 index 30ab504d8..000000000 --- a/packages/router-core/src/utils/route.rs +++ /dev/null @@ -1,483 +0,0 @@ -use either::Either; -use urlencoding::decode; - -use crate::{ - navigation::NavigationTarget, - routes::{ParameterRoute, Route, RouteContent, Segment}, - RouterState, -}; - -pub fn route_segment( - segment: &Segment, - values: &[&str], - state: RouterState, -) -> Either, NavigationTarget> { - route_segment_internal(segment, values, state, None, false) -} - -fn route_segment_internal( - segment: &Segment, - values: &[&str], - state: RouterState, - mut fallback: Option>, - mut clear_fallback: bool, -) -> Either, NavigationTarget> { - // fallback - if let Some(fb) = &segment.fallback { - fallback = Some(fb.clone()); - } - if let Some(clear) = &segment.clear_fallback { - clear_fallback = *clear; - } - - // index route - if values.is_empty() { - if let Some(c) = &segment.index { - return merge(state, c.clone()); - } - return Either::Left(state); - } - - // fixed route - if let Some(r) = segment.fixed.get(values[0]) { - return merge_route(values, r, state, fallback, clear_fallback); - } - - // matching routes - for (m, r) in &segment.matching { - if m.matches(values[0]) { - return merge_parameter_route(values, r, state, fallback, clear_fallback); - } - } - - // catchall - if let Some(c) = &segment.catch_all { - return merge_parameter_route(values, c.as_ref(), state, fallback, clear_fallback); - } - - merge_fallback(state, fallback, clear_fallback) -} - -fn merge( - mut state: RouterState, - content: RouteContent, -) -> Either, NavigationTarget> { - match content { - RouteContent::Content(c) => state.content.push(c), - RouteContent::Redirect(t) => return Either::Right(t), - RouteContent::MultiContent { main, named } => { - if let Some(main) = main { - state.content.push(main); - } - - for (name, content) in named { - state.named_content.entry(name).or_default().push(content); - } - } - } - Either::Left(state) -} - -fn merge_route( - values: &[&str], - route: &Route, - mut state: RouterState, - fallback: Option>, - clear_fallback: bool, -) -> Either, NavigationTarget> { - // merge content - if let Some(c) = &route.content { - match merge(state, c.clone()) { - Either::Left(s) => state = s, - Either::Right(t) => return Either::Right(t), - } - } - - if let Some(n) = &route.name { - state.names.insert(n.clone()); - } - - match (&route.nested, values.is_empty()) { - (Some(n), _) => route_segment_internal(n, &values[1..], state, fallback, clear_fallback), - (None, false) => merge_fallback(state, fallback, clear_fallback), - _ => Either::Left(state), - } -} - -fn merge_parameter_route( - values: &[&str], - route: &ParameterRoute, - mut state: RouterState, - fallback: Option>, - clear_fallback: bool, -) -> Either, NavigationTarget> { - // merge content - if let Some(c) = &route.content { - match merge(state, c.clone()) { - Either::Left(s) => state = s, - Either::Right(t) => return Either::Right(t), - } - } - - if let Some(n) = &route.name { - state.names.insert(n.clone()); - } - - state.parameters.insert( - route.key.clone(), - decode(values[0]).unwrap(/* string already is UTF-8 */).into_owned(), - ); - - match (&route.nested, values.is_empty()) { - (Some(n), _) => route_segment_internal(n, &values[1..], state, fallback, clear_fallback), - (None, false) => merge_fallback(state, fallback, clear_fallback), - _ => Either::Left(state), - } -} - -fn merge_fallback( - mut state: RouterState, - fallback: Option>, - clear_fallback: bool, -) -> Either, NavigationTarget> { - // fallback clearing - if clear_fallback { - state.content.clear(); - state.names.clear(); - state.parameters.clear(); - } - - // fallback content - match fallback { - Some(fallback) => merge(state, fallback), - None => Either::Left(state), - } -} - -#[cfg(test)] -mod tests { - use std::collections::{BTreeMap, HashMap, HashSet}; - - use crate::{ - routes::{multi, ContentAtom}, - Name, - }; - - use super::*; - - fn test_segment() -> Segment<&'static str> { - Segment::content(ContentAtom("index")) - .fixed("fixed", Route::content(ContentAtom("fixed")).name::()) - .matching( - String::from("matching"), - ParameterRoute::content::(ContentAtom("matching")) - .nested(Segment::empty().fixed("nested", ContentAtom("matching nested"))), - ) - .catch_all( - ParameterRoute::content::(ContentAtom("catch all")) - .nested(Segment::empty().fixed("nested", ContentAtom("catch all nested"))), - ) - .fixed( - "nested", - Route::content(ContentAtom("nested")).name::().nested( - Segment::content(ContentAtom("nested index")) - .fixed("again", ContentAtom("nested again")), - ), - ) - .fixed("redirect", "/redirect") - .fixed( - "fallback", - Route::content(ContentAtom("fallback")).nested( - Segment::empty() - .fixed( - "keep", - Route::content(ContentAtom("keep route")).nested( - Segment::content(ContentAtom("keep index")) - .fallback(ContentAtom("keep")), - ), - ) - .fixed( - "clear", - Route::content(ContentAtom("clear route")).nested( - Segment::empty() - .fallback(ContentAtom("clear")) - .clear_fallback(true), - ), - ), - ), - ) - .fixed( - "no_fallback", - Route::content(ContentAtom("no fallback")).nested( - Segment::empty() - .fixed( - "keep", - Route::content(ContentAtom("keep route")) - .nested(Segment::empty().clear_fallback(false)), - ) - .fixed( - "clear", - Route::content(ContentAtom("clear route")) - .nested(Segment::empty().clear_fallback(true)), - ), - ), - ) - .fixed( - "named_content", - Route::content( - multi(None) - .add_named::(ContentAtom("1")) - .add_named::(ContentAtom("2")), - ) - .nested(Segment::content(multi(Some(ContentAtom("3"))))), - ) - } - - #[test] - fn route_index() { - let state = route_segment( - &test_segment(), - &[], - RouterState { - path: String::from("/"), - can_go_back: false, - can_go_forward: true, - ..Default::default() - }, - ); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!(state.content, vec![ContentAtom("index")]); - assert!(state.names.is_empty()); - assert!(state.parameters.is_empty()); - assert_eq!(state.path, String::from("/")); - assert!(!state.can_go_back); - assert!(state.can_go_forward); - } - - #[test] - fn route_fixed() { - let state = route_segment(&test_segment(), &["fixed"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - 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()); - } - - #[test] - fn route_matching() { - let state = route_segment(&test_segment(), &["matching"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!(state.content, vec![ContentAtom("matching")]); - assert!(state.names.is_empty()); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("matching")); - r - }); - } - - #[test] - fn route_matching_nested() { - let state = route_segment(&test_segment(), &["matching", "nested"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!( - state.content, - vec![ContentAtom("matching"), ContentAtom("matching nested")] - ); - assert!(state.names.is_empty()); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("matching")); - r - }); - } - - #[test] - fn route_catch_all() { - let state = route_segment(&test_segment(), &["invalid"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!(state.content, vec![ContentAtom("catch all")]); - assert!(state.names.is_empty()); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("invalid")); - r - }); - } - - #[test] - fn route_catch_all_nested() { - let state = route_segment(&test_segment(), &["invalid", "nested"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!( - state.content, - vec![ContentAtom("catch all"), ContentAtom("catch all nested")] - ); - assert!(state.names.is_empty()); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert(Name::of::(), String::from("invalid")); - r - }); - } - - #[test] - fn route_nested_index() { - let state = route_segment(&test_segment(), &["nested"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!( - state.content, - vec![ContentAtom("nested"), ContentAtom("nested index")] - ); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - } - - #[test] - fn route_nested_again() { - let state = route_segment(&test_segment(), &["nested", "again"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!( - state.content, - vec![ContentAtom("nested"), ContentAtom("nested again")] - ); - assert_eq!(state.names, { - let mut r = HashSet::new(); - r.insert(Name::of::()); - r - }); - assert!(state.parameters.is_empty()); - } - - #[test] - fn route_redirect() { - let state = route_segment(&test_segment(), &["redirect"], Default::default()); - assert_eq!(state.unwrap_right(), "/redirect".into()); - } - - #[test] - fn route_fallback_keep() { - let state = route_segment( - &test_segment(), - &["fallback", "keep", "invalid"], - Default::default(), - ); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!( - state.content, - vec![ - ContentAtom("fallback"), - ContentAtom("keep route"), - ContentAtom("keep") - ] - ); - assert!(state.names.is_empty()); - assert!(state.parameters.is_empty()); - } - - #[test] - fn route_fallback_clear() { - let state = route_segment( - &test_segment(), - &["fallback", "clear", "invalid"], - Default::default(), - ); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!(state.content, vec![ContentAtom("clear")]); - assert!(state.names.is_empty()); - assert!(state.parameters.is_empty()); - } - - #[test] - fn route_named_content() { - let state = route_segment(&test_segment(), &["named_content"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!(state.content, vec![ContentAtom("3")]); - assert_eq!(state.named_content, { - let mut r = BTreeMap::new(); - r.insert(Name::of::(), vec![ContentAtom("1")]); - r.insert(Name::of::(), vec![ContentAtom("2")]); - r - }); - assert!(state.names.is_empty()); - assert!(state.parameters.is_empty()); - } - - #[test] - fn no_fallback() { - let state = route_segment( - &test_segment(), - &["no_fallback", "keep", "invalid"], - Default::default(), - ); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!( - state.content, - vec![ContentAtom("no fallback"), ContentAtom("keep route"),] - ); - assert!(state.names.is_empty()); - assert!(state.parameters.is_empty()); - } - - #[test] - fn no_fallback_with_clearing() { - let state = route_segment( - &test_segment(), - &["no_fallback", "clear", "invalid"], - Default::default(), - ); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert!(state.content.is_empty()); - assert!(state.names.is_empty()); - assert!(state.parameters.is_empty()); - } - - #[test] - fn url_encoding() { - let state = route_segment(&test_segment(), &["%F0%9F%A5%B3"], Default::default()); - assert!(state.is_left()); - - let state = state.unwrap_left(); - assert_eq!(state.content, vec![ContentAtom("catch all")]); - assert!(state.names.is_empty()); - assert_eq!(state.parameters, { - let mut r = HashMap::new(); - r.insert(Name::of::(), "🥳".to_string()); - r - }); - } -} diff --git a/packages/router-core/src/utils/sitemap.rs b/packages/router-core/src/utils/sitemap.rs index e2076ea50..6c0d5260a 100644 --- a/packages/router-core/src/utils/sitemap.rs +++ b/packages/router-core/src/utils/sitemap.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use urlencoding::encode; -use crate::{routes::Segment, Name}; pub fn gen_sitemap(seg: &Segment, current: &str, map: &mut Vec) { for (p, r) in &seg.fixed { diff --git a/packages/router-core/src/utils/target.rs b/packages/router-core/src/utils/target.rs deleted file mode 100644 index a41001d38..000000000 --- a/packages/router-core/src/utils/target.rs +++ /dev/null @@ -1,120 +0,0 @@ -use either::Either; - -use crate::{ - navigation::{NavigationTarget, Query}, - segments::NameMap, - Name, -}; - -use super::resolve_name; - -pub fn resolve_target( - names: &NameMap, - target: &NavigationTarget, -) -> Either, String> { - match target { - NavigationTarget::Internal(i) => Either::Left(Either::Left(i.clone())), - NavigationTarget::Named { - name, - parameters, - query, - } => Either::Left( - resolve_name(names, name, parameters) - .map(|mut p| { - if let Some(q) = query { - match q { - Query::Single(s) => { - if !s.starts_with('?') { - p += "?"; - } - p += s; - } - #[cfg(feature = "serde")] - Query::List(l) => { - let res = serde_urlencoded::to_string(l); - // TODO: find a test case where this assertion is not met - debug_assert!(res.is_ok(), "cannot serialize query list: {l:?}"); - if let Ok(q) = res { - p += "?"; - p += &q; - } - } - } - } - - p - }) - .map(Either::Left) - .unwrap_or_else(|| Either::Right(name.clone())), - ), - NavigationTarget::External(e) => Either::Right(e.to_string()), - } -} - -#[cfg(test)] -mod tests { - use crate::{prelude::RootIndex, routes::Segment, segments::NamedSegment}; - - use super::*; - - #[test] - fn resolve_internal() { - let names = NamedSegment::from_segment(&Segment::<&str>::empty()); - assert_eq!( - resolve_target(&names, &NavigationTarget::Internal("/test".to_string())), - Either::Left(Either::Left(String::from("/test"))) - ); - } - - #[test] - fn resolve_named() { - let names = NamedSegment::from_segment(&Segment::<&str>::empty()); - assert_eq!( - resolve_target(&names, &NavigationTarget::named::()), - Either::Left(Either::Left(String::from("/"))) - ); - } - - #[test] - fn resolve_named_with_query_single() { - let names = NamedSegment::from_segment(&Segment::<&str>::empty()); - let without = resolve_target( - &names, - &NavigationTarget::named::().query("huhu"), - ); - let with = resolve_target( - &names, - &NavigationTarget::named::().query("?huhu"), - ); - let correct = Either::Left(Either::Left(String::from("/?huhu"))); - assert_eq!(with, correct); - assert_eq!(without, correct); - assert_eq!(with, without); - } - - #[test] - #[cfg(feature = "serde")] - fn resolve_named_with_query_list() { - let names = NamedSegment::from_segment(&Segment::<&str>::empty()); - assert_eq!( - resolve_target( - &names, - &NavigationTarget::named::() - .query(vec![("some", "test"), ("another", "value")]) - ), - Either::Left(Either::Left(String::from("/?some=test&another=value"))) - ); - } - - #[test] - fn resolve_external() { - let names = NamedSegment::from_segment(&Segment::<&str>::empty()); - assert_eq!( - resolve_target( - &names, - &NavigationTarget::External("https://dioxuslabs.com/".to_string()) - ), - Either::Right(String::from("https://dioxuslabs.com/")) - ); - } -} diff --git a/packages/router-macro/Cargo.toml b/packages/router-macro/Cargo.toml new file mode 100644 index 000000000..cae396da0 --- /dev/null +++ b/packages/router-macro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dioxus-router-macro" +version = "0.3.0" +authors = ["Evan Almloff"] +edition = "2021" +description = "Macro for Dioxus Router" +license = "MIT/Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "router"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0.11", features = ["extra-traits", "full"] } +quote = "1.0" +proc-macro2 = "1.0.56" + +[features] +default = [] diff --git a/packages/router-macro/README.md b/packages/router-macro/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs new file mode 100644 index 000000000..89570d581 --- /dev/null +++ b/packages/router-macro/src/lib.rs @@ -0,0 +1,206 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::{__private::Span, format_ident, quote, ToTokens}; +use route::Route; +use route_tree::RouteTreeSegment; +use syn::{parse_macro_input, Ident}; + +use proc_macro2::TokenStream as TokenStream2; + +mod route; +mod route_tree; + +#[proc_macro_derive(Routable, attributes(route))] +pub fn derive_routable(input: TokenStream) -> TokenStream { + let routes_enum = parse_macro_input!(input as syn::DeriveInput); + + let route_enum = match RouteEnum::parse(routes_enum) { + Ok(route_enum) => route_enum, + Err(err) => return TokenStream2::from(err.to_compile_error()).into(), + }; + + let error_type = route_enum.error_type(); + let parse_impl = route_enum.parse_impl(); + let display_impl = route_enum.impl_display(); + let routable_impl = route_enum.routable_impl(); + + quote! { + #route_enum + + #error_type + + #parse_impl + + #display_impl + + #routable_impl + } + .into() +} + +struct RouteEnum { + route_name: Ident, + routes: Vec, +} + +impl RouteEnum { + fn parse(input: syn::DeriveInput) -> syn::Result { + let name = &input.ident; + + if let syn::Data::Enum(data) = input.data { + let mut routes = Vec::new(); + + for variant in data.variants { + let route = Route::parse(variant)?; + routes.push(route); + } + + let myself = Self { + route_name: name.clone(), + routes, + }; + + Ok(myself) + } else { + Err(syn::Error::new_spanned( + input.clone(), + "Routable can only be derived for enums", + )) + } + } + + fn impl_display(&self) -> TokenStream2 { + let mut display_match = Vec::new(); + + for route in &self.routes { + display_match.push(route.display_match()); + } + + let name = &self.route_name; + + quote! { + impl std::fmt::Display for #name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #(#display_match)* + } + Ok(()) + } + } + } + } + + fn parse_impl(&self) -> TokenStream2 { + let tree = RouteTreeSegment::build(&self.routes); + let name = &self.route_name; + + let error_name = format_ident!("{}MatchError", self.route_name); + let tokens = tree + .into_iter() + .map(|t| t.to_tokens(self.route_name.clone(), error_name.clone())); + + quote! { + impl<'a> TryFrom<&'a str> for #name { + type Error = ::Err; + + fn try_from(s: &'a str) -> Result { + s.parse() + } + } + + impl std::str::FromStr for #name { + type Err = RouteParseError<#error_name>; + + fn from_str(s: &str) -> Result { + let mut segments = s.strip_prefix('/').unwrap_or(s).split('/'); + let mut errors = Vec::new(); + + if let Some(segment) = segments.next() { + #(#tokens)* + } + + Err(RouteParseError { + attempted_routes: errors, + }) + } + } + } + } + + fn error_name(&self) -> Ident { + Ident::new( + &(self.route_name.to_string() + "MatchError"), + Span::call_site(), + ) + } + + fn error_type(&self) -> TokenStream2 { + let match_error_name = self.error_name(); + + let mut type_defs = Vec::new(); + let mut error_variants = Vec::new(); + let mut display_match = Vec::new(); + + for route in &self.routes { + let route_name = &route.route_name; + + let error_name = Ident::new(&format!("{}ParseError", route_name), Span::call_site()); + let route_str = &route.route; + + error_variants.push(quote! { #route_name(#error_name) }); + display_match.push(quote! { Self::#route_name(err) => write!(f, "Route '{}' ('{}') did not match:\n{}", stringify!(#route_name), #route_str, err)? }); + type_defs.push(route.error_type()); + } + + quote! { + #(#type_defs)* + + #[derive(Debug, PartialEq)] + pub enum #match_error_name { + #(#error_variants),* + } + + impl std::fmt::Display for #match_error_name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #(#display_match),* + } + Ok(()) + } + } + } + } + + fn routable_impl(&self) -> TokenStream2 { + let mut routable_match = Vec::new(); + + for route in &self.routes { + routable_match.push(route.routable_match()); + } + + quote! { + impl Routable for Route { + fn render<'a>(self, cx: &'a ScopeState) -> Element<'a> { + match self { + #(#routable_match)* + } + } + } + } + } +} + +impl ToTokens for RouteEnum { + fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { + let routes = &self.routes; + + tokens.extend(quote!( + #[path = "pages"] + mod pages { + #(#routes)* + } + pub use pages::*; + )); + } +} diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs new file mode 100644 index 000000000..bc6720dd0 --- /dev/null +++ b/packages/router-macro/src/route.rs @@ -0,0 +1,338 @@ +use quote::{__private::Span, format_ident, quote, ToTokens}; +use syn::{Ident, LitStr, Type, Variant}; +use syn::parse::ParseStream; +use syn::parse::Parse; + +use proc_macro2::TokenStream as TokenStream2; + +struct RouteArgs{ + route: LitStr, + comp_name: Option, + props_name: Option, +} + +impl Parse for RouteArgs{ + fn parse(input: ParseStream<'_>) -> syn::Result{ + let route = input.parse::()?; + + Ok(RouteArgs { + route, + comp_name:input.parse().ok(), + props_name: input.parse().ok(), + }) + } +} + +#[derive(Debug)] +pub struct Route { + pub file_based: bool, + pub route_name: Ident, + pub comp_name: Ident, + pub props_name: Ident, + pub route: LitStr, + pub route_segments: Vec, +} + +impl Route { + pub fn parse(input: syn::Variant) -> syn::Result { + let route_attr = input + .attrs + .iter() + .find(|attr| attr.path.is_ident("route")) + .ok_or_else(|| { + syn::Error::new_spanned( + input.clone(), + "Routable variants must have a #[route(...)] attribute", + ) + })?; + + let route_name = input.ident.clone(); + let args = route_attr.parse_args::()?; + let route = args.route; + let file_based= args.comp_name.is_none(); + let comp_name = args.comp_name.unwrap_or_else(|| format_ident!("{}", route_name)); + let props_name = args.props_name.unwrap_or_else(|| format_ident!("{}Props", comp_name)); + + let route_segments = parse_route_segments(&input, &route)?; + + Ok(Self { + comp_name, + props_name, + route_name, + route_segments, + route, + file_based, + }) + } + + pub fn display_match(&self) -> TokenStream2 { + let name = &self.route_name; + let dynamic_segments = self.route_segments.iter().filter_map(|s| s.name()); + let write_segments = self.route_segments.iter().map(|s| s.write_segment()); + + quote! { + Self::#name { #(#dynamic_segments,)* } => { + #(#write_segments)* + } + } + } + + pub fn routable_match(&self) -> TokenStream2 { + let name = &self.route_name; + let dynamic_segments: Vec<_> = self + .route_segments + .iter() + .filter_map(|s| s.name()) + .collect(); + let props_name = &self.props_name; + let comp_name = &self.comp_name; + + quote! { + Self::#name { #(#dynamic_segments,)* } => { + let comp = #props_name { #(#dynamic_segments,)* }; + let cx = cx.bump().alloc(Scoped { + props: cx.bump().alloc(comp), + scope: cx, + }); + #comp_name(cx) + } + } + } + + pub fn construct(&self, enum_name: Ident) -> TokenStream2 { + let segments = self.route_segments.iter().filter_map(|seg| { + seg.name().map(|name| { + quote! { + #name + } + }) + }); + let name = &self.route_name; + + quote! { + #enum_name::#name { + #(#segments,)* + } + } + } + + pub fn error_ident(&self) -> Ident { + format_ident!("{}ParseError", self.route_name) + } + + pub fn error_type(&self) -> TokenStream2 { + let error_name = self.error_ident(); + + let mut error_variants = Vec::new(); + let mut display_match = Vec::new(); + + for (i, segment) in self.route_segments.iter().enumerate() { + let error_name = segment.error_name(i); + match segment { + RouteSegment::Static(index) => { + error_variants.push(quote! { #error_name }); + display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? }); + } + RouteSegment::Dynamic(ident, ty) => { + error_variants.push(quote! { #error_name(<#ty as std::str::FromStr>::Err) }); + display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); + } + RouteSegment::CatchAll(ident, ty) => { + error_variants.push(quote! { #error_name(<#ty as std::str::FromStr>::Err) }); + display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); + } + } + } + + quote! { + #[allow(non_camel_case_types)] + #[derive(Debug, PartialEq)] + pub enum #error_name { + ExtraSegments(String), + #(#error_variants,)* + } + + impl std::fmt::Display for #error_name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ExtraSegments(segments) => { + write!(f, "Found additional trailing segments: {segments}")? + } + #(#display_match,)* + } + Ok(()) + } + } + } + } +} + +impl ToTokens for Route { + fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { + if !self.file_based { + return; + } + + let without_leading_slash = &self.route.value()[1..]; + let route_path = std::path::Path::new(without_leading_slash); + let with_extension = route_path.with_extension("rs"); + let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let dir = std::path::Path::new(&dir); + let route = dir.join("src").join("pages").join(with_extension.clone()); + + // check if the route exists or if not use the index route + let route = if route.exists() && without_leading_slash != "" { + with_extension.to_str().unwrap().to_string() + } else { + route_path.join("index.rs").to_str().unwrap().to_string() + }; + + let route_name: Ident = self.route_name.clone(); + let prop_name = &self.props_name; + + tokens.extend(quote!( + #[path = #route] + #[allow(non_snake_case)] + mod #route_name; + pub use #route_name::{#prop_name, #route_name}; + )); + } +} + +fn parse_route_segments(varient: &Variant, route: &LitStr) -> syn::Result> { + let mut route_segments = Vec::new(); + + let route_string = route.value(); + let mut iterator = route_string.split('/'); + + // skip the first empty segment + let first = iterator.next(); + if first != Some("") { + return Err(syn::Error::new_spanned( + varient, + format!( + "Routes should start with /. Error found in the route '{}'", + route.value() + ), + )); + } + + while let Some(segment) = iterator.next() { + if segment.starts_with('(') && segment.ends_with(')') { + let spread = segment.starts_with("(..."); + + let ident = if spread { + segment[3..segment.len() - 1].to_string() + } else { + segment[1..segment.len() - 1].to_string() + }; + + let field = varient.fields.iter().find(|field| match field.ident { + Some(ref field_ident) => field_ident.to_string() == ident, + None => false, + }); + + let ty = if let Some(field) = field { + field.ty.clone() + } else { + return Err(syn::Error::new_spanned( + varient, + format!( + "Could not find a field with the name '{}' in the variant '{}'", + ident, varient.ident + ), + )); + }; + if spread { + route_segments.push(RouteSegment::CatchAll( + Ident::new(&ident, Span::call_site()), + ty, + )); + + if iterator.next().is_some() { + return Err(syn::Error::new_spanned( + route, + "Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.", + )); + } else { + break; + } + } else { + route_segments.push(RouteSegment::Dynamic( + Ident::new(&ident, Span::call_site()), + ty, + )); + } + } else { + route_segments.push(RouteSegment::Static(segment.to_string())); + } + } + + Ok(route_segments) +} + +#[derive(Debug)] +pub enum RouteSegment { + Static(String), + Dynamic(Ident, Type), + CatchAll(Ident, Type), +} + +impl RouteSegment { + pub fn name(&self) -> Option { + match self { + Self::Static(_) => None, + Self::Dynamic(ident, _) => Some(ident.clone()), + Self::CatchAll(ident, _) => Some(ident.clone()), + } + } + + pub fn write_segment(&self) -> TokenStream2 { + match self { + Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; }, + Self::Dynamic(ident, _) => quote! { write!(f, "/{}", #ident)?; }, + Self::CatchAll(ident, _) => quote! { write!(f, "/{}", #ident)?; }, + } + } + + fn error_name(&self, idx: usize) -> Ident { + match self { + Self::Static(_) => static_segment_idx(idx), + Self::Dynamic(ident, _) => format_ident!("{}ParseError", ident), + Self::CatchAll(ident, _) => format_ident!("{}ParseError", ident), + } + } + + pub fn try_parse( + &self, + idx: usize, + error_enum_name: &Ident, + error_enum_varient: &Ident, + inner_parse_enum: &Ident, + ) -> TokenStream2 { + let error_name = self.error_name(idx); + match self { + Self::Static(segment) => { + quote! { + let parsed = if segment == #segment { + Ok(()) + } else { + Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name)) + }; + } + } + Self::Dynamic(_, ty) => { + quote! { + let parsed = <#ty as std::str::FromStr>::from_str(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))); + } + } + Self::CatchAll(_, _) => { + todo!() + } + } + } +} + +pub fn static_segment_idx(idx: usize) -> Ident { + format_ident!("StaticSegment{}ParseError", idx) +} diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs new file mode 100644 index 000000000..287e6be37 --- /dev/null +++ b/packages/router-macro/src/route_tree.rs @@ -0,0 +1,258 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::Ident; + +use crate::route::{static_segment_idx, Route, RouteSegment}; + +// First deduplicate the routes by the static part of the route +#[derive(Debug)] +pub enum RouteTreeSegment<'a> { + Static { + index: usize, + segment: &'a str, + children: Vec>, + from_route: &'a Route, + }, + Dynamic(&'a Route), + StaticEnd(&'a Route), +} + +impl<'a> RouteTreeSegment<'a> { + pub fn build(routes: &'a Vec) -> Vec> { + let routes = routes.into_iter().map(PartialRoute::new).collect(); + Self::construct(routes) + } + + fn construct(routes: Vec>) -> Vec> { + let mut static_segments = Vec::new(); + let mut dyn_segments = Vec::new(); + + // Add all routes we can to the tree + for mut route in routes { + match route.next_static_segment() { + // If there is a static segment, check if it already exists in the tree + Some((i, segment)) => { + let found = static_segments.iter_mut().find_map(|seg| match seg { + RouteTreeSegment::Static { + segment: s, + children, + .. + } => (s == &segment).then(|| children), + _ => None, + }); + + match found { + Some(children) => { + // If it does, add the route to the children of the segment + children.append(&mut RouteTreeSegment::construct(vec![route])) + } + None => { + // If it doesn't, add the route as a new segment + static_segments.push(RouteTreeSegment::Static { + segment, + from_route: route.route, + children: RouteTreeSegment::construct(vec![route]), + index: i, + }) + } + } + } + // If there is no static segment, add the route to the dynamic routes + None => { + // This route is entirely static + if route.route.route_segments.len() == route.static_segment_index { + static_segments.push(RouteTreeSegment::StaticEnd(route.route)); + } else { + dyn_segments.push(RouteTreeSegment::Dynamic(route.route)); + } + } + } + } + + // All static routes are checked before dynamic routes + static_segments.append(&mut dyn_segments); + + static_segments + } +} + +impl<'a> RouteTreeSegment<'a> { + pub fn to_tokens(&self, enum_name: syn::Ident, error_enum_name: syn::Ident) -> TokenStream { + match self { + RouteTreeSegment::Static { + segment, + children, + index, + from_route, + } => { + let varient_parse_error = from_route.error_ident(); + let enum_varient = &from_route.route_name; + let error_ident = static_segment_idx(*index); + + let children_with_next_segment = children.iter().filter_map(|child| match child { + RouteTreeSegment::StaticEnd { .. } => None, + _ => Some(child.to_tokens(enum_name.clone(), error_enum_name.clone())), + }); + let children_without_next_segment = + children.iter().filter_map(|child| match child { + RouteTreeSegment::StaticEnd { .. } => { + Some(child.to_tokens(enum_name.clone(), error_enum_name.clone())) + } + _ => None, + }); + + quote! { + if #segment == segment { + let mut segments = segments.clone(); + #(#children_without_next_segment)* + if let Some(segment) = segments.next() { + #(#children_with_next_segment)* + } + } + else { + errors.push(#error_enum_name::#enum_varient(#varient_parse_error::#error_ident)) + } + } + } + RouteTreeSegment::Dynamic(route) => { + // At this point, we have matched all static segments, so we can just check if the remaining segments match the route + let varient_parse_error = route.error_ident(); + let enum_varient = &route.route_name; + + let route_segments = route + .route_segments + .iter() + .enumerate() + .skip_while(|(_, seg)| match seg { + RouteSegment::Static(_) => true, + _ => false, + }) + .map(|(i, seg)| { + ( + seg.name(), + seg.try_parse(i, &error_enum_name, enum_varient, &varient_parse_error), + ) + }); + + fn print_route_segment, TokenStream)>>( + mut s: std::iter::Peekable, + sucess_tokens: TokenStream, + ) -> TokenStream { + if let Some((name, first)) = s.next() { + let has_next = s.peek().is_some(); + let children = print_route_segment(s, sucess_tokens); + let name = name + .map(|name| quote! {#name}) + .unwrap_or_else(|| quote! {_}); + + let sucess = if has_next { + quote! { + let mut segments = segments.clone(); + if let Some(segment) = segments.next() { + #children + } + } + } else { + children + }; + + quote! { + #first + match parsed { + Ok(#name) => { + #sucess + } + Err(err) => { + errors.push(err); + } + } + } + } else { + quote! { + #sucess_tokens + } + } + } + + let construct_variant = route.construct(enum_name); + + print_route_segment( + route_segments.peekable(), + return_constructed( + construct_variant, + &error_enum_name, + enum_varient, + &varient_parse_error, + ), + ) + } + Self::StaticEnd(route) => { + let varient_parse_error = route.error_ident(); + let enum_varient = &route.route_name; + let construct_variant = route.construct(enum_name); + + return_constructed( + construct_variant, + &error_enum_name, + enum_varient, + &varient_parse_error, + ) + } + } + } +} + +fn return_constructed( + construct_variant: TokenStream, + error_enum_name: &Ident, + enum_varient: &Ident, + varient_parse_error: &Ident, +) -> TokenStream { + quote! { + let remaining_segments = segments.clone(); + let mut segments_clone = segments.clone(); + let next_segment = segments_clone.next(); + let segment_after_next = segments_clone.next(); + match (next_segment, segment_after_next) { + // This is the last segment, return the parsed route + (None, _) | (Some(""), None) => { + return Ok(#construct_variant); + } + _ => { + let mut trailing = String::new(); + for seg in remaining_segments { + trailing += seg; + trailing += "/"; + } + trailing.pop(); + errors.push(#error_enum_name::#enum_varient(#varient_parse_error::ExtraSegments(trailing))) + } + } + } +} + +struct PartialRoute<'a> { + route: &'a Route, + static_segment_index: usize, +} + +impl<'a> PartialRoute<'a> { + fn new(route: &'a Route) -> Self { + Self { + route, + static_segment_index: 0, + } + } + + fn next_static_segment(&mut self) -> Option<(usize, &'a str)> { + let idx = self.static_segment_index; + let segment = self.route.route_segments.get(idx)?; + match segment { + RouteSegment::Static(segment) => { + self.static_segment_index += 1; + Some((idx, segment)) + } + _ => None, + } + } +}