feat: add OptionalParamSegment (closes #2896) (#3140)

This commit is contained in:
Greg Johnston
2024-10-21 21:15:14 -04:00
committed by GitHub
parent 7904e0c395
commit d0ef7b904d
20 changed files with 537 additions and 272 deletions

View File

@@ -4,7 +4,7 @@ mod routes;
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
use leptos_router::{
components::{FlatRoutes, Route, Router, RoutingProgress},
ParamSegment, StaticSegment,
OptionalParamSegment, ParamSegment, StaticSegment,
};
use routes::{nav::*, stories::*, story::*, users::*};
use std::time::Duration;
@@ -28,9 +28,7 @@ pub fn App() -> impl IntoView {
<FlatRoutes fallback=|| "Not found.">
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
<Route path=ParamSegment("stories") view=Stories/>
// TODO allow optional params without duplication
<Route path=StaticSegment("") view=Stories/>
<Route path=OptionalParamSegment("stories") view=Stories/>
</FlatRoutes>
</main>
</Router>

View File

@@ -4,7 +4,7 @@ mod routes;
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
use leptos_router::{
components::{FlatRoutes, Route, Router, RoutingProgress},
ParamSegment, StaticSegment,
OptionalParamSegment, ParamSegment, StaticSegment,
};
use routes::{nav::*, stories::*, story::*, users::*};
use std::time::Duration;
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
<FlatRoutes fallback=|| "Not found.">
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
<Route path=ParamSegment("stories") view=Stories/>
// TODO allow optional params without duplication
<Route path=StaticSegment("") view=Stories/>
<Route path=OptionalParamSegment("stories") view=Stories/>
</FlatRoutes>
</main>
</Router>

View File

@@ -4,7 +4,7 @@ mod routes;
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
use leptos_router::{
components::{FlatRoutes, Route, Router},
ParamSegment, StaticSegment,
OptionalParamSegment, ParamSegment, StaticSegment,
};
use routes::{nav::*, stories::*, story::*, users::*};
#[cfg(feature = "ssr")]
@@ -42,9 +42,7 @@ pub fn App() -> impl IntoView {
<FlatRoutes fallback=|| "Not found.">
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
<Route path=ParamSegment("stories") view=Stories/>
// TODO allow optional params without duplication
<Route path=StaticSegment("") view=Stories/>
<Route path=OptionalParamSegment("stories") view=Stories/>
</FlatRoutes>
</main>
</Router>

View File

@@ -4,7 +4,7 @@ mod routes;
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
use leptos_router::{
components::{FlatRoutes, Route, Router, RoutingProgress},
ParamSegment, StaticSegment,
OptionalParamSegment, ParamSegment, StaticSegment,
};
use routes::{nav::*, stories::*, story::*, users::*};
use std::time::Duration;
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
<FlatRoutes fallback=|| "Not found.">
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
<Route path=ParamSegment("stories") view=Stories/>
// TODO allow optional params without duplication
<Route path=StaticSegment("") view=Stories/>
<Route path=OptionalParamSegment("stories") view=Stories/>
</FlatRoutes>
</main>
</Router>

View File

@@ -35,7 +35,7 @@ use leptos_router::{
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, ResolvedStaticPath},
Method, PathSegment, RouteList, RouteListing, SsrMode,
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
@@ -901,7 +901,7 @@ trait ActixPath {
fn to_actix_path(&self) -> String;
}
impl ActixPath for &[PathSegment] {
impl ActixPath for Vec<PathSegment> {
fn to_actix_path(&self) -> String {
let mut path = String::new();
for segment in self.iter() {
@@ -923,6 +923,14 @@ impl ActixPath for &[PathSegment] {
path.push_str(":.*}");
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
#[cfg(feature = "tracing")]
tracing::error!(
"to_axum_path should only be called on expanded \
paths, which do not have OptionalParam any longer"
);
Default::default()
}
}
}
path
@@ -938,23 +946,34 @@ pub struct ActixRouteListing {
regenerate: Vec<RegenerationFn>,
}
impl From<RouteListing> for ActixRouteListing {
fn from(value: RouteListing) -> Self {
let path = value.path().to_actix_path();
let path = if path.is_empty() {
"/".to_string()
} else {
path
};
let mode = value.mode();
let methods = value.methods().collect();
let regenerate = value.regenerate().into();
Self {
path,
mode: mode.clone(),
methods,
regenerate,
}
trait IntoRouteListing: Sized {
fn into_route_listing(self) -> Vec<ActixRouteListing>;
}
impl IntoRouteListing for RouteListing {
fn into_route_listing(self) -> Vec<ActixRouteListing> {
self.path()
.to_vec()
.expand_optionals()
.into_iter()
.map(|path| {
let path = path.to_actix_path();
let path = if path.is_empty() {
"/".to_string()
} else {
path
};
let mode = self.mode();
let methods = self.methods().collect();
let regenerate = self.regenerate().into();
ActixRouteListing {
path,
mode: mode.clone(),
methods,
regenerate,
}
})
.collect()
}
}
@@ -1033,7 +1052,7 @@ where
let mut routes = routes
.into_inner()
.into_iter()
.map(ActixRouteListing::from)
.flat_map(IntoRouteListing::into_route_listing)
.collect::<Vec<_>>();
(

View File

@@ -66,7 +66,7 @@ use leptos_router::{
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, StaticParamsMap},
PathSegment, RouteList, RouteListing, SsrMode,
ExpandOptionals, PathSegment, RouteList, RouteListing, SsrMode,
};
#[cfg(feature = "default")]
use once_cell::sync::Lazy;
@@ -1267,23 +1267,34 @@ pub struct AxumRouteListing {
regenerate: Vec<RegenerationFn>,
}
impl From<RouteListing> for AxumRouteListing {
fn from(value: RouteListing) -> Self {
let path = value.path().to_axum_path();
let path = if path.is_empty() {
"/".to_string()
} else {
path
};
let mode = value.mode();
let methods = value.methods().collect();
let regenerate = value.regenerate().into();
Self {
path,
mode: mode.clone(),
methods,
regenerate,
}
trait IntoRouteListing: Sized {
fn into_route_listing(self) -> Vec<AxumRouteListing>;
}
impl IntoRouteListing for RouteListing {
fn into_route_listing(self) -> Vec<AxumRouteListing> {
self.path()
.to_vec()
.expand_optionals()
.into_iter()
.map(|path| {
let path = path.to_axum_path();
let path = if path.is_empty() {
"/".to_string()
} else {
path
};
let mode = self.mode();
let methods = self.methods().collect();
let regenerate = self.regenerate().into();
AxumRouteListing {
path,
mode: mode.clone(),
methods,
regenerate,
}
})
.collect()
}
}
@@ -1367,7 +1378,7 @@ where
let mut routes = routes
.into_inner()
.into_iter()
.map(AxumRouteListing::from)
.flat_map(IntoRouteListing::into_route_listing)
.collect::<Vec<_>>();
(
@@ -1700,7 +1711,7 @@ trait AxumPath {
fn to_axum_path(&self) -> String;
}
impl AxumPath for &[PathSegment] {
impl AxumPath for Vec<PathSegment> {
fn to_axum_path(&self) -> String {
let mut path = String::new();
for segment in self.iter() {
@@ -1720,6 +1731,14 @@ impl AxumPath for &[PathSegment] {
path.push_str(s);
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
#[cfg(feature = "tracing")]
tracing::error!(
"to_axum_path should only be called on expanded \
paths, which do not have OptionalParam any longer"
);
Default::default()
}
}
}
path

View File

@@ -1,11 +1,11 @@
use crate::{
hooks::Matched,
location::{LocationProvider, Url},
matching::Routes,
matching::{MatchParams, Routes},
params::ParamsMap,
view_transition::start_view_transition,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
RouteList, RouteListing, RouteMatchId,
ChooseView, MatchInterface, MatchNestedRoutes, PathSegment, RouteList,
RouteListing, RouteMatchId,
};
use any_spawner::Executor;
use either_of::Either;

View File

@@ -1,5 +1,4 @@
use super::{PartialPathMatch, PathSegment};
use std::borrow::Cow;
mod param_segments;
mod static_segment;
mod tuples;
@@ -13,12 +12,9 @@ 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 {
type ParamsIter: IntoIterator<Item = (Cow<'static, str>, String)>;
const OPTIONAL: bool = false;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter>>;
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
fn generate_path(&self, path: &mut Vec<PathSegment>);
}

View File

@@ -14,14 +14,16 @@ use std::borrow::Cow;
///
/// // Manual definition
/// let manual = (ParamSegment("message"),);
/// let (key, value) = manual.test(path)?.params().last()?;
/// let params = manual.test(path)?.params();
/// let (key, value) = params.last()?;
///
/// assert_eq!(key, "message");
/// assert_eq!(value, "hello");
///
/// // Macro definition
/// let using_macro = path!("/:message");
/// let (key, value) = using_macro.test(path)?.params().last()?;
/// let params = using_macro.test(path)?.params();
/// let (key, value) = params.last()?;
///
/// assert_eq!(key, "message");
/// assert_eq!(value, "hello");
@@ -33,12 +35,7 @@ use std::borrow::Cow;
pub struct ParamSegment(pub &'static str);
impl PossibleRouteMatch for ParamSegment {
type ParamsIter = iter::Once<(Cow<'static, str>, String)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut param_offset = 0;
let mut param_len = 0;
@@ -66,10 +63,10 @@ impl PossibleRouteMatch for ParamSegment {
}
let (matched, remaining) = path.split_at(matched_len);
let param_value = iter::once((
let param_value = vec![(
Cow::Borrowed(self.0),
path[param_offset..param_len + param_offset].to_string(),
));
)];
Some(PartialPathMatch::new(remaining, param_value, matched))
}
@@ -93,14 +90,16 @@ impl PossibleRouteMatch for ParamSegment {
///
/// // Manual definition
/// let manual = (StaticSegment("echo"), WildcardSegment("kitchen_sink"));
/// let (key, value) = manual.test(path)?.params().last()?;
/// let params = manual.test(path)?.params();
/// let (key, value) = params.last()?;
///
/// assert_eq!(key, "kitchen_sink");
/// assert_eq!(value, "send/sync/and/static");
///
/// // Macro definition
/// let using_macro = path!("/echo/*else");
/// let (key, value) = using_macro.test(path)?.params().last()?;
/// let params = using_macro.test(path)?.params();
/// let (key, value) = params.last()?;
///
/// assert_eq!(key, "else");
/// assert_eq!(value, "send/sync/and/static");
@@ -122,12 +121,7 @@ impl PossibleRouteMatch for ParamSegment {
pub struct WildcardSegment(pub &'static str);
impl PossibleRouteMatch for WildcardSegment {
type ParamsIter = iter::Once<(Cow<'static, str>, String)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut param_offset = 0;
let mut param_len = 0;
@@ -148,7 +142,11 @@ impl PossibleRouteMatch for WildcardSegment {
Cow::Borrowed(self.0),
path[param_offset..param_len + param_offset].to_string(),
));
Some(PartialPathMatch::new(remaining, param_value, matched))
Some(PartialPathMatch::new(
remaining,
param_value.into_iter().collect(),
matched,
))
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
@@ -156,10 +154,64 @@ impl PossibleRouteMatch for WildcardSegment {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct OptionalParamSegment(pub &'static str);
impl PossibleRouteMatch for OptionalParamSegment {
const OPTIONAL: bool = true;
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut param_offset = 0;
let mut param_len = 0;
let mut test = path.chars();
// match an initial /
if let Some('/') = test.next() {
matched_len += 1;
param_offset = 1;
}
for char in test {
// when we get a closing /, stop matching
if char == '/' {
break;
}
// otherwise, push into the matched param
else {
matched_len += char.len_utf8();
param_len += char.len_utf8();
}
}
let matched_len = if matched_len == 1 && path.starts_with('/') {
0
} else {
matched_len
};
let (matched, remaining) = path.split_at(matched_len);
let param_value = (matched_len > 0)
.then(|| {
(
Cow::Borrowed(self.0),
path[param_offset..param_len + param_offset].to_string(),
)
})
.into_iter()
.collect();
Some(PartialPathMatch::new(remaining, param_value, matched))
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
path.push(PathSegment::OptionalParam(self.0.into()));
}
}
#[cfg(test)]
mod tests {
use super::PossibleRouteMatch;
use crate::{ParamSegment, StaticSegment, WildcardSegment};
use crate::{
OptionalParamSegment, ParamSegment, StaticSegment, WildcardSegment,
};
#[test]
fn single_param_match() {
@@ -168,7 +220,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
}
@@ -179,7 +231,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "/");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
}
@@ -190,7 +242,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
assert_eq!(params[1], ("b".into(), "bar".into()));
}
@@ -206,7 +258,94 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar/////");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert_eq!(params[0], ("rest".into(), "////".into()));
}
#[test]
fn optional_param_can_match() {
let path = "/foo";
let def = OptionalParamSegment("a");
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
}
#[test]
fn optional_param_can_not_match() {
let path = "/";
let def = OptionalParamSegment("a");
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "");
assert_eq!(matched.remaining(), "/");
let params = matched.params();
assert_eq!(params.first(), None);
}
#[test]
fn optional_params_match_first() {
let path = "/foo";
let def = (OptionalParamSegment("a"), OptionalParamSegment("b"));
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
}
#[test]
fn optional_params_can_match_both() {
let path = "/foo/bar";
let def = (OptionalParamSegment("a"), OptionalParamSegment("b"));
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
assert_eq!(params[1], ("b".into(), "bar".into()));
}
#[test]
fn matching_after_optional_param() {
let path = "/bar";
let def = (OptionalParamSegment("a"), StaticSegment("bar"));
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert!(params.is_empty());
}
#[test]
fn multiple_optional_params_match_first() {
let path = "/foo/bar";
let def = (
OptionalParamSegment("a"),
OptionalParamSegment("b"),
StaticSegment("bar"),
);
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
}
#[test]
fn multiple_optionals_can_match_both() {
let path = "/foo/qux/bar";
let def = (
OptionalParamSegment("a"),
OptionalParamSegment("b"),
StaticSegment("bar"),
);
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/qux/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert_eq!(params[0], ("a".into(), "foo".into()));
assert_eq!(params[1], ("b".into(), "qux".into()));
}
}

View File

@@ -1,15 +1,9 @@
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
use core::iter;
use std::{borrow::Cow, fmt::Debug};
use std::fmt::Debug;
impl PossibleRouteMatch for () {
type ParamsIter = iter::Empty<(Cow<'static, str>, String)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
Some(PartialPathMatch::new(path, iter::empty(), ""))
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
Some(PartialPathMatch::new(path, vec![], ""))
}
fn generate_path(&self, _path: &mut Vec<PathSegment>) {}
@@ -44,14 +38,14 @@ impl AsPath for &'static str {
///
/// // Params are empty as we had no `ParamSegement`s or `WildcardSegment`s
/// // If you did have additional dynamic segments, this would not be empty.
/// assert_eq!(matched.params().count(), 0);
/// assert_eq!(matched.params().len(), 0);
///
/// // Macro definition
/// let using_macro = path!("/users");
/// let matched = manual.test(path)?;
/// assert_eq!(matched.matched(), "/users");
///
/// assert_eq!(matched.params().count(), 0);
/// assert_eq!(matched.params().len(), 0);
///
/// # Some(())
/// # })().unwrap();
@@ -60,12 +54,7 @@ impl AsPath for &'static str {
pub struct StaticSegment<T: AsPath>(pub T);
impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
type ParamsIter = iter::Empty<(Cow<'static, str>, String)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut test = path.chars().peekable();
let mut this = self.0.as_path().chars();
@@ -113,8 +102,7 @@ impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
// the remaining is built from the path in, with the slice moved
// by the length of this match
let (matched, remaining) = path.split_at(matched_len);
has_matched
.then(|| PartialPathMatch::new(remaining, iter::empty(), matched))
has_matched.then(|| PartialPathMatch::new(remaining, vec![], matched))
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
@@ -151,7 +139,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
@@ -162,7 +150,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
@@ -187,7 +175,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "/");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
@@ -198,7 +186,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "/");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
@@ -209,7 +197,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
@@ -220,7 +208,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
@@ -252,7 +240,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
@@ -270,7 +258,7 @@ mod tests {
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
let params = matched.params();
assert!(params.is_empty());
}
}

View File

@@ -1,23 +1,4 @@
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
use core::iter::Chain;
macro_rules! chain_types {
($first:ty, $second:ty, ) => {
Chain<
$first,
<<$second as PossibleRouteMatch>::ParamsIter as IntoIterator>::IntoIter
>
};
($first:ty, $second:ty, $($rest:ty,)+) => {
chain_types!(
Chain<
$first,
<<$second as PossibleRouteMatch>::ParamsIter as IntoIterator>::IntoIter,
>,
$($rest,)+
)
}
}
macro_rules! tuples {
($first:ident => $($ty:ident),*) => {
@@ -27,34 +8,69 @@ macro_rules! tuples {
$first: PossibleRouteMatch,
$($ty: PossibleRouteMatch),*,
{
type ParamsIter = chain_types!(<<$first>::ParamsIter as IntoIterator>::IntoIter, $($ty,)*);
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
// on the first run, include all optionals
let mut include_optionals = {
[$first::OPTIONAL, $($ty::OPTIONAL),*].into_iter().filter(|n| *n).count()
};
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
let mut matched_len = 0;
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
let remaining = path;
let PartialPathMatch {
remaining,
matched,
params
} = $first.test(remaining)?;
matched_len += matched.len();
let params_iter = params.into_iter();
$(
let PartialPathMatch {
remaining,
matched,
params
} = $ty.test(remaining)?;
matched_len += matched.len();
let params_iter = params_iter.chain(params);
)*
Some(PartialPathMatch {
remaining,
matched: &path[0..matched_len],
params: params_iter
})
loop {
let mut nth_field = 0;
let mut matched_len = 0;
let mut r = path;
let mut p = Vec::new();
let mut m = String::new();
if !$first::OPTIONAL || nth_field < include_optionals {
match $first.test(r) {
None => {
return None;
},
Some(PartialPathMatch { remaining, matched, params }) => {
p.extend(params.into_iter());
m.push_str(matched);
r = remaining;
},
}
}
matched_len += m.len();
$(
if $ty::OPTIONAL {
nth_field += 1;
}
if !$ty::OPTIONAL || nth_field < include_optionals {
let PartialPathMatch {
remaining,
matched,
params
} = match $ty.test(r) {
None => if $ty::OPTIONAL {
return None;
} else {
if include_optionals == 0 {
return None;
}
include_optionals -= 1;
continue;
},
Some(v) => v,
};
r = remaining;
matched_len += matched.len();
p.extend(params);
}
)*
return Some(PartialPathMatch {
remaining: r,
matched: &path[0..matched_len],
params: p
});
}
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
@@ -74,12 +90,7 @@ where
Self: core::fmt::Debug,
A: PossibleRouteMatch,
{
type ParamsIter = A::ParamsIter;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let remaining = path;
let PartialPathMatch {
remaining,

View File

@@ -103,9 +103,7 @@ pub trait MatchInterface {
}
pub trait MatchParams {
type Params: IntoIterator<Item = (Cow<'static, str>, String)>;
fn to_params(&self) -> Self::Params;
fn to_params(&self) -> Vec<(Cow<'static, str>, String)>;
}
pub trait MatchNestedRoutes {
@@ -255,13 +253,13 @@ mod tests {
);
let matched = routes.match_route("/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
let params = matched.to_params();
assert!(params.is_empty());
let matched = routes.match_route("/blog").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
let params = matched.to_params();
assert!(params.is_empty());
let matched = routes.match_route("/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
let params = matched.to_params();
assert_eq!(params, vec![("id".into(), "42".into())]);
}
@@ -297,34 +295,34 @@ mod tests {
assert!(matched.is_none());
let matched = routes.match_route("/portfolio/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
let params = matched.to_params();
assert!(params.is_empty());
let matched = routes.match_route("/portfolio/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
let params = matched.to_params();
assert_eq!(params, vec![("id".into(), "42".into())]);
let matched = routes.match_route("/portfolio/contact").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
let params = matched.to_params();
assert_eq!(params, vec![("any".into(), "".into())]);
let matched = routes.match_route("/portfolio/contact/foobar").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
let params = matched.to_params();
assert_eq!(params, vec![("any".into(), "foobar".into())]);
}
}
#[derive(Debug)]
pub struct PartialPathMatch<'a, ParamsIter> {
pub struct PartialPathMatch<'a> {
pub(crate) remaining: &'a str,
pub(crate) params: ParamsIter,
pub(crate) params: Vec<(Cow<'static, str>, String)>,
pub(crate) matched: &'a str,
}
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
impl<'a> PartialPathMatch<'a> {
pub fn new(
remaining: &'a str,
params: ParamsIter,
params: Vec<(Cow<'static, str>, String)>,
matched: &'a str,
) -> Self {
Self {
@@ -342,7 +340,7 @@ impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
self.remaining
}
pub fn params(self) -> ParamsIter {
pub fn params(self) -> Vec<(Cow<'static, str>, String)> {
self.params
}

View File

@@ -96,21 +96,19 @@ impl<Segments, Data, View> NestedRoute<Segments, (), Data, View> {
}
#[derive(PartialEq, Eq)]
pub struct NestedMatch<ParamsIter, Child, View> {
pub struct NestedMatch<Child, View> {
id: RouteMatchId,
/// The portion of the full path matched only by this nested route.
matched: String,
/// The map of params matched only by this nested route.
params: ParamsIter,
params: Vec<(Cow<'static, str>, String)>,
/// The nested route.
child: Option<Child>,
view_fn: View,
}
impl<ParamsIter, Child, View> fmt::Debug
for NestedMatch<ParamsIter, Child, View>
impl<Child, View> fmt::Debug for NestedMatch<Child, View>
where
ParamsIter: fmt::Debug,
Child: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -122,21 +120,14 @@ where
}
}
impl<ParamsIter, Child, View> MatchParams
for NestedMatch<ParamsIter, Child, View>
where
ParamsIter: IntoIterator<Item = (Cow<'static, str>, String)> + Clone,
{
type Params = ParamsIter;
impl<Child, View> MatchParams for NestedMatch<Child, View> {
#[inline(always)]
fn to_params(&self) -> Self::Params {
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
self.params.clone()
}
}
impl<ParamsIter, Child, View> MatchInterface
for NestedMatch<ParamsIter, Child, View>
impl<Child, View> MatchInterface for NestedMatch<Child, View>
where
Child: MatchInterface + MatchParams + 'static,
View: ChooseView,
@@ -161,21 +152,13 @@ impl<Segments, Children, Data, View> MatchNestedRoutes
where
Self: 'static,
Segments: PossibleRouteMatch + std::fmt::Debug,
<<Segments as PossibleRouteMatch>::ParamsIter as IntoIterator>::IntoIter: Clone,
Children: MatchNestedRoutes,
<<<Children as MatchNestedRoutes>::Match as MatchParams>::Params as IntoIterator>::IntoIter: Clone,
Children::Match: MatchParams,
Children: 'static,
<Children::Match as MatchParams>::Params: Clone,
Children::Match: MatchParams,
Children: 'static,
View: ChooseView + Clone,
{
type Data = Data;
type Match = NestedMatch<iter::Chain<
<Segments::ParamsIter as IntoIterator>::IntoIter,
Either<iter::Empty::<
(Cow<'static, str>, String)
>, <<Children::Match as MatchParams>::Params as IntoIterator>::IntoIter>
>, Children::Match, View>;
type Match = NestedMatch<Children::Match, View>;
fn match_nested<'a>(
&'a self,
@@ -186,33 +169,34 @@ where
.and_then(
|PartialPathMatch {
remaining,
params,
mut params,
matched,
}| {
let (_, inner, remaining) = match &self.children {
None => (None, None, remaining),
Some(children) => {
let (inner, remaining) = children.match_nested(remaining);
let (inner, remaining) =
children.match_nested(remaining);
let (id, inner) = inner?;
(Some(id), Some(inner), remaining)
(Some(id), Some(inner), remaining)
}
};
let params = params.into_iter();
let inner_params = match &inner {
None => Either::Left(iter::empty()),
Some(inner) => Either::Right(inner.to_params().into_iter())
};
let inner_params = inner
.as_ref()
.map(|inner| inner.to_params())
.unwrap_or_default();
let id = RouteMatchId(self.id);
if remaining.is_empty() || remaining == "/" {
params.extend(inner_params);
Some((
Some((
id,
NestedMatch {
id,
matched: matched.to_string(),
params: params.chain(inner_params),
params,
child: inner,
view_fn: self.view.clone(),
},
@@ -238,9 +222,9 @@ where
let regenerate = match &ssr_mode {
SsrMode::Static(data) => match data.regenerate.as_ref() {
None => vec![],
Some(regenerate) => vec![regenerate.clone()]
}
_ => vec![]
Some(regenerate) => vec![regenerate.clone()],
},
_ => vec![],
};
match children {
@@ -248,32 +232,41 @@ where
segments: segment_routes,
ssr_mode,
methods,
regenerate
regenerate,
})),
Some(children) => {
Either::Right(children.generate_routes().into_iter().map(move |child| {
// extend this route's segments with child segments
let segments = segment_routes.clone().into_iter().chain(child.segments).collect();
Either::Right(children.generate_routes().into_iter().map(
move |child| {
// extend this route's segments with child segments
let segments = segment_routes
.clone()
.into_iter()
.chain(child.segments)
.collect();
let mut methods = methods.clone();
methods.extend(child.methods);
let mut methods = methods.clone();
methods.extend(child.methods);
let mut regenerate = regenerate.clone();
regenerate.extend(child.regenerate);
let mut regenerate = regenerate.clone();
regenerate.extend(child.regenerate);
if child.ssr_mode > ssr_mode {
GeneratedRouteData {
segments,
ssr_mode: child.ssr_mode,
methods, regenerate
if child.ssr_mode > ssr_mode {
GeneratedRouteData {
segments,
ssr_mode: child.ssr_mode,
methods,
regenerate,
}
} else {
GeneratedRouteData {
segments,
ssr_mode: ssr_mode.clone(),
methods,
regenerate,
}
}
} else {
GeneratedRouteData {
segments,
ssr_mode: ssr_mode.clone(), methods, regenerate
}
}
}))
},
))
}
}
}

View File

@@ -5,10 +5,8 @@ use either_of::*;
use std::borrow::Cow;
impl MatchParams for () {
type Params = iter::Empty<(Cow<'static, str>, String)>;
fn to_params(&self) -> Self::Params {
iter::empty()
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
Vec::new()
}
}
@@ -53,9 +51,7 @@ impl<A> MatchParams for (A,)
where
A: MatchParams,
{
type Params = A::Params;
fn to_params(&self) -> Self::Params {
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
self.0.to_params()
}
}
@@ -105,15 +101,10 @@ where
A: MatchParams,
B: MatchParams,
{
type Params = Either<
<A::Params as IntoIterator>::IntoIter,
<B::Params as IntoIterator>::IntoIter,
>;
fn to_params(&self) -> Self::Params {
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
match self {
Either::Left(i) => Either::Left(i.to_params().into_iter()),
Either::Right(i) => Either::Right(i.to_params().into_iter()),
Either::Left(i) => i.to_params(),
Either::Right(i) => i.to_params(),
}
}
}
@@ -208,13 +199,9 @@ macro_rules! tuples {
where
$($ty: MatchParams),*,
{
type Params = $either<$(
<$ty::Params as IntoIterator>::IntoIter,
)*>;
fn to_params(&self) -> Self::Params {
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
match self {
$($either::$ty(i) => $either::$ty(i.to_params().into_iter()),)*
$($either::$ty(i) => i.to_params(),)*
}
}
}

View File

@@ -5,6 +5,7 @@ pub enum PathSegment {
Unit,
Static(Cow<'static, str>),
Param(Cow<'static, str>),
OptionalParam(Cow<'static, str>),
Splat(Cow<'static, str>),
}
@@ -14,7 +15,98 @@ impl PathSegment {
PathSegment::Unit => "",
PathSegment::Static(i) => i,
PathSegment::Param(i) => i,
PathSegment::OptionalParam(i) => i,
PathSegment::Splat(i) => i,
}
}
}
pub trait ExpandOptionals {
fn expand_optionals(&self) -> Vec<Vec<PathSegment>>;
}
impl ExpandOptionals for Vec<PathSegment> {
fn expand_optionals(&self) -> Vec<Vec<PathSegment>> {
let mut segments = vec![self.to_vec()];
let mut checked = Vec::new();
while let Some(next_to_check) = segments.pop() {
let mut had_optional = false;
for (idx, segment) in next_to_check.iter().enumerate() {
if let PathSegment::OptionalParam(name) = segment {
had_optional = true;
let mut unit_variant = next_to_check.to_vec();
unit_variant.remove(idx);
let mut param_variant = next_to_check.to_vec();
param_variant[idx] = PathSegment::Param(name.clone());
segments.push(unit_variant);
segments.push(param_variant);
break;
}
}
if !had_optional {
checked.push(next_to_check.to_vec());
}
}
checked
}
}
#[cfg(test)]
mod tests {
use crate::{ExpandOptionals, PathSegment};
#[test]
fn expand_optionals_on_plain() {
let plain = vec![
PathSegment::Static("a".into()),
PathSegment::Param("b".into()),
];
assert_eq!(plain.expand_optionals(), vec![plain]);
}
#[test]
fn expand_optionals_once() {
let plain = vec![
PathSegment::OptionalParam("a".into()),
PathSegment::Static("b".into()),
];
assert_eq!(
plain.expand_optionals(),
vec![
vec![
PathSegment::Param("a".into()),
PathSegment::Static("b".into())
],
vec![PathSegment::Static("b".into())]
]
);
}
#[test]
fn expand_optionals_twice() {
let plain = vec![
PathSegment::OptionalParam("a".into()),
PathSegment::OptionalParam("b".into()),
PathSegment::Static("c".into()),
];
assert_eq!(
plain.expand_optionals(),
vec![
vec![
PathSegment::Param("a".into()),
PathSegment::Param("b".into()),
PathSegment::Static("c".into()),
],
vec![
PathSegment::Param("a".into()),
PathSegment::Static("c".into()),
],
vec![
PathSegment::Param("b".into()),
PathSegment::Static("c".into()),
],
vec![PathSegment::Static("c".into())]
]
);
}
}

View File

@@ -1,10 +1,5 @@
use super::PartialPathMatch;
pub trait ChooseRoute {
fn choose_route<'a>(
&self,
path: &'a str,
) -> Option<
PartialPathMatch<'a, impl IntoIterator<Item = (&'a str, &'a str)>>,
>;
fn choose_route<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
}

View File

@@ -247,6 +247,7 @@ impl StaticPath {
}
paths = new_paths;
}
OptionalParam(_) => todo!(),
}
}
paths

View File

@@ -18,4 +18,4 @@ proc-macro2 = "1.0"
quote = "1.0"
[dev-dependencies]
leptos_router = { version = "0.7.0-beta" }
leptos_router = { path = "../router" }

View File

@@ -14,12 +14,16 @@ const RFC3986_PCHAR_OTHER: [char; 1] = ['@'];
/// # Examples
///
/// ```rust
/// use leptos_router::{path, ParamSegment, StaticSegment, WildcardSegment};
/// use leptos_router::{
/// path, OptionalParamSegment, ParamSegment, StaticSegment,
/// WildcardSegment,
/// };
///
/// let path = path!("/foo/:bar/*any");
/// let path = path!("/foo/:bar/:baz?/*any");
/// let output = (
/// StaticSegment("foo"),
/// ParamSegment("bar"),
/// OptionalParamSegment("baz"),
/// WildcardSegment("any"),
/// );
///
@@ -41,6 +45,7 @@ struct Segments(pub Vec<Segment>);
enum Segment {
Static(String),
Param(String),
OptionalParam(String),
Wildcard(String),
}
@@ -93,7 +98,11 @@ impl SegmentParser {
for segment in current_str.split('/') {
if let Some(segment) = segment.strip_prefix(':') {
segments.push(Segment::Param(segment.to_string()));
if let Some(segment) = segment.strip_suffix('?') {
segments.push(Segment::OptionalParam(segment.to_string()));
} else {
segments.push(Segment::Param(segment.to_string()));
}
} else if let Some(segment) = segment.strip_prefix('*') {
segments.push(Segment::Wildcard(segment.to_string()));
} else {
@@ -156,6 +165,10 @@ impl ToTokens for Segment {
Segment::Param(p) => {
tokens.extend(quote! { leptos_router::ParamSegment(#p) });
}
Segment::OptionalParam(p) => {
tokens
.extend(quote! { leptos_router::OptionalParamSegment(#p) });
}
}
}
}

View File

@@ -1,4 +1,6 @@
use leptos_router::{ParamSegment, StaticSegment, WildcardSegment};
use leptos_router::{
OptionalParamSegment, ParamSegment, StaticSegment, WildcardSegment,
};
use leptos_router_macro::path;
#[test]
@@ -86,6 +88,12 @@ fn parses_single_param() {
assert_eq!(output, (ParamSegment("id"),));
}
#[test]
fn parses_optional_param() {
let output = path!("/:id?");
assert_eq!(output, (OptionalParamSegment("id"),));
}
#[test]
fn parses_static_and_param() {
let output = path!("/home/:id");
@@ -144,9 +152,22 @@ fn parses_consecutive_param() {
);
}
#[test]
fn parses_consecutive_optional_param() {
let output = path!("/:foo?/:bar?/:baz?");
assert_eq!(
output,
(
OptionalParamSegment("foo"),
OptionalParamSegment("bar"),
OptionalParamSegment("baz")
)
);
}
#[test]
fn parses_complex() {
let output = path!("/home/:id/foo/:bar/*any");
let output = path!("/home/:id/foo/:bar/:baz?/*any");
assert_eq!(
output,
(
@@ -154,6 +175,7 @@ fn parses_complex() {
ParamSegment("id"),
StaticSegment("foo"),
ParamSegment("bar"),
OptionalParamSegment("baz"),
WildcardSegment("any"),
)
);