浏览代码

add router core

Adrian Wannenmacher 2 年之前
父节点
当前提交
1f1c49b9c9

+ 2 - 1
Cargo.toml

@@ -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

+ 21 - 0
packages/router-core/Cargo.toml

@@ -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"]

+ 1 - 0
packages/router-core/README.md

@@ -0,0 +1 @@
+# Dioxus Router Core

+ 256 - 0
packages/router-core/src/history/memory.rs

@@ -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());
+    }
+}

+ 187 - 0
packages/router-core/src/history/mod.rs

@@ -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>) {}
+}

+ 80 - 0
packages/router-core/src/lib.rs

@@ -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;
+}

+ 76 - 0
packages/router-core/src/name/mod.rs

@@ -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);
+    }
+}

+ 133 - 0
packages/router-core/src/name/segments.rs

@@ -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
+            }
+        )
+    }
+}

+ 488 - 0
packages/router-core/src/navigation.rs

@@ -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())])
+        )
+    }
+}

+ 201 - 0
packages/router-core/src/outlet.rs

@@ -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);
+    }
+}

+ 29 - 0
packages/router-core/src/routes/atom.rs

@@ -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> {}

+ 306 - 0
packages/router-core/src/routes/content.rs

@@ -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"),
+        };
+    }
+}

+ 25 - 0
packages/router-core/src/routes/matcher.rs

@@ -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)
+    }
+}

+ 275 - 0
packages/router-core/src/routes/parameter_route.rs

@@ -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")))
+    }
+}

+ 247 - 0
packages/router-core/src/routes/route.rs

@@ -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")))
+    }
+}

+ 457 - 0
packages/router-core/src/routes/segment.rs

@@ -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(&parameters);
+    /// assert_eq!(sitemap, vec!["/", "/fixed", "/1", "/2"]);
+    /// ```
+    ///
+    /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a
+    /// path. Fixed routes are passed in as is, while matching and catch all will be represented
+    /// with all `parameters` provided for their key. Matching routes will also filter out all
+    /// invalid parameters.
+    pub fn gen_parameter_sitemap(&self, parameters: &BTreeMap<Name, Vec<String>>) -> Vec<String> {
+        let mut res = Vec::new();
+        res.push(String::from("/"));
+        gen_parameter_sitemap(&self, parameters, "", &mut res);
+        res
+    }
+}
+
+impl<T: Clone> Default for Segment<T> {
+    fn default() -> Self {
+        Self {
+            index: None,
+            fallback: None,
+            clear_fallback: None,
+            fixed: BTreeMap::new(),
+            matching: Vec::new(),
+            catch_all: None,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::routes::{content::test_content, ContentAtom};
+
+    use super::*;
+
+    #[test]
+    fn default() {
+        let seg: Segment<&str> = Default::default();
+
+        assert!(seg.index.is_none());
+        assert!(seg.fallback.is_none());
+        assert!(seg.clear_fallback.is_none());
+        assert!(seg.fixed.is_empty());
+        assert!(seg.matching.is_empty());
+        assert!(seg.catch_all.is_none());
+    }
+
+    #[test]
+    fn empty() {
+        let seg = Segment::<&str>::empty();
+
+        assert!(seg.index.is_none());
+        assert!(seg.fallback.is_none());
+        assert!(seg.clear_fallback.is_none());
+        assert!(seg.fixed.is_empty());
+        assert!(seg.matching.is_empty());
+        assert!(seg.catch_all.is_none());
+    }
+
+    #[test]
+    fn content() {
+        let seg = Segment::content(test_content());
+
+        assert_eq!(seg.index, Some(test_content()));
+        assert!(seg.fallback.is_none());
+        assert!(seg.clear_fallback.is_none());
+        assert!(seg.fixed.is_empty());
+        assert!(seg.matching.is_empty());
+        assert!(seg.catch_all.is_none());
+    }
+
+    #[test]
+    fn new_empty() {
+        let seg = Segment::<&str>::new(None::<String>);
+
+        assert!(seg.index.is_none());
+        assert!(seg.fallback.is_none());
+        assert!(seg.clear_fallback.is_none());
+        assert!(seg.fixed.is_empty());
+        assert!(seg.matching.is_empty());
+        assert!(seg.catch_all.is_none());
+    }
+
+    #[test]
+    fn new_content() {
+        let seg = Segment::new(Some(test_content()));
+
+        assert_eq!(seg.index, Some(test_content()));
+        assert!(seg.fallback.is_none());
+        assert!(seg.clear_fallback.is_none());
+        assert!(seg.fixed.is_empty());
+        assert!(seg.matching.is_empty());
+        assert!(seg.catch_all.is_none());
+    }
+
+    #[test]
+    fn fallback_initial() {
+        let seg = Segment::empty().fallback(test_content());
+
+        assert_eq!(seg.fallback, Some(test_content()));
+    }
+
+    #[test]
+    #[should_panic = "fallback content cannot be changed"]
+    #[cfg(debug_assertions)]
+    fn fallback_debug() {
+        Segment::empty()
+            .fallback(test_content())
+            .fallback(test_content());
+    }
+
+    #[test]
+    #[cfg(not(debug_assertions))]
+    fn fallback_release() {
+        let seg = Segment::empty()
+            .fallback(test_content())
+            .fallback(RouteContent::Content(ContentAtom("invalid")));
+
+        assert_eq!(seg.fallback, Some(test_content()));
+    }
+
+    #[test]
+    fn clear_fallback() {
+        let mut seg = Segment::<&str>::empty();
+        assert!(seg.clear_fallback.is_none());
+
+        seg = seg.clear_fallback(true);
+        assert_eq!(seg.clear_fallback, Some(true));
+    }
+
+    #[test]
+    #[should_panic = "fallback clearing cannot be changed"]
+    #[cfg(debug_assertions)]
+    fn clear_fallback_debug() {
+        Segment::<&str>::empty()
+            .clear_fallback(true)
+            .clear_fallback(false);
+    }
+
+    #[test]
+    #[cfg(not(debug_assertions))]
+    fn clear_fallback_release() {
+        let seg = Segment::<&str>::empty()
+            .clear_fallback(true)
+            .clear_fallback(false);
+        assert_eq!(seg.clear_fallback, Some(true));
+    }
+
+    #[test]
+    fn fixed() {
+        let test = RouteContent::Content(ContentAtom("test"));
+        let other = RouteContent::Content(ContentAtom("other"));
+        let seg = Segment::empty()
+            .fixed("test", Route::content(test.clone()))
+            .fixed("other", Route::content(other.clone()));
+
+        assert_eq!(seg.fixed.len(), 2);
+        assert_eq!(seg.fixed["test"].content, Some(test));
+        assert_eq!(seg.fixed["other"].content, Some(other));
+    }
+
+    #[test]
+    #[should_panic = "duplicate fixed route: test"]
+    #[cfg(debug_assertions)]
+    fn fixed_debug() {
+        Segment::empty()
+            .fixed(
+                "test",
+                Route::content(RouteContent::Content(ContentAtom("test"))),
+            )
+            .fixed(
+                "test",
+                Route::content(RouteContent::Content(ContentAtom("other"))),
+            );
+    }
+
+    #[test]
+    #[cfg(not(debug_assertions))]
+    fn fixed_release() {
+        let test = RouteContent::Content(ContentAtom("test"));
+        let other = RouteContent::Content(ContentAtom("other"));
+        let seg = Segment::empty()
+            .fixed("test", Route::content(test.clone()))
+            .fixed("test", Route::content(other.clone()));
+
+        assert_eq!(seg.fixed.len(), 1);
+        assert_eq!(seg.fixed["test"].content, Some(test));
+    }
+
+    #[test]
+    fn matching() {
+        let test = RouteContent::Content(ContentAtom("test"));
+        let other = RouteContent::Content(ContentAtom("other"));
+        let seg = Segment::empty()
+            .matching(
+                String::from("test"),
+                ParameterRoute::content::<String>(test.clone()),
+            )
+            .matching(
+                String::from("other"),
+                ParameterRoute::content::<String>(other.clone()),
+            );
+
+        assert_eq!(seg.matching.len(), 2);
+        assert_eq!(seg.matching[0].1.content, Some(test));
+        assert_eq!(seg.matching[1].1.content, Some(other));
+    }
+
+    #[test]
+    fn catch_all_initial() {
+        let seg = Segment::empty().catch_all(ParameterRoute::content::<String>(test_content()));
+
+        assert!(seg.catch_all.is_some());
+        assert_eq!(seg.catch_all.unwrap().content, Some(test_content()));
+    }
+
+    #[test]
+    #[should_panic = "duplicate catch all route"]
+    #[cfg(debug_assertions)]
+    fn catch_all_debug() {
+        Segment::empty()
+            .catch_all(ParameterRoute::content::<String>(test_content()))
+            .catch_all(ParameterRoute::content::<String>(test_content()));
+    }
+
+    #[test]
+    #[cfg(not(debug_assertions))]
+    fn catch_all_release() {
+        let seg = Segment::empty()
+            .catch_all(ParameterRoute::content::<String>(test_content()))
+            .catch_all(ParameterRoute::empty::<bool>());
+
+        assert!(seg.catch_all.is_some());
+        assert_eq!(seg.catch_all.unwrap().content, Some(test_content()));
+    }
+
+    // Check whether the returned sitemap includes "/". More elaborate tests are located alongside
+    // the internal `gen_sitemap` function.
+    #[test]
+    fn gen_sitemap() {
+        assert_eq!(Segment::<&'static str>::empty().gen_sitemap(), vec!["/"]);
+    }
+
+    // Check whether the returned sitemap includes "/". More elaborate tests are located alongside
+    // the internal `gen_parameter_sitemap` function.
+    #[test]
+    fn gen_parameter_sitemap() {
+        assert_eq!(
+            Segment::<&'static str>::empty().gen_parameter_sitemap(&BTreeMap::new()),
+            vec!["/"]
+        );
+    }
+}

+ 1033 - 0
packages/router-core/src/service.rs

@@ -0,0 +1,1033 @@
+use std::{
+    collections::{BTreeMap, HashMap, HashSet},
+    sync::{Arc, Weak},
+};
+
+use async_rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard};
+use either::Either;
+use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
+use futures_util::StreamExt;
+
+use crate::{
+    history::HistoryProvider,
+    navigation::NavigationTarget,
+    prelude::{
+        FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex,
+    },
+    routes::{ContentAtom, Segment},
+    segments::{NameMap, NamedSegment},
+    utils::{resolve_target, route_segment},
+    Name, RouterState,
+};
+
+/// Messages that the [`RouterService`] can handle.
+pub enum RouterMessage<I> {
+    /// Subscribe to router update.
+    Subscribe(Arc<I>),
+    /// Navigate to the specified target.
+    Push(NavigationTarget),
+    /// Replace the current location with the specified target.
+    Replace(NavigationTarget),
+    /// Trigger a routing update.
+    Update,
+    /// Navigate to the previous history entry.
+    GoBack,
+    /// Navigate to the next history entry.
+    GoForward,
+}
+
+enum NavigationFailure {
+    External(String),
+    Named(Name),
+}
+
+/// A function the router will call after every routing update.
+pub type RoutingCallback<T> = Arc<dyn Fn(&RouterState<T>) -> Option<NavigationTarget>>;
+
+/// A collection of router data that manages all routing functionality.
+pub struct RouterService<T, I>
+where
+    T: Clone,
+    I: Clone + PartialEq + Eq,
+{
+    history: Box<dyn HistoryProvider>,
+    routes: Segment<T>,
+    names: Arc<NameMap>,
+
+    receiver: UnboundedReceiver<RouterMessage<I>>,
+    state: Arc<RwLock<RouterState<T>>>,
+
+    subscribers: Vec<Weak<I>>,
+    subscriber_update: Arc<dyn Fn(I)>,
+    routing_callback: Option<RoutingCallback<T>>,
+
+    failure_external_navigation: ContentAtom<T>,
+    failure_named_navigation: ContentAtom<T>,
+    failure_redirection_limit: ContentAtom<T>,
+}
+
+impl<T, I> RouterService<T, I>
+where
+    T: Clone,
+    I: Clone + PartialEq + Eq + Send + Sync + 'static,
+{
+    /// Create a new [`RouterService`].
+    ///
+    /// # Parameters
+    /// 1. `routes`: The root [`Segment`] the router should handle.
+    /// 2. `history`: A [`HistoryProvider`] to handle the navigation history.
+    /// 3. `subscriber_callback`: A function the rooter can call to update UI integrations.
+    /// 4. `failure_external_navigation`: Content to be displayed when an external navigation fails.
+    /// 5. `failure_named_navigation`: Content to be displayed when a named navigation fails.
+    /// 6. `failure_redirection_limit`: Content to be displayed when the redirection limit is
+    ///    breached.
+    ///
+    /// # Returns
+    /// 1. The [`RouterService`].
+    /// 2. A [`Sender`] to send [`RouterMessage`]s to the [`RouterService`].
+    /// 3. Access to the [`RouterState`]. **DO NOT WRITE TO THIS!!!** Seriously, **READ ONLY!!!**
+    #[allow(clippy::type_complexity)]
+    pub fn new(
+        routes: Segment<T>,
+        mut history: Box<dyn HistoryProvider>,
+        subscriber_update: Arc<dyn Fn(I)>,
+        routing_callback: Option<RoutingCallback<T>>,
+        failure_external_navigation: ContentAtom<T>,
+        failure_named_navigation: ContentAtom<T>,
+        failure_redirection_limit: ContentAtom<T>,
+    ) -> (
+        Self,
+        UnboundedSender<RouterMessage<I>>,
+        Arc<RwLock<RouterState<T>>>,
+    ) {
+        // index names
+        let names = Arc::new(NamedSegment::from_segment(&routes));
+
+        // create channel
+        let (sender, receiver) = unbounded();
+
+        // initialize history
+        let history_sender = sender.clone();
+        history.updater(Arc::new(move || {
+            let _ = history_sender.unbounded_send(RouterMessage::Update);
+        }));
+        let state = Arc::new(RwLock::new(RouterState {
+            name_map: Arc::clone(&names),
+            ..Default::default()
+        }));
+
+        (
+            Self {
+                history,
+                names: names,
+                routes,
+                receiver,
+                state: Arc::clone(&state),
+                subscribers: Vec::new(),
+                subscriber_update,
+                routing_callback,
+                failure_external_navigation,
+                failure_named_navigation,
+                failure_redirection_limit,
+            },
+            sender,
+            state,
+        )
+    }
+
+    /// Perform the initial routing.
+    ///
+    /// Call this once, as soon as possible after creating the [`RouterService`]. Do not call this,
+    /// if you are going to call the `run` function.
+    pub fn init(&mut self) {
+        *self.sync_state_write_lock() = self
+            .update_routing()
+            .map_right(|err| self.handle_navigation_failure(&self.sync_state_read_lock(), err))
+            .either_into();
+    }
+
+    /// Handle all messages the router has received and then return.
+    ///
+    /// Call `init` before calling this function.
+    pub fn run_current(&mut self) {
+        let mut state = None;
+        while let Ok(Some(msg)) = self.receiver.try_next() {
+            let current = match self.handle_message(msg) {
+                (_, Some(err)) => Either::Right(err),
+                (true, _) => self.update_routing(),
+                _ => continue,
+            }
+            .left_and_then(|state| {
+                if let Some(cb) = &self.routing_callback {
+                    if let Some(nt) = cb(&state) {
+                        self.replace(nt);
+                        return self.update_routing();
+                    }
+                }
+
+                Either::Left(state)
+            })
+            .map_right(|err| self.handle_navigation_failure(&self.sync_state_read_lock(), err))
+            .either_into();
+            state = Some(current);
+        }
+
+        if let Some(state) = state {
+            *self.sync_state_write_lock() = state;
+        }
+
+        self.update_subscribers();
+    }
+
+    fn sync_state_read_lock(&self) -> RwLockReadGuard<RouterState<T>> {
+        loop {
+            if let Some(s) = self.state.try_read() {
+                return s;
+            }
+        }
+    }
+
+    fn sync_state_write_lock(&mut self) -> RwLockWriteGuard<RouterState<T>> {
+        loop {
+            if let Some(s) = self.state.try_write() {
+                return s;
+            }
+        }
+    }
+
+    /// Handle all routing messages until ended from the outside.
+    pub async fn run(&mut self) {
+        // init (unlike function with same name this is async)
+        {
+            *self.state.write().await = match self.update_routing() {
+                Either::Left(state) => state,
+                Either::Right(err) => {
+                    self.handle_navigation_failure(&*self.state.read().await, err)
+                }
+            };
+        }
+        self.update_subscribers();
+
+        while let Some(msg) = self.receiver.next().await {
+            let state = match self.handle_message(msg) {
+                (_, Some(err)) => Either::Right(err),
+                (true, _) => self.update_routing(),
+                _ => continue,
+            }
+            .left_and_then(|state| {
+                if let Some(cb) = &self.routing_callback {
+                    if let Some(nt) = cb(&state) {
+                        self.replace(nt);
+                        return self.update_routing();
+                    }
+                }
+
+                Either::Left(state)
+            });
+
+            *self.state.write().await = match state {
+                Either::Left(state) => state,
+                Either::Right(err) => {
+                    self.handle_navigation_failure(&*self.state.read().await, err)
+                }
+            };
+
+            self.update_subscribers();
+        }
+    }
+
+    fn handle_navigation_failure(
+        &self,
+        state: &RouterState<T>,
+        err: NavigationFailure,
+    ) -> RouterState<T> {
+        match err {
+            NavigationFailure::External(url) => RouterState {
+                can_go_back: state.can_go_back,
+                can_go_forward: state.can_go_forward,
+                path: state.path.clone(),
+                query: state.query.clone(),
+                prefix: state.prefix.clone(),
+                names: {
+                    let mut r = HashSet::new();
+                    r.insert(Name::of::<FailureExternalNavigation>());
+                    r
+                },
+                parameters: {
+                    let mut r = HashMap::new();
+                    r.insert(Name::of::<FailureExternalNavigation>(), url);
+                    r
+                },
+                name_map: Arc::clone(&state.name_map),
+                content: vec![self.failure_external_navigation.clone()],
+                named_content: BTreeMap::new(),
+            },
+            NavigationFailure::Named(n) => RouterState {
+                can_go_back: state.can_go_back,
+                can_go_forward: state.can_go_forward,
+                path: state.path.clone(),
+                query: state.query.clone(),
+                prefix: state.prefix.clone(),
+                names: {
+                    let mut r = HashSet::new();
+                    r.insert(Name::of::<FailureNamedNavigation>());
+                    r
+                },
+                parameters: {
+                    let mut r = HashMap::new();
+                    r.insert(Name::of::<FailureExternalNavigation>(), n.to_string());
+                    r
+                },
+                name_map: Arc::clone(&state.name_map),
+                content: vec![self.failure_named_navigation.clone()],
+                named_content: BTreeMap::new(),
+            },
+        }
+    }
+
+    #[must_use]
+    fn handle_message(&mut self, msg: RouterMessage<I>) -> (bool, Option<NavigationFailure>) {
+        let failure = match msg {
+            RouterMessage::Subscribe(id) => {
+                self.subscribe(id);
+                return (false, None);
+            }
+            RouterMessage::Push(nt) => self.push(nt),
+            RouterMessage::Replace(nt) => self.replace(nt),
+            RouterMessage::Update => None,
+            RouterMessage::GoBack => {
+                self.history.go_back();
+                None
+            }
+            RouterMessage::GoForward => {
+                self.history.go_forward();
+                None
+            }
+        };
+
+        (true, failure)
+    }
+
+    #[must_use]
+    fn update_routing(&mut self) -> Either<RouterState<T>, NavigationFailure> {
+        for _ in 0..=25 {
+            match self.update_routing_inner() {
+                Either::Left(state) => return Either::Left(state),
+                Either::Right(nt) => {
+                    if let Some(err) = self.replace(nt) {
+                        return Either::Right(err);
+                    }
+                }
+            }
+        }
+
+        #[cfg(debug_assertions)]
+        panic!("reached redirect limit of 25");
+        #[allow(unreachable_code)]
+        Either::Left(RouterState {
+            content: vec![self.failure_redirection_limit.clone()],
+            can_go_back: self.history.can_go_back(),
+            can_go_forward: self.history.can_go_forward(),
+            path: self.history.current_path(),
+            query: self.history.current_query(),
+            prefix: self.history.current_prefix(),
+            name_map: Arc::clone(&self.names),
+            names: {
+                let mut r = HashSet::new();
+                r.insert(Name::of::<FailureRedirectionLimit>());
+                r
+            },
+            ..Default::default()
+        })
+    }
+
+    #[must_use]
+    fn update_routing_inner(&mut self) -> Either<RouterState<T>, NavigationTarget> {
+        // prepare path
+        let mut path = self.history.current_path();
+        path.remove(0);
+        if path.ends_with('/') {
+            path.pop();
+        }
+
+        let values = match path.is_empty() {
+            false => path.split('/').collect::<Vec<_>>(),
+            true => Vec::new(),
+        };
+
+        // add root index name
+        let mut names = HashSet::new();
+        if values.is_empty() {
+            names.insert(Name::of::<RootIndex>());
+        };
+
+        route_segment(
+            &self.routes,
+            &values,
+            RouterState {
+                can_go_back: self.history.can_go_back(),
+                can_go_forward: self.history.can_go_forward(),
+                path: self.history.current_path(),
+                query: self.history.current_query(),
+                prefix: self.history.current_prefix(),
+                name_map: Arc::clone(&self.names),
+                names,
+                ..Default::default()
+            },
+        )
+    }
+
+    fn push(&mut self, target: NavigationTarget) -> Option<NavigationFailure> {
+        match resolve_target(&self.names, &target) {
+            Either::Left(Either::Left(p)) => self.history.push(p),
+            Either::Left(Either::Right(n)) => return Some(NavigationFailure::Named(n)),
+            Either::Right(e) => return self.external(e),
+        }
+
+        None
+    }
+
+    fn replace(&mut self, target: NavigationTarget) -> Option<NavigationFailure> {
+        match resolve_target(&self.names, &target) {
+            Either::Left(Either::Left(p)) => self.history.replace(p),
+            Either::Left(Either::Right(n)) => return Some(NavigationFailure::Named(n)),
+            Either::Right(e) => return self.external(e),
+        }
+
+        None
+    }
+
+    fn external(&mut self, external: String) -> Option<NavigationFailure> {
+        match self.history.external(external.clone()) {
+            true => None,
+            false => Some(NavigationFailure::External(external)),
+        }
+    }
+
+    fn subscribe(&mut self, id: Arc<I>) {
+        self.subscribers.push(Arc::downgrade(&id));
+        (self.subscriber_update)(id.as_ref().clone());
+    }
+
+    fn update_subscribers(&mut self) {
+        let mut previous = Vec::new();
+        self.subscribers.retain(|id| {
+            if let Some(id) = id.upgrade() {
+                if previous.contains(&id) {
+                    false
+                } else {
+                    (self.subscriber_update)(id.as_ref().clone());
+                    previous.push(id.clone());
+                    true
+                }
+            } else {
+                false
+            }
+        });
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    //! The tests for [`RouterService`] test various functions that are not exposed as public.
+    //! However, several of those have an observable effect on the behavior of exposed functions.
+    //!
+    //! The alternative would be to send messages via the services channel and calling one of the
+    //! `run` functions. However, for readability and clarity, it was chosen to directly call the
+    //! private functions.
+
+    use std::sync::Mutex;
+
+    use crate::{
+        history::MemoryHistory,
+        routes::{ParameterRoute, Route, RouteContent},
+    };
+
+    use super::*;
+
+    fn test_segment() -> Segment<&'static str> {
+        Segment::content(RouteContent::Content(ContentAtom("index")))
+            .fixed(
+                "fixed",
+                Route::content(RouteContent::Content(ContentAtom("fixed"))).name::<bool>(),
+            )
+            .fixed(
+                "redirect",
+                Route::content(RouteContent::Redirect(NavigationTarget::Internal(
+                    String::from("fixed"),
+                ))),
+            )
+            .fixed(
+                "redirection-loop",
+                Route::content(RouteContent::Redirect(NavigationTarget::Internal(
+                    String::from("/redirection-loop"),
+                ))),
+            )
+            .fixed(
+                "%F0%9F%8E%BA",
+                Route::content(RouteContent::Content(ContentAtom("🎺"))),
+            )
+            .catch_all(ParameterRoute::empty::<bool>())
+    }
+
+    #[test]
+    fn new_provides_update_to_history() {
+        struct TestHistory {}
+
+        impl HistoryProvider for TestHistory {
+            fn current_path(&self) -> String {
+                todo!()
+            }
+
+            fn current_query(&self) -> Option<String> {
+                todo!()
+            }
+
+            fn go_back(&mut self) {
+                todo!()
+            }
+
+            fn go_forward(&mut self) {
+                todo!()
+            }
+
+            fn push(&mut self, _path: String) {
+                todo!()
+            }
+
+            fn replace(&mut self, _path: String) {
+                todo!()
+            }
+
+            fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
+                callback();
+            }
+        }
+
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(TestHistory {}),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+
+        assert!(matches!(
+            s.receiver.try_next().unwrap().unwrap(),
+            RouterMessage::Update
+        ));
+    }
+
+    #[test]
+    fn update_routing() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        assert_eq!(s.names, s.state.try_read().unwrap().name_map);
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        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());
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.query, Some(String::from("test=value")));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+        assert_eq!(s.names, state.name_map);
+    }
+
+    #[test]
+    fn update_routing_root_index() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::default()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        assert_eq!(state.content, vec![ContentAtom("index")]);
+        assert_eq!(state.names, {
+            let mut r = HashSet::new();
+            r.insert(Name::of::<RootIndex>());
+            r
+        });
+        assert!(state.parameters.is_empty());
+        assert_eq!(state.path, String::from("/"));
+        assert!(state.query.is_none());
+        assert!(state.prefix.is_none());
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn update_routing_redirect() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        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());
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    #[should_panic = "reached redirect limit of 25"]
+    #[cfg(debug_assertions)]
+    fn update_routing_redirect_debug() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+    }
+
+    #[test]
+    #[cfg(not(debug_assertions))]
+    fn update_routing_redirect_release() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        assert_eq!(state.content, vec![ContentAtom("redirect limit")]);
+        assert_eq!(state.names, {
+            let mut r = HashSet::new();
+            r.insert(Name::of::<FailureRedirectionLimit>());
+            r
+        });
+        assert!(state.parameters.is_empty());
+        assert_eq!(state.path, String::from("/redirection-loop"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn update_subscribers() {
+        let ids = Arc::new(Mutex::new(Vec::new()));
+        let ids2 = Arc::clone(&ids);
+
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            Segment::empty(),
+            Box::new(MemoryHistory::default()),
+            Arc::new(move |id| {
+                ids2.lock().unwrap().push(id);
+            }),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+
+        let id0 = Arc::new(0);
+        s.subscribe(Arc::clone(&id0));
+
+        let id1 = Arc::new(1);
+        s.subscribe(Arc::clone(&id1));
+
+        let id1 = Arc::try_unwrap(id1).unwrap();
+        s.update_subscribers();
+
+        assert_eq!(s.subscribers.len(), 1);
+        assert_eq!(s.subscribers[0].upgrade().unwrap(), id0);
+        assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]);
+    }
+
+    #[test]
+    fn push_internal() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::default()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.push(NavigationTarget::Internal(String::from("/fixed")));
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        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());
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.can_go_back, true);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn push_named() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::default()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.push(NavigationTarget::named::<bool>());
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        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());
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.can_go_back, true);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn push_external() {
+        let (mut s, tx, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+        tx.unbounded_send(RouterMessage::Push(NavigationTarget::External(
+            String::from("https://dioxuslabs.com/"),
+        )))
+        .unwrap();
+        s.run_current();
+
+        let state = s.state.try_read().unwrap();
+        assert_eq!(state.content, vec![ContentAtom("external target")]);
+        assert_eq!(state.names, {
+            let mut r = HashSet::new();
+            r.insert(Name::of::<FailureExternalNavigation>());
+            r
+        });
+        assert_eq!(state.parameters, {
+            let mut r = HashMap::new();
+            r.insert(
+                Name::of::<FailureExternalNavigation>(),
+                String::from("https://dioxuslabs.com/"),
+            );
+            r
+        });
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn replace_named() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::default()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.replace(NavigationTarget::named::<bool>());
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        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());
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn replace_internal() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::default()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.replace(NavigationTarget::Internal(String::from("/fixed")));
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        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());
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn replace_external() {
+        let (mut s, tx, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+        tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External(
+            String::from("https://dioxuslabs.com/"),
+        )))
+        .unwrap();
+        s.run_current();
+
+        let state = s.state.try_read().unwrap();
+        assert_eq!(state.content, vec![ContentAtom("external target")]);
+        assert_eq!(state.names, {
+            let mut r = HashSet::new();
+            r.insert(Name::of::<FailureExternalNavigation>());
+            r
+        });
+        assert_eq!(state.parameters, {
+            let mut r = HashMap::new();
+            r.insert(
+                Name::of::<FailureExternalNavigation>(),
+                String::from("https://dioxuslabs.com/"),
+            );
+            r
+        });
+        assert_eq!(state.path, String::from("/fixed"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn subscribe() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            Segment::empty(),
+            Box::new(MemoryHistory::default()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+
+        let id = Arc::new(0);
+        s.subscribe(Arc::clone(&id));
+
+        assert_eq!(s.subscribers.len(), 1);
+        assert_eq!(s.subscribers[0].upgrade().unwrap(), id);
+    }
+
+    #[test]
+    fn routing_callback() {
+        let paths = Arc::new(Mutex::new(Vec::new()));
+        let paths2 = Arc::clone(&paths);
+
+        let (mut s, c, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
+            Arc::new(|_| {}),
+            Some(Arc::new(move |state| {
+                paths2.lock().unwrap().push(state.path.clone());
+                Some("/%F0%9F%8E%BA".into())
+            })),
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+
+        s.init();
+        assert!(paths.lock().unwrap().is_empty());
+
+        c.unbounded_send(RouterMessage::Update).unwrap();
+        s.run_current();
+        assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]);
+
+        let state = s.state.try_read().unwrap();
+        assert_eq!(state.content, vec![ContentAtom("🎺")])
+    }
+
+    #[test]
+    fn url_decoding_do() {
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        assert!(state.content.is_empty());
+        assert!(state.names.is_empty());
+        assert_eq!(state.parameters, {
+            let mut r = HashMap::new();
+            r.insert(Name::of::<bool>(), String::from("🥳"));
+            r
+        });
+        assert_eq!(state.path, String::from("/%F0%9F%A5%B3"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn url_decoding_do_not() {
+        let (mut s, c, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+        c.unbounded_send(RouterMessage::Update).unwrap();
+        s.run_current();
+
+        let state = s.state.try_read().unwrap();
+        assert_eq!(state.content, vec![ContentAtom("🎺")]);
+        assert!(state.names.is_empty());
+        assert!(state.parameters.is_empty());
+        assert_eq!(state.path, String::from("/%F0%9F%8E%BA"));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+
+    #[test]
+    fn prefix() {
+        struct TestHistory {}
+
+        impl HistoryProvider for TestHistory {
+            fn current_path(&self) -> String {
+                String::from("/")
+            }
+
+            fn current_query(&self) -> Option<String> {
+                None
+            }
+
+            fn current_prefix(&self) -> Option<String> {
+                Some(String::from("/prefix"))
+            }
+
+            fn can_go_back(&self) -> bool {
+                false
+            }
+
+            fn can_go_forward(&self) -> bool {
+                false
+            }
+
+            fn go_back(&mut self) {
+                todo!()
+            }
+
+            fn go_forward(&mut self) {
+                todo!()
+            }
+
+            fn push(&mut self, _path: String) {
+                todo!()
+            }
+
+            fn replace(&mut self, _path: String) {
+                todo!()
+            }
+
+            fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
+                callback();
+            }
+        }
+
+        let (mut s, _, _) = RouterService::<_, u8>::new(
+            test_segment(),
+            Box::new(TestHistory {}),
+            Arc::new(|_| {}),
+            None,
+            ContentAtom("external target"),
+            ContentAtom("named target"),
+            ContentAtom("redirect limit"),
+        );
+        s.init();
+
+        let state = s.state.try_read().unwrap();
+        assert_eq!(state.content, vec![ContentAtom("index")]);
+        assert_eq!(state.names, {
+            let mut r = HashSet::new();
+            r.insert(Name::of::<RootIndex>());
+            r
+        });
+        assert!(state.parameters.is_empty());
+        assert_eq!(state.path, String::from("/"));
+        assert!(state.query.is_none());
+        assert_eq!(state.prefix, Some(String::from("/prefix")));
+        assert_eq!(state.can_go_back, false);
+        assert_eq!(state.can_go_forward, false);
+    }
+}

+ 288 - 0
packages/router-core/src/state.rs

@@ -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()
+        }
+    }
+}

+ 144 - 0
packages/router-core/src/utils/name.rs

@@ -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"))
+        );
+    }
+}

+ 489 - 0
packages/router-core/src/utils/route.rs

@@ -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
+        });
+    }
+}

+ 147 - 0
packages/router-core/src/utils/sitemap.rs

@@ -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, &current, 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, &current, 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, &current, 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, &current, 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, &current, 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, &current, 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(), &parameters, "", &mut result);
+
+        assert_eq!(
+            result,
+            vec![
+                "/",
+                "/fixed",
+                "/fixed/nested",
+                "/m1",
+                "/m1/n2",
+                "/catch%20all",
+                "/catch%20all/n1",
+                "/catch%20all/n2"
+            ]
+        );
+    }
+}

+ 120 - 0
packages/router-core/src/utils/target.rs

@@ -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/"))
+        );
+    }
+}