fix: correctly handle optional parameters in ParentRoute (#3784)

This commit is contained in:
Greg Johnston
2025-03-24 20:15:31 -04:00
committed by GitHub
parent b4731e61c6
commit 1a1e436cff
4 changed files with 89 additions and 9 deletions

View File

@@ -128,6 +128,8 @@ pub trait MatchNestedRoutes {
fn generate_routes(
&self,
) -> impl IntoIterator<Item = GeneratedRouteData> + '_;
fn optional(&self) -> bool;
}
#[derive(Default, Debug, PartialEq)]

View File

@@ -17,6 +17,7 @@ pub struct AnyNestedRoute {
)
-> (Option<(RouteMatchId, AnyNestedMatch)>, &'a str),
generate_routes: fn(&Erased) -> Vec<GeneratedRouteData>,
optional: fn(&Erased) -> bool,
}
impl Clone for AnyNestedRoute {
@@ -74,11 +75,18 @@ where
value.get_ref::<T>().generate_routes().into_iter().collect()
}
fn optional<T: MatchNestedRoutes + Send + Clone + 'static>(
value: &Erased,
) -> bool {
value.get_ref::<T>().optional()
}
AnyNestedRoute {
value: Erased::new(self),
clone: clone::<T>,
match_nested: match_nested::<T>,
generate_routes: generate_routes::<T>,
optional: optional::<T>,
}
}
}
@@ -97,4 +105,8 @@ impl MatchNestedRoutes for AnyNestedRoute {
fn generate_routes(&self) -> impl IntoIterator<Item = GeneratedRouteData> {
(self.generate_routes)(&self.value)
}
fn optional(&self) -> bool {
(self.optional)(&self.value)
}
}

View File

@@ -196,10 +196,19 @@ where
type Data = Data;
type Match = NestedMatch<Children::Match, View>;
fn optional(&self) -> bool {
self.segments.optional()
&& self.children.as_ref().map(|n| n.optional()).unwrap_or(true)
}
fn match_nested<'a>(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
// if this was optional (for example, this whole nested route definition consisted of an optional param),
// then we'll need to retest the inner value against the starting path, if this one succeeds and the inner one fails
let this_was_optional = self.segments.optional();
self.segments
.test(path)
.and_then(
@@ -208,15 +217,49 @@ where
mut params,
matched,
}| {
let (_, inner, remaining) = match &self.children {
None => (None, None, remaining),
Some(children) => {
let (inner, remaining) =
children.match_nested(remaining);
let (id, inner) = inner?;
(Some(id), Some(inner), remaining)
}
};
let (_, inner, remaining, was_optional_fallback) =
match &self.children {
None => (None, None, remaining, false),
Some(children) => {
let (inner, remaining) =
children.match_nested(remaining);
match inner {
Some((id, inner)) => (
Some(id),
Some(inner),
remaining,
false,
),
None if this_was_optional => {
// if the parent route was optional, re-match children against full path
let (inner, remaining) =
children.match_nested(path);
let (id, inner) = inner?;
(Some(id), Some(inner), remaining, true)
}
None => {
return None;
}
}
}
};
// if this was an optional route, re-parse its params
if was_optional_fallback {
// new params are based on the path it matched (up to the point where the matched child begins)
// e.g., if we have /:foo?/bar, for /bar we should *not* have { "foo": "bar" }
// so, we re-parse based on "" to yield { "foo": "" }
let matched = inner
.as_ref()
.map(|inner| inner.as_matched())
.unwrap_or("");
let rematch = path
.trim_end_matches(&format!("{matched}{remaining}"));
let new_partial = self.segments.test(rematch).unwrap();
params = new_partial.params;
}
let inner_params = inner
.as_ref()
.map(|inner| inner.to_params())

View File

@@ -31,6 +31,10 @@ impl MatchNestedRoutes for () {
type Data = ();
type Match = ();
fn optional(&self) -> bool {
false
}
fn match_nested<'a>(
&self,
path: &'a str,
@@ -95,6 +99,10 @@ where
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
self.0.generate_routes()
}
fn optional(&self) -> bool {
self.0.optional()
}
}
impl<A, B> MatchParams for Either<A, B>
@@ -180,6 +188,10 @@ where
A.chain(B)
}
fn optional(&self) -> bool {
self.0.optional() && self.1.optional()
}
}
impl<T> MatchNestedRoutes for StaticVec<T>
@@ -206,6 +218,10 @@ where
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
self.iter().flat_map(T::generate_routes)
}
fn optional(&self) -> bool {
self.iter().all(|n| n.optional())
}
}
macro_rules! chain_generated {
@@ -273,6 +289,13 @@ macro_rules! tuples {
type Data = ($($ty::Data,)*);
type Match = $either<$($ty::Match,)*>;
fn optional(&self) -> bool {
#[allow(non_snake_case)]
let ($($ty,)*) = &self;
$($ty.optional() &&)*
true
}
fn match_nested<'a>(&'a self, path: &'a str) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
#[allow(non_snake_case)]