add router core
This commit is contained in:
22 files changed
+5005
-1
No files matched your search
@@ -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!["/"]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user