work on routing

This commit is contained in:
Greg Johnston 2024-02-24 14:47:53 -05:00
parent 3e3359bea1
commit 3379462633
7 changed files with 312 additions and 54 deletions

View file

@ -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! {
<button on:click=move |_| {
a.update(|n| *n += 1);
}>
{a}
</button>
<button on:click=move |_| {
b.update(|n| *n += 1);
}>
{b}
</button>
<Suspense>
<button on:click=move |_| a.update(|n| *n += 1)> {a} </button> " "
<button on:click=move |_| b.update(|n| *n += 1)> {b} </button>
/*<Suspense>
{move || a2().map(move |a2| {
b2().map(move |b2| {
view! {
@ -52,7 +50,7 @@ pub fn async_example() -> impl IntoView {
}
})
})}
</Suspense>
</Suspense>*/
<p>
//{times}
</p>

View file

@ -29,7 +29,7 @@ impl Location for RequestUrl {
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error> {
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()

View file

@ -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<PartialPathMatch<'a>>;

View file

@ -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<PartialPathMatch<'a>> {
@ -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<PartialPathMatch<'a>> {
@ -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(), "");

View file

@ -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<PartialPathMatch<'a>> {
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<PathSegment>) {}
@ -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<PartialPathMatch<'a>> {
@ -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(), "");

View file

@ -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<PartialPathMatch<'a>>

View file

@ -6,32 +6,215 @@ pub use horizontal::*;
pub use vertical::*;
pub struct Routes<Children> {
base: Cow<'static, str>,
base: Option<Cow<'static, str>>,
children: Children,
}
impl<Children> Routes<Children> {
pub fn new(children: Children) -> Self {
Self {
base: None,
children,
}
}
pub fn new_with_base(
children: Children,
base: impl Into<Cow<'static, str>>,
) -> Self {
Self {
base: Some(base.into()),
children,
}
}
}
impl<Children> Routes<Children>
where
Children: MatchNestedRoutes,
{
pub fn match_route<'a>(&self, path: &'a str) -> Option<RouteMatch<'a>> {
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<NestedRouteMatch<'a>>,
}
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<NestedRouteMatch<'a>>,
);
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NestedRoute<Segments, Children> {
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<NestedRouteMatch<'a>>,
) {
}
}
impl<Segments, Children> MatchNestedRoutes for NestedRoute<Segments, Children>
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<NestedRouteMatch<'a>>,
) {
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<A> 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<NestedRouteMatch<'a>>,
) {
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<PartialPathMatch<'a>>
{
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<PathSegment>) {
#[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
);*/