delete unneeded files

This commit is contained in:
Evan Almloff committed 2023-05-12 09:18:55 -05:00
1 parent 90c7e22f8b
commit e26ff6a5c0
21 files changed
+990 -2537

No files matched your search

+2 -37
View File
@@ -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.
///
-76
View File
@@ -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::<bool>();
/// ```
#[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::<bool>();
/// ```
#[must_use]
pub fn of<T: 'static>() -> Self {
Self {
id: TypeId::of::<T>(),
name: type_name::<T>(),
}
}
}
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<std::cmp::Ordering> {
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<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
-133
View File
@@ -1,133 +0,0 @@
use std::collections::BTreeMap;
use crate::{
prelude::RootIndex,
routes::{ParameterRoute, Route, Segment},
Name,
};
pub type NameMap = BTreeMap<Name, Vec<NamedSegment>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NamedSegment {
Fixed(String),
Parameter(Name),
}
impl NamedSegment {
pub fn from_segment<T: Clone>(segment: &Segment<T>) -> NameMap {
let mut res = BTreeMap::new();
res.insert(Name::of::<RootIndex>(), vec![]);
Self::from_segment_inner(Vec::new(), segment, &mut res);
res
}
fn from_segment_inner<T: Clone>(
current: Vec<NamedSegment>,
segment: &Segment<T>,
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<T: Clone>(
mut current: Vec<NamedSegment>,
path: &str,
route: &Route<T>,
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<T: Clone>(
mut current: Vec<NamedSegment>,
route: &ParameterRoute<T>,
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::<u32>()
.nested(Segment::empty().fixed("nested", Route::empty().name::<u64>()))
)
.matching(
String::from(""),
ParameterRoute::empty::<i32>().name::<i32>().nested(
Segment::empty().matching(
String::from(""),
ParameterRoute::empty::<i64>().name::<i64>()
)
)
)
.catch_all(ParameterRoute::empty::<f32>().name::<f32>().nested(
Segment::empty().catch_all(ParameterRoute::empty::<f64>().name::<f64>())
))
),
{
let mut r = BTreeMap::new();
r.insert(Name::of::<RootIndex>(), vec![]);
r.insert(Name::of::<u32>(), vec![Fixed(String::from("fixed"))]);
r.insert(
Name::of::<u64>(),
vec![Fixed(String::from("fixed")), Fixed(String::from("nested"))],
);
r.insert(Name::of::<i32>(), vec![Parameter(Name::of::<i32>())]);
r.insert(
Name::of::<i64>(),
vec![Parameter(Name::of::<i32>()), Parameter(Name::of::<i64>())],
);
r.insert(Name::of::<f32>(), vec![Parameter(Name::of::<f32>())]);
r.insert(
Name::of::<f64>(),
vec![Parameter(Name::of::<f32>()), Parameter(Name::of::<f64>())],
);
r
}
)
}
}
-2
View File
@@ -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 {
-201
View File
@@ -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<usize>,
named: BTreeMap<Name, usize>,
}
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::<bool>()));
///
/// assert_eq!(m, 0);
/// assert_eq!(a, 1);
/// assert_eq!(n, 0);
/// ```
pub fn next(&self, name: &Option<Name>) -> (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<Name>) -> Option<usize> {
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<Name>, 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::<bool>(), 0);
named.insert(Name::of::<u8>(), 8);
named.insert(Name::of::<u16>(), 16);
named.insert(Name::of::<u32>(), 32);
named.insert(Name::of::<u64>(), 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::<bool>())), Some(0));
assert_eq!(td.depth(&Some(Name::of::<u8>())), Some(8));
assert_eq!(td.depth(&Some(Name::of::<u16>())), Some(16));
assert_eq!(td.depth(&Some(Name::of::<u32>())), Some(32));
assert_eq!(td.depth(&Some(Name::of::<u64>())), Some(64));
assert_eq!(td.depth(&Some(Name::of::<i8>())), None);
assert_eq!(td.depth(&Some(Name::of::<i16>())), None);
assert_eq!(td.depth(&Some(Name::of::<i32>())), None);
assert_eq!(td.depth(&Some(Name::of::<i64>())), None);
}
#[test]
fn set_depth() {
let mut td = test_data();
// set
td.set_depth(&None, 0);
td.set_depth(&Some(Name::of::<bool>()), 1);
td.set_depth(&Some(Name::of::<u8>()), 2);
td.set_depth(&Some(Name::of::<u16>()), 4);
td.set_depth(&Some(Name::of::<u32>()), 8);
td.set_depth(&Some(Name::of::<u64>()), 16);
td.set_depth(&Some(Name::of::<i8>()), 32);
td.set_depth(&Some(Name::of::<i16>()), 64);
td.set_depth(&Some(Name::of::<i32>()), 128);
td.set_depth(&Some(Name::of::<i64>()), 256);
// check
assert_eq!(td.depth(&None), Some(0));
assert_eq!(*td.named.get(&Name::of::<bool>()).unwrap(), 1);
assert_eq!(*td.named.get(&Name::of::<u8>()).unwrap(), 2);
assert_eq!(*td.named.get(&Name::of::<u16>()).unwrap(), 4);
assert_eq!(*td.named.get(&Name::of::<u32>()).unwrap(), 8);
assert_eq!(*td.named.get(&Name::of::<u64>()).unwrap(), 16);
assert_eq!(*td.named.get(&Name::of::<i8>()).unwrap(), 32);
assert_eq!(*td.named.get(&Name::of::<i16>()).unwrap(), 64);
assert_eq!(*td.named.get(&Name::of::<i32>()).unwrap(), 128);
assert_eq!(*td.named.get(&Name::of::<i64>()).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::<bool>()));
assert_eq!(current, 1);
assert_eq!(*next.named.get(&Name::of::<bool>()).unwrap(), 1);
let (current, next) = td.next(&Some(Name::of::<u8>()));
assert_eq!(current, 9);
assert_eq!(*next.named.get(&Name::of::<u8>()).unwrap(), 9);
let (current, next) = td.next(&Some(Name::of::<u16>()));
assert_eq!(current, 17);
assert_eq!(*next.named.get(&Name::of::<u16>()).unwrap(), 17);
let (current, next) = td.next(&Some(Name::of::<u32>()));
assert_eq!(current, 33);
assert_eq!(*next.named.get(&Name::of::<u32>()).unwrap(), 33);
let (current, next) = td.next(&Some(Name::of::<u64>()));
assert_eq!(current, 65);
assert_eq!(*next.named.get(&Name::of::<u64>()).unwrap(), 65);
let (current, next) = td.next(&Some(Name::of::<i8>()));
assert_eq!(current, 0);
assert_eq!(*next.named.get(&Name::of::<i8>()).unwrap(), 0);
let (current, next) = td.next(&Some(Name::of::<i16>()));
assert_eq!(current, 0);
assert_eq!(*next.named.get(&Name::of::<i16>()).unwrap(), 0);
let (current, next) = td.next(&Some(Name::of::<i32>()));
assert_eq!(current, 0);
assert_eq!(*next.named.get(&Name::of::<i32>()).unwrap(), 0);
let (current, next) = td.next(&Some(Name::of::<i64>()));
assert_eq!(current, 0);
assert_eq!(*next.named.get(&Name::of::<i64>()).unwrap(), 0);
}
}
+163
View File
@@ -0,0 +1,163 @@
use crate::history::HistoryProvider;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct RouteParseError<E: std::fmt::Display> {
attempted_routes: Vec<E>,
}
impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
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<R: Routable, H: HistoryProvider>
where
<R as FromStr>::Err: std::fmt::Display,
{
history: H,
route: R,
}
impl<R: Routable, H: HistoryProvider> Router<R, H>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn new(history: H) -> Result<Self, R::Err> {
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
<Self as FromStr>::Err: std::fmt::Display,
{
// fn render(self, cx: &ScopeState) -> Element;
// fn comp(cx: Scope<RouterProps>) -> 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));
}
}
}
-29
View File
@@ -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<T>(pub T)
where
T: Clone;
impl<T: Clone + Debug> Debug for ContentAtom<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ContentAtom").field(&self.0).finish()
}
}
impl<T: Clone + PartialEq> PartialEq for ContentAtom<T> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<T: Clone + Eq> Eq for ContentAtom<T> {}
-306
View File
@@ -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<T: Clone> {
/// 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<T>),
/// 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::<u8>(), ContentAtom("first"));
/// r.insert(Name::of::<u16>(), ContentAtom("second"));
/// r
/// }
/// };
/// let implicit = multi(Some(ContentAtom("main")))
/// .add_named::<u8>(ContentAtom("first"))
/// .add_named::<u16>(ContentAtom("second"));
/// assert_eq!(explicit, implicit);
/// ```
MultiContent {
/// The main content.
main: Option<ContentAtom<T>>,
/// Named content.
named: BTreeMap<Name, ContentAtom<T>>,
},
}
impl<T: Clone> RouteContent<T> {
/// Create a new [`RouteContent::MultiContent`].
///
/// ```rust
/// # use dioxus_router_core::routes::{ContentAtom, multi, RouteContent};
/// let content = multi(Some(ContentAtom("main")))
/// .add_named::<u8>(ContentAtom("first"))
/// .add_named::<u16>(ContentAtom("second"));
/// ```
pub fn multi(main: Option<ContentAtom<T>>) -> 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::<u8>(ContentAtom("first"))
/// .add_named::<u16>(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<N: 'static>(mut self, content: ContentAtom<T>) -> 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::<N>();
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::<u8>(ContentAtom("first"))
/// .add_named::<u16>(ContentAtom("second"));
/// ```
///
/// This is a shortcut for [`RouteContent`]s `multi` method.
pub fn multi<T: Clone>(main: Option<ContentAtom<T>>) -> RouteContent<T> {
RouteContent::multi(main)
}
#[cfg(test)]
pub(crate) fn test_content() -> RouteContent<&'static str> {
RouteContent::Content(ContentAtom("test content"))
}
impl<T: Clone> From<ContentAtom<T>> for RouteContent<T> {
fn from(c: ContentAtom<T>) -> Self {
Self::Content(c)
}
}
impl<T: Clone, N: Into<NavigationTarget>> From<N> for RouteContent<T> {
fn from(nt: N) -> Self {
Self::Redirect(nt.into())
}
}
impl<T: Clone> Debug for RouteContent<T> {
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<T: Clone + PartialEq> PartialEq for RouteContent<T> {
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<T: Clone + Eq> Eq for RouteContent<T> {}
#[cfg(test)]
mod tests {
use crate::{navigation::named, Name};
use super::*;
#[test]
fn content_from_content() {
assert_eq!(
Into::<RouteContent<_>>::into(ContentAtom("test content")),
test_content()
)
}
#[test]
fn content_from_target() {
assert_eq!(
Into::<RouteContent<_>>::into(named::<bool>()),
RouteContent::<&str>::Redirect(NavigationTarget::Named {
name: Name::of::<bool>(),
parameters: Default::default(),
query: None
})
)
}
#[test]
fn content_from_string() {
let internal = "/test";
assert_eq!(
Into::<RouteContent<&str>>::into(internal.to_string()),
RouteContent::Redirect(internal.into())
);
let external = "https://dioxuslabs.com/";
assert_eq!(
Into::<RouteContent<&str>>::into(external.to_string()),
RouteContent::Redirect(external.into())
)
}
#[test]
fn content_from_str() {
let internal = "/test";
assert_eq!(
Into::<RouteContent<&str>>::into(internal),
RouteContent::Redirect(internal.into())
);
let external = "https://dioxuslabs.com/";
assert_eq!(
Into::<RouteContent<&str>>::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::<u8>(ContentAtom("1"))
.add_named::<u16>(ContentAtom("2"));
match c {
RouteContent::MultiContent { main, named } => {
assert!(main.is_none());
assert_eq!(named, {
let mut r = BTreeMap::new();
r.insert(Name::of::<u8>(), ContentAtom("1"));
r.insert(Name::of::<u16>(), 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::<u8>(ContentAtom("2"));
}
#[test]
#[cfg(not(debug_assertions))]
fn multi_add_wrong_kind_release() {
assert_eq!(
RouteContent::Content(ContentAtom("1")).add_named::<u8>(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::<u8>(ContentAtom("1"))
.add_named::<u8>(ContentAtom("2"));
}
#[test]
#[cfg(not(debug_assertions))]
fn multi_add_duplicate_release() {
let c = RouteContent::multi(None)
.add_named::<u8>(ContentAtom("1"))
.add_named::<u8>(ContentAtom("2"));
match c {
RouteContent::MultiContent { main, named } => {
assert!(main.is_none());
assert_eq!(named, {
let mut r = BTreeMap::new();
r.insert(Name::of::<u8>(), ContentAtom("1"));
r
});
}
_ => panic!("wrong kind"),
};
}
}
@@ -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)
}
}
@@ -1,275 +0,0 @@
use super::{RouteContent, Segment};
use crate::{
prelude::{
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex,
},
Name,
};
/// A parameter route.
#[derive(Debug)]
pub struct ParameterRoute<T: Clone> {
pub(crate) content: Option<RouteContent<T>>,
pub(crate) name: Option<Name>,
pub(crate) nested: Option<Segment<T>>,
pub(crate) key: Name,
}
impl<T: Clone> ParameterRoute<T> {
/// Create a new [`ParameterRoute`] with `N` as the key.
///
/// ```rust
/// # use dioxus_router_core::routes::ParameterRoute;
/// let route: ParameterRoute<&'static str> = ParameterRoute::empty::<bool>();
/// ```
///
/// **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<N: 'static>() -> Self {
Self {
content: None,
name: None,
nested: None,
key: Name::of::<N>(),
}
}
/// Create a new [`ParameterRoute`] with `N` as the key and some `content`.
///
/// ```rust
/// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute};
/// let route = ParameterRoute::content::<bool>(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<N: 'static>(content: impl Into<RouteContent<T>>) -> Self {
Self {
content: Some(content.into()),
name: None,
nested: None,
key: Name::of::<N>(),
}
}
/// 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::<bool>(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<N: 'static>(content: Option<impl Into<RouteContent<T>>>) -> Self {
match content {
Some(c) => Self::content::<N>(c),
None => Self::empty::<N>(),
}
}
/// Add a name to a [`ParameterRoute`].
///
/// ```rust
/// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute};
/// ParameterRoute::content::<bool>(ContentAtom("some content")).name::<bool>();
/// ```
///
/// 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<N: 'static>(mut self) -> Self {
let new = Name::of::<N>();
debug_assert!(
self.name.is_none(),
"name cannot be changed: {} to {new}",
self.name.as_ref().unwrap(),
);
assert_ne!(new, Name::of::<RootIndex>(), "forbidden name: {new}");
assert_ne!(
new,
Name::of::<FailureExternalNavigation>(),
"forbidden name: {new}"
);
assert_ne!(
new,
Name::of::<FailureNamedNavigation>(),
"forbidden name: {new}"
);
assert_ne!(
new,
Name::of::<FailureRedirectionLimit>(),
"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::<bool>(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<Segment<T>>) -> Self {
debug_assert!(self.nested.is_none(), "nested segment cannot be changed");
self.nested.get_or_insert(nested.into());
self
}
}
impl<T: Clone, C: Into<RouteContent<T>>, N: 'static> From<(C, N)> for ParameterRoute<T> {
fn from((c, _): (C, N)) -> Self {
Self::content::<N>(c)
}
}
#[cfg(test)]
mod tests {
use crate::routes::{test_content, ContentAtom};
use super::*;
#[test]
fn empty() {
let p = ParameterRoute::<&str>::empty::<String>();
assert!(p.content.is_none());
assert!(p.name.is_none());
assert!(p.nested.is_none());
assert_eq!(p.key, Name::of::<String>());
}
#[test]
fn content() {
let p = ParameterRoute::content::<String>(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::<String>());
}
#[test]
fn new_empty() {
let p = ParameterRoute::<&str>::new::<String>(None::<String>);
assert!(p.content.is_none());
assert!(p.name.is_none());
assert!(p.nested.is_none());
assert_eq!(p.key, Name::of::<String>());
}
#[test]
fn new_content() {
let p = ParameterRoute::new::<String>(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::<String>());
}
#[test]
fn name_initial() {
let route = ParameterRoute::<&str>::empty::<String>().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::<String>()
.name::<String>()
.name::<&str>();
}
#[test]
#[cfg(not(debug_assertions))]
fn name_release() {
let route = ParameterRoute::<&str>::empty::<bool>()
.name::<String>()
.name::<&str>();
assert_eq!(route.name, Some(Name::of::<String>()));
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::RootIndex"]
fn name_root_index() {
ParameterRoute::<&str>::empty::<&str>().name::<RootIndex>();
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::FailureExternalNavigation"]
fn name_external_navigation() {
ParameterRoute::<&str>::empty::<&str>().name::<FailureExternalNavigation>();
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::FailureNamedNavigation"]
fn name_named_navigation() {
ParameterRoute::<&str>::empty::<&str>().name::<FailureNamedNavigation>();
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::FailureRedirectionLimit"]
fn name_redirection_limit() {
ParameterRoute::<&str>::empty::<&str>().name::<FailureRedirectionLimit>();
}
#[test]
fn nested_initial() {
let route = ParameterRoute::empty::<bool>().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::<bool>()
.nested(nested_segment())
.nested(nested_segment());
}
#[test]
#[cfg(not(debug_assertions))]
fn nested_release() {
let route = ParameterRoute::empty::<bool>()
.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")))
}
}
-247
View File
@@ -1,247 +0,0 @@
use super::{RouteContent, Segment};
use crate::{
prelude::{
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex,
},
Name,
};
/// A fixed route.
#[derive(Debug)]
pub struct Route<T: Clone> {
pub(crate) content: Option<RouteContent<T>>,
pub(crate) name: Option<Name>,
pub(crate) nested: Option<Segment<T>>,
}
impl<T: Clone> Route<T> {
/// 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<RouteContent<T>>) -> 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<impl Into<RouteContent<T>>>) -> 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::<bool>();
/// ```
///
/// 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<N: 'static>(mut self) -> Self {
let new = Name::of::<N>();
debug_assert!(
self.name.is_none(),
"name cannot be changed: {} to {new}",
self.name.as_ref().unwrap(),
);
if new == Name::of::<RootIndex>()
|| new == Name::of::<FailureExternalNavigation>()
|| new == Name::of::<FailureNamedNavigation>()
|| new == Name::of::<FailureRedirectionLimit>()
{
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<Segment<T>>) -> Self {
debug_assert!(self.nested.is_none(), "nested segment cannot be changed");
self.nested.get_or_insert(nested.into());
self
}
}
impl<T: Clone, C: Into<RouteContent<T>>> From<C> for Route<T> {
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::<String>().name::<&str>();
}
#[test]
#[cfg(not(debug_assertions))]
fn name_release() {
let route = Route::<&str>::empty().name::<String>().name::<&str>();
assert_eq!(route.name, Some(Name::of::<String>()));
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::RootIndex"]
fn name_root_index() {
Route::<&str>::empty().name::<RootIndex>();
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::FailureExternalNavigation"]
fn name_external_navigation() {
Route::<&str>::empty().name::<FailureExternalNavigation>();
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::FailureNamedNavigation"]
fn name_named_navigation() {
Route::<&str>::empty().name::<FailureNamedNavigation>();
}
#[test]
#[should_panic = "forbidden name: dioxus_router_core::prelude::FailureRedirectionLimit"]
fn name_redirection_limit() {
Route::<&str>::empty().name::<FailureRedirectionLimit>();
}
#[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")))
}
}
-458
View File
@@ -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<T: Clone> {
pub(crate) index: Option<RouteContent<T>>,
pub(crate) fallback: Option<RouteContent<T>>,
pub(crate) clear_fallback: Option<bool>,
pub(crate) fixed: BTreeMap<String, Route<T>>,
pub(crate) matching: Vec<(Box<dyn Matcher>, ParameterRoute<T>)>,
pub(crate) catch_all: Option<Box<ParameterRoute<T>>>,
}
impl<T: Clone> Segment<T> {
/// 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<RouteContent<T>>) -> 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<impl Into<RouteContent<T>>>) -> 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<RouteContent<T>>) -> 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<String>, content: impl Into<Route<T>>) -> 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<ParameterRoute<T>>,
) -> 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<ParameterRoute<T>>) -> 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::<u8>::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<String> {
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::<u8>::empty().fixed("fixed", "").catch_all(("", true));
/// let mut parameters = BTreeMap::new();
/// parameters.insert(Name::of::<bool>(), vec![String::from("1"), String::from("2")]);
///
/// let sitemap = seg.gen_parameter_sitemap(&parameters);
/// 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<Name, Vec<String>>) -> Vec<String> {
let mut res = Vec::new();
res.push(String::from("/"));
gen_parameter_sitemap(self, parameters, "", &mut res);
res
}
}
impl<T: Clone> Default for Segment<T> {
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::<String>);
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::<String>(test.clone()),
)
.matching(
String::from("other"),
ParameterRoute::content::<String>(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::<String>(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::<String>(test_content()))
.catch_all(ParameterRoute::content::<String>(test_content()));
}
#[test]
#[cfg(not(debug_assertions))]
fn catch_all_release() {
let seg = Segment::empty()
.catch_all(ParameterRoute::content::<String>(test_content()))
.catch_all(ParameterRoute::empty::<bool>());
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!["/"]
);
}
}
-144
View File
@@ -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<Name, String>,
) -> Option<String> {
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::<u8>().nested(
Segment::empty().catch_all(ParameterRoute::empty::<u16>().name::<u32>()),
),
)
.catch_all(ParameterRoute::empty::<i8>().name::<i16>()),
)
}
#[test]
fn root_index() {
assert_eq!(
resolve_name(&test_map(), &Name::of::<RootIndex>(), &HashMap::new()),
Some(String::from("/"))
)
}
#[test]
fn fixed() {
assert_eq!(
resolve_name(&test_map(), &Name::of::<u8>(), &HashMap::new()),
Some(String::from("/fixed"))
)
}
#[test]
fn matching() {
assert_eq!(
resolve_name(&test_map(), &Name::of::<i16>(), &{
let mut r = HashMap::new();
r.insert(Name::of::<i8>(), String::from("test"));
r
}),
Some(String::from("/test"))
);
}
#[test]
fn nested() {
assert_eq!(
resolve_name(&test_map(), &Name::of::<u32>(), &{
let mut r = HashMap::new();
r.insert(Name::of::<u16>(), 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::<bool>(), &HashMap::new());
}
#[test]
#[cfg(not(debug_assertions))]
fn missing_name_release() {
assert!(resolve_name(&test_map(), &Name::of::<bool>(), &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::<u32>(), &HashMap::new());
}
#[test]
#[cfg(not(debug_assertions))]
fn missing_parameter_release() {
assert!(resolve_name(&test_map(), &Name::of::<u32>(), &HashMap::new()).is_none());
}
#[test]
fn url_encoding() {
assert_eq!(
resolve_name(&test_map(), &Name::of::<u32>(), &{
let mut r = HashMap::new();
r.insert(Name::of::<u16>(), String::from("🥳"));
r
}),
Some(String::from("/fixed/%F0%9F%A5%B3"))
);
}
}
-483
View File
@@ -1,483 +0,0 @@
use either::Either;
use urlencoding::decode;
use crate::{
navigation::NavigationTarget,
routes::{ParameterRoute, Route, RouteContent, Segment},
RouterState,
};
pub fn route_segment<T: Clone>(
segment: &Segment<T>,
values: &[&str],
state: RouterState<T>,
) -> Either<RouterState<T>, NavigationTarget> {
route_segment_internal(segment, values, state, None, false)
}
fn route_segment_internal<T: Clone>(
segment: &Segment<T>,
values: &[&str],
state: RouterState<T>,
mut fallback: Option<RouteContent<T>>,
mut clear_fallback: bool,
) -> Either<RouterState<T>, 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<T: Clone>(
mut state: RouterState<T>,
content: RouteContent<T>,
) -> Either<RouterState<T>, 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<T: Clone>(
values: &[&str],
route: &Route<T>,
mut state: RouterState<T>,
fallback: Option<RouteContent<T>>,
clear_fallback: bool,
) -> Either<RouterState<T>, 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<T: Clone>(
values: &[&str],
route: &ParameterRoute<T>,
mut state: RouterState<T>,
fallback: Option<RouteContent<T>>,
clear_fallback: bool,
) -> Either<RouterState<T>, 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<T: Clone>(
mut state: RouterState<T>,
fallback: Option<RouteContent<T>>,
clear_fallback: bool,
) -> Either<RouterState<T>, 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::<bool>())
.matching(
String::from("matching"),
ParameterRoute::content::<u8>(ContentAtom("matching"))
.nested(Segment::empty().fixed("nested", ContentAtom("matching nested"))),
)
.catch_all(
ParameterRoute::content::<u16>(ContentAtom("catch all"))
.nested(Segment::empty().fixed("nested", ContentAtom("catch all nested"))),
)
.fixed(
"nested",
Route::content(ContentAtom("nested")).name::<u32>().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::<i8>(ContentAtom("1"))
.add_named::<i16>(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::<bool>());
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::<u8>(), 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::<u8>(), 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::<u16>(), 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::<u16>(), 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::<u32>());
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::<u32>());
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::<i8>(), vec![ContentAtom("1")]);
r.insert(Name::of::<i16>(), 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::<u16>(), "🥳".to_string());
r
});
}
}
@@ -2,7 +2,6 @@ use std::collections::BTreeMap;
use urlencoding::encode;
use crate::{routes::Segment, Name};
pub fn gen_sitemap<T: Clone>(seg: &Segment<T>, current: &str, map: &mut Vec<String>) {
for (p, r) in &seg.fixed {
-120
View File
@@ -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<Either<String, Name>, 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::<RootIndex>()),
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::<RootIndex>().query("huhu"),
);
let with = resolve_target(
&names,
&NavigationTarget::named::<RootIndex>().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::<RootIndex>()
.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/"))
);
}
}
+23
View File
@@ -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 = []
View File
Whitespace-only changes.
+206
View File
@@ -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<Route>,
}
impl RouteEnum {
fn parse(input: syn::DeriveInput) -> syn::Result<Self> {
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 = <Self as std::str::FromStr>::Err;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl std::str::FromStr for #name {
type Err = RouteParseError<#error_name>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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::*;
));
}
}
+338
View File
@@ -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<Ident>,
props_name: Option<Ident>,
}
impl Parse for RouteArgs{
fn parse(input: ParseStream<'_>) -> syn::Result<Self>{
let route = input.parse::<LitStr>()?;
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<RouteSegment>,
}
impl Route {
pub fn parse(input: syn::Variant) -> syn::Result<Self> {
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::<RouteArgs>()?;
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<Vec<RouteSegment>> {
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<Ident> {
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)
}
+258
View File
@@ -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<RouteTreeSegment<'a>>,
from_route: &'a Route,
},
Dynamic(&'a Route),
StaticEnd(&'a Route),
}
impl<'a> RouteTreeSegment<'a> {
pub fn build(routes: &'a Vec<Route>) -> Vec<RouteTreeSegment<'a>> {
let routes = routes.into_iter().map(PartialRoute::new).collect();
Self::construct(routes)
}
fn construct(routes: Vec<PartialRoute<'a>>) -> Vec<RouteTreeSegment<'a>> {
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<I: Iterator<Item = (Option<Ident>, TokenStream)>>(
mut s: std::iter::Peekable<I>,
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,
}
}
}