Selaa lähdekoodia

delete unneeded files

Evan Almloff 2 vuotta sitten
vanhempi
commit
e26ff6a5c0

+ 2 - 37
packages/router-core/src/lib.rs

@@ -3,38 +3,14 @@
 
 pub mod history;
 
-mod name;
-pub use name::*;
+mod router;
+pub use router::*;
 
 pub mod navigation;
 
 mod navigator;
 pub use navigator::*;
 
-mod outlet;
-pub use outlet::*;
-
-/// Types for defining the available routes.
-pub mod routes {
-    mod atom;
-    pub use atom::*;
-
-    mod content;
-    pub use content::*;
-
-    mod matcher;
-    pub use matcher::*;
-
-    mod route;
-    pub use route::*;
-
-    mod segment;
-    pub use segment::*;
-
-    mod parameter_route;
-    pub use parameter_route::*;
-}
-
 mod service;
 pub use service::*;
 
@@ -42,24 +18,13 @@ mod state;
 pub use state::*;
 
 mod utils {
-    mod name;
-    pub use name::*;
-
-    mod route;
-    pub use route::*;
-
     mod sitemap;
     pub use sitemap::*;
-
-    mod target;
-    pub use target::*;
 }
 
 /// A collection of useful types most applications might need.
 pub mod prelude {
-    pub use crate::name::*;
     pub use crate::navigation::*;
-    pub use crate::routes::*;
 
     /// An external navigation failure.
     ///

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

@@ -1,76 +0,0 @@
-use std::{
-    any::{type_name, TypeId},
-    fmt::Display,
-    hash::Hash,
-};
-
-pub(crate) mod segments;
-
-/// A combination of a types [`TypeId`] and its name.
-///
-/// This is used inside the router wherever a name is needed. This has the advantage that typos will
-/// be caught by the compiler.
-///
-/// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only for
-/// brevity. It is recommend to use types with descriptive names, and create unit structs if needed,
-/// like this.
-///
-/// ```rust
-/// # use dioxus_router_core::Name;
-/// struct SomeName;
-/// let name = Name::of::<bool>();
-/// ```
-#[derive(Clone, Debug)]
-pub struct Name {
-    id: TypeId,
-    name: &'static str,
-}
-
-impl Name {
-    /// Get the [`Name`] of `T`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::Name;
-    /// struct SomeName;
-    /// let name = Name::of::<bool>();
-    /// ```
-    #[must_use]
-    pub fn of<T: 'static>() -> Self {
-        Self {
-            id: TypeId::of::<T>(),
-            name: type_name::<T>(),
-        }
-    }
-}
-
-impl PartialEq for Name {
-    fn eq(&self, other: &Self) -> bool {
-        self.id == other.id
-    }
-}
-
-impl Eq for Name {}
-
-impl PartialOrd for Name {
-    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        self.id.partial_cmp(&other.id)
-    }
-}
-
-impl Ord for Name {
-    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-        self.id.cmp(&other.id)
-    }
-}
-
-impl Display for Name {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.name)
-    }
-}
-
-impl Hash for Name {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.id.hash(state);
-    }
-}

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

@@ -1,133 +0,0 @@
-use std::collections::BTreeMap;
-
-use crate::{
-    prelude::RootIndex,
-    routes::{ParameterRoute, Route, Segment},
-    Name,
-};
-
-pub type NameMap = BTreeMap<Name, Vec<NamedSegment>>;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum NamedSegment {
-    Fixed(String),
-    Parameter(Name),
-}
-
-impl NamedSegment {
-    pub fn from_segment<T: Clone>(segment: &Segment<T>) -> NameMap {
-        let mut res = BTreeMap::new();
-        res.insert(Name::of::<RootIndex>(), vec![]);
-        Self::from_segment_inner(Vec::new(), segment, &mut res);
-        res
-    }
-
-    fn from_segment_inner<T: Clone>(
-        current: Vec<NamedSegment>,
-        segment: &Segment<T>,
-        result: &mut NameMap,
-    ) {
-        for (p, r) in &segment.fixed {
-            Self::from_route(current.clone(), p, r, result);
-        }
-
-        for (_, r) in &segment.matching {
-            Self::from_parameter_route(current.clone(), r, result);
-        }
-
-        if let Some(r) = &segment.catch_all {
-            Self::from_parameter_route(current, r, result);
-        }
-    }
-
-    fn from_route<T: Clone>(
-        mut current: Vec<NamedSegment>,
-        path: &str,
-        route: &Route<T>,
-        result: &mut NameMap,
-    ) {
-        current.push(Self::Fixed(path.to_string()));
-
-        if let Some(n) = &route.name {
-            debug_assert!(!result.contains_key(n), "duplicate name: {n}");
-            result.entry(n.clone()).or_insert_with(|| current.clone());
-        }
-
-        if let Some(n) = &route.nested {
-            Self::from_segment_inner(current, n, result);
-        }
-    }
-
-    fn from_parameter_route<T: Clone>(
-        mut current: Vec<NamedSegment>,
-        route: &ParameterRoute<T>,
-        result: &mut NameMap,
-    ) {
-        current.push(Self::Parameter(route.key.clone()));
-
-        if let Some(n) = &route.name {
-            debug_assert!(!result.contains_key(n), "duplicate name: {n}");
-            result.entry(n.clone()).or_insert_with(|| current.clone());
-        }
-
-        if let Some(n) = &route.nested {
-            Self::from_segment_inner(current, n, result);
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::{NamedSegment::*, *};
-
-    #[test]
-    fn create_map() {
-        assert_eq!(
-            NamedSegment::from_segment(
-                &Segment::<&'static str>::empty()
-                    .fixed(
-                        "fixed",
-                        Route::empty()
-                            .name::<u32>()
-                            .nested(Segment::empty().fixed("nested", Route::empty().name::<u64>()))
-                    )
-                    .matching(
-                        String::from(""),
-                        ParameterRoute::empty::<i32>().name::<i32>().nested(
-                            Segment::empty().matching(
-                                String::from(""),
-                                ParameterRoute::empty::<i64>().name::<i64>()
-                            )
-                        )
-                    )
-                    .catch_all(ParameterRoute::empty::<f32>().name::<f32>().nested(
-                        Segment::empty().catch_all(ParameterRoute::empty::<f64>().name::<f64>())
-                    ))
-            ),
-            {
-                let mut r = BTreeMap::new();
-                r.insert(Name::of::<RootIndex>(), vec![]);
-
-                r.insert(Name::of::<u32>(), vec![Fixed(String::from("fixed"))]);
-                r.insert(
-                    Name::of::<u64>(),
-                    vec![Fixed(String::from("fixed")), Fixed(String::from("nested"))],
-                );
-
-                r.insert(Name::of::<i32>(), vec![Parameter(Name::of::<i32>())]);
-                r.insert(
-                    Name::of::<i64>(),
-                    vec![Parameter(Name::of::<i32>()), Parameter(Name::of::<i64>())],
-                );
-
-                r.insert(Name::of::<f32>(), vec![Parameter(Name::of::<f32>())]);
-                r.insert(
-                    Name::of::<f64>(),
-                    vec![Parameter(Name::of::<f32>()), Parameter(Name::of::<f64>())],
-                );
-
-                r
-            }
-        )
-    }
-}

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

@@ -4,8 +4,6 @@ use std::{collections::HashMap, str::FromStr};
 
 use url::{ParseError, Url};
 
-use crate::Name;
-
 /// A target for the router to navigate to.
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum NavigationTarget {

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

@@ -1,201 +0,0 @@
-use std::collections::BTreeMap;
-
-use crate::Name;
-
-/// Information outlets can use to find out what to render.
-///
-/// Outlets (which must be implemented by crates tying dioxus-router-core to UI crates) can use this
-/// information to find out how deeply nested they are within other outlets, and communicate the
-/// same to outlets nested inside them.
-#[derive(Debug, Default, Clone)]
-pub struct OutletData {
-    main: Option<usize>,
-    named: BTreeMap<Name, usize>,
-}
-
-impl OutletData {
-    /// Create some [`OutletData`] nested one level deeper and get the current depth.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::{Name, OutletData};
-    /// let mut d = OutletData::default();
-    /// let (m, a, n);
-    /// (m, d) = d.next(&None);
-    /// (a, d) = d.next(&None);
-    /// (n, d) = d.next(&Some(Name::of::<bool>()));
-    ///
-    /// assert_eq!(m, 0);
-    /// assert_eq!(a, 1);
-    /// assert_eq!(n, 0);
-    /// ```
-    pub fn next(&self, name: &Option<Name>) -> (usize, Self) {
-        let mut next = self.clone();
-
-        let depth = next.depth(name).map(|d| d + 1).unwrap_or(0);
-
-        next.set_depth(name, depth);
-
-        (depth, next)
-    }
-
-    /// Get the current depth for `name`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::OutletData;
-    /// let mut d = OutletData::default();
-    /// let b = d.depth(&None);
-    /// d.set_depth(&None, 18);
-    /// let a = d.depth(&None);
-    ///
-    /// assert_eq!(b, None);
-    /// assert_eq!(a, Some(18));
-    /// ```
-    pub fn depth(&self, name: &Option<Name>) -> Option<usize> {
-        match name {
-            None => self.main,
-            Some(n) => self.named.get(n).copied(),
-        }
-    }
-
-    /// Set the depth for `name`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::OutletData;
-    /// let mut d = OutletData::default();
-    /// let b = d.depth(&None);
-    /// d.set_depth(&None, 18);
-    /// let a = d.depth(&None);
-    ///
-    /// assert_eq!(b, None);
-    /// assert_eq!(a, Some(18));
-    /// ```
-    pub fn set_depth(&mut self, name: &Option<Name>, depth: usize) {
-        match name {
-            None => self.main = Some(depth),
-            Some(n) => _ = self.named.insert(n.clone(), depth),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    fn test_data() -> OutletData {
-        let mut named = BTreeMap::new();
-        named.insert(Name::of::<bool>(), 0);
-        named.insert(Name::of::<u8>(), 8);
-        named.insert(Name::of::<u16>(), 16);
-        named.insert(Name::of::<u32>(), 32);
-        named.insert(Name::of::<u64>(), 64);
-
-        OutletData {
-            main: Some(18),
-            named,
-        }
-    }
-
-    #[test]
-    fn default() {
-        let d = OutletData::default();
-
-        assert!(d.main.is_none());
-        assert!(d.named.is_empty());
-    }
-
-    #[test]
-    fn depth() {
-        let td = test_data();
-
-        assert_eq!(td.depth(&None), Some(18));
-        assert_eq!(td.depth(&Some(Name::of::<bool>())), Some(0));
-
-        assert_eq!(td.depth(&Some(Name::of::<u8>())), Some(8));
-        assert_eq!(td.depth(&Some(Name::of::<u16>())), Some(16));
-        assert_eq!(td.depth(&Some(Name::of::<u32>())), Some(32));
-        assert_eq!(td.depth(&Some(Name::of::<u64>())), Some(64));
-
-        assert_eq!(td.depth(&Some(Name::of::<i8>())), None);
-        assert_eq!(td.depth(&Some(Name::of::<i16>())), None);
-        assert_eq!(td.depth(&Some(Name::of::<i32>())), None);
-        assert_eq!(td.depth(&Some(Name::of::<i64>())), None);
-    }
-
-    #[test]
-    fn set_depth() {
-        let mut td = test_data();
-
-        // set
-        td.set_depth(&None, 0);
-        td.set_depth(&Some(Name::of::<bool>()), 1);
-
-        td.set_depth(&Some(Name::of::<u8>()), 2);
-        td.set_depth(&Some(Name::of::<u16>()), 4);
-        td.set_depth(&Some(Name::of::<u32>()), 8);
-        td.set_depth(&Some(Name::of::<u64>()), 16);
-
-        td.set_depth(&Some(Name::of::<i8>()), 32);
-        td.set_depth(&Some(Name::of::<i16>()), 64);
-        td.set_depth(&Some(Name::of::<i32>()), 128);
-        td.set_depth(&Some(Name::of::<i64>()), 256);
-
-        // check
-        assert_eq!(td.depth(&None), Some(0));
-        assert_eq!(*td.named.get(&Name::of::<bool>()).unwrap(), 1);
-
-        assert_eq!(*td.named.get(&Name::of::<u8>()).unwrap(), 2);
-        assert_eq!(*td.named.get(&Name::of::<u16>()).unwrap(), 4);
-        assert_eq!(*td.named.get(&Name::of::<u32>()).unwrap(), 8);
-        assert_eq!(*td.named.get(&Name::of::<u64>()).unwrap(), 16);
-
-        assert_eq!(*td.named.get(&Name::of::<i8>()).unwrap(), 32);
-        assert_eq!(*td.named.get(&Name::of::<i16>()).unwrap(), 64);
-        assert_eq!(*td.named.get(&Name::of::<i32>()).unwrap(), 128);
-        assert_eq!(*td.named.get(&Name::of::<i64>()).unwrap(), 256);
-    }
-
-    #[test]
-    fn next() {
-        let td = test_data();
-
-        let (current, next) = td.next(&None);
-        assert_eq!(current, 19);
-        assert_eq!(next.depth(&None), Some(19));
-
-        let (current, next) = td.next(&Some(Name::of::<bool>()));
-        assert_eq!(current, 1);
-        assert_eq!(*next.named.get(&Name::of::<bool>()).unwrap(), 1);
-
-        let (current, next) = td.next(&Some(Name::of::<u8>()));
-        assert_eq!(current, 9);
-        assert_eq!(*next.named.get(&Name::of::<u8>()).unwrap(), 9);
-
-        let (current, next) = td.next(&Some(Name::of::<u16>()));
-        assert_eq!(current, 17);
-        assert_eq!(*next.named.get(&Name::of::<u16>()).unwrap(), 17);
-
-        let (current, next) = td.next(&Some(Name::of::<u32>()));
-        assert_eq!(current, 33);
-        assert_eq!(*next.named.get(&Name::of::<u32>()).unwrap(), 33);
-
-        let (current, next) = td.next(&Some(Name::of::<u64>()));
-        assert_eq!(current, 65);
-        assert_eq!(*next.named.get(&Name::of::<u64>()).unwrap(), 65);
-
-        let (current, next) = td.next(&Some(Name::of::<i8>()));
-        assert_eq!(current, 0);
-        assert_eq!(*next.named.get(&Name::of::<i8>()).unwrap(), 0);
-
-        let (current, next) = td.next(&Some(Name::of::<i16>()));
-        assert_eq!(current, 0);
-        assert_eq!(*next.named.get(&Name::of::<i16>()).unwrap(), 0);
-
-        let (current, next) = td.next(&Some(Name::of::<i32>()));
-        assert_eq!(current, 0);
-        assert_eq!(*next.named.get(&Name::of::<i32>()).unwrap(), 0);
-
-        let (current, next) = td.next(&Some(Name::of::<i64>()));
-        assert_eq!(current, 0);
-        assert_eq!(*next.named.get(&Name::of::<i64>()).unwrap(), 0);
-    }
-}

+ 163 - 0
packages/router-core/src/router.rs

@@ -0,0 +1,163 @@
+use crate::history::HistoryProvider;
+use std::str::FromStr;
+
+#[derive(Debug, PartialEq)]
+struct RouteParseError<E: std::fmt::Display> {
+    attempted_routes: Vec<E>,
+}
+
+impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Route did not match:\nAttempted Matches:\n")?;
+        for (i, route) in self.attempted_routes.iter().enumerate() {
+            writeln!(f, "{}) {route}", i + 1)?;
+        }
+        Ok(())
+    }
+}
+
+struct Router<R: Routable, H: HistoryProvider>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    history: H,
+    route: R,
+}
+
+impl<R: Routable, H: HistoryProvider> Router<R, H>
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    fn new(history: H) -> Result<Self, R::Err> {
+        let path = history.current_path();
+        Ok(Self {
+            history,
+            route: R::from_str(path.as_str())?,
+        })
+    }
+}
+
+// #[derive(Props, PartialEq)]
+// struct RouterProps {
+//     current_route: String,
+// }
+
+trait Routable: FromStr + std::fmt::Display + Clone
+where
+    <Self as FromStr>::Err: std::fmt::Display,
+{
+//     fn render(self, cx: &ScopeState) -> Element;
+
+//     fn comp(cx: Scope<RouterProps>) -> Element
+//     where
+//         Self: 'static,
+//     {
+//         let router = Self::from_str(&cx.props.current_route);
+//         match router {
+//             Ok(router) => router.render(cx),
+//             Err(err) => {
+//                 render! {pre {
+//                     "{err}"
+//                 }}
+//             }
+//         }
+//     }
+}
+
+#[derive(Routable, Clone, Debug, PartialEq)]
+enum Route {
+    #[route("/(dynamic)")]
+    Route1 { dynamic: String },
+    #[route("/hello_world")]
+    Route2 {},
+    #[redirect("/(dynamic)/hello_world")]
+    #[route("/hello_world/(dynamic)")]
+    Route3 { dynamic: u32 },
+    #[route("/(number1)/(number2)")]
+    Route4 { number1: u32, number2: u32 },
+    #[route("/")]
+    Route5 {},
+}
+
+#[test]
+fn display_works() {
+    let route = Route::Route1 {
+        dynamic: "hello".to_string(),
+    };
+
+    assert_eq!(route.to_string(), "/hello");
+
+    let route = Route::Route3 { dynamic: 1234 };
+
+    assert_eq!(route.to_string(), "/hello_world/1234");
+
+    let route = Route::Route1 {
+        dynamic: "hello_world2".to_string(),
+    };
+
+    assert_eq!(route.to_string(), "/hello_world2");
+}
+
+#[test]
+fn from_string_works() {
+    let w = "/hello";
+    assert_eq!(
+        Route::from_str(w),
+        Ok(Route::Route1 {
+            dynamic: "hello".to_string()
+        })
+    );
+    let w = "/hello/";
+    assert_eq!(
+        Route::from_str(w),
+        Ok(Route::Route1 {
+            dynamic: "hello".to_string()
+        })
+    );
+
+    let w = "/hello_world/1234";
+    assert_eq!(Route::from_str(w), Ok(Route::Route3 { dynamic: 1234 }));
+    let w = "/hello_world/1234/";
+    assert_eq!(Route::from_str(w), Ok(Route::Route3 { dynamic: 1234 }));
+
+    let w = "/hello_world2";
+    assert_eq!(
+        Route::from_str(w),
+        Ok(Route::Route1 {
+            dynamic: "hello_world2".to_string()
+        })
+    );
+
+    let w = "/hello_world/-1";
+    match Route::from_str(w) {
+        Ok(r) => panic!("should not parse {r:?}"),
+        Err(err) => println!("{err}"),
+    }
+}
+
+#[test]
+fn round_trip() {
+    // Route1
+    let string = "hello_world2";
+    let route = Route::Route1 {
+        dynamic: string.to_string(),
+    };
+    assert_eq!(Route::from_str(&route.to_string()), Ok(route));
+
+    // Route2
+    for num in 0..100 {
+        let route = Route::Route3 { dynamic: num };
+        assert_eq!(Route::from_str(&route.to_string()), Ok(route));
+    }
+
+    // Route3
+    for num1 in 0..100 {
+        for num2 in 0..100 {
+            let route = Route::Route4 {
+                number1: num1,
+                number2: num2,
+            };
+            assert_eq!(Route::from_str(&route.to_string()), Ok(route));
+        }
+    }
+}

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

@@ -1,29 +0,0 @@
-use std::fmt::Debug;
-
-/// The basic content type of dioxus-router-core.
-///
-/// For actual route definitions this type is basically useless. However, it allows the router to
-/// support convenience [`From`] implementations which can tell content and redirects apart.
-///
-/// ```rust
-/// # use dioxus_router_core::routes::ContentAtom;
-/// let content = ContentAtom("some content");
-/// ```
-#[derive(Clone)]
-pub struct ContentAtom<T>(pub T)
-where
-    T: Clone;
-
-impl<T: Clone + Debug> Debug for ContentAtom<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_tuple("ContentAtom").field(&self.0).finish()
-    }
-}
-
-impl<T: Clone + PartialEq> PartialEq for ContentAtom<T> {
-    fn eq(&self, other: &Self) -> bool {
-        self.0 == other.0
-    }
-}
-
-impl<T: Clone + Eq> Eq for ContentAtom<T> {}

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

@@ -1,306 +0,0 @@
-use std::{collections::BTreeMap, fmt::Debug};
-
-use crate::{navigation::NavigationTarget, Name};
-
-use super::ContentAtom;
-
-/// The content of a route.
-#[derive(Clone)]
-pub enum RouteContent<T: Clone> {
-    /// Some actual content.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, RouteContent};
-    /// let explicit = RouteContent::Content(ContentAtom("content"));
-    /// let implicit: RouteContent<_> = ContentAtom("content").into();
-    /// assert_eq!(explicit, implicit);
-    /// ```
-    Content(ContentAtom<T>),
-    /// A redirect to another location.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::RouteContent;
-    /// let explicit = RouteContent::<&'static str>::Redirect("/some_path".into());
-    /// let implicit: RouteContent<&'static str> = "/some_path".into();
-    /// assert_eq!(explicit, implicit);
-    /// ```
-    Redirect(NavigationTarget),
-    /// Multiple content.
-    ///
-    /// This may contain some main content, and named content.
-    ///
-    /// ```rust
-    /// # use std::collections::BTreeMap;
-    /// # use dioxus_router_core::{Name, routes::{ContentAtom, multi, RouteContent}};
-    /// let explicit = RouteContent::MultiContent{
-    ///     main: Some(ContentAtom("main")),
-    ///     named: {
-    ///         let mut r = BTreeMap::new();
-    ///         r.insert(Name::of::<u8>(), ContentAtom("first"));
-    ///         r.insert(Name::of::<u16>(), ContentAtom("second"));
-    ///         r
-    ///     }
-    /// };
-    /// let implicit = multi(Some(ContentAtom("main")))
-    ///     .add_named::<u8>(ContentAtom("first"))
-    ///     .add_named::<u16>(ContentAtom("second"));
-    /// assert_eq!(explicit, implicit);
-    /// ```
-    MultiContent {
-        /// The main content.
-        main: Option<ContentAtom<T>>,
-        /// Named content.
-        named: BTreeMap<Name, ContentAtom<T>>,
-    },
-}
-
-impl<T: Clone> RouteContent<T> {
-    /// Create a new [`RouteContent::MultiContent`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, multi, RouteContent};
-    /// let content = multi(Some(ContentAtom("main")))
-    ///     .add_named::<u8>(ContentAtom("first"))
-    ///     .add_named::<u16>(ContentAtom("second"));
-    /// ```
-    pub fn multi(main: Option<ContentAtom<T>>) -> Self {
-        Self::MultiContent {
-            main,
-            named: BTreeMap::new(),
-        }
-    }
-
-    /// Add some named content to a [`RouteContent::MultiContent`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, multi, RouteContent};
-    /// let content = multi(Some(ContentAtom("main")))
-    ///     .add_named::<u8>(ContentAtom("first"))
-    ///     .add_named::<u16>(ContentAtom("second"));
-    /// ```
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-    /// needed, like this.
-    ///
-    /// # Error Handling
-    /// An error occurs if `self` is any other [`RouteContent`] variant then
-    /// [`RouteContent::MultiContent`]. In _debug mode_, this will trigger a panic. In _release
-    /// mode_ nothing will happen.
-    pub fn add_named<N: 'static>(mut self, content: ContentAtom<T>) -> Self {
-        debug_assert!(
-            matches!(self, Self::MultiContent { main: _, named: _ }),
-            "add_named only available for MultiContent"
-        );
-
-        if let Self::MultiContent { main: _, named } = &mut self {
-            let name = Name::of::<N>();
-            debug_assert!(
-                !named.contains_key(&name),
-                "name not unique within MultiContent: {name}"
-            );
-            named.entry(name).or_insert(content);
-        }
-
-        self
-    }
-}
-
-/// Create a new [`RouteContent::MultiContent`].
-///
-/// ```rust
-/// # use dioxus_router_core::routes::{ContentAtom, multi, RouteContent};
-/// let content = multi(Some(ContentAtom("main")))
-///     .add_named::<u8>(ContentAtom("first"))
-///     .add_named::<u16>(ContentAtom("second"));
-/// ```
-///
-/// This is a shortcut for [`RouteContent`]s `multi` method.
-pub fn multi<T: Clone>(main: Option<ContentAtom<T>>) -> RouteContent<T> {
-    RouteContent::multi(main)
-}
-
-#[cfg(test)]
-pub(crate) fn test_content() -> RouteContent<&'static str> {
-    RouteContent::Content(ContentAtom("test content"))
-}
-
-impl<T: Clone> From<ContentAtom<T>> for RouteContent<T> {
-    fn from(c: ContentAtom<T>) -> Self {
-        Self::Content(c)
-    }
-}
-
-impl<T: Clone, N: Into<NavigationTarget>> From<N> for RouteContent<T> {
-    fn from(nt: N) -> Self {
-        Self::Redirect(nt.into())
-    }
-}
-
-impl<T: Clone> Debug for RouteContent<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::Content(_) => f.debug_tuple("Content").finish(),
-            Self::Redirect(nt) => f.debug_tuple("Target").field(nt).finish(),
-            Self::MultiContent { main: _, named } => {
-                f.debug_tuple("MultiContent").field(&named.keys()).finish()
-            }
-        }
-    }
-}
-
-impl<T: Clone + PartialEq> PartialEq for RouteContent<T> {
-    fn eq(&self, other: &Self) -> bool {
-        match (self, other) {
-            (Self::Content(l0), Self::Content(r0)) => l0 == r0,
-            (Self::Redirect(l), Self::Redirect(r)) => l == r,
-            (
-                Self::MultiContent {
-                    main: lm,
-                    named: ln,
-                },
-                Self::MultiContent {
-                    main: rm,
-                    named: rn,
-                },
-            ) => lm == rm && ln == rn,
-            _ => false,
-        }
-    }
-}
-
-impl<T: Clone + Eq> Eq for RouteContent<T> {}
-
-#[cfg(test)]
-mod tests {
-    use crate::{navigation::named, Name};
-
-    use super::*;
-
-    #[test]
-    fn content_from_content() {
-        assert_eq!(
-            Into::<RouteContent<_>>::into(ContentAtom("test content")),
-            test_content()
-        )
-    }
-
-    #[test]
-    fn content_from_target() {
-        assert_eq!(
-            Into::<RouteContent<_>>::into(named::<bool>()),
-            RouteContent::<&str>::Redirect(NavigationTarget::Named {
-                name: Name::of::<bool>(),
-                parameters: Default::default(),
-                query: None
-            })
-        )
-    }
-
-    #[test]
-    fn content_from_string() {
-        let internal = "/test";
-        assert_eq!(
-            Into::<RouteContent<&str>>::into(internal.to_string()),
-            RouteContent::Redirect(internal.into())
-        );
-
-        let external = "https://dioxuslabs.com/";
-        assert_eq!(
-            Into::<RouteContent<&str>>::into(external.to_string()),
-            RouteContent::Redirect(external.into())
-        )
-    }
-
-    #[test]
-    fn content_from_str() {
-        let internal = "/test";
-        assert_eq!(
-            Into::<RouteContent<&str>>::into(internal),
-            RouteContent::Redirect(internal.into())
-        );
-
-        let external = "https://dioxuslabs.com/";
-        assert_eq!(
-            Into::<RouteContent<&str>>::into(external),
-            RouteContent::Redirect(external.into())
-        )
-    }
-
-    #[test]
-    fn multi() {
-        let c = RouteContent::multi(Some(ContentAtom("test")));
-        match c {
-            RouteContent::MultiContent { main, named } => {
-                assert_eq!(main, Some(ContentAtom("test")));
-                assert!(named.is_empty());
-            }
-            _ => panic!("wrong kind"),
-        };
-    }
-
-    #[test]
-    fn multi_add() {
-        let c = RouteContent::multi(None)
-            .add_named::<u8>(ContentAtom("1"))
-            .add_named::<u16>(ContentAtom("2"));
-
-        match c {
-            RouteContent::MultiContent { main, named } => {
-                assert!(main.is_none());
-                assert_eq!(named, {
-                    let mut r = BTreeMap::new();
-                    r.insert(Name::of::<u8>(), ContentAtom("1"));
-                    r.insert(Name::of::<u16>(), ContentAtom("2"));
-                    r
-                });
-            }
-            _ => panic!("wrong kind"),
-        };
-    }
-
-    #[test]
-    #[should_panic = "add_named only available for MultiContent"]
-    #[cfg(debug_assertions)]
-    fn multi_add_wrong_kind_debug() {
-        RouteContent::Content(ContentAtom("1")).add_named::<u8>(ContentAtom("2"));
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn multi_add_wrong_kind_release() {
-        assert_eq!(
-            RouteContent::Content(ContentAtom("1")).add_named::<u8>(ContentAtom("2")),
-            RouteContent::Content(ContentAtom("1"))
-        );
-    }
-
-    #[test]
-    #[should_panic = "name not unique within MultiContent: u8"]
-    #[cfg(debug_assertions)]
-    fn multi_add_duplicate_debug() {
-        RouteContent::multi(None)
-            .add_named::<u8>(ContentAtom("1"))
-            .add_named::<u8>(ContentAtom("2"));
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn multi_add_duplicate_release() {
-        let c = RouteContent::multi(None)
-            .add_named::<u8>(ContentAtom("1"))
-            .add_named::<u8>(ContentAtom("2"));
-
-        match c {
-            RouteContent::MultiContent { main, named } => {
-                assert!(main.is_none());
-                assert_eq!(named, {
-                    let mut r = BTreeMap::new();
-                    r.insert(Name::of::<u8>(), ContentAtom("1"));
-                    r
-                });
-            }
-            _ => panic!("wrong kind"),
-        };
-    }
-}

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

@@ -1,25 +0,0 @@
-/// Something that can check whether a string meets a condition.
-///
-/// This is used by matching routes (see the [`Segment`](super::Segment) `matching` function for
-/// more details) to see if they are active.
-pub trait Matcher: std::fmt::Debug {
-    /// Check whether `segment_value` fulfills the [`Matcher`]s requirement.
-    fn matches(&self, segment_value: &str) -> bool;
-}
-
-// The following implementation is for test purposes only. It could later be replaced by an
-// implementation providing wildcard syntax or something similar.
-
-#[cfg(test)]
-impl Matcher for String {
-    fn matches(&self, segment_value: &str) -> bool {
-        self == segment_value
-    }
-}
-
-#[cfg(feature = "regex")]
-impl Matcher for regex::Regex {
-    fn matches(&self, segment_value: &str) -> bool {
-        self.is_match(segment_value)
-    }
-}

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

@@ -1,275 +0,0 @@
-use super::{RouteContent, Segment};
-use crate::{
-    prelude::{
-        FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex,
-    },
-    Name,
-};
-
-/// A parameter route.
-#[derive(Debug)]
-pub struct ParameterRoute<T: Clone> {
-    pub(crate) content: Option<RouteContent<T>>,
-    pub(crate) name: Option<Name>,
-    pub(crate) nested: Option<Segment<T>>,
-    pub(crate) key: Name,
-}
-
-impl<T: Clone> ParameterRoute<T> {
-    /// Create a new [`ParameterRoute`] with `N` as the key.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::ParameterRoute;
-    /// let route: ParameterRoute<&'static str> = ParameterRoute::empty::<bool>();
-    /// ```
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive keys, and create unit structs if
-    /// needed.
-    pub fn empty<N: 'static>() -> Self {
-        Self {
-            content: None,
-            name: None,
-            nested: None,
-            key: Name::of::<N>(),
-        }
-    }
-
-    /// Create a new [`ParameterRoute`] with `N` as the key and some `content`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute};
-    /// let route = ParameterRoute::content::<bool>(ContentAtom("some content"));
-    /// ```
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-    /// needed.
-    pub fn content<N: 'static>(content: impl Into<RouteContent<T>>) -> Self {
-        Self {
-            content: Some(content.into()),
-            name: None,
-            nested: None,
-            key: Name::of::<N>(),
-        }
-    }
-
-    /// Create a new [`ParameterRoute`] with `N` as the key and possibly some `content`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute};
-    /// let route = ParameterRoute::new::<bool>(Some(ContentAtom("some content")));
-    /// ```
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-    /// needed.
-    pub fn new<N: 'static>(content: Option<impl Into<RouteContent<T>>>) -> Self {
-        match content {
-            Some(c) => Self::content::<N>(c),
-            None => Self::empty::<N>(),
-        }
-    }
-
-    /// Add a name to a [`ParameterRoute`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute};
-    /// ParameterRoute::content::<bool>(ContentAtom("some content")).name::<bool>();
-    /// ```
-    ///
-    /// Names must be unique within their top level [`Segment`]. [`RootIndex`],
-    /// [`FailureExternalNavigation`], [`FailureNamedNavigation`] and [`FailureRedirectionLimit`]
-    /// are reserved and **may not** be used.
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-    /// needed.
-    ///
-    /// # Error Handling
-    /// 1. This function may only be called once per [`ParameterRoute`]. In _debug mode_, the second
-    ///    call will panic. In _release mode_, all calls after the first will be ignored.
-    /// 2. If one of the forbidden names (see above) is used, this function will panic, even in
-    ///    _release mode_.
-    pub fn name<N: 'static>(mut self) -> Self {
-        let new = Name::of::<N>();
-
-        debug_assert!(
-            self.name.is_none(),
-            "name cannot be changed: {} to {new}",
-            self.name.as_ref().unwrap(),
-        );
-        assert_ne!(new, Name::of::<RootIndex>(), "forbidden name: {new}");
-        assert_ne!(
-            new,
-            Name::of::<FailureExternalNavigation>(),
-            "forbidden name: {new}"
-        );
-        assert_ne!(
-            new,
-            Name::of::<FailureNamedNavigation>(),
-            "forbidden name: {new}"
-        );
-        assert_ne!(
-            new,
-            Name::of::<FailureRedirectionLimit>(),
-            "forbidden name: {new}"
-        );
-
-        self.name.get_or_insert(new);
-        self
-    }
-
-    /// Add a nested [`Segment`] to the [`ParameterRoute`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, ParameterRoute, Segment};
-    /// ParameterRoute::content::<bool>(ContentAtom("some content")).nested(Segment::empty());
-    /// ```
-    ///
-    /// # Error Handling
-    /// This function may only be called once per [`ParameterRoute`]. In _debug mode_, the second
-    /// call will panic. In _release mode_, all calls after the first will be ignored.
-    pub fn nested(mut self, nested: impl Into<Segment<T>>) -> Self {
-        debug_assert!(self.nested.is_none(), "nested segment cannot be changed");
-        self.nested.get_or_insert(nested.into());
-        self
-    }
-}
-
-impl<T: Clone, C: Into<RouteContent<T>>, N: 'static> From<(C, N)> for ParameterRoute<T> {
-    fn from((c, _): (C, N)) -> Self {
-        Self::content::<N>(c)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::routes::{test_content, ContentAtom};
-
-    use super::*;
-
-    #[test]
-    fn empty() {
-        let p = ParameterRoute::<&str>::empty::<String>();
-
-        assert!(p.content.is_none());
-        assert!(p.name.is_none());
-        assert!(p.nested.is_none());
-        assert_eq!(p.key, Name::of::<String>());
-    }
-
-    #[test]
-    fn content() {
-        let p = ParameterRoute::content::<String>(test_content());
-
-        assert_eq!(p.content, Some(test_content()));
-        assert!(p.name.is_none());
-        assert!(p.nested.is_none());
-        assert_eq!(p.key, Name::of::<String>());
-    }
-
-    #[test]
-    fn new_empty() {
-        let p = ParameterRoute::<&str>::new::<String>(None::<String>);
-
-        assert!(p.content.is_none());
-        assert!(p.name.is_none());
-        assert!(p.nested.is_none());
-        assert_eq!(p.key, Name::of::<String>());
-    }
-
-    #[test]
-    fn new_content() {
-        let p = ParameterRoute::new::<String>(Some(test_content()));
-
-        assert_eq!(p.content, Some(test_content()));
-        assert!(p.name.is_none());
-        assert!(p.nested.is_none());
-        assert_eq!(p.key, Name::of::<String>());
-    }
-
-    #[test]
-    fn name_initial() {
-        let route = ParameterRoute::<&str>::empty::<String>().name::<&str>();
-
-        assert_eq!(route.name, Some(Name::of::<&str>()))
-    }
-
-    #[test]
-    #[should_panic = "name cannot be changed: alloc::string::String to &str"]
-    #[cfg(debug_assertions)]
-    fn name_debug() {
-        ParameterRoute::<&str>::empty::<String>()
-            .name::<String>()
-            .name::<&str>();
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn name_release() {
-        let route = ParameterRoute::<&str>::empty::<bool>()
-            .name::<String>()
-            .name::<&str>();
-
-        assert_eq!(route.name, Some(Name::of::<String>()));
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::RootIndex"]
-    fn name_root_index() {
-        ParameterRoute::<&str>::empty::<&str>().name::<RootIndex>();
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureExternalNavigation"]
-    fn name_external_navigation() {
-        ParameterRoute::<&str>::empty::<&str>().name::<FailureExternalNavigation>();
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureNamedNavigation"]
-    fn name_named_navigation() {
-        ParameterRoute::<&str>::empty::<&str>().name::<FailureNamedNavigation>();
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureRedirectionLimit"]
-    fn name_redirection_limit() {
-        ParameterRoute::<&str>::empty::<&str>().name::<FailureRedirectionLimit>();
-    }
-
-    #[test]
-    fn nested_initial() {
-        let route = ParameterRoute::empty::<bool>().nested(nested_segment());
-        assert!(route.nested.is_some());
-
-        let n = route.nested.unwrap();
-        assert_eq!(n.index, nested_segment().index);
-    }
-
-    #[test]
-    #[should_panic = "nested segment cannot be changed"]
-    #[cfg(debug_assertions)]
-    fn nested_debug() {
-        ParameterRoute::empty::<bool>()
-            .nested(nested_segment())
-            .nested(nested_segment());
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn nested_release() {
-        let route = ParameterRoute::empty::<bool>()
-            .nested(nested_segment())
-            .nested(Segment::empty());
-        assert!(route.nested.is_some());
-
-        let n = route.nested.unwrap();
-        assert_eq!(n.index, nested_segment().index);
-    }
-
-    fn nested_segment() -> Segment<&'static str> {
-        Segment::content(RouteContent::Content(ContentAtom("nested")))
-    }
-}

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

@@ -1,247 +0,0 @@
-use super::{RouteContent, Segment};
-use crate::{
-    prelude::{
-        FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex,
-    },
-    Name,
-};
-
-/// A fixed route.
-#[derive(Debug)]
-pub struct Route<T: Clone> {
-    pub(crate) content: Option<RouteContent<T>>,
-    pub(crate) name: Option<Name>,
-    pub(crate) nested: Option<Segment<T>>,
-}
-
-impl<T: Clone> Route<T> {
-    /// Create a new [`Route`] without content.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::Route;
-    /// let r: Route<&'static str> = Route::empty();
-    /// ```
-    #[must_use]
-    pub fn empty() -> Self {
-        Self {
-            content: None,
-            name: None,
-            nested: None,
-        }
-    }
-
-    /// Create a new [`Route`] with some `content`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Route};
-    /// let r = Route::content(ContentAtom("some content"));
-    /// ```
-    #[must_use]
-    pub fn content(content: impl Into<RouteContent<T>>) -> Self {
-        Self {
-            content: Some(content.into()),
-            name: None,
-            nested: None,
-        }
-    }
-
-    /// Create a new [`Route`], possible with some `content`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Route};
-    /// let r = Route::new(Some(ContentAtom("some content")));
-    /// ```
-    #[must_use]
-    pub fn new(content: Option<impl Into<RouteContent<T>>>) -> Self {
-        match content {
-            Some(c) => Self::content(c),
-            None => Self::empty(),
-        }
-    }
-
-    /// Add a name to a [`Route`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Route};
-    /// Route::content(ContentAtom("some content")).name::<bool>();
-    /// ```
-    ///
-    /// Names must be unique within their top level [`Segment`]. [`RootIndex`],
-    /// [`FailureExternalNavigation`], [`FailureNamedNavigation`] and [`FailureRedirectionLimit`]
-    /// are reserved and **may not** be used.
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-    /// needed.
-    ///
-    /// # Error Handling
-    /// 1. This function may only be called once per [`Route`]. In _debug mode_, the second call
-    ///    will panic. In _release mode_, all calls after the first will be ignored.
-    /// 2. If one of the forbidden names (see above) is used, this function will panic, even in
-    ///    _release mode_.
-    pub fn name<N: 'static>(mut self) -> Self {
-        let new = Name::of::<N>();
-
-        debug_assert!(
-            self.name.is_none(),
-            "name cannot be changed: {} to {new}",
-            self.name.as_ref().unwrap(),
-        );
-        if new == Name::of::<RootIndex>()
-            || new == Name::of::<FailureExternalNavigation>()
-            || new == Name::of::<FailureNamedNavigation>()
-            || new == Name::of::<FailureRedirectionLimit>()
-        {
-            panic!("forbidden name: {new}");
-        }
-
-        self.name.get_or_insert(new);
-        self
-    }
-
-    /// Add a nested [`Segment`] to the [`Route`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Route, Segment};
-    /// Route::content(ContentAtom("some content")).nested(Segment::empty());
-    /// ```
-    ///
-    /// # Error Handling
-    /// This function may only be called once per [`Route`]. In _debug mode_, the second call will
-    /// panic. In _release mode_, all calls after the first will be ignored.
-    pub fn nested(mut self, nested: impl Into<Segment<T>>) -> Self {
-        debug_assert!(self.nested.is_none(), "nested segment cannot be changed");
-        self.nested.get_or_insert(nested.into());
-        self
-    }
-}
-
-impl<T: Clone, C: Into<RouteContent<T>>> From<C> for Route<T> {
-    fn from(c: C) -> Self {
-        Self::content(c)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::routes::{test_content, ContentAtom};
-
-    use super::*;
-
-    #[test]
-    fn empty() {
-        let route = Route::<&str>::empty();
-
-        assert!(route.content.is_none());
-        assert!(route.name.is_none());
-        assert!(route.nested.is_none());
-    }
-
-    #[test]
-    fn content() {
-        let route = Route::content(test_content());
-
-        assert_eq!(route.content, Some(test_content()));
-        assert!(route.name.is_none());
-        assert!(route.nested.is_none());
-    }
-
-    #[test]
-    fn new_empty() {
-        let route = Route::<&str>::new(None::<&str>);
-
-        assert!(route.content.is_none());
-        assert!(route.name.is_none());
-        assert!(route.nested.is_none());
-    }
-
-    #[test]
-    fn new_content() {
-        let route = Route::new(Some(test_content()));
-
-        assert_eq!(route.content, Some(test_content()));
-        assert!(route.name.is_none());
-        assert!(route.nested.is_none());
-    }
-
-    #[test]
-    fn name_initial() {
-        let route = Route::<&str>::empty().name::<&str>();
-
-        assert_eq!(route.name, Some(Name::of::<&str>()))
-    }
-
-    #[test]
-    #[should_panic = "name cannot be changed: alloc::string::String to &str"]
-    #[cfg(debug_assertions)]
-    fn name_debug() {
-        Route::<&str>::empty().name::<String>().name::<&str>();
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn name_release() {
-        let route = Route::<&str>::empty().name::<String>().name::<&str>();
-
-        assert_eq!(route.name, Some(Name::of::<String>()));
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::RootIndex"]
-    fn name_root_index() {
-        Route::<&str>::empty().name::<RootIndex>();
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureExternalNavigation"]
-    fn name_external_navigation() {
-        Route::<&str>::empty().name::<FailureExternalNavigation>();
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureNamedNavigation"]
-    fn name_named_navigation() {
-        Route::<&str>::empty().name::<FailureNamedNavigation>();
-    }
-
-    #[test]
-    #[should_panic = "forbidden name: dioxus_router_core::prelude::FailureRedirectionLimit"]
-    fn name_redirection_limit() {
-        Route::<&str>::empty().name::<FailureRedirectionLimit>();
-    }
-
-    #[test]
-    fn nested_initial() {
-        let route = Route::empty().nested(nested_segment());
-        assert!(route.nested.is_some());
-
-        let n = route.nested.unwrap();
-        assert_eq!(n.index, nested_segment().index);
-    }
-
-    #[test]
-    #[should_panic = "nested segment cannot be changed"]
-    #[cfg(debug_assertions)]
-    fn nested_debug() {
-        Route::empty()
-            .nested(nested_segment())
-            .nested(nested_segment());
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn nested_release() {
-        let route = Route::empty()
-            .nested(nested_segment())
-            .nested(Segment::empty());
-
-        assert!(route.nested.is_some());
-
-        let n = route.nested.unwrap();
-        assert_eq!(n.index, nested_segment().index);
-    }
-
-    fn nested_segment() -> Segment<&'static str> {
-        Segment::content(RouteContent::Content(ContentAtom("nested")))
-    }
-}

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

@@ -1,458 +0,0 @@
-use std::collections::BTreeMap;
-
-use crate::{
-    utils::{gen_parameter_sitemap, gen_sitemap},
-    Name,
-};
-
-use super::{Matcher, ParameterRoute, Route, RouteContent};
-
-/// A segment, representing a segment of the URLs path part (i.e. the stuff between two slashes).
-#[derive(Debug)]
-pub struct Segment<T: Clone> {
-    pub(crate) index: Option<RouteContent<T>>,
-
-    pub(crate) fallback: Option<RouteContent<T>>,
-    pub(crate) clear_fallback: Option<bool>,
-
-    pub(crate) fixed: BTreeMap<String, Route<T>>,
-    pub(crate) matching: Vec<(Box<dyn Matcher>, ParameterRoute<T>)>,
-    pub(crate) catch_all: Option<Box<ParameterRoute<T>>>,
-}
-
-impl<T: Clone> Segment<T> {
-    /// Create a new [`Segment`] without index content.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::Segment;
-    /// let seg: Segment<&'static str> = Segment::empty();
-    /// ```
-    pub fn empty() -> Self {
-        Default::default()
-    }
-
-    /// Create a new [`Segment`] with some index `content`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Segment};
-    /// let seg = Segment::content(ContentAtom("some content"));
-    /// ```
-    pub fn content(content: impl Into<RouteContent<T>>) -> Self {
-        Self {
-            index: Some(content.into()),
-            ..Default::default()
-        }
-    }
-
-    /// Create a new [`Segment`], possibly with some index `content`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Segment};
-    /// let seg = Segment::new(Some(ContentAtom("some content")));
-    /// ```
-    pub fn new(content: Option<impl Into<RouteContent<T>>>) -> Self {
-        match content {
-            Some(content) => Self::content(content),
-            None => Self::empty(),
-        }
-    }
-
-    /// Add fallback content to a [`Segment`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Segment};
-    /// Segment::content(ContentAtom("some content")).fallback(ContentAtom("fallback content"));
-    /// ```
-    ///
-    /// The fallback content of the innermost matched [`Segment`] is used, if the router cannot find
-    /// a full matching route.
-    ///
-    /// # Error Handling
-    /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will
-    /// panic. In _release mode_, all calls after the first will be ignored.
-    pub fn fallback(mut self, content: impl Into<RouteContent<T>>) -> Self {
-        debug_assert!(
-            self.fallback.is_none(),
-            "fallback content cannot be changed"
-        );
-        self.fallback.get_or_insert(content.into());
-
-        self
-    }
-
-    /// Set whether to clear matched content when using the fallback.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Segment};
-    /// Segment::content(ContentAtom("some content"))
-    ///     .fallback(ContentAtom("fallback content"))
-    ///     .clear_fallback(true);
-    /// ```
-    ///
-    /// When this is [`true`], the router will remove all content it previously found when falling
-    /// back to this [`Segment`]s fallback content. If not set, a [`Segment`] will inherit this
-    /// value from its parent segment. For the root [`Segment`], this defaults to [`false`].
-    ///
-    /// # Error Handling
-    /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will
-    /// panic. In _release mode_, all calls after the first will be ignored.
-    pub fn clear_fallback(mut self, clear: bool) -> Self {
-        debug_assert!(
-            self.clear_fallback.is_none(),
-            "fallback clearing cannot be changed"
-        );
-        self.clear_fallback.get_or_insert(clear);
-
-        self
-    }
-
-    /// Add a fixed [`Route`] to the [`Segment`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Segment};
-    /// Segment::empty().fixed("path", ContentAtom("fixed route content"));
-    /// ```
-    ///
-    /// A fixed route is active only when the corresponding URL segment is exactly the same as its
-    /// path.
-    ///
-    /// # Error Handling
-    /// An error occurs if multiple fixed routes on the same [`Segment`] have the same `path`. In
-    /// _debug mode_, the second call with panic. In _release mode_, the later routes will be
-    /// ignored and the initial preserved.
-    pub fn fixed(mut self, path: impl Into<String>, content: impl Into<Route<T>>) -> Self {
-        let path = path.into();
-
-        debug_assert!(
-            !self.fixed.contains_key(&path),
-            "duplicate fixed route: {path}"
-        );
-        self.fixed.entry(path).or_insert_with(|| content.into());
-
-        self
-    }
-
-    /// Add a matching [`ParameterRoute`] to the [`Segment`].
-    ///
-    /// ```rust,ignore
-    /// # // Test is ignored because there are scenarios where no matcher is available.
-    /// # use dioxus_router_core::routes::Segment;
-    /// Segment::empty().matching("some matcher", (true, ContentAtom("matching route content")));
-    /// ```
-    ///
-    /// A matching route is active only when the corresponding URL segment is accepted by its
-    /// [`Matcher`], and no previously added matching route is.
-    ///
-    /// The example above is not checked by the compiler. This is because dioxus-router-core doesn't ship any
-    /// [`Matcher`]s by default. However, you can implement your own, or turn on the `regex` feature
-    /// to enable a regex implementation.
-    pub fn matching(
-        mut self,
-        matcher: impl Matcher + 'static,
-        content: impl Into<ParameterRoute<T>>,
-    ) -> Self {
-        self.matching.push((Box::new(matcher), content.into()));
-        self
-    }
-
-    /// Add a catch all [`ParameterRoute`] to the [`Segment`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::routes::{ContentAtom, Segment};
-    /// Segment::empty().catch_all((ContentAtom("catch all route content"), true));
-    /// ```
-    ///
-    /// A catch all route is active only if no fixed or matching route is.
-    ///
-    /// # Error Handling
-    /// This function may only be called once per [`Segment`]. In _debug mode_ the second call will
-    /// panic. In _release mode_, all calls after the first will be ignored.
-    pub fn catch_all(mut self, content: impl Into<ParameterRoute<T>>) -> Self {
-        debug_assert!(self.catch_all.is_none(), "duplicate catch all route");
-        self.catch_all.get_or_insert(Box::new(content.into()));
-        self
-    }
-
-    /// Generate a site map.
-    ///
-    /// ```rust
-    /// # use std::collections::BTreeMap;
-    /// # use dioxus_router_core::{Name, routes::Segment};
-    /// let seg = Segment::<u8>::empty().fixed("fixed", "").catch_all(("", true));
-    /// let sitemap = seg.gen_sitemap();
-    /// assert_eq!(sitemap, vec!["/", "/fixed", "/\\bool"]);
-    /// ```
-    ///
-    /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a
-    /// path. Fixed routes are passed in as is, while matching and catch all routes are represented
-    /// by their key, marked with a leading `\`. Since the otherwise all paths should be valid in
-    /// URLs, and `\` is not, this doesn't cause a conflict.
-    pub fn gen_sitemap(&self) -> Vec<String> {
-        let mut res = Vec::new();
-        res.push(String::from("/"));
-        gen_sitemap(self, "", &mut res);
-        res
-    }
-
-    /// Generate a site map with parameters filled in.
-    ///
-    /// ```rust
-    /// # use std::collections::BTreeMap;
-    /// # use dioxus_router_core::{Name, routes::Segment};
-    /// let seg = Segment::<u8>::empty().fixed("fixed", "").catch_all(("", true));
-    /// let mut parameters = BTreeMap::new();
-    /// parameters.insert(Name::of::<bool>(), vec![String::from("1"), String::from("2")]);
-    ///
-    /// let sitemap = seg.gen_parameter_sitemap(&parameters);
-    /// assert_eq!(sitemap, vec!["/", "/fixed", "/1", "/2"]);
-    /// ```
-    ///
-    /// This function returns a [`Vec`] containing all routes the [`Segment`] knows about, as a
-    /// path. Fixed routes are passed in as is, while matching and catch all will be represented
-    /// with all `parameters` provided for their key. Matching routes will also filter out all
-    /// invalid parameters.
-    pub fn gen_parameter_sitemap(&self, parameters: &BTreeMap<Name, Vec<String>>) -> Vec<String> {
-        let mut res = Vec::new();
-        res.push(String::from("/"));
-        gen_parameter_sitemap(self, parameters, "", &mut res);
-        res
-    }
-}
-
-impl<T: Clone> Default for Segment<T> {
-    fn default() -> Self {
-        Self {
-            index: None,
-            fallback: None,
-            clear_fallback: None,
-            fixed: BTreeMap::new(),
-            matching: Vec::new(),
-            catch_all: None,
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::routes::{content::test_content, ContentAtom};
-
-    use super::*;
-
-    #[test]
-    fn default() {
-        let seg: Segment<&str> = Default::default();
-
-        assert!(seg.index.is_none());
-        assert!(seg.fallback.is_none());
-        assert!(seg.clear_fallback.is_none());
-        assert!(seg.fixed.is_empty());
-        assert!(seg.matching.is_empty());
-        assert!(seg.catch_all.is_none());
-    }
-
-    #[test]
-    fn empty() {
-        let seg = Segment::<&str>::empty();
-
-        assert!(seg.index.is_none());
-        assert!(seg.fallback.is_none());
-        assert!(seg.clear_fallback.is_none());
-        assert!(seg.fixed.is_empty());
-        assert!(seg.matching.is_empty());
-        assert!(seg.catch_all.is_none());
-    }
-
-    #[test]
-    fn content() {
-        let seg = Segment::content(test_content());
-
-        assert_eq!(seg.index, Some(test_content()));
-        assert!(seg.fallback.is_none());
-        assert!(seg.clear_fallback.is_none());
-        assert!(seg.fixed.is_empty());
-        assert!(seg.matching.is_empty());
-        assert!(seg.catch_all.is_none());
-    }
-
-    #[test]
-    fn new_empty() {
-        let seg = Segment::<&str>::new(None::<String>);
-
-        assert!(seg.index.is_none());
-        assert!(seg.fallback.is_none());
-        assert!(seg.clear_fallback.is_none());
-        assert!(seg.fixed.is_empty());
-        assert!(seg.matching.is_empty());
-        assert!(seg.catch_all.is_none());
-    }
-
-    #[test]
-    fn new_content() {
-        let seg = Segment::new(Some(test_content()));
-
-        assert_eq!(seg.index, Some(test_content()));
-        assert!(seg.fallback.is_none());
-        assert!(seg.clear_fallback.is_none());
-        assert!(seg.fixed.is_empty());
-        assert!(seg.matching.is_empty());
-        assert!(seg.catch_all.is_none());
-    }
-
-    #[test]
-    fn fallback_initial() {
-        let seg = Segment::empty().fallback(test_content());
-
-        assert_eq!(seg.fallback, Some(test_content()));
-    }
-
-    #[test]
-    #[should_panic = "fallback content cannot be changed"]
-    #[cfg(debug_assertions)]
-    fn fallback_debug() {
-        Segment::empty()
-            .fallback(test_content())
-            .fallback(test_content());
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn fallback_release() {
-        let seg = Segment::empty()
-            .fallback(test_content())
-            .fallback(RouteContent::Content(ContentAtom("invalid")));
-
-        assert_eq!(seg.fallback, Some(test_content()));
-    }
-
-    #[test]
-    fn clear_fallback() {
-        let mut seg = Segment::<&str>::empty();
-        assert!(seg.clear_fallback.is_none());
-
-        seg = seg.clear_fallback(true);
-        assert_eq!(seg.clear_fallback, Some(true));
-    }
-
-    #[test]
-    #[should_panic = "fallback clearing cannot be changed"]
-    #[cfg(debug_assertions)]
-    fn clear_fallback_debug() {
-        Segment::<&str>::empty()
-            .clear_fallback(true)
-            .clear_fallback(false);
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn clear_fallback_release() {
-        let seg = Segment::<&str>::empty()
-            .clear_fallback(true)
-            .clear_fallback(false);
-        assert_eq!(seg.clear_fallback, Some(true));
-    }
-
-    #[test]
-    fn fixed() {
-        let test = RouteContent::Content(ContentAtom("test"));
-        let other = RouteContent::Content(ContentAtom("other"));
-        let seg = Segment::empty()
-            .fixed("test", Route::content(test.clone()))
-            .fixed("other", Route::content(other.clone()));
-
-        assert_eq!(seg.fixed.len(), 2);
-        assert_eq!(seg.fixed["test"].content, Some(test));
-        assert_eq!(seg.fixed["other"].content, Some(other));
-    }
-
-    #[test]
-    #[should_panic = "duplicate fixed route: test"]
-    #[cfg(debug_assertions)]
-    fn fixed_debug() {
-        Segment::empty()
-            .fixed(
-                "test",
-                Route::content(RouteContent::Content(ContentAtom("test"))),
-            )
-            .fixed(
-                "test",
-                Route::content(RouteContent::Content(ContentAtom("other"))),
-            );
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn fixed_release() {
-        let test = RouteContent::Content(ContentAtom("test"));
-        let other = RouteContent::Content(ContentAtom("other"));
-        let seg = Segment::empty()
-            .fixed("test", Route::content(test.clone()))
-            .fixed("test", Route::content(other.clone()));
-
-        assert_eq!(seg.fixed.len(), 1);
-        assert_eq!(seg.fixed["test"].content, Some(test));
-    }
-
-    #[test]
-    fn matching() {
-        let test = RouteContent::Content(ContentAtom("test"));
-        let other = RouteContent::Content(ContentAtom("other"));
-        let seg = Segment::empty()
-            .matching(
-                String::from("test"),
-                ParameterRoute::content::<String>(test.clone()),
-            )
-            .matching(
-                String::from("other"),
-                ParameterRoute::content::<String>(other.clone()),
-            );
-
-        assert_eq!(seg.matching.len(), 2);
-        assert_eq!(seg.matching[0].1.content, Some(test));
-        assert_eq!(seg.matching[1].1.content, Some(other));
-    }
-
-    #[test]
-    fn catch_all_initial() {
-        let seg = Segment::empty().catch_all(ParameterRoute::content::<String>(test_content()));
-
-        assert!(seg.catch_all.is_some());
-        assert_eq!(seg.catch_all.unwrap().content, Some(test_content()));
-    }
-
-    #[test]
-    #[should_panic = "duplicate catch all route"]
-    #[cfg(debug_assertions)]
-    fn catch_all_debug() {
-        Segment::empty()
-            .catch_all(ParameterRoute::content::<String>(test_content()))
-            .catch_all(ParameterRoute::content::<String>(test_content()));
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn catch_all_release() {
-        let seg = Segment::empty()
-            .catch_all(ParameterRoute::content::<String>(test_content()))
-            .catch_all(ParameterRoute::empty::<bool>());
-
-        assert!(seg.catch_all.is_some());
-        assert_eq!(seg.catch_all.unwrap().content, Some(test_content()));
-    }
-
-    // Check whether the returned sitemap includes "/". More elaborate tests are located alongside
-    // the internal `gen_sitemap` function.
-    #[test]
-    fn gen_sitemap() {
-        assert_eq!(Segment::<&'static str>::empty().gen_sitemap(), vec!["/"]);
-    }
-
-    // Check whether the returned sitemap includes "/". More elaborate tests are located alongside
-    // the internal `gen_parameter_sitemap` function.
-    #[test]
-    fn gen_parameter_sitemap() {
-        assert_eq!(
-            Segment::<&'static str>::empty().gen_parameter_sitemap(&BTreeMap::new()),
-            vec!["/"]
-        );
-    }
-}

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

@@ -1,144 +0,0 @@
-use std::collections::HashMap;
-
-use urlencoding::encode;
-
-use crate::{
-    segments::{NameMap, NamedSegment},
-    Name,
-};
-
-pub fn resolve_name(
-    map: &NameMap,
-    name: &Name,
-    parameters: &HashMap<Name, String>,
-) -> Option<String> {
-    debug_assert!(
-        map.contains_key(name),
-        "named navigation to unknown name: {name}"
-    );
-    let target = map.get(name)?;
-
-    let mut res = String::new();
-    for t in target {
-        res += "/";
-        match t {
-            NamedSegment::Fixed(f) => res += f,
-            NamedSegment::Parameter(p) => {
-                debug_assert!(
-                    parameters.contains_key(p),
-                    "named navigation is missing parameter: target {name} parameter {p}"
-                );
-                let val = parameters.get(p)?;
-
-                res += &encode(val);
-            }
-        }
-    }
-
-    if res.is_empty() {
-        res += "/";
-    }
-
-    Some(res)
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::{
-        prelude::RootIndex,
-        routes::{ParameterRoute, Route, Segment},
-    };
-
-    use super::*;
-
-    fn test_map() -> NameMap {
-        NamedSegment::from_segment(
-            &Segment::<&str>::empty()
-                .fixed(
-                    "fixed",
-                    Route::empty().name::<u8>().nested(
-                        Segment::empty().catch_all(ParameterRoute::empty::<u16>().name::<u32>()),
-                    ),
-                )
-                .catch_all(ParameterRoute::empty::<i8>().name::<i16>()),
-        )
-    }
-
-    #[test]
-    fn root_index() {
-        assert_eq!(
-            resolve_name(&test_map(), &Name::of::<RootIndex>(), &HashMap::new()),
-            Some(String::from("/"))
-        )
-    }
-
-    #[test]
-    fn fixed() {
-        assert_eq!(
-            resolve_name(&test_map(), &Name::of::<u8>(), &HashMap::new()),
-            Some(String::from("/fixed"))
-        )
-    }
-
-    #[test]
-    fn matching() {
-        assert_eq!(
-            resolve_name(&test_map(), &Name::of::<i16>(), &{
-                let mut r = HashMap::new();
-                r.insert(Name::of::<i8>(), String::from("test"));
-                r
-            }),
-            Some(String::from("/test"))
-        );
-    }
-
-    #[test]
-    fn nested() {
-        assert_eq!(
-            resolve_name(&test_map(), &Name::of::<u32>(), &{
-                let mut r = HashMap::new();
-                r.insert(Name::of::<u16>(), String::from("nested"));
-                r
-            }),
-            Some(String::from("/fixed/nested"))
-        );
-    }
-
-    #[test]
-    #[should_panic = "named navigation to unknown name: bool"]
-    #[cfg(debug_assertions)]
-    fn missing_name_debug() {
-        resolve_name(&test_map(), &Name::of::<bool>(), &HashMap::new());
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn missing_name_release() {
-        assert!(resolve_name(&test_map(), &Name::of::<bool>(), &HashMap::new()).is_none());
-    }
-
-    #[test]
-    #[should_panic = "named navigation is missing parameter: target u32 parameter u16"]
-    #[cfg(debug_assertions)]
-    fn missing_parameter_debug() {
-        resolve_name(&test_map(), &Name::of::<u32>(), &HashMap::new());
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn missing_parameter_release() {
-        assert!(resolve_name(&test_map(), &Name::of::<u32>(), &HashMap::new()).is_none());
-    }
-
-    #[test]
-    fn url_encoding() {
-        assert_eq!(
-            resolve_name(&test_map(), &Name::of::<u32>(), &{
-                let mut r = HashMap::new();
-                r.insert(Name::of::<u16>(), String::from("🥳"));
-                r
-            }),
-            Some(String::from("/fixed/%F0%9F%A5%B3"))
-        );
-    }
-}

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

@@ -1,483 +0,0 @@
-use either::Either;
-use urlencoding::decode;
-
-use crate::{
-    navigation::NavigationTarget,
-    routes::{ParameterRoute, Route, RouteContent, Segment},
-    RouterState,
-};
-
-pub fn route_segment<T: Clone>(
-    segment: &Segment<T>,
-    values: &[&str],
-    state: RouterState<T>,
-) -> Either<RouterState<T>, NavigationTarget> {
-    route_segment_internal(segment, values, state, None, false)
-}
-
-fn route_segment_internal<T: Clone>(
-    segment: &Segment<T>,
-    values: &[&str],
-    state: RouterState<T>,
-    mut fallback: Option<RouteContent<T>>,
-    mut clear_fallback: bool,
-) -> Either<RouterState<T>, NavigationTarget> {
-    // fallback
-    if let Some(fb) = &segment.fallback {
-        fallback = Some(fb.clone());
-    }
-    if let Some(clear) = &segment.clear_fallback {
-        clear_fallback = *clear;
-    }
-
-    // index route
-    if values.is_empty() {
-        if let Some(c) = &segment.index {
-            return merge(state, c.clone());
-        }
-        return Either::Left(state);
-    }
-
-    // fixed route
-    if let Some(r) = segment.fixed.get(values[0]) {
-        return merge_route(values, r, state, fallback, clear_fallback);
-    }
-
-    // matching routes
-    for (m, r) in &segment.matching {
-        if m.matches(values[0]) {
-            return merge_parameter_route(values, r, state, fallback, clear_fallback);
-        }
-    }
-
-    // catchall
-    if let Some(c) = &segment.catch_all {
-        return merge_parameter_route(values, c.as_ref(), state, fallback, clear_fallback);
-    }
-
-    merge_fallback(state, fallback, clear_fallback)
-}
-
-fn merge<T: Clone>(
-    mut state: RouterState<T>,
-    content: RouteContent<T>,
-) -> Either<RouterState<T>, NavigationTarget> {
-    match content {
-        RouteContent::Content(c) => state.content.push(c),
-        RouteContent::Redirect(t) => return Either::Right(t),
-        RouteContent::MultiContent { main, named } => {
-            if let Some(main) = main {
-                state.content.push(main);
-            }
-
-            for (name, content) in named {
-                state.named_content.entry(name).or_default().push(content);
-            }
-        }
-    }
-    Either::Left(state)
-}
-
-fn merge_route<T: Clone>(
-    values: &[&str],
-    route: &Route<T>,
-    mut state: RouterState<T>,
-    fallback: Option<RouteContent<T>>,
-    clear_fallback: bool,
-) -> Either<RouterState<T>, NavigationTarget> {
-    // merge content
-    if let Some(c) = &route.content {
-        match merge(state, c.clone()) {
-            Either::Left(s) => state = s,
-            Either::Right(t) => return Either::Right(t),
-        }
-    }
-
-    if let Some(n) = &route.name {
-        state.names.insert(n.clone());
-    }
-
-    match (&route.nested, values.is_empty()) {
-        (Some(n), _) => route_segment_internal(n, &values[1..], state, fallback, clear_fallback),
-        (None, false) => merge_fallback(state, fallback, clear_fallback),
-        _ => Either::Left(state),
-    }
-}
-
-fn merge_parameter_route<T: Clone>(
-    values: &[&str],
-    route: &ParameterRoute<T>,
-    mut state: RouterState<T>,
-    fallback: Option<RouteContent<T>>,
-    clear_fallback: bool,
-) -> Either<RouterState<T>, NavigationTarget> {
-    // merge content
-    if let Some(c) = &route.content {
-        match merge(state, c.clone()) {
-            Either::Left(s) => state = s,
-            Either::Right(t) => return Either::Right(t),
-        }
-    }
-
-    if let Some(n) = &route.name {
-        state.names.insert(n.clone());
-    }
-
-    state.parameters.insert(
-        route.key.clone(),
-        decode(values[0]).unwrap(/* string already is UTF-8 */).into_owned(),
-    );
-
-    match (&route.nested, values.is_empty()) {
-        (Some(n), _) => route_segment_internal(n, &values[1..], state, fallback, clear_fallback),
-        (None, false) => merge_fallback(state, fallback, clear_fallback),
-        _ => Either::Left(state),
-    }
-}
-
-fn merge_fallback<T: Clone>(
-    mut state: RouterState<T>,
-    fallback: Option<RouteContent<T>>,
-    clear_fallback: bool,
-) -> Either<RouterState<T>, NavigationTarget> {
-    // fallback clearing
-    if clear_fallback {
-        state.content.clear();
-        state.names.clear();
-        state.parameters.clear();
-    }
-
-    // fallback content
-    match fallback {
-        Some(fallback) => merge(state, fallback),
-        None => Either::Left(state),
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use std::collections::{BTreeMap, HashMap, HashSet};
-
-    use crate::{
-        routes::{multi, ContentAtom},
-        Name,
-    };
-
-    use super::*;
-
-    fn test_segment() -> Segment<&'static str> {
-        Segment::content(ContentAtom("index"))
-            .fixed("fixed", Route::content(ContentAtom("fixed")).name::<bool>())
-            .matching(
-                String::from("matching"),
-                ParameterRoute::content::<u8>(ContentAtom("matching"))
-                    .nested(Segment::empty().fixed("nested", ContentAtom("matching nested"))),
-            )
-            .catch_all(
-                ParameterRoute::content::<u16>(ContentAtom("catch all"))
-                    .nested(Segment::empty().fixed("nested", ContentAtom("catch all nested"))),
-            )
-            .fixed(
-                "nested",
-                Route::content(ContentAtom("nested")).name::<u32>().nested(
-                    Segment::content(ContentAtom("nested index"))
-                        .fixed("again", ContentAtom("nested again")),
-                ),
-            )
-            .fixed("redirect", "/redirect")
-            .fixed(
-                "fallback",
-                Route::content(ContentAtom("fallback")).nested(
-                    Segment::empty()
-                        .fixed(
-                            "keep",
-                            Route::content(ContentAtom("keep route")).nested(
-                                Segment::content(ContentAtom("keep index"))
-                                    .fallback(ContentAtom("keep")),
-                            ),
-                        )
-                        .fixed(
-                            "clear",
-                            Route::content(ContentAtom("clear route")).nested(
-                                Segment::empty()
-                                    .fallback(ContentAtom("clear"))
-                                    .clear_fallback(true),
-                            ),
-                        ),
-                ),
-            )
-            .fixed(
-                "no_fallback",
-                Route::content(ContentAtom("no fallback")).nested(
-                    Segment::empty()
-                        .fixed(
-                            "keep",
-                            Route::content(ContentAtom("keep route"))
-                                .nested(Segment::empty().clear_fallback(false)),
-                        )
-                        .fixed(
-                            "clear",
-                            Route::content(ContentAtom("clear route"))
-                                .nested(Segment::empty().clear_fallback(true)),
-                        ),
-                ),
-            )
-            .fixed(
-                "named_content",
-                Route::content(
-                    multi(None)
-                        .add_named::<i8>(ContentAtom("1"))
-                        .add_named::<i16>(ContentAtom("2")),
-                )
-                .nested(Segment::content(multi(Some(ContentAtom("3"))))),
-            )
-    }
-
-    #[test]
-    fn route_index() {
-        let state = route_segment(
-            &test_segment(),
-            &[],
-            RouterState {
-                path: String::from("/"),
-                can_go_back: false,
-                can_go_forward: true,
-                ..Default::default()
-            },
-        );
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(state.content, vec![ContentAtom("index")]);
-        assert!(state.names.is_empty());
-        assert!(state.parameters.is_empty());
-        assert_eq!(state.path, String::from("/"));
-        assert!(!state.can_go_back);
-        assert!(state.can_go_forward);
-    }
-
-    #[test]
-    fn route_fixed() {
-        let state = route_segment(&test_segment(), &["fixed"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(state.content, vec![ContentAtom("fixed")]);
-        assert_eq!(state.names, {
-            let mut r = HashSet::new();
-            r.insert(Name::of::<bool>());
-            r
-        });
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn route_matching() {
-        let state = route_segment(&test_segment(), &["matching"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(state.content, vec![ContentAtom("matching")]);
-        assert!(state.names.is_empty());
-        assert_eq!(state.parameters, {
-            let mut r = HashMap::new();
-            r.insert(Name::of::<u8>(), String::from("matching"));
-            r
-        });
-    }
-
-    #[test]
-    fn route_matching_nested() {
-        let state = route_segment(&test_segment(), &["matching", "nested"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(
-            state.content,
-            vec![ContentAtom("matching"), ContentAtom("matching nested")]
-        );
-        assert!(state.names.is_empty());
-        assert_eq!(state.parameters, {
-            let mut r = HashMap::new();
-            r.insert(Name::of::<u8>(), String::from("matching"));
-            r
-        });
-    }
-
-    #[test]
-    fn route_catch_all() {
-        let state = route_segment(&test_segment(), &["invalid"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(state.content, vec![ContentAtom("catch all")]);
-        assert!(state.names.is_empty());
-        assert_eq!(state.parameters, {
-            let mut r = HashMap::new();
-            r.insert(Name::of::<u16>(), String::from("invalid"));
-            r
-        });
-    }
-
-    #[test]
-    fn route_catch_all_nested() {
-        let state = route_segment(&test_segment(), &["invalid", "nested"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(
-            state.content,
-            vec![ContentAtom("catch all"), ContentAtom("catch all nested")]
-        );
-        assert!(state.names.is_empty());
-        assert_eq!(state.parameters, {
-            let mut r = HashMap::new();
-            r.insert(Name::of::<u16>(), String::from("invalid"));
-            r
-        });
-    }
-
-    #[test]
-    fn route_nested_index() {
-        let state = route_segment(&test_segment(), &["nested"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(
-            state.content,
-            vec![ContentAtom("nested"), ContentAtom("nested index")]
-        );
-        assert_eq!(state.names, {
-            let mut r = HashSet::new();
-            r.insert(Name::of::<u32>());
-            r
-        });
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn route_nested_again() {
-        let state = route_segment(&test_segment(), &["nested", "again"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(
-            state.content,
-            vec![ContentAtom("nested"), ContentAtom("nested again")]
-        );
-        assert_eq!(state.names, {
-            let mut r = HashSet::new();
-            r.insert(Name::of::<u32>());
-            r
-        });
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn route_redirect() {
-        let state = route_segment(&test_segment(), &["redirect"], Default::default());
-        assert_eq!(state.unwrap_right(), "/redirect".into());
-    }
-
-    #[test]
-    fn route_fallback_keep() {
-        let state = route_segment(
-            &test_segment(),
-            &["fallback", "keep", "invalid"],
-            Default::default(),
-        );
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(
-            state.content,
-            vec![
-                ContentAtom("fallback"),
-                ContentAtom("keep route"),
-                ContentAtom("keep")
-            ]
-        );
-        assert!(state.names.is_empty());
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn route_fallback_clear() {
-        let state = route_segment(
-            &test_segment(),
-            &["fallback", "clear", "invalid"],
-            Default::default(),
-        );
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(state.content, vec![ContentAtom("clear")]);
-        assert!(state.names.is_empty());
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn route_named_content() {
-        let state = route_segment(&test_segment(), &["named_content"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(state.content, vec![ContentAtom("3")]);
-        assert_eq!(state.named_content, {
-            let mut r = BTreeMap::new();
-            r.insert(Name::of::<i8>(), vec![ContentAtom("1")]);
-            r.insert(Name::of::<i16>(), vec![ContentAtom("2")]);
-            r
-        });
-        assert!(state.names.is_empty());
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn no_fallback() {
-        let state = route_segment(
-            &test_segment(),
-            &["no_fallback", "keep", "invalid"],
-            Default::default(),
-        );
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(
-            state.content,
-            vec![ContentAtom("no fallback"), ContentAtom("keep route"),]
-        );
-        assert!(state.names.is_empty());
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn no_fallback_with_clearing() {
-        let state = route_segment(
-            &test_segment(),
-            &["no_fallback", "clear", "invalid"],
-            Default::default(),
-        );
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert!(state.content.is_empty());
-        assert!(state.names.is_empty());
-        assert!(state.parameters.is_empty());
-    }
-
-    #[test]
-    fn url_encoding() {
-        let state = route_segment(&test_segment(), &["%F0%9F%A5%B3"], Default::default());
-        assert!(state.is_left());
-
-        let state = state.unwrap_left();
-        assert_eq!(state.content, vec![ContentAtom("catch all")]);
-        assert!(state.names.is_empty());
-        assert_eq!(state.parameters, {
-            let mut r = HashMap::new();
-            r.insert(Name::of::<u16>(), "🥳".to_string());
-            r
-        });
-    }
-}

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

@@ -2,7 +2,6 @@ use std::collections::BTreeMap;
 
 use urlencoding::encode;
 
-use crate::{routes::Segment, Name};
 
 pub fn gen_sitemap<T: Clone>(seg: &Segment<T>, current: &str, map: &mut Vec<String>) {
     for (p, r) in &seg.fixed {

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

@@ -1,120 +0,0 @@
-use either::Either;
-
-use crate::{
-    navigation::{NavigationTarget, Query},
-    segments::NameMap,
-    Name,
-};
-
-use super::resolve_name;
-
-pub fn resolve_target(
-    names: &NameMap,
-    target: &NavigationTarget,
-) -> Either<Either<String, Name>, String> {
-    match target {
-        NavigationTarget::Internal(i) => Either::Left(Either::Left(i.clone())),
-        NavigationTarget::Named {
-            name,
-            parameters,
-            query,
-        } => Either::Left(
-            resolve_name(names, name, parameters)
-                .map(|mut p| {
-                    if let Some(q) = query {
-                        match q {
-                            Query::Single(s) => {
-                                if !s.starts_with('?') {
-                                    p += "?";
-                                }
-                                p += s;
-                            }
-                            #[cfg(feature = "serde")]
-                            Query::List(l) => {
-                                let res = serde_urlencoded::to_string(l);
-                                // TODO: find a test case where this assertion is not met
-                                debug_assert!(res.is_ok(), "cannot serialize query list: {l:?}");
-                                if let Ok(q) = res {
-                                    p += "?";
-                                    p += &q;
-                                }
-                            }
-                        }
-                    }
-
-                    p
-                })
-                .map(Either::Left)
-                .unwrap_or_else(|| Either::Right(name.clone())),
-        ),
-        NavigationTarget::External(e) => Either::Right(e.to_string()),
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::{prelude::RootIndex, routes::Segment, segments::NamedSegment};
-
-    use super::*;
-
-    #[test]
-    fn resolve_internal() {
-        let names = NamedSegment::from_segment(&Segment::<&str>::empty());
-        assert_eq!(
-            resolve_target(&names, &NavigationTarget::Internal("/test".to_string())),
-            Either::Left(Either::Left(String::from("/test")))
-        );
-    }
-
-    #[test]
-    fn resolve_named() {
-        let names = NamedSegment::from_segment(&Segment::<&str>::empty());
-        assert_eq!(
-            resolve_target(&names, &NavigationTarget::named::<RootIndex>()),
-            Either::Left(Either::Left(String::from("/")))
-        );
-    }
-
-    #[test]
-    fn resolve_named_with_query_single() {
-        let names = NamedSegment::from_segment(&Segment::<&str>::empty());
-        let without = resolve_target(
-            &names,
-            &NavigationTarget::named::<RootIndex>().query("huhu"),
-        );
-        let with = resolve_target(
-            &names,
-            &NavigationTarget::named::<RootIndex>().query("?huhu"),
-        );
-        let correct = Either::Left(Either::Left(String::from("/?huhu")));
-        assert_eq!(with, correct);
-        assert_eq!(without, correct);
-        assert_eq!(with, without);
-    }
-
-    #[test]
-    #[cfg(feature = "serde")]
-    fn resolve_named_with_query_list() {
-        let names = NamedSegment::from_segment(&Segment::<&str>::empty());
-        assert_eq!(
-            resolve_target(
-                &names,
-                &NavigationTarget::named::<RootIndex>()
-                    .query(vec![("some", "test"), ("another", "value")])
-            ),
-            Either::Left(Either::Left(String::from("/?some=test&another=value")))
-        );
-    }
-
-    #[test]
-    fn resolve_external() {
-        let names = NamedSegment::from_segment(&Segment::<&str>::empty());
-        assert_eq!(
-            resolve_target(
-                &names,
-                &NavigationTarget::External("https://dioxuslabs.com/".to_string())
-            ),
-            Either::Right(String::from("https://dioxuslabs.com/"))
-        );
-    }
-}

+ 23 - 0
packages/router-macro/Cargo.toml

@@ -0,0 +1,23 @@
+[package]
+name = "dioxus-router-macro"
+version = "0.3.0"
+authors = ["Evan Almloff"]
+edition = "2021"
+description = "Macro for Dioxus Router"
+license = "MIT/Apache-2.0"
+repository = "https://github.com/DioxusLabs/dioxus/"
+homepage = "https://dioxuslabs.com"
+documentation = "https://dioxuslabs.com"
+keywords = ["dom", "ui", "gui", "react", "router"]
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "1.0.11", features = ["extra-traits", "full"] }
+quote = "1.0"
+proc-macro2 = "1.0.56"
+
+[features]
+default = []

+ 0 - 0
packages/router-macro/README.md


+ 206 - 0
packages/router-macro/src/lib.rs

@@ -0,0 +1,206 @@
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use quote::{__private::Span, format_ident, quote, ToTokens};
+use route::Route;
+use route_tree::RouteTreeSegment;
+use syn::{parse_macro_input, Ident};
+
+use proc_macro2::TokenStream as TokenStream2;
+
+mod route;
+mod route_tree;
+
+#[proc_macro_derive(Routable, attributes(route))]
+pub fn derive_routable(input: TokenStream) -> TokenStream {
+    let routes_enum = parse_macro_input!(input as syn::DeriveInput);
+
+    let route_enum = match RouteEnum::parse(routes_enum) {
+        Ok(route_enum) => route_enum,
+        Err(err) => return TokenStream2::from(err.to_compile_error()).into(),
+    };
+
+    let error_type = route_enum.error_type();
+    let parse_impl = route_enum.parse_impl();
+    let display_impl = route_enum.impl_display();
+    let routable_impl = route_enum.routable_impl();
+
+    quote! {
+        #route_enum
+
+        #error_type
+
+        #parse_impl
+
+        #display_impl
+
+        #routable_impl
+    }
+    .into()
+}
+
+struct RouteEnum {
+    route_name: Ident,
+    routes: Vec<Route>,
+}
+
+impl RouteEnum {
+    fn parse(input: syn::DeriveInput) -> syn::Result<Self> {
+        let name = &input.ident;
+
+        if let syn::Data::Enum(data) = input.data {
+            let mut routes = Vec::new();
+
+            for variant in data.variants {
+                let route = Route::parse(variant)?;
+                routes.push(route);
+            }
+
+            let myself = Self {
+                route_name: name.clone(),
+                routes,
+            };
+
+            Ok(myself)
+        } else {
+            Err(syn::Error::new_spanned(
+                input.clone(),
+                "Routable can only be derived for enums",
+            ))
+        }
+    }
+
+    fn impl_display(&self) -> TokenStream2 {
+        let mut display_match = Vec::new();
+
+        for route in &self.routes {
+            display_match.push(route.display_match());
+        }
+
+        let name = &self.route_name;
+
+        quote! {
+            impl std::fmt::Display for #name {
+                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                    match self {
+                        #(#display_match)*
+                    }
+                    Ok(())
+                }
+            }
+        }
+    }
+
+    fn parse_impl(&self) -> TokenStream2 {
+        let tree = RouteTreeSegment::build(&self.routes);
+        let name = &self.route_name;
+
+        let error_name = format_ident!("{}MatchError", self.route_name);
+        let tokens = tree
+            .into_iter()
+            .map(|t| t.to_tokens(self.route_name.clone(), error_name.clone()));
+
+        quote! {
+            impl<'a> TryFrom<&'a str> for #name {
+                type Error = <Self as std::str::FromStr>::Err;
+
+                fn try_from(s: &'a str) -> Result<Self, Self::Error> {
+                    s.parse()
+                }
+            }
+
+            impl std::str::FromStr for #name {
+                type Err = RouteParseError<#error_name>;
+
+                fn from_str(s: &str) -> Result<Self, Self::Err> {
+                    let mut segments = s.strip_prefix('/').unwrap_or(s).split('/');
+                    let mut errors = Vec::new();
+
+                    if let Some(segment) = segments.next() {
+                        #(#tokens)*
+                    }
+
+                    Err(RouteParseError {
+                        attempted_routes: errors,
+                    })
+                }
+            }
+        }
+    }
+
+    fn error_name(&self) -> Ident {
+        Ident::new(
+            &(self.route_name.to_string() + "MatchError"),
+            Span::call_site(),
+        )
+    }
+
+    fn error_type(&self) -> TokenStream2 {
+        let match_error_name = self.error_name();
+
+        let mut type_defs = Vec::new();
+        let mut error_variants = Vec::new();
+        let mut display_match = Vec::new();
+
+        for route in &self.routes {
+            let route_name = &route.route_name;
+
+            let error_name = Ident::new(&format!("{}ParseError", route_name), Span::call_site());
+            let route_str = &route.route;
+
+            error_variants.push(quote! { #route_name(#error_name) });
+            display_match.push(quote! { Self::#route_name(err) => write!(f, "Route '{}' ('{}') did not match:\n{}", stringify!(#route_name), #route_str, err)? });
+            type_defs.push(route.error_type());
+        }
+
+        quote! {
+            #(#type_defs)*
+
+            #[derive(Debug, PartialEq)]
+            pub enum #match_error_name {
+                #(#error_variants),*
+            }
+
+            impl std::fmt::Display for #match_error_name {
+                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                    match self {
+                        #(#display_match),*
+                    }
+                    Ok(())
+                }
+            }
+        }
+    }
+
+    fn routable_impl(&self) -> TokenStream2 {
+        let mut routable_match = Vec::new();
+
+        for route in &self.routes {
+            routable_match.push(route.routable_match());
+        }
+
+        quote! {
+            impl Routable for Route {
+                fn render<'a>(self, cx: &'a ScopeState) -> Element<'a> {
+                    match self {
+                        #(#routable_match)*
+                    }
+                }
+            }
+        }
+    }
+}
+
+impl ToTokens for RouteEnum {
+    fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) {
+        let routes = &self.routes;
+
+        tokens.extend(quote!(
+            #[path = "pages"]
+            mod pages {
+                #(#routes)*
+            }
+            pub use pages::*;
+        ));
+    }
+}

+ 338 - 0
packages/router-macro/src/route.rs

@@ -0,0 +1,338 @@
+use quote::{__private::Span, format_ident, quote, ToTokens};
+use syn::{Ident, LitStr, Type, Variant};
+use syn::parse::ParseStream;
+use syn::parse::Parse;
+
+use proc_macro2::TokenStream as TokenStream2;
+
+struct RouteArgs{
+    route: LitStr,
+    comp_name: Option<Ident>,
+    props_name: Option<Ident>,
+}
+
+impl Parse for RouteArgs{
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self>{
+        let route = input.parse::<LitStr>()?;
+
+        Ok(RouteArgs {
+            route,
+            comp_name:input.parse().ok(),
+            props_name: input.parse().ok(),
+        })
+    }
+}
+
+#[derive(Debug)]
+pub struct Route {
+    pub file_based: bool,
+    pub route_name: Ident,
+    pub comp_name: Ident,
+    pub props_name: Ident,
+    pub route: LitStr,
+    pub route_segments: Vec<RouteSegment>,
+}
+
+impl Route {
+    pub fn parse(input: syn::Variant) -> syn::Result<Self> {
+        let route_attr = input
+            .attrs
+            .iter()
+            .find(|attr| attr.path.is_ident("route"))
+            .ok_or_else(|| {
+                syn::Error::new_spanned(
+                    input.clone(),
+                    "Routable variants must have a #[route(...)] attribute",
+                )
+            })?;
+
+        let route_name = input.ident.clone();
+        let args = route_attr.parse_args::<RouteArgs>()?;
+        let route = args.route;
+        let file_based= args.comp_name.is_none();
+        let comp_name = args.comp_name.unwrap_or_else(|| format_ident!("{}", route_name));
+        let props_name = args.props_name.unwrap_or_else(|| format_ident!("{}Props", comp_name));
+
+        let route_segments = parse_route_segments(&input, &route)?;
+
+        Ok(Self {
+            comp_name,
+            props_name,
+            route_name,
+            route_segments,
+            route,
+            file_based,
+        })
+    }
+
+    pub fn display_match(&self) -> TokenStream2 {
+        let name = &self.route_name;
+        let dynamic_segments = self.route_segments.iter().filter_map(|s| s.name());
+        let write_segments = self.route_segments.iter().map(|s| s.write_segment());
+
+        quote! {
+            Self::#name { #(#dynamic_segments,)* } => {
+                #(#write_segments)*
+            }
+        }
+    }
+
+    pub fn routable_match(&self) -> TokenStream2 {
+        let name = &self.route_name;
+        let dynamic_segments: Vec<_> = self
+            .route_segments
+            .iter()
+            .filter_map(|s| s.name())
+            .collect();
+        let props_name = &self.props_name;
+        let comp_name = &self.comp_name;
+
+        quote! {
+            Self::#name { #(#dynamic_segments,)* } => {
+                let comp = #props_name { #(#dynamic_segments,)* };
+                let cx = cx.bump().alloc(Scoped {
+                    props: cx.bump().alloc(comp),
+                    scope: cx,
+                });
+                #comp_name(cx)
+            }
+        }
+    }
+
+    pub fn construct(&self, enum_name: Ident) -> TokenStream2 {
+        let segments = self.route_segments.iter().filter_map(|seg| {
+            seg.name().map(|name| {
+                quote! {
+                    #name
+                }
+            })
+        });
+        let name = &self.route_name;
+
+        quote! {
+            #enum_name::#name {
+                #(#segments,)*
+            }
+        }
+    }
+
+    pub fn error_ident(&self) -> Ident {
+        format_ident!("{}ParseError", self.route_name)
+    }
+
+    pub fn error_type(&self) -> TokenStream2 {
+        let error_name = self.error_ident();
+
+        let mut error_variants = Vec::new();
+        let mut display_match = Vec::new();
+
+        for (i, segment) in self.route_segments.iter().enumerate() {
+            let error_name = segment.error_name(i);
+            match segment {
+                RouteSegment::Static(index) => {
+                    error_variants.push(quote! { #error_name });
+                    display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? });
+                }
+                RouteSegment::Dynamic(ident, ty) => {
+                    error_variants.push(quote! { #error_name(<#ty as std::str::FromStr>::Err) });
+                    display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
+                }
+                RouteSegment::CatchAll(ident, ty) => {
+                    error_variants.push(quote! { #error_name(<#ty as std::str::FromStr>::Err) });
+                    display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
+                }
+            }
+        }
+
+        quote! {
+            #[allow(non_camel_case_types)]
+            #[derive(Debug, PartialEq)]
+            pub enum #error_name {
+                ExtraSegments(String),
+                #(#error_variants,)*
+            }
+
+            impl std::fmt::Display for #error_name {
+                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                    match self {
+                        Self::ExtraSegments(segments) => {
+                            write!(f, "Found additional trailing segments: {segments}")?
+                        }
+                        #(#display_match,)*
+                    }
+                    Ok(())
+                }
+            }
+        }
+    }
+}
+
+impl ToTokens for Route {
+    fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) {
+        if !self.file_based {
+            return;
+       }
+
+        let without_leading_slash = &self.route.value()[1..];
+        let route_path = std::path::Path::new(without_leading_slash);
+        let with_extension = route_path.with_extension("rs");
+        let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
+        let dir = std::path::Path::new(&dir);
+        let route = dir.join("src").join("pages").join(with_extension.clone());
+
+        // check if the route exists or if not use the index route
+        let route = if route.exists() && without_leading_slash != "" {
+            with_extension.to_str().unwrap().to_string()
+        } else {
+            route_path.join("index.rs").to_str().unwrap().to_string()
+        };
+
+        let route_name: Ident = self.route_name.clone();
+        let prop_name = &self.props_name;
+
+        tokens.extend(quote!(
+            #[path = #route]
+            #[allow(non_snake_case)]
+            mod #route_name;
+            pub use #route_name::{#prop_name, #route_name};
+        ));
+    }
+}
+
+fn parse_route_segments(varient: &Variant, route: &LitStr) -> syn::Result<Vec<RouteSegment>> {
+    let mut route_segments = Vec::new();
+
+    let route_string = route.value();
+    let mut iterator = route_string.split('/');
+
+    // skip the first empty segment
+    let first = iterator.next();
+    if first != Some("") {
+        return Err(syn::Error::new_spanned(
+            varient,
+            format!(
+                "Routes should start with /. Error found in the route '{}'",
+                route.value()
+            ),
+        ));
+    }
+
+    while let Some(segment) = iterator.next() {
+        if segment.starts_with('(') && segment.ends_with(')') {
+            let spread = segment.starts_with("(...");
+
+            let ident = if spread {
+                segment[3..segment.len() - 1].to_string()
+            } else {
+                segment[1..segment.len() - 1].to_string()
+            };
+
+            let field = varient.fields.iter().find(|field| match field.ident {
+                Some(ref field_ident) => field_ident.to_string() == ident,
+                None => false,
+            });
+
+            let ty = if let Some(field) = field {
+                field.ty.clone()
+            } else {
+                return Err(syn::Error::new_spanned(
+                    varient,
+                    format!(
+                        "Could not find a field with the name '{}' in the variant '{}'",
+                        ident, varient.ident
+                    ),
+                ));
+            };
+            if spread {
+                route_segments.push(RouteSegment::CatchAll(
+                    Ident::new(&ident, Span::call_site()),
+                    ty,
+                ));
+
+                if iterator.next().is_some() {
+                    return Err(syn::Error::new_spanned(
+                        route,
+                        "Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.",
+                    ));
+                } else {
+                    break;
+                }
+            } else {
+                route_segments.push(RouteSegment::Dynamic(
+                    Ident::new(&ident, Span::call_site()),
+                    ty,
+                ));
+            }
+        } else {
+            route_segments.push(RouteSegment::Static(segment.to_string()));
+        }
+    }
+
+    Ok(route_segments)
+}
+
+#[derive(Debug)]
+pub enum RouteSegment {
+    Static(String),
+    Dynamic(Ident, Type),
+    CatchAll(Ident, Type),
+}
+
+impl RouteSegment {
+    pub fn name(&self) -> Option<Ident> {
+        match self {
+            Self::Static(_) => None,
+            Self::Dynamic(ident, _) => Some(ident.clone()),
+            Self::CatchAll(ident, _) => Some(ident.clone()),
+        }
+    }
+
+    pub fn write_segment(&self) -> TokenStream2 {
+        match self {
+            Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; },
+            Self::Dynamic(ident, _) => quote! { write!(f, "/{}", #ident)?; },
+            Self::CatchAll(ident, _) => quote! { write!(f, "/{}", #ident)?; },
+        }
+    }
+
+    fn error_name(&self, idx: usize) -> Ident {
+        match self {
+            Self::Static(_) => static_segment_idx(idx),
+            Self::Dynamic(ident, _) => format_ident!("{}ParseError", ident),
+            Self::CatchAll(ident, _) => format_ident!("{}ParseError", ident),
+        }
+    }
+
+    pub fn try_parse(
+        &self,
+        idx: usize,
+        error_enum_name: &Ident,
+        error_enum_varient: &Ident,
+        inner_parse_enum: &Ident,
+    ) -> TokenStream2 {
+        let error_name = self.error_name(idx);
+        match self {
+            Self::Static(segment) => {
+                quote! {
+                    let parsed = if segment == #segment {
+                        Ok(())
+                    } else {
+                        Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name))
+                    };
+                }
+            }
+            Self::Dynamic(_, ty) => {
+                quote! {
+                    let parsed = <#ty as std::str::FromStr>::from_str(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)));
+                }
+            }
+            Self::CatchAll(_, _) => {
+                todo!()
+            }
+        }
+    }
+}
+
+pub fn static_segment_idx(idx: usize) -> Ident {
+    format_ident!("StaticSegment{}ParseError", idx)
+}

+ 258 - 0
packages/router-macro/src/route_tree.rs

@@ -0,0 +1,258 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::Ident;
+
+use crate::route::{static_segment_idx, Route, RouteSegment};
+
+// First deduplicate the routes by the static part of the route
+#[derive(Debug)]
+pub enum RouteTreeSegment<'a> {
+    Static {
+        index: usize,
+        segment: &'a str,
+        children: Vec<RouteTreeSegment<'a>>,
+        from_route: &'a Route,
+    },
+    Dynamic(&'a Route),
+    StaticEnd(&'a Route),
+}
+
+impl<'a> RouteTreeSegment<'a> {
+    pub fn build(routes: &'a Vec<Route>) -> Vec<RouteTreeSegment<'a>> {
+        let routes = routes.into_iter().map(PartialRoute::new).collect();
+        Self::construct(routes)
+    }
+
+    fn construct(routes: Vec<PartialRoute<'a>>) -> Vec<RouteTreeSegment<'a>> {
+        let mut static_segments = Vec::new();
+        let mut dyn_segments = Vec::new();
+
+        // Add all routes we can to the tree
+        for mut route in routes {
+            match route.next_static_segment() {
+                // If there is a static segment, check if it already exists in the tree
+                Some((i, segment)) => {
+                    let found = static_segments.iter_mut().find_map(|seg| match seg {
+                        RouteTreeSegment::Static {
+                            segment: s,
+                            children,
+                            ..
+                        } => (s == &segment).then(|| children),
+                        _ => None,
+                    });
+
+                    match found {
+                        Some(children) => {
+                            // If it does, add the route to the children of the segment
+                            children.append(&mut RouteTreeSegment::construct(vec![route]))
+                        }
+                        None => {
+                            // If it doesn't, add the route as a new segment
+                            static_segments.push(RouteTreeSegment::Static {
+                                segment,
+                                from_route: route.route,
+                                children: RouteTreeSegment::construct(vec![route]),
+                                index: i,
+                            })
+                        }
+                    }
+                }
+                // If there is no static segment, add the route to the dynamic routes
+                None => {
+                    // This route is entirely static
+                    if route.route.route_segments.len() == route.static_segment_index {
+                        static_segments.push(RouteTreeSegment::StaticEnd(route.route));
+                    } else {
+                        dyn_segments.push(RouteTreeSegment::Dynamic(route.route));
+                    }
+                }
+            }
+        }
+
+        // All static routes are checked before dynamic routes
+        static_segments.append(&mut dyn_segments);
+
+        static_segments
+    }
+}
+
+impl<'a> RouteTreeSegment<'a> {
+    pub fn to_tokens(&self, enum_name: syn::Ident, error_enum_name: syn::Ident) -> TokenStream {
+        match self {
+            RouteTreeSegment::Static {
+                segment,
+                children,
+                index,
+                from_route,
+            } => {
+                let varient_parse_error = from_route.error_ident();
+                let enum_varient = &from_route.route_name;
+                let error_ident = static_segment_idx(*index);
+
+                let children_with_next_segment = children.iter().filter_map(|child| match child {
+                    RouteTreeSegment::StaticEnd { .. } => None,
+                    _ => Some(child.to_tokens(enum_name.clone(), error_enum_name.clone())),
+                });
+                let children_without_next_segment =
+                    children.iter().filter_map(|child| match child {
+                        RouteTreeSegment::StaticEnd { .. } => {
+                            Some(child.to_tokens(enum_name.clone(), error_enum_name.clone()))
+                        }
+                        _ => None,
+                    });
+
+                quote! {
+                    if #segment == segment {
+                        let mut segments = segments.clone();
+                        #(#children_without_next_segment)*
+                        if let Some(segment) = segments.next() {
+                            #(#children_with_next_segment)*
+                        }
+                    }
+                    else {
+                        errors.push(#error_enum_name::#enum_varient(#varient_parse_error::#error_ident))
+                    }
+                }
+            }
+            RouteTreeSegment::Dynamic(route) => {
+                // At this point, we have matched all static segments, so we can just check if the remaining segments match the route
+                let varient_parse_error = route.error_ident();
+                let enum_varient = &route.route_name;
+
+                let route_segments = route
+                    .route_segments
+                    .iter()
+                    .enumerate()
+                    .skip_while(|(_, seg)| match seg {
+                        RouteSegment::Static(_) => true,
+                        _ => false,
+                    })
+                    .map(|(i, seg)| {
+                        (
+                            seg.name(),
+                            seg.try_parse(i, &error_enum_name, enum_varient, &varient_parse_error),
+                        )
+                    });
+
+                fn print_route_segment<I: Iterator<Item = (Option<Ident>, TokenStream)>>(
+                    mut s: std::iter::Peekable<I>,
+                    sucess_tokens: TokenStream,
+                ) -> TokenStream {
+                    if let Some((name, first)) = s.next() {
+                        let has_next = s.peek().is_some();
+                        let children = print_route_segment(s, sucess_tokens);
+                        let name = name
+                            .map(|name| quote! {#name})
+                            .unwrap_or_else(|| quote! {_});
+
+                        let sucess = if has_next {
+                            quote! {
+                                let mut segments = segments.clone();
+                                if let Some(segment) = segments.next() {
+                                    #children
+                                }
+                            }
+                        } else {
+                            children
+                        };
+
+                        quote! {
+                            #first
+                            match parsed {
+                                Ok(#name) => {
+                                    #sucess
+                                }
+                                Err(err) => {
+                                    errors.push(err);
+                                }
+                            }
+                        }
+                    } else {
+                        quote! {
+                            #sucess_tokens
+                        }
+                    }
+                }
+
+                let construct_variant = route.construct(enum_name);
+
+                print_route_segment(
+                    route_segments.peekable(),
+                    return_constructed(
+                        construct_variant,
+                        &error_enum_name,
+                        enum_varient,
+                        &varient_parse_error,
+                    ),
+                )
+            }
+            Self::StaticEnd(route) => {
+                let varient_parse_error = route.error_ident();
+                let enum_varient = &route.route_name;
+                let construct_variant = route.construct(enum_name);
+
+                return_constructed(
+                    construct_variant,
+                    &error_enum_name,
+                    enum_varient,
+                    &varient_parse_error,
+                )
+            }
+        }
+    }
+}
+
+fn return_constructed(
+    construct_variant: TokenStream,
+    error_enum_name: &Ident,
+    enum_varient: &Ident,
+    varient_parse_error: &Ident,
+) -> TokenStream {
+    quote! {
+        let remaining_segments = segments.clone();
+        let mut segments_clone = segments.clone();
+        let next_segment = segments_clone.next();
+        let segment_after_next = segments_clone.next();
+        match (next_segment, segment_after_next) {
+            // This is the last segment, return the parsed route
+            (None, _) | (Some(""), None) => {
+                return Ok(#construct_variant);
+            }
+            _ => {
+                let mut trailing = String::new();
+                for seg in remaining_segments {
+                    trailing += seg;
+                    trailing += "/";
+                }
+                trailing.pop();
+                errors.push(#error_enum_name::#enum_varient(#varient_parse_error::ExtraSegments(trailing)))
+            }
+        }
+    }
+}
+
+struct PartialRoute<'a> {
+    route: &'a Route,
+    static_segment_index: usize,
+}
+
+impl<'a> PartialRoute<'a> {
+    fn new(route: &'a Route) -> Self {
+        Self {
+            route,
+            static_segment_index: 0,
+        }
+    }
+
+    fn next_static_segment(&mut self) -> Option<(usize, &'a str)> {
+        let idx = self.static_segment_index;
+        let segment = self.route.route_segments.get(idx)?;
+        match segment {
+            RouteSegment::Static(segment) => {
+                self.static_segment_index += 1;
+                Some((idx, segment))
+            }
+            _ => None,
+        }
+    }
+}