//! # Routable #![allow(non_snake_case)] use dioxus::prelude::*; use std::{fmt::Display, str::FromStr}; /// An error that occurs when parsing a route #[derive(Debug, PartialEq)] pub struct RouteParseError { /// The attempted routes that failed to match pub attempted_routes: Vec, } 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(()) } } /// Something that can be created from a query string. /// /// This trait needs to be implemented if you want to turn a query string into a struct. /// /// A working example can be found in the `examples` folder in the root package under `query_segments_demo` pub trait FromQuery { /// Create an instance of `Self` from a query string fn from_query(query: &str) -> Self; } impl From<&'a str>> FromQuery for T { fn from_query(query: &str) -> Self { T::from(&*urlencoding::decode(query).expect("Failed to decode url encoding")) } } /// Something that can be created from a route segment pub trait FromRouteSegment: Sized { /// The error that can occur when parsing a route segment type Err; /// Create an instance of `Self` from a route segment fn from_route_segment(route: &str) -> Result; } impl FromRouteSegment for T where ::Err: std::fmt::Display, { type Err = ::Err; fn from_route_segment(route: &str) -> Result { match urlencoding::decode(route) { Ok(segment) => T::from_str(&segment), Err(err) => { log::error!("Failed to decode url encoding: {}", err); T::from_str(route) } } } } #[test] fn full_circle() { let route = "testing 1234 hello world"; assert_eq!(String::from_route_segment(route).unwrap(), route); } /// Something that can be converted to route segments pub trait ToRouteSegments { /// Display the route segments fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; } impl ToRouteSegments for I where I: IntoIterator, { fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for segment in self { write!(f, "/")?; let segment = segment.to_string(); match urlencoding::decode(&segment) { Ok(segment) => write!(f, "{}", segment)?, Err(err) => { log::error!("Failed to decode url encoding: {}", err); write!(f, "{}", segment)? } } } Ok(()) } } #[test] fn to_route_segments() { struct DisplaysRoute; impl std::fmt::Display for DisplaysRoute { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let segments = vec!["hello", "world"]; segments.display_route_segements(f) } } assert_eq!(DisplaysRoute.to_string(), "/hello/world"); } /// Something that can be created from route segments pub trait FromRouteSegments: Sized { /// The error that can occur when parsing route segments type Err; /// Create an instance of `Self` from route segments fn from_route_segments(segments: &[&str]) -> Result; } impl> FromRouteSegments for I { type Err = ::Err; fn from_route_segments(segments: &[&str]) -> Result { segments .iter() .map(|s| String::from_route_segment(s)) .collect() } } /// Something that can be: /// 1. Converted from a route /// 2. Converted to a route /// 3. Rendered as a component /// /// This trait can be derived using the `#[derive(Routable)]` macro pub trait Routable: std::fmt::Display + std::str::FromStr + Clone + 'static { /// The error that can occur when parsing a route const SITE_MAP: &'static [SiteMapSegment]; /// Render the route at the given level fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>; /// Checks if this route is a child of the given route /// /// # Example /// ```rust /// use dioxus_router::prelude::*; /// use dioxus::prelude::*; /// /// #[inline_props] /// fn Home(cx: Scope) -> Element { todo!() } /// #[inline_props] /// fn About(cx: Scope) -> Element { todo!() } /// /// #[derive(Routable, Clone, PartialEq, Debug)] /// enum Route { /// #[route("/")] /// Home {}, /// #[route("/about")] /// About {}, /// } /// /// let route = Route::About {}; /// let parent = Route::Home {}; /// assert!(route.is_child_of(&parent)); /// ``` fn is_child_of(&self, other: &Self) -> bool { let self_str = self.to_string(); let self_str = self_str.trim_matches('/'); let other_str = other.to_string(); let other_str = other_str.trim_matches('/'); if other_str.is_empty() { return true; } let self_segments = self_str.split('/'); let other_segments = other_str.split('/'); for (self_seg, other_seg) in self_segments.zip(other_segments) { if self_seg != other_seg { return false; } } true } /// Get the parent route of this route /// /// # Example /// ```rust /// use dioxus_router::prelude::*; /// use dioxus::prelude::*; /// /// #[inline_props] /// fn Home(cx: Scope) -> Element { todo!() } /// #[inline_props] /// fn About(cx: Scope) -> Element { todo!() } /// /// #[derive(Routable, Clone, PartialEq, Debug)] /// enum Route { /// #[route("/")] /// Home {}, /// #[route("/about")] /// About {}, /// } /// /// let route = Route::About {}; /// let parent = route.parent().unwrap(); /// assert_eq!(parent, Route::Home {}); /// ``` fn parent(&self) -> Option { let as_str = self.to_string(); let as_str = as_str.trim_matches('/'); let segments = as_str.split('/'); let segment_count = segments.clone().count(); let new_route = segments .take(segment_count - 1) .fold(String::new(), |mut acc, segment| { acc.push('/'); acc.push_str(segment); acc }); Self::from_str(&new_route).ok() } /// Gets a list of all static routes fn static_routes() -> Vec { Self::SITE_MAP .iter() .flat_map(|segment| segment.flatten()) .filter_map(|route| { if route .iter() .all(|segment| matches!(segment, SegmentType::Static(_))) { Self::from_str( &route .iter() .map(|segment| match segment { SegmentType::Static(s) => s.to_string(), _ => unreachable!(), }) .collect::>() .join("/"), ) .ok() } else { None } }) .collect() } } trait RoutableFactory { type Err: std::fmt::Display; type Routable: Routable + FromStr; } impl RoutableFactory for R where ::Err: std::fmt::Display, { type Err = ::Err; type Routable = R; } trait RouteRenderable: std::fmt::Display + 'static { fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>; } impl RouteRenderable for R where ::Err: std::fmt::Display, { fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> { self.render(cx, level) } } /// A type erased map of the site structurens #[derive(Debug, Clone, PartialEq)] pub struct SiteMapSegment { /// The type of the route segment pub segment_type: SegmentType, /// The children of the route segment pub children: &'static [SiteMapSegment], } impl SiteMapSegment { /// Take a map of the site structure and flatten it into a vector of routes pub fn flatten(&self) -> Vec> { let mut routes = Vec::new(); self.flatten_inner(&mut routes, Vec::new()); routes } fn flatten_inner(&self, routes: &mut Vec>, current: Vec) { let mut current = current; current.push(self.segment_type.clone()); if self.children.is_empty() { routes.push(current); } else { for child in self.children { child.flatten_inner(routes, current.clone()); } } } } /// The type of a route segment #[derive(Debug, Clone, PartialEq)] pub enum SegmentType { /// A static route segment Static(&'static str), /// A dynamic route segment Dynamic(&'static str), /// A catch all route segment CatchAll(&'static str), /// A child router Child, } impl Display for SegmentType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self { SegmentType::Static(s) => write!(f, "/{}", s), SegmentType::Child => Ok(()), SegmentType::Dynamic(s) => write!(f, "/:{}", s), SegmentType::CatchAll(s) => write!(f, "/:...{}", s), } } }