add router core
This commit is contained in:
22 files changed
+5005
-1
No files matched your search
+2
-1
@@ -4,6 +4,7 @@ members = [
|
||||
"packages/dioxus",
|
||||
"packages/core",
|
||||
"packages/core-macro",
|
||||
"packages/router-core",
|
||||
"packages/router",
|
||||
"packages/html",
|
||||
"packages/hooks",
|
||||
@@ -63,4 +64,4 @@ env_logger = "0.9.0"
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
debug = true
|
||||
debug = true
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "dioxus-router-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-rwlock = "1.3.0"
|
||||
either = "1.8.0"
|
||||
futures-channel = "0.3.25"
|
||||
futures-util = "0.3.25"
|
||||
regex = { version = "1.6.0", optional = true }
|
||||
serde = { version = "1.0.147", optional = true }
|
||||
serde_urlencoded = { version = "0.7.1", optional = true }
|
||||
url = "2.3.1"
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
[features]
|
||||
regex = ["dep:regex"]
|
||||
serde = ["dep:serde", "serde_urlencoded"]
|
||||
@@ -0,0 +1 @@
|
||||
# Dioxus Router Core
|
||||
@@ -0,0 +1,256 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use url::{ParseError, Url};
|
||||
|
||||
use super::HistoryProvider;
|
||||
|
||||
const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
|
||||
|
||||
/// A [`HistoryProvider`] that stores all navigation information in memory.
|
||||
pub struct MemoryHistory {
|
||||
current: Url,
|
||||
history: Vec<String>,
|
||||
future: Vec<String>,
|
||||
}
|
||||
|
||||
impl MemoryHistory {
|
||||
/// Create a [`MemoryHistory`] starting at `path`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
|
||||
/// assert_eq!(history.current_path(), "/some/path");
|
||||
/// assert_eq!(history.can_go_back(), false);
|
||||
/// ```
|
||||
pub fn with_initial_path(path: impl Into<String>) -> Result<Self, ParseError> {
|
||||
let mut path = path.into();
|
||||
if path.starts_with('/') {
|
||||
path.remove(0);
|
||||
}
|
||||
let url = Url::parse(&format!("{INITIAL_URL}{path}"))?;
|
||||
|
||||
Ok(Self {
|
||||
current: url,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemoryHistory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current: Url::from_str(INITIAL_URL).unwrap(),
|
||||
history: Vec::new(),
|
||||
future: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryProvider for MemoryHistory {
|
||||
fn current_path(&self) -> String {
|
||||
self.current.path().to_string()
|
||||
}
|
||||
|
||||
fn current_query(&self) -> Option<String> {
|
||||
self.current.query().map(|q| q.to_string())
|
||||
}
|
||||
|
||||
fn can_go_back(&self) -> bool {
|
||||
!self.history.is_empty()
|
||||
}
|
||||
|
||||
fn go_back(&mut self) {
|
||||
if let Some(last) = self.history.pop() {
|
||||
self.future.push(self.current.to_string());
|
||||
self.current = Url::parse(&last).unwrap(/* past URLs are always valid */);
|
||||
}
|
||||
}
|
||||
|
||||
fn can_go_forward(&self) -> bool {
|
||||
!self.future.is_empty()
|
||||
}
|
||||
|
||||
fn go_forward(&mut self) {
|
||||
if let Some(next) = self.future.pop() {
|
||||
self.history.push(self.current.to_string());
|
||||
self.current = Url::parse(&next).unwrap(/* future URLs are always valid */);
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, path: String) {
|
||||
let wrong = path.starts_with("//");
|
||||
debug_assert!(
|
||||
!wrong,
|
||||
"cannot navigate to paths starting with \"//\": {path}"
|
||||
);
|
||||
if wrong {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(new) = self.current.join(&path) {
|
||||
self.history.push(self.current.to_string());
|
||||
self.current = new;
|
||||
self.future.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn replace(&mut self, path: String) {
|
||||
let wrong = path.starts_with("//");
|
||||
debug_assert!(
|
||||
!wrong,
|
||||
"cannot navigate to paths starting with \"//\": {path}"
|
||||
);
|
||||
if wrong {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(new) = self.current.join(&path) {
|
||||
self.current = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let mem = MemoryHistory::default();
|
||||
assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
assert_eq!(mem.history, Vec::<String>::new());
|
||||
assert_eq!(mem.future, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_initial_path() {
|
||||
let mem = MemoryHistory::with_initial_path("something").unwrap();
|
||||
assert_eq!(
|
||||
mem.current,
|
||||
Url::parse(&format!("{INITIAL_URL}something")).unwrap()
|
||||
);
|
||||
assert_eq!(mem.history, Vec::<String>::new());
|
||||
assert_eq!(mem.future, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_initial_path_with_leading_slash() {
|
||||
let mem = MemoryHistory::with_initial_path("/something").unwrap();
|
||||
assert_eq!(
|
||||
mem.current,
|
||||
Url::parse(&format!("{INITIAL_URL}something")).unwrap()
|
||||
);
|
||||
assert_eq!(mem.history, Vec::<String>::new());
|
||||
assert_eq!(mem.future, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_go_back() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
assert!(!mem.can_go_back());
|
||||
|
||||
mem.push(String::from("/test"));
|
||||
assert!(mem.can_go_back());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_back() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.push(String::from("/test"));
|
||||
mem.go_back();
|
||||
|
||||
assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
assert!(mem.history.is_empty());
|
||||
assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_go_forward() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
assert!(!mem.can_go_forward());
|
||||
|
||||
mem.push(String::from("/test"));
|
||||
mem.go_back();
|
||||
|
||||
assert!(mem.can_go_forward());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_forward() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.push(String::from("/test"));
|
||||
mem.go_back();
|
||||
mem.go_forward();
|
||||
|
||||
assert_eq!(
|
||||
mem.current,
|
||||
Url::parse(&format!("{INITIAL_URL}test")).unwrap()
|
||||
);
|
||||
assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
assert!(mem.future.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.push(String::from("/test"));
|
||||
|
||||
assert_eq!(
|
||||
mem.current,
|
||||
Url::parse(&format!("{INITIAL_URL}test")).unwrap()
|
||||
);
|
||||
assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
assert!(mem.future.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
|
||||
#[cfg(debug_assertions)]
|
||||
fn push_debug() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.push(String::from("//test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn push_release() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.push(String::from("//test"));
|
||||
|
||||
assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
assert!(mem.history.is_empty())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.push(String::from("/test"));
|
||||
mem.push(String::from("/other"));
|
||||
mem.go_back();
|
||||
mem.replace(String::from("/replace"));
|
||||
|
||||
assert_eq!(
|
||||
mem.current,
|
||||
Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
|
||||
);
|
||||
assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
|
||||
#[cfg(debug_assertions)]
|
||||
fn replace_debug() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.replace(String::from("//test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn replace_release() {
|
||||
let mut mem = MemoryHistory::default();
|
||||
mem.replace(String::from("//test"));
|
||||
|
||||
assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! History Integration
|
||||
//!
|
||||
//! dioxus-router-core relies on so-called [`HistoryProvider`]s to store the current URL, and possibly a
|
||||
//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
|
||||
//!
|
||||
//! To integrate dioxus-router-core with a any type of history, all you have to do is implement the
|
||||
//! [`HistoryProvider`] trait. dioxus-router-core also comes with some (for now one) default implementations.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
mod memory;
|
||||
pub use memory::*;
|
||||
|
||||
/// An integration with some kind of navigation history.
|
||||
///
|
||||
/// Depending on your use case, your implementation may deviate from the described procedure. This
|
||||
/// is fine, as long as both `current_path` and `current_query` match the described format.
|
||||
///
|
||||
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
|
||||
/// The described behaviors are designed to mimic a web browser, which most users should already
|
||||
/// know. Deviations might confuse them.
|
||||
pub trait HistoryProvider {
|
||||
/// Get the path of the current URL.
|
||||
///
|
||||
/// **Must start** with `/`. **Must _not_ contain** the prefix.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.push(String::from("/path"));
|
||||
/// assert_eq!(history.current_path(), "/path");
|
||||
/// ```
|
||||
#[must_use]
|
||||
fn current_path(&self) -> String;
|
||||
/// Get the query string of the current URL.
|
||||
///
|
||||
/// **Must _not_** start with `?`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_query(), None);
|
||||
///
|
||||
/// history.push(String::from("?some=value"));
|
||||
/// assert_eq!(history.current_query(), Some("some=value".to_string()));
|
||||
/// ```
|
||||
fn current_query(&self) -> Option<String>;
|
||||
/// Get the current path prefix of the URL.
|
||||
///
|
||||
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
|
||||
/// dioxus-router-core-routed application is not running on `/`. The [`HistoryProvider`] is responsible
|
||||
/// for removing the prefix from the dioxus-router-core-internal path, and also for adding it back in
|
||||
/// during navigation. This functions value is only used for creating `href`s (e.g. for SSR or
|
||||
/// display (but not navigation) in a web app).
|
||||
fn current_prefix(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Check whether there is a previous page to navigate back to.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.can_go_back(), false);
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.can_go_back(), true);
|
||||
/// ```
|
||||
#[must_use]
|
||||
fn can_go_back(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// Go back to a previous page.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||
/// might be called, even if `can_go_back` returns [`false`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
/// ```
|
||||
fn go_back(&mut self);
|
||||
|
||||
/// Check whether there is a future page to navigate forward to.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.can_go_forward(), false);
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.can_go_forward(), false);
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.can_go_forward(), true);
|
||||
/// ```
|
||||
#[must_use]
|
||||
fn can_go_forward(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// Go forward to a future page.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||
/// might be called, even if `can_go_forward` returns [`false`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
///
|
||||
/// history.go_back();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.go_forward();
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// ```
|
||||
fn go_forward(&mut self);
|
||||
|
||||
/// Go to another page.
|
||||
///
|
||||
/// This should do three things:
|
||||
/// 1. Merge the current URL with the `path` parameter (which may also include a query part).
|
||||
/// 2. Remove the previous URL to the navigation history.
|
||||
/// 3. Clear the navigation future.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.push(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// assert!(history.can_go_back());
|
||||
/// ```
|
||||
fn push(&mut self, path: String);
|
||||
/// Replace the current page with another one.
|
||||
///
|
||||
/// This should merge the current URL with the `path` parameter (which may also include a query
|
||||
/// part). In contrast to the `push` function, the navigation history and future should stay
|
||||
/// untouched.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::default();
|
||||
/// assert_eq!(history.current_path(), "/");
|
||||
///
|
||||
/// history.replace(String::from("/some-other-page"));
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// assert!(!history.can_go_back());
|
||||
/// ```
|
||||
fn replace(&mut self, path: String);
|
||||
|
||||
/// Navigate to an external URL.
|
||||
///
|
||||
/// This should navigate to an external URL, which isn't controlled by the router. If a
|
||||
/// [`HistoryProvider`] cannot do that, it should return [`false`], otherwise [`true`].
|
||||
///
|
||||
/// Returning [`false`] will cause the router to handle the external navigation failure.
|
||||
#[allow(unused_variables)]
|
||||
fn external(&mut self, url: String) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Provide the [`HistoryProvider`] with an update callback.
|
||||
///
|
||||
/// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
|
||||
/// updates are received, they should call `callback`, which will cause the router to update.
|
||||
#[allow(unused_variables)]
|
||||
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![forbid(missing_docs)]
|
||||
|
||||
pub mod history;
|
||||
|
||||
mod name;
|
||||
pub use name::*;
|
||||
|
||||
pub mod navigation;
|
||||
|
||||
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::*;
|
||||
|
||||
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::navigation::*;
|
||||
pub use crate::routes::*;
|
||||
|
||||
/// An external navigation failure.
|
||||
///
|
||||
/// These occur when the router tries to navigate to a [`NavigationTarget::External`] and the
|
||||
/// [`HistoryProvider`](crate::history::HistoryProvider) doesn't support that.
|
||||
pub struct FailureExternalNavigation;
|
||||
|
||||
/// A named navigation failure.
|
||||
///
|
||||
/// These occur when the router tries to navigate to a [`NavigationTarget::Named`] and the
|
||||
/// specified [`Name`](crate::Name) is unknown, or a parameter is missing.
|
||||
pub struct FailureNamedNavigation;
|
||||
|
||||
/// A redirection limit breach.
|
||||
///
|
||||
/// These occur when the router tries to navigate to any target, but encounters 25 consecutive
|
||||
/// redirects.
|
||||
pub struct FailureRedirectionLimit;
|
||||
|
||||
/// The [`Name`](crate::Name) equivalent of `/`.
|
||||
pub struct RootIndex;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
//! Types pertaining to navigation.
|
||||
|
||||
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 {
|
||||
/// An internal path that the router can navigate to by itself.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||
/// let explicit = NavigationTarget::Internal(String::from("/internal"));
|
||||
/// let implicit: NavigationTarget = "/internal".into();
|
||||
/// assert_eq!(explicit, implicit);
|
||||
/// ```
|
||||
Internal(String),
|
||||
/// An internal target that the router can navigate to by itself.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::collections::HashMap;
|
||||
/// # use dioxus_router_core::{Name, navigation::{named, NavigationTarget}};
|
||||
/// let mut parameters = HashMap::new();
|
||||
/// parameters.insert(Name::of::<bool>(), String::from("some parameter"));
|
||||
///
|
||||
/// let explicit = NavigationTarget::Named {
|
||||
/// name: Name::of::<bool>(),
|
||||
/// parameters,
|
||||
/// query: Some("some=query".into())
|
||||
/// };
|
||||
///
|
||||
/// let implicit = named::<bool>().parameter::<bool>("some parameter").query("some=query");
|
||||
///
|
||||
/// assert_eq!(explicit, implicit);
|
||||
/// ```
|
||||
///
|
||||
/// It will automatically find the route with the matching name, insert all required parameters
|
||||
/// and add the query.
|
||||
///
|
||||
/// **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.
|
||||
Named {
|
||||
/// The name of the [`Route`](crate::routes::Route) or
|
||||
/// [`ParameterRoute`](crate::routes::ParameterRoute) to navigate to.
|
||||
///
|
||||
/// **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.
|
||||
name: Name,
|
||||
/// The parameters required to get to the specified route.
|
||||
parameters: HashMap<Name, String>,
|
||||
/// A query to add to the route.
|
||||
query: Option<Query>,
|
||||
},
|
||||
/// An external target that the router doesn't control.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||
/// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/"));
|
||||
/// let implicit: NavigationTarget = "https://dioxuslabs.com/".into();
|
||||
/// assert_eq!(explicit, implicit);
|
||||
/// ```
|
||||
External(String),
|
||||
}
|
||||
|
||||
impl NavigationTarget {
|
||||
/// Create a new [`NavigationTarget::Named`] with `N` as the name.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||
/// let target = NavigationTarget::named::<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 names, and create unit structs if
|
||||
/// needed.
|
||||
pub fn named<N: 'static>() -> Self {
|
||||
Self::Named {
|
||||
name: Name::of::<N>(),
|
||||
parameters: HashMap::new(),
|
||||
query: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a parameter to a [`NavigationTarget::Named`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||
/// let target = NavigationTarget::named::<bool>().parameter::<bool>("some parameter");
|
||||
/// ```
|
||||
///
|
||||
/// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
|
||||
/// for brevity. It is recommend to use types with descriptive names, and create unit structs if
|
||||
/// needed.
|
||||
///
|
||||
/// # Error Handling
|
||||
/// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than
|
||||
/// [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_
|
||||
/// nothing will happen.
|
||||
/// 2. Parameters need to be unique within the [`NavigationTarget`]. In _debug mode_ the
|
||||
/// second call with a duplicate name will panic. In _release mode_, all calls after the
|
||||
/// first will be ignored.
|
||||
pub fn parameter<N: 'static>(mut self, value: impl Into<String>) -> Self {
|
||||
let n = Name::of::<N>();
|
||||
|
||||
if let Self::Named {
|
||||
name,
|
||||
parameters,
|
||||
query: _,
|
||||
} = &mut self
|
||||
{
|
||||
debug_assert!(
|
||||
!parameters.contains_key(&n),
|
||||
"duplicate parameter: {name} - {n}",
|
||||
);
|
||||
parameters.entry(n).or_insert_with(|| value.into());
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("parameter only available for named target: {n}");
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a parameter to a [`NavigationTarget::Named`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||
/// let target = NavigationTarget::named::<bool>().query("some=query");
|
||||
/// ```
|
||||
///
|
||||
/// # Error Handling
|
||||
/// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than
|
||||
/// [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_
|
||||
/// nothing will happen.
|
||||
/// 2. This function may only be called once per [`NavigationTarget`]. In _debug mode_, the
|
||||
/// second call will panic. In _release mode_, all calls after the first will be ignored.
|
||||
pub fn query(mut self, query: impl Into<Query>) -> Self {
|
||||
if let Self::Named {
|
||||
name,
|
||||
parameters: _,
|
||||
query: q,
|
||||
} = &mut self
|
||||
{
|
||||
debug_assert!(q.is_none(), "query cannot be changed: {name}",);
|
||||
q.get_or_insert(query.into());
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("query only available for named target",);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`NavigationTarget::Named`] with `N` as the name.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::named;
|
||||
/// let target = named::<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 names, and create unit structs if
|
||||
/// needed.
|
||||
///
|
||||
/// This is a shortcut for [`NavigationTarget`]s `named` function.
|
||||
pub fn named<T: 'static>() -> NavigationTarget {
|
||||
NavigationTarget::named::<T>()
|
||||
}
|
||||
|
||||
impl FromStr for NavigationTarget {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match Url::parse(s) {
|
||||
Ok(_) => Ok(Self::External(s.to_string())),
|
||||
Err(ParseError::RelativeUrlWithoutBase) => Ok(Self::Internal(s.to_string())),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<String>> From<T> for NavigationTarget {
|
||||
fn from(v: T) -> Self {
|
||||
let v = v.into();
|
||||
v.clone().parse().unwrap_or(Self::Internal(v))
|
||||
}
|
||||
}
|
||||
|
||||
/// A representation of a query string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Query {
|
||||
/// A basic query string.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::Query;
|
||||
/// let explicit = Query::Single(String::from("some=query"));
|
||||
/// let implicit: Query = "some=query".into();
|
||||
/// assert_eq!(explicit, implicit);
|
||||
/// ```
|
||||
Single(String),
|
||||
/// A query separated into key-value-pairs.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::Query;
|
||||
/// let explicit = Query::List(vec![(String::from("some"), String::from("query"))]);
|
||||
/// let implicit: Query = vec![("some", "query")].into();
|
||||
/// assert_eq!(explicit, implicit);
|
||||
/// ```
|
||||
#[cfg(feature = "serde")]
|
||||
List(Vec<(String, String)>),
|
||||
}
|
||||
|
||||
impl Query {
|
||||
/// Create a [`Query`] from a [`Serialize`](serde::Serialize)able object.
|
||||
#[cfg(feature = "serde")]
|
||||
pub fn from_serde(query: impl serde::Serialize) -> Result<Self, serde_urlencoded::ser::Error> {
|
||||
serde_urlencoded::to_string(query).map(|q| Self::Single(q))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Query {
|
||||
fn from(v: String) -> Self {
|
||||
Self::Single(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Query {
|
||||
fn from(v: &str) -> Self {
|
||||
v.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<T: Into<String>> From<Vec<(T, T)>> for Query {
|
||||
fn from(v: Vec<(T, T)>) -> Self {
|
||||
Self::List(v.into_iter().map(|(a, b)| (a.into(), b.into())).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn named_enum() {
|
||||
assert_eq!(
|
||||
NavigationTarget::named::<bool>(),
|
||||
NavigationTarget::Named {
|
||||
name: Name::of::<bool>(),
|
||||
parameters: HashMap::new(),
|
||||
query: None
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_func() {
|
||||
assert_eq!(
|
||||
named::<bool>(),
|
||||
NavigationTarget::Named {
|
||||
name: Name::of::<bool>(),
|
||||
parameters: HashMap::new(),
|
||||
query: None
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameter() {
|
||||
assert_eq!(
|
||||
named::<bool>()
|
||||
.parameter::<i32>("integer")
|
||||
.parameter::<u32>("unsigned"),
|
||||
NavigationTarget::Named {
|
||||
name: Name::of::<bool>(),
|
||||
parameters: {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<i32>(), "integer".to_string());
|
||||
r.insert(Name::of::<u32>(), "unsigned".to_string());
|
||||
r
|
||||
},
|
||||
query: None
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "duplicate parameter: bool - i32"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn parameter_duplicate_debug() {
|
||||
named::<bool>()
|
||||
.parameter::<i32>("integer")
|
||||
.parameter::<i32>("duplicate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn parameter_duplicate_release() {
|
||||
assert_eq!(
|
||||
named::<bool>()
|
||||
.parameter::<i32>("integer")
|
||||
.parameter::<i32>("duplicate"),
|
||||
NavigationTarget::Named {
|
||||
name: Name::of::<bool>(),
|
||||
parameters: {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<i32>(), "integer".to_string());
|
||||
r
|
||||
},
|
||||
query: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "parameter only available for named target: i32"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn parameter_internal_debug() {
|
||||
NavigationTarget::from("/test").parameter::<i32>("integer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn parameter_internal_release() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("/test").parameter::<i32>("integer"),
|
||||
NavigationTarget::from("/test")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "parameter only available for named target: i32"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn parameter_external_debug() {
|
||||
NavigationTarget::from("https://dioxuslabs.com/").parameter::<i32>("integer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn parameter_external_release() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("https://dioxuslabs.com/").parameter::<i32>("integer"),
|
||||
NavigationTarget::from("https://dioxuslabs.com/")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query() {
|
||||
assert_eq!(
|
||||
named::<bool>().query("test"),
|
||||
NavigationTarget::Named {
|
||||
name: Name::of::<bool>(),
|
||||
parameters: HashMap::new(),
|
||||
query: Some(Query::Single("test".to_string()))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "query cannot be changed: bool"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn query_multiple_debug() {
|
||||
named::<bool>().query("test").query("other");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn query_multiple_release() {
|
||||
assert_eq!(
|
||||
named::<bool>().query("test").query("other"),
|
||||
NavigationTarget::Named {
|
||||
name: Name::of::<bool>(),
|
||||
parameters: HashMap::new(),
|
||||
query: Some(Query::Single("test".to_string()))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "query only available for named target"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn query_internal_debug() {
|
||||
NavigationTarget::from("/test").query("test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn query_internal_release() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("/test").query("test"),
|
||||
NavigationTarget::from("/test")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "query only available for named target"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn query_external_debug() {
|
||||
NavigationTarget::from("https://dioxuslabs.com/").query("test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn query_external_release() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("https://dioxuslabs.com/").query("test"),
|
||||
NavigationTarget::from("https://dioxuslabs.com/")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_parse_internal() {
|
||||
assert_eq!(
|
||||
"/test".parse::<NavigationTarget>(),
|
||||
Ok(NavigationTarget::Internal("/test".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_parse_external() {
|
||||
assert_eq!(
|
||||
"https://dioxuslabs.com/".parse::<NavigationTarget>(),
|
||||
Ok(NavigationTarget::External(
|
||||
"https://dioxuslabs.com/".to_string()
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_from_str_internal() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("/test"),
|
||||
NavigationTarget::Internal("/test".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_from_str_external() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("https://dioxuslabs.com/"),
|
||||
NavigationTarget::External("https://dioxuslabs.com/".to_string())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_from_string_internal() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("/test".to_string()),
|
||||
NavigationTarget::Internal("/test".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_from_string_external() {
|
||||
assert_eq!(
|
||||
NavigationTarget::from("https://dioxuslabs.com/".to_string()),
|
||||
NavigationTarget::External("https://dioxuslabs.com/".to_string())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_from_string() {
|
||||
assert_eq!(
|
||||
Query::from("test".to_string()),
|
||||
Query::Single("test".to_string())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_from_str() {
|
||||
assert_eq!(Query::from("test"), Query::Single("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "serde")]
|
||||
fn query_form_vec() {
|
||||
assert_eq!(
|
||||
Query::from(vec![("test", "1234")]),
|
||||
Query::List(vec![("test".to_string(), "1234".to_string())])
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
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.clone()).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, 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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> {}
|
||||
@@ -0,0 +1,306 @@
|
||||
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"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
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")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
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")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
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
|
||||
/// # 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(¶meters);
|
||||
/// assert_eq!(sitemap, vec!["/", "/fixed", "/1", "/2"]);
|
||||
/// ```
|
||||
///
|
||||
/// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a
|
||||
/// path. Fixed routes are passed in as is, while matching and catch all will be represented
|
||||
/// with all `parameters` provided for their key. Matching routes will also filter out all
|
||||
/// invalid parameters.
|
||||
pub fn gen_parameter_sitemap(&self, parameters: &BTreeMap<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!["/"]
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,288 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use either::Either;
|
||||
|
||||
use crate::{
|
||||
navigation::NavigationTarget, routes::ContentAtom, segments::NameMap, utils::resolve_target,
|
||||
Name,
|
||||
};
|
||||
|
||||
/// The current state of the router.
|
||||
#[derive(Debug)]
|
||||
pub struct RouterState<T: Clone> {
|
||||
/// Whether there is a previous page to navigate back to.
|
||||
///
|
||||
/// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
|
||||
/// safe to tell the router to go back.
|
||||
pub can_go_back: bool,
|
||||
/// Whether there is a future page to navigate forward to.
|
||||
///
|
||||
/// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
|
||||
/// to tell the router to go forward.
|
||||
pub can_go_forward: bool,
|
||||
|
||||
/// The current path.
|
||||
pub path: String,
|
||||
/// The current query.
|
||||
pub query: Option<String>,
|
||||
/// The current prefix.
|
||||
pub prefix: Option<String>,
|
||||
|
||||
/// The names of currently active routes.
|
||||
pub names: HashSet<Name>,
|
||||
/// The current path parameters.
|
||||
pub parameters: HashMap<Name, String>,
|
||||
pub(crate) name_map: Arc<NameMap>,
|
||||
|
||||
/// The current main content.
|
||||
///
|
||||
/// This should only be used by UI integration crates, and not by applications.
|
||||
pub content: Vec<ContentAtom<T>>,
|
||||
/// The current named content.
|
||||
///
|
||||
/// This should only be used by UI integration crates, and not by applications.
|
||||
pub named_content: BTreeMap<Name, Vec<ContentAtom<T>>>,
|
||||
}
|
||||
|
||||
impl<T: Clone> RouterState<T> {
|
||||
/// Get a parameter.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::{RouterState, Name};
|
||||
/// let mut state = RouterState::<&'static str>::default();
|
||||
/// assert_eq!(state.parameter::<bool>(), None);
|
||||
///
|
||||
/// // Do not do this! For illustrative purposes only!
|
||||
/// state.parameters.insert(Name::of::<bool>(), String::from("some parameter"));
|
||||
/// assert_eq!(state.parameter::<bool>(), Some("some parameter".to_string()));
|
||||
/// ```
|
||||
pub fn parameter<N: 'static>(&self) -> Option<String> {
|
||||
self.parameters.get(&Name::of::<N>()).cloned()
|
||||
}
|
||||
|
||||
/// Get the `href` for the `target`.
|
||||
pub fn href(&self, target: &NavigationTarget) -> String {
|
||||
match resolve_target(&self.name_map, &target) {
|
||||
Either::Left(Either::Left(i)) => match &self.prefix {
|
||||
Some(p) => format!("{p}{i}"),
|
||||
None => i,
|
||||
},
|
||||
Either::Left(Either::Right(n)) => {
|
||||
// the following assert currently cannot trigger, as resolve_target (or more
|
||||
// precisely resolve_name, which is called by resolve_targe) will panic in debug
|
||||
debug_assert!(false, "requested href for unknown name or parameter: {n}");
|
||||
String::new()
|
||||
}
|
||||
Either::Right(e) => e,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the `target` is currently active.
|
||||
///
|
||||
/// # Normal mode
|
||||
/// 1. For internal targets wrapping an absolute path, the current path has to start with it.
|
||||
/// 2. For internal targets wrapping a relative path, it has to match the last current segment
|
||||
/// exactly.
|
||||
/// 3. For named targets, the provided name needs to be active.
|
||||
/// 4. For external targets [`false`].
|
||||
///
|
||||
/// # Exact mode
|
||||
/// 1. For internal targets, the current path must match the wrapped path exactly.
|
||||
/// 2. For named targets, the provided name needs to be active and all parameters need to match
|
||||
/// exactly.
|
||||
/// 3. For external targets [`false`].
|
||||
pub fn is_at(&self, target: &NavigationTarget, exact: bool) -> bool {
|
||||
match target {
|
||||
NavigationTarget::Internal(i) => {
|
||||
if exact {
|
||||
i == &self.path
|
||||
} else if i.starts_with('/') {
|
||||
self.path.starts_with(i)
|
||||
} else if let Some((_, s)) = self.path.rsplit_once('/') {
|
||||
s == i
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
NavigationTarget::Named {
|
||||
name,
|
||||
parameters,
|
||||
query: _,
|
||||
} => {
|
||||
if !self.names.contains(name) {
|
||||
false
|
||||
} else if exact {
|
||||
for (k, v) in parameters {
|
||||
match self.parameters.get(k) {
|
||||
Some(p) if p != v => return false,
|
||||
None => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
NavigationTarget::External(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// manual impl required because derive macro requires default for T unnecessarily
|
||||
impl<T: Clone> Default for RouterState<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
can_go_back: Default::default(),
|
||||
can_go_forward: Default::default(),
|
||||
path: Default::default(),
|
||||
query: Default::default(),
|
||||
prefix: Default::default(),
|
||||
names: Default::default(),
|
||||
parameters: Default::default(),
|
||||
name_map: Default::default(),
|
||||
content: Default::default(),
|
||||
named_content: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{navigation::named, prelude::RootIndex, routes::Segment, segments::NamedSegment};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn href_internal() {
|
||||
let state = RouterState::<&str> {
|
||||
prefix: Some(String::from("/prefix")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(state.href(&"/test".into()), String::from("/prefix/test"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn href_named() {
|
||||
let state = RouterState::<&str> {
|
||||
name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
|
||||
prefix: Some(String::from("/prefix")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(state.href(&named::<RootIndex>()), String::from("/prefix/"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "named navigation to unknown name: bool"]
|
||||
#[cfg(debug_assertions)]
|
||||
fn href_named_debug() {
|
||||
let state = RouterState::<&str> {
|
||||
name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
|
||||
prefix: Some(String::from("/prefix")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
state.href(&named::<bool>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn href_named_release() {
|
||||
let state = RouterState::<&str> {
|
||||
name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
|
||||
prefix: Some(String::from("/prefix")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(state.href(&named::<bool>()), String::new())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn href_external() {
|
||||
let state = RouterState::<&str> {
|
||||
prefix: Some(String::from("/prefix")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state.href(&"https://dioxuslabs.com/".into()),
|
||||
String::from("https://dioxuslabs.com/")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_at_internal_absolute() {
|
||||
let state = test_state();
|
||||
|
||||
assert!(!state.is_at(&"/levels".into(), false));
|
||||
assert!(!state.is_at(&"/levels".into(), true));
|
||||
|
||||
assert!(state.is_at(&"/test".into(), false));
|
||||
assert!(!state.is_at(&"/test".into(), true));
|
||||
|
||||
assert!(state.is_at(&"/test/with/some/nested/levels".into(), false));
|
||||
assert!(state.is_at(&"/test/with/some/nested/levels".into(), true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_at_internal_relative() {
|
||||
let state = test_state();
|
||||
|
||||
assert!(state.is_at(&"levels".into(), false));
|
||||
assert!(!state.is_at(&"levels".into(), true));
|
||||
|
||||
assert!(!state.is_at(&"test".into(), false));
|
||||
assert!(!state.is_at(&"test".into(), true));
|
||||
|
||||
assert!(!state.is_at(&"test/with/some/nested/levels".into(), false));
|
||||
assert!(!state.is_at(&"test/with/some/nested/levels".into(), true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_at_named() {
|
||||
let state = test_state();
|
||||
|
||||
assert!(!state.is_at(&named::<RootIndex>(), false));
|
||||
assert!(!state.is_at(&named::<RootIndex>(), true));
|
||||
|
||||
assert!(state.is_at(&named::<bool>(), false));
|
||||
assert!(state.is_at(&named::<bool>(), true));
|
||||
|
||||
assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), false));
|
||||
assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), true));
|
||||
|
||||
assert!(state.is_at(&named::<bool>().parameter::<i8>("test"), false));
|
||||
assert!(!state.is_at(&named::<bool>().parameter::<i8>("test"), true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_at_external() {
|
||||
let state = test_state();
|
||||
|
||||
assert!(!state.is_at(&"https://dioxuslabs.com/".into(), false));
|
||||
assert!(!state.is_at(&"https://dioxuslabs.com/".into(), true));
|
||||
}
|
||||
|
||||
fn test_state() -> RouterState<&'static str> {
|
||||
RouterState {
|
||||
path: String::from("/test/with/some/nested/levels"),
|
||||
names: {
|
||||
let mut r = HashSet::new();
|
||||
r.insert(Name::of::<bool>());
|
||||
r
|
||||
},
|
||||
parameters: {
|
||||
let mut r = HashMap::new();
|
||||
r.insert(Name::of::<bool>(), String::from("test"));
|
||||
r
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
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_eq!(state.can_go_back, false);
|
||||
assert_eq!(state.can_go_forward, true);
|
||||
}
|
||||
|
||||
#[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]
|
||||
#[ignore = "not yet implemented"]
|
||||
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("fallback"),
|
||||
ContentAtom("keep route"),
|
||||
ContentAtom("keep")
|
||||
]
|
||||
);
|
||||
assert!(state.names.is_empty());
|
||||
assert!(state.parameters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "not yet implemented"]
|
||||
fn no_fallback_with_clearing() {
|
||||
let state = route_segment(
|
||||
&test_segment(),
|
||||
&["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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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 {
|
||||
let current = format!("{current}/{p}");
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_sitemap(n, ¤t, map);
|
||||
}
|
||||
}
|
||||
|
||||
for (_, r) in &seg.matching {
|
||||
let current = format!("{current}/\\{}", r.key);
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_sitemap(n, ¤t, map);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(r) = &seg.catch_all {
|
||||
let current = format!("{current}/\\{}", r.key);
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_sitemap(n, ¤t, map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_parameter_sitemap<T: Clone>(
|
||||
seg: &Segment<T>,
|
||||
parameters: &BTreeMap<Name, Vec<String>>,
|
||||
current: &str,
|
||||
map: &mut Vec<String>,
|
||||
) {
|
||||
for (p, r) in &seg.fixed {
|
||||
let current = format!("{current}/{p}");
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_parameter_sitemap(n, parameters, ¤t, map);
|
||||
}
|
||||
}
|
||||
|
||||
for (m, r) in &seg.matching {
|
||||
if let Some(rp) = parameters.get(&r.key) {
|
||||
for p in rp {
|
||||
if m.matches(p) {
|
||||
let current = format!("{current}/{}", encode(p).into_owned());
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_parameter_sitemap(n, parameters, ¤t, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(r) = &seg.catch_all {
|
||||
if let Some(rp) = parameters.get(&r.key) {
|
||||
for p in rp {
|
||||
let current = format!("{current}/{}", encode(p).into_owned());
|
||||
map.push(current.clone());
|
||||
if let Some(n) = &r.nested {
|
||||
gen_parameter_sitemap(n, parameters, ¤t, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::routes::{ParameterRoute, Route};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_segment() -> Segment<&'static str> {
|
||||
Segment::empty()
|
||||
.fixed(
|
||||
"fixed",
|
||||
Route::empty().nested(Segment::empty().fixed("nested", Route::empty())),
|
||||
)
|
||||
.matching(
|
||||
String::from("m1"),
|
||||
ParameterRoute::empty::<u8>().nested(
|
||||
Segment::empty().matching(String::from("n2"), ParameterRoute::empty::<u16>()),
|
||||
),
|
||||
)
|
||||
.matching(String::from("no match"), ParameterRoute::empty::<u32>())
|
||||
.matching(String::from("no parameter"), ParameterRoute::empty::<u64>())
|
||||
.catch_all(
|
||||
ParameterRoute::empty::<u32>()
|
||||
.nested(Segment::empty().catch_all(ParameterRoute::empty::<u16>())),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sitemap() {
|
||||
let mut result = Vec::new();
|
||||
result.push(String::from("/"));
|
||||
gen_sitemap(&test_segment(), "", &mut result);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
"/",
|
||||
"/fixed",
|
||||
"/fixed/nested",
|
||||
"/\\u8",
|
||||
"/\\u8/\\u16",
|
||||
"/\\u32",
|
||||
"/\\u64",
|
||||
"/\\u32",
|
||||
"/\\u32/\\u16"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sitemap_with_parameters() {
|
||||
let mut parameters = BTreeMap::new();
|
||||
parameters.insert(Name::of::<u8>(), vec!["m1".to_string(), "m2".to_string()]);
|
||||
parameters.insert(Name::of::<u16>(), vec!["n1".to_string(), "n2".to_string()]);
|
||||
parameters.insert(Name::of::<u32>(), vec!["catch all".to_string()]);
|
||||
|
||||
let mut result = Vec::new();
|
||||
result.push(String::from("/"));
|
||||
gen_parameter_sitemap(&test_segment(), ¶meters, "", &mut result);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
"/",
|
||||
"/fixed",
|
||||
"/fixed/nested",
|
||||
"/m1",
|
||||
"/m1/n2",
|
||||
"/catch%20all",
|
||||
"/catch%20all/n1",
|
||||
"/catch%20all/n2"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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(|p| Either::Left(p))
|
||||
.unwrap_or(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/"))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user