From 3379462633caff09c245fc85569a6e0e9156736c Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sat, 24 Feb 2024 14:47:53 -0500 Subject: [PATCH] work on routing --- examples/async-demo/src/lib.rs | 28 +- routing/src/location/server.rs | 2 +- routing/src/matching/horizontal/mod.rs | 7 +- .../src/matching/horizontal/param_segments.rs | 20 +- .../src/matching/horizontal/static_segment.rs | 26 +- routing/src/matching/horizontal/tuples.rs | 6 +- routing/src/matching/mod.rs | 277 +++++++++++++++++- 7 files changed, 312 insertions(+), 54 deletions(-) diff --git a/examples/async-demo/src/lib.rs b/examples/async-demo/src/lib.rs index 6743dacb1..85c31f782 100644 --- a/examples/async-demo/src/lib.rs +++ b/examples/async-demo/src/lib.rs @@ -1,7 +1,10 @@ use gloo_timers::future::TimeoutFuture; use leptos::{ prelude::*, - reactive_graph::{computed::AsyncDerived, signal::RwSignal}, + reactive_graph::{ + computed::{AsyncDerived, AsyncState}, + signal::RwSignal, + }, tachys::log, view, IntoView, }; @@ -30,21 +33,16 @@ pub fn async_example() -> impl IntoView { let a = RwSignal::new(0); let b = RwSignal::new(1); - let a2 = create_resource(a, |a| wait('A', 1, a)); - let b2 = create_resource(b, |a| wait('A', 1, b)); + let a2 = || wait('A', 1, 1); + let b2 = || wait('B', 3, 3); + + //let a2 = create_resource(a, |a| wait('A', 1, a)); + //let b2 = create_resource(b, |a| wait('A', 1, b)); view! { - - - + " " + + /* {move || a2().map(move |a2| { b2().map(move |b2| { view! { @@ -52,7 +50,7 @@ pub fn async_example() -> impl IntoView { } }) })} - + */

//{times}

diff --git a/routing/src/location/server.rs b/routing/src/location/server.rs index aebda7cce..1b1986d77 100644 --- a/routing/src/location/server.rs +++ b/routing/src/location/server.rs @@ -29,7 +29,7 @@ impl Location for RequestUrl { fn parse_with_base(url: &str, base: &str) -> Result { let base = url::Url::parse(base)?; - let url = url::Url::options().base_url(Some(&base)).parse(&url)?; + let url = url::Url::options().base_url(Some(&base)).parse(url)?; let search_params = url .query_pairs() diff --git a/routing/src/matching/horizontal/mod.rs b/routing/src/matching/horizontal/mod.rs index 9c80e7da4..b6197fe21 100644 --- a/routing/src/matching/horizontal/mod.rs +++ b/routing/src/matching/horizontal/mod.rs @@ -1,6 +1,5 @@ use crate::PathSegment; use alloc::vec::Vec; -use core::str::Chars; mod param_segments; mod static_segment; @@ -16,11 +15,7 @@ pub use static_segment::*; /// as subsequent segments of the URL and tries to match them all. For a "vertical" /// matching that sees a tuple as alternatives to one another, see [`RouteChild`](super::RouteChild). pub trait PossibleRouteMatch { - fn matches(&self, path: &str) -> bool { - self.matches_iter(&mut path.chars()) - } - - fn matches_iter(&self, path: &mut Chars) -> bool; + fn matches<'a>(&self, path: &'a str) -> Option<&'a str>; fn test<'a>(&self, path: &'a str) -> Option>; diff --git a/routing/src/matching/horizontal/param_segments.rs b/routing/src/matching/horizontal/param_segments.rs index be87f4078..024667f68 100644 --- a/routing/src/matching/horizontal/param_segments.rs +++ b/routing/src/matching/horizontal/param_segments.rs @@ -7,8 +7,9 @@ use core::str::Chars; pub struct ParamSegment(pub &'static str); impl PossibleRouteMatch for ParamSegment { - fn matches_iter(&self, test: &mut Chars) -> bool { - let mut test = test.peekable(); + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { + let mut matched_len = 0; + let mut test = path.chars().peekable(); // match an initial / if test.peek() == Some(&'/') { test.next(); @@ -18,8 +19,9 @@ impl PossibleRouteMatch for ParamSegment { if char == '/' { break; } + matched_len += char.len_utf8(); } - true + Some(&path[matched_len..]) } fn test<'a>(&self, path: &'a str) -> Option> { @@ -60,8 +62,8 @@ impl PossibleRouteMatch for ParamSegment { pub struct WildcardSegment(pub &'static str); impl PossibleRouteMatch for WildcardSegment { - fn matches_iter(&self, _path: &mut Chars) -> bool { - true + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { + Some(path) } fn test<'a>(&self, path: &'a str) -> Option> { @@ -101,7 +103,7 @@ mod tests { fn single_param_match() { let path = "/foo"; let def = ParamSegment("a"); - assert!(def.matches(path)); + assert!(def.matches(path).is_some()); let matched = def.test(path).expect("couldn't match route"); assert_eq!(matched.matched(), "/foo"); assert_eq!(matched.remaining(), ""); @@ -112,7 +114,7 @@ mod tests { fn single_param_match_with_trailing_slash() { let path = "/foo/"; let def = ParamSegment("a"); - assert!(def.matches(path)); + assert!(def.matches(path).is_some()); let matched = def.test(path).expect("couldn't match route"); assert_eq!(matched.matched(), "/foo"); assert_eq!(matched.remaining(), "/"); @@ -123,7 +125,7 @@ mod tests { fn tuple_of_param_matches() { let path = "/foo/bar"; let def = (ParamSegment("a"), ParamSegment("b")); - assert!(def.matches(path)); + assert!(def.matches(path).is_some()); let matched = def.test(path).expect("couldn't match route"); assert_eq!(matched.matched(), "/foo/bar"); assert_eq!(matched.remaining(), ""); @@ -139,7 +141,7 @@ mod tests { StaticSegment("bar"), WildcardSegment("rest"), ); - assert!(def.matches(path)); + assert!(def.matches(path).is_some()); let matched = def.test(path).expect("couldn't match route"); assert_eq!(matched.matched(), "/foo/bar/////"); assert_eq!(matched.remaining(), ""); diff --git a/routing/src/matching/horizontal/static_segment.rs b/routing/src/matching/horizontal/static_segment.rs index 1a30ecabe..cf46f1e21 100644 --- a/routing/src/matching/horizontal/static_segment.rs +++ b/routing/src/matching/horizontal/static_segment.rs @@ -1,15 +1,14 @@ use super::{PartialPathMatch, PossibleRouteMatch}; use crate::PathSegment; use alloc::{string::String, vec::Vec}; -use core::str::Chars; impl PossibleRouteMatch for () { fn test<'a>(&self, path: &'a str) -> Option> { Some(PartialPathMatch::new(path, [], "")) } - fn matches_iter(&self, _path: &mut Chars) -> bool { - true + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { + Some(path) } fn generate_path(&self, _path: &mut Vec) {} @@ -19,16 +18,18 @@ impl PossibleRouteMatch for () { pub struct StaticSegment(pub &'static str); impl PossibleRouteMatch for StaticSegment { - fn matches_iter(&self, test: &mut Chars) -> bool { + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { + let mut matched_len = 0; let mut this = self.0.chars(); - let mut test = test.peekable(); + let mut test = path.chars().peekable(); if test.peek() == Some(&'/') { + matched_len += '/'.len_utf8(); test.next(); } // unless this segment is empty, we start by // assuming that it has not actually matched - let mut has_matched = self.0.is_empty(); + let mut has_matched = self.0.is_empty() || self.0 == "/"; for char in test { // when we get a closing /, stop matching if char == '/' { @@ -37,13 +38,14 @@ impl PossibleRouteMatch for StaticSegment { // if the next character in the path doesn't match the // next character in the segment, we don't match else if this.next() != Some(char) { - return false; + return None; } else { + matched_len += char.len_utf8(); has_matched = true; } } - has_matched + has_matched.then(|| &path[matched_len..]) } fn test<'a>(&self, path: &'a str) -> Option> { @@ -113,7 +115,7 @@ mod tests { fn single_static_match_with_trailing_slash() { let path = "/foo/"; let def = StaticSegment("foo"); - assert!(def.matches(path)); + assert!(def.matches(path).is_some()); let matched = def.test(path).expect("couldn't match route"); assert_eq!(matched.matched(), "/foo"); assert_eq!(matched.remaining(), "/"); @@ -124,7 +126,7 @@ mod tests { fn tuple_of_static_matches() { let path = "/foo/bar"; let def = (StaticSegment("foo"), StaticSegment("bar")); - assert!(def.matches(path)); + assert!(def.matches(path).is_some()); let matched = def.test(path).expect("couldn't match route"); assert_eq!(matched.matched(), "/foo/bar"); assert_eq!(matched.remaining(), ""); @@ -135,7 +137,7 @@ mod tests { fn tuple_static_mismatch() { let path = "/foo/baz"; let def = (StaticSegment("foo"), StaticSegment("bar")); - assert!(!def.matches(path)); + assert!(def.matches(path).is_none()); assert!(def.test(path).is_none()); } @@ -150,7 +152,7 @@ mod tests { StaticSegment("bar"), (), ); - assert!(def.matches(path)); + assert!(def.matches(path).is_some()); let matched = def.test(path).expect("couldn't match route"); assert_eq!(matched.matched(), "/foo/bar"); assert_eq!(matched.remaining(), ""); diff --git a/routing/src/matching/horizontal/tuples.rs b/routing/src/matching/horizontal/tuples.rs index fc081e666..6bcc3f1f4 100644 --- a/routing/src/matching/horizontal/tuples.rs +++ b/routing/src/matching/horizontal/tuples.rs @@ -1,6 +1,5 @@ use super::{PartialPathMatch, PathSegment, PossibleRouteMatch}; use alloc::{string::String, vec::Vec}; -use core::str::Chars; macro_rules! tuples { ($($ty:ident),*) => { @@ -8,11 +7,12 @@ macro_rules! tuples { where $($ty: PossibleRouteMatch),*, { - fn matches_iter(&self, path: &mut Chars) -> bool + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { #[allow(non_snake_case)] let ($($ty,)*) = &self; - $($ty.matches_iter(path) &&)* true + $(let path = $ty.matches(path)?;)* + Some(path) } fn test<'a>(&self, path: &'a str) -> Option> diff --git a/routing/src/matching/mod.rs b/routing/src/matching/mod.rs index 7c268bacd..a763f31d9 100644 --- a/routing/src/matching/mod.rs +++ b/routing/src/matching/mod.rs @@ -6,32 +6,215 @@ pub use horizontal::*; pub use vertical::*; pub struct Routes { - base: Cow<'static, str>, + base: Option>, children: Children, } +impl Routes { + pub fn new(children: Children) -> Self { + Self { + base: None, + children, + } + } + + pub fn new_with_base( + children: Children, + base: impl Into>, + ) -> Self { + Self { + base: Some(base.into()), + children, + } + } +} + +impl Routes +where + Children: MatchNestedRoutes, +{ + pub fn match_route<'a>(&self, path: &'a str) -> Option> { + let path = match &self.base { + None => path, + Some(base) if base.starts_with('/') => { + path.trim_start_matches(base.as_ref()) + } + Some(base) => path + .trim_start_matches('/') + .trim_start_matches(base.as_ref()), + }; + + let mut matched_nested_routes = Vec::with_capacity(Children::DEPTH); + self.children + .match_nested_routes(path, &mut matched_nested_routes); + + // TODO check for completeness + + if matched_nested_routes.is_empty() { + None + } else { + Some(RouteMatch { + path, + matched_nested_routes, + }) + } + } +} + +#[derive(Debug)] pub struct RouteMatch<'a> { + path: &'a str, matched_nested_routes: Vec>, } +impl<'a> RouteMatch<'a> { + pub fn path(&self) -> &'a str { + self.path + } + + pub fn matches(&self) -> &[NestedRouteMatch<'a>] { + &self.matched_nested_routes + } +} + +#[derive(Debug)] pub struct NestedRouteMatch<'a> { - /// The portion of the full path matched by this nested route. - matched_path: String, - /// The map of params matched by this route. + /// The portion of the full path matched only by this nested route. + matched_path: &'a str, + /// The map of params matched only by this nested route. params: Params<&'static str>, } +impl<'a> NestedRouteMatch<'a> { + pub fn matched_path(&self) -> &'a str { + self.matched_path + } + + pub fn matched_params(&self) -> &Params<&'static str> { + &self.params + } +} + +pub trait MatchNestedRoutes { + const DEPTH: usize; + + fn matches<'a>(&self, path: &'a str) -> Option<&'a str>; + + fn match_nested_routes<'a>( + &self, + path: &'a str, + matches: &mut Vec>, + ); +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct NestedRoute { - segments: Segments, - children: Children, + pub segments: Segments, + pub children: Children, +} + +impl MatchNestedRoutes for () { + const DEPTH: usize = 0; + + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { + Some(path) + } + + fn match_nested_routes<'a>( + &self, + _path: &'a str, + _matches: &mut Vec>, + ) { + } +} + +impl MatchNestedRoutes for NestedRoute +where + Segments: PossibleRouteMatch, + Children: MatchNestedRoutes, +{ + const DEPTH: usize = Children::DEPTH; + + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { + if let Some(remaining) = self.segments.matches(path) { + self.children.matches(remaining) + } else { + None + } + } + + fn match_nested_routes<'a>( + &self, + path: &'a str, + matches: &mut Vec>, + ) { + if let Some(remaining) = self.segments.matches(path) { + if let partial = self.segments.test(path) { + let PartialPathMatch { + params, matched, .. + } = partial; + matches.push(NestedRouteMatch { + matched_path: matched, + params, + }); + } + self.children.match_nested_routes(path, matches); + } + } +} + +impl MatchNestedRoutes for (A,) +where + A: MatchNestedRoutes, +{ + const DEPTH: usize = A::DEPTH; + + fn matches<'a>(&self, path: &'a str) -> Option<&'a str> { + self.0.matches(path) + } + + fn match_nested_routes<'a>( + &self, + path: &'a str, + matches: &mut Vec>, + ) { + self.0.match_nested_routes(path, matches); + } +} + +#[cfg(test)] +mod tests { + use super::{NestedRoute, Routes}; + use crate::matching::StaticSegment; + + #[test] + pub fn does_not_match_none() { + let routes = Routes::new(NestedRoute { + segments: (), + children: (), + }); + let matched = routes.match_route("/"); + assert!(matched.is_none()); + let matched = routes.match_route(""); + assert!(matched.is_none()); + } + + #[test] + pub fn matches_single_root_route() { + let routes = Routes::new(NestedRoute { + segments: StaticSegment("/"), + children: (), + }); + let matched = routes.match_route("/"); + assert!(matched.is_some()) + } } #[derive(Debug)] pub struct PartialPathMatch<'a> { pub(crate) remaining: &'a str, - pub(crate) params: Params<&'static str>, - pub(crate) matched: String, + pub(crate) params: Vec<(&'static str, &'a str)>, + pub(crate) matched: &'a str, } impl<'a> PartialPathMatch<'a> { @@ -63,3 +246,81 @@ impl<'a> PartialPathMatch<'a> { self.matched.as_str() } } + +macro_rules! tuples { + ($($ty:ident),*) => { + impl<$($ty),*> PossibleRouteMatch for ($($ty,)*) + where + $($ty: PossibleRouteMatch),*, + { + fn matches_iter(&self, path: &mut Chars) -> bool + { + #[allow(non_snake_case)] + let ($($ty,)*) = &self; + $($ty.matches_iter(path) &&)* true + } + + fn test<'a>(&self, path: &'a str) -> Option> + { + let mut full_params = Vec::new(); + let mut full_matched = String::new(); + #[allow(non_snake_case)] + let ($($ty,)*) = &self; + $( + let PartialPathMatch { + remaining, + matched, + params + } = $ty.test(path)?; + let path = remaining; + full_matched.push_str(&matched); + full_params.extend(params); + )* + Some(PartialPathMatch { + remaining: path, + matched: full_matched, + params: full_params + }) + } + + fn generate_path(&self, path: &mut Vec) { + #[allow(non_snake_case)] + let ($($ty,)*) = &self; + $( + $ty.generate_path(path); + )* + } + } + }; +} + +//tuples!(A, B); +/*tuples!(A, B, C); +tuples!(A, B, C, D); +tuples!(A, B, C, D, E); +tuples!(A, B, C, D, E, F); +tuples!(A, B, C, D, E, F, G); +tuples!(A, B, C, D, E, F, G, H); +tuples!(A, B, C, D, E, F, G, H, I); +tuples!(A, B, C, D, E, F, G, H, I, J); +tuples!(A, B, C, D, E, F, G, H, I, J, K); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W); +tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X); +tuples!( + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y +); +tuples!( + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, + Z +);*/