nested route matching

This commit is contained in:
Greg Johnston 2024-03-02 15:36:05 -05:00
parent f894d6e4f6
commit f122f9109f
6 changed files with 200 additions and 215 deletions

View file

@ -2,7 +2,7 @@
use core::fmt::Display;
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub enum Either<A, B> {
Left(A),
Right(B),
@ -41,6 +41,19 @@ macro_rules! tuples {
}
}
impl<Item, $($ty,)*> Iterator for $name<$($ty,)*>
where
$($ty: Iterator<Item = Item>,)*
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
$($name::$ty(i) => i.next(),)*
}
}
}
/*impl<$($ty,)*> Iterator for $name<$($ty,)*>
where
$($ty: Iterator,)*

View file

@ -17,8 +17,6 @@ pub use static_segment::*;
pub trait PossibleRouteMatch {
type ParamsIter<'a>: IntoIterator<Item = (&'a str, &'a str)>;
fn matches<'a>(&self, path: &'a str) -> Option<&'a str>;
fn test<'a>(
&self,
path: &'a str,

View file

@ -9,24 +9,6 @@ pub struct ParamSegment(pub &'static str);
impl PossibleRouteMatch for ParamSegment {
type ParamsIter<'a> = iter::Once<(&'a str, &'a str)>;
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(&'/') {
matched_len += 1;
test.next();
}
for char in test {
// when we get a closing /, stop matching
if char == '/' {
break;
}
matched_len += char.len_utf8();
}
Some(&path[0..matched_len])
}
fn test<'a>(
&self,
path: &'a str,
@ -70,10 +52,6 @@ pub struct WildcardSegment(pub &'static str);
impl PossibleRouteMatch for WildcardSegment {
type ParamsIter<'a> = iter::Once<(&'a str, &'a str)>;
fn matches<'a>(&self, path: &'a str) -> Option<&'a str> {
Some(path)
}
fn test<'a>(
&self,
path: &'a str,
@ -113,7 +91,6 @@ mod tests {
fn single_param_match() {
let path = "/foo";
let def = ParamSegment("a");
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(), "");
@ -125,7 +102,6 @@ mod tests {
fn single_param_match_with_trailing_slash() {
let path = "/foo/";
let def = ParamSegment("a");
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(), "/");
@ -137,7 +113,6 @@ mod tests {
fn tuple_of_param_matches() {
let path = "/foo/bar";
let def = (ParamSegment("a"), ParamSegment("b"));
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(), "");
@ -154,7 +129,6 @@ mod tests {
StaticSegment("bar"),
WildcardSegment("rest"),
);
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

@ -6,10 +6,6 @@ use core::iter;
impl PossibleRouteMatch for () {
type ParamsIter<'a> = iter::Empty<(&'a str, &'a str)>;
fn matches<'a>(&self, path: &'a str) -> Option<&'a str> {
Some(path)
}
fn test<'a>(
&self,
path: &'a str,
@ -26,53 +22,27 @@ pub struct StaticSegment(pub &'static str);
impl PossibleRouteMatch for StaticSegment {
type ParamsIter<'a> = iter::Empty<(&'a str, &'a str)>;
fn matches<'a>(&self, path: &'a str) -> Option<&'a str> {
let mut matched_len = 0;
let mut this = self.0.chars();
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() || self.0 == "/";
if !self.0.is_empty() {
for char in test {
// when we get a closing /, stop matching
if char == '/' {
break;
}
// 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 None;
} else {
matched_len += char.len_utf8();
has_matched = true;
}
}
}
has_matched.then(|| &path[matched_len..])
}
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
let mut matched_len = 0;
let mut test = path.chars();
let mut test = path.chars().peekable();
let mut this = self.0.chars();
let mut has_matched = self.0.is_empty() || self.0 == "/";
// match an initial /
if let Some('/') = test.next() {
if let Some('/') = test.peek() {
test.next();
if !self.0.is_empty() {
matched_len += 1;
}
if self.0.starts_with('/') || self.0.is_empty() {
this.next();
}
}
for char in test {
let n = this.next();
// when we get a closing /, stop matching
@ -131,7 +101,6 @@ mod tests {
fn single_static_match_with_trailing_slash() {
let path = "/foo/";
let def = StaticSegment("foo");
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(), "/");
@ -143,7 +112,6 @@ mod tests {
fn tuple_of_static_matches() {
let path = "/foo/bar";
let def = (StaticSegment("foo"), StaticSegment("bar"));
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(), "");
@ -155,7 +123,6 @@ mod tests {
fn tuple_static_mismatch() {
let path = "/foo/baz";
let def = (StaticSegment("foo"), StaticSegment("bar"));
assert!(def.matches(path).is_none());
assert!(def.test(path).is_none());
}
@ -170,7 +137,6 @@ mod tests {
StaticSegment("bar"),
(),
);
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

@ -23,20 +23,12 @@ macro_rules! tuples {
($first:ident => $($ty:ident),*) => {
impl<$first, $($ty),*> PossibleRouteMatch for ($first, $($ty,)*)
where
Self: core::fmt::Debug,
$first: PossibleRouteMatch,
$($ty: PossibleRouteMatch),*,
{
type ParamsIter<'a> = chain_types!(<<$first>::ParamsIter<'a> as IntoIterator>::IntoIter, $($ty,)*);
fn matches<'a>(&self, path: &'a str) -> Option<&'a str>
{
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
let path = $first.matches(path)?;
$(let path = $ty.matches(path)?;)*
Some(path)
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
let mut matched_len = 0;
#[allow(non_snake_case)]

View file

@ -38,8 +38,17 @@ where
pub fn match_route(&self, path: &'a str) -> Option<Children::Match> {
let path = match &self.base {
None => path,
Some(base) if base.starts_with('/') => {
path.trim_start_matches(base.as_ref())
Some(base) => {
let (base, path) = if base.starts_with('/') {
(base.trim_start_matches('/'), path.trim_start_matches('/'))
} else {
(base.as_ref(), path)
};
if let Some(path) = path.strip_prefix(base) {
path
} else {
return None;
}
}
Some(base) => path
.trim_start_matches('/')
@ -74,11 +83,9 @@ pub trait IntoParams<'a> {
pub trait MatchNestedRoutes<'a> {
type Data;
type ParamsIter: IntoIterator<Item = (&'a str, &'a str)> + Clone;
//type ParamsIter: IntoIterator<Item = (&'a str, &'a str)> + Clone;
type Match: IntoParams<'a>;
fn matches(&self, path: &'a str) -> Option<&'a str>;
fn match_nested(&self, path: &'a str) -> (Option<Self::Match>, &'a str);
fn generate_routes(
@ -124,13 +131,9 @@ pub struct NestedRoute<Segments, Children, Data, View> {
impl<'a> MatchNestedRoutes<'a> for () {
type Data = ();
type ParamsIter = iter::Empty<(&'a str, &'a str)>;
//type ParamsIter = iter::Empty<(&'a str, &'a str)>;
type Match = ();
fn matches(&self, path: &'a str) -> Option<&'a str> {
Some(path)
}
fn match_nested(&self, path: &'a str) -> (Option<Self::Match>, &'a str) {
(Some(()), path)
}
@ -160,19 +163,10 @@ where
Clone,
{
type Data = Data;
type ParamsIter = iter::Chain<
type Match = NestedMatch<'a, iter::Chain<
<Segments::ParamsIter<'a> as IntoIterator>::IntoIter,
<<Children::Match as IntoParams<'a>>::IntoParams as IntoIterator>::IntoIter,
>;
type Match = NestedMatch<'a, Self::ParamsIter, Children::Match>;
fn matches(&self, path: &'a str) -> Option<&'a str> {
if let Some(remaining) = self.segments.matches(path) {
self.children.matches(remaining)
} else {
None
}
}
>, Children::Match>;
fn match_nested(&self, path: &'a str) -> (Option<Self::Match>, &'a str) {
self.segments
@ -188,14 +182,18 @@ where
let inner = inner?;
let params = params.into_iter();
Some((
Some(NestedMatch {
matched,
params: params.chain(inner.to_params()),
child: inner,
}),
remaining,
))
if remaining.is_empty() {
Some((
Some(NestedMatch {
matched,
params: params.chain(inner.to_params()),
child: inner,
}),
remaining,
))
} else {
None
}
},
)
.unwrap_or((None, path))
@ -223,13 +221,8 @@ where
A: MatchNestedRoutes<'a>,
{
type Data = A::Data;
type ParamsIter = A::ParamsIter;
type Match = A::Match;
fn matches(&self, path: &'a str) -> Option<&'a str> {
self.0.matches(path)
}
fn match_nested(&self, path: &'a str) -> (Option<Self::Match>, &'a str) {
self.0.match_nested(path)
}
@ -244,7 +237,10 @@ where
#[cfg(test)]
mod tests {
use super::{NestedRoute, ParamSegment, Routes};
use crate::{matching::StaticSegment, PathSegment};
use crate::{
matching::{IntoParams, StaticSegment, WildcardSegment},
PathSegment,
};
#[test]
pub fn matches_single_root_route() {
@ -380,27 +376,104 @@ mod tests {
]
);
let matched = routes.match_route("/about");
let matched = routes.match_route("/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
}
/*#[test]
pub fn chooses_between_nested_routes() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment("/"),
children: (
#[test]
pub fn arbitrary_nested_routes() {
let routes = Routes::new_with_base(
(
NestedRoute {
segments: StaticSegment(""),
children: (),
segments: StaticSegment("/"),
children: (
NestedRoute {
segments: StaticSegment("/"),
children: (),
data: (),
view: (),
},
NestedRoute {
segments: StaticSegment("about"),
children: (),
data: (),
view: (),
},
),
data: (),
view: (),
},
NestedRoute {
segments: StaticSegment("about"),
segments: StaticSegment("/blog"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: (),
},
NestedRoute {
segments: StaticSegment("category"),
children: (),
data: (),
view: (),
},
NestedRoute {
segments: (
StaticSegment("post"),
ParamSegment("id"),
),
children: (),
data: (),
view: (),
},
),
data: (),
view: (),
},
NestedRoute {
segments: (
StaticSegment("/contact"),
WildcardSegment("any"),
),
children: (),
data: (),
view: (),
},
),
});
let matched = routes.match_route("/");
panic!("matched = {matched:?}");
}*/
"/portfolio",
);
// generates routes correctly
let (base, paths) = routes.generate_routes();
assert_eq!(base, Some("/portfolio"));
let matched = routes.match_route("/about");
assert!(matched.is_none());
let matched = routes.match_route("/portfolio/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/portfolio/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
let matched = routes.match_route("/portfolio/contact").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "")]);
let matched = routes.match_route("/portfolio/contact/foobar").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "foobar")]);
}
}
#[derive(Debug)]
@ -441,12 +514,12 @@ impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
}
macro_rules! chain_generated {
($first:ident, $second:ident, ) => {
($first:expr, $second:expr, ) => {
$first.chain($second)
};
($first:ident, $second:ident, $($rest:ident,)+) => {
($first:expr, $second:ident, $($rest:ident,)+) => {
chain_generated!(
$firsts.chain($second)
$first.chain($second),
$($rest,)+
)
}
@ -463,7 +536,10 @@ where
>;
fn to_params(&self) -> Self::IntoParams {
todo!()
match self {
Either::Left(i) => Either::Left(i.to_params().into_iter()),
Either::Right(i) => Either::Right(i.to_params().into_iter()),
}
}
}
@ -473,15 +549,20 @@ where
B: MatchNestedRoutes<'a>,
{
type Data = (A::Data, B::Data);
type ParamsIter = core::iter::Empty<(&'a str, &'a str)>;
type Match = Either<A::Match, B::Match>;
fn matches(&self, path: &'a str) -> Option<&'a str> {
todo!()
}
fn match_nested(&self, path: &'a str) -> (Option<Self::Match>, &'a str) {
todo!()
#[allow(non_snake_case)]
let (A, B) = &self;
if let (Some(matched), remaining) = A.match_nested(path) {
return (Some(Either::Left(matched)), remaining);
}
if let (Some(matched), remaining) = B.match_nested(path) {
return (Some(Either::Right(matched)), remaining);
}
(None, path)
}
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
@ -496,21 +577,36 @@ where
}
macro_rules! tuples {
($either:ty => $($ty:ident),*) => {
($either:ident => $($ty:ident),*) => {
impl<'a, $($ty,)*> IntoParams<'a> for $either <$($ty,)*>
where
$($ty: IntoParams<'a>),*,
{
type IntoParams = $either<$(
<$ty::IntoParams as IntoIterator>::IntoIter,
)*>;
fn to_params(&self) -> Self::IntoParams {
match self {
$($either::$ty(i) => $either::$ty(i.to_params().into_iter()),)*
}
}
}
impl<'a, $($ty),*> MatchNestedRoutes<'a> for ($($ty,)*)
where
$($ty: MatchNestedRoutes<'a>),*,
{
type Data = ($($ty::Data,)*);
type ParamsIter = core::iter::Empty<(&'a str, &'a str)>; // TODO
type Match = $either<$($ty,)*>;
fn matches(&self, path: &'a str) -> Option<&'a str> {
todo!()
}
type Match = $either<$($ty::Match,)*>;
fn match_nested(&self, path: &'a str) -> (Option<Self::Match>, &'a str) {
todo!()
#[allow(non_snake_case)]
let ($($ty,)*) = &self;
$(if let (Some(matched), remaining) = $ty.match_nested(path) {
return (Some($either::$ty(matched)), remaining);
})*
(None, path)
}
fn generate_routes(
@ -518,79 +614,25 @@ macro_rules! tuples {
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
#[allow(non_snake_case)]
let ($($ty,)*) = &self;
#[allow(non_snake_case)]
$(let $ty = $ty.generate_routes().into_iter();)*
chain_generated!($($ty,)*)
}
}
/*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!(Either => 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
);*/
tuples!(EitherOf3 => A, B, C);
tuples!(EitherOf4 => A, B, C, D);
tuples!(EitherOf5 => A, B, C, D, E);
tuples!(EitherOf6 => A, B, C, D, E, F);
tuples!(EitherOf7 => A, B, C, D, E, F, G);
tuples!(EitherOf8 => A, B, C, D, E, F, G, H);
tuples!(EitherOf9 => A, B, C, D, E, F, G, H, I);
tuples!(EitherOf10 => A, B, C, D, E, F, G, H, I, J);
tuples!(EitherOf11 => A, B, C, D, E, F, G, H, I, J, K);
tuples!(EitherOf12 => A, B, C, D, E, F, G, H, I, J, K, L);
tuples!(EitherOf13 => A, B, C, D, E, F, G, H, I, J, K, L, M);
tuples!(EitherOf14 => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(EitherOf15 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(EitherOf16 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);