diff --git a/examples/hackernews/src/lib.rs b/examples/hackernews/src/lib.rs
index 4d391ea825..17d3c24402 100644
--- a/examples/hackernews/src/lib.rs
+++ b/examples/hackernews/src/lib.rs
@@ -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 {
-
- // TODO allow optional params without duplication
-
+
diff --git a/examples/hackernews_axum/src/lib.rs b/examples/hackernews_axum/src/lib.rs
index 6e5b11b996..04c2efbb8c 100644
--- a/examples/hackernews_axum/src/lib.rs
+++ b/examples/hackernews_axum/src/lib.rs
@@ -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 {
-
- // TODO allow optional params without duplication
-
+
diff --git a/examples/hackernews_islands_axum/src/lib.rs b/examples/hackernews_islands_axum/src/lib.rs
index 260e4849a4..4558a78d67 100644
--- a/examples/hackernews_islands_axum/src/lib.rs
+++ b/examples/hackernews_islands_axum/src/lib.rs
@@ -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 {
-
- // TODO allow optional params without duplication
-
+
diff --git a/examples/hackernews_js_fetch/src/lib.rs b/examples/hackernews_js_fetch/src/lib.rs
index c365687e7e..f12c6f31a6 100644
--- a/examples/hackernews_js_fetch/src/lib.rs
+++ b/examples/hackernews_js_fetch/src/lib.rs
@@ -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 {
-
- // TODO allow optional params without duplication
-
+
diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs
index c2d06cc268..4eae047e4c 100644
--- a/integrations/actix/src/lib.rs
+++ b/integrations/actix/src/lib.rs
@@ -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 {
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,
}
-impl From 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;
+}
+
+impl IntoRouteListing for RouteListing {
+ fn into_route_listing(self) -> Vec {
+ 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::>();
(
diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs
index 74cf7c82a4..f0450bcfb0 100644
--- a/integrations/axum/src/lib.rs
+++ b/integrations/axum/src/lib.rs
@@ -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,
}
-impl From 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;
+}
+
+impl IntoRouteListing for RouteListing {
+ fn into_route_listing(self) -> Vec {
+ 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::>();
(
@@ -1700,7 +1711,7 @@ trait AxumPath {
fn to_axum_path(&self) -> String;
}
-impl AxumPath for &[PathSegment] {
+impl AxumPath for Vec {
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
diff --git a/router/src/flat_router.rs b/router/src/flat_router.rs
index 2308886fc6..f80a4bd6a4 100644
--- a/router/src/flat_router.rs
+++ b/router/src/flat_router.rs
@@ -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;
diff --git a/router/src/matching/horizontal/mod.rs b/router/src/matching/horizontal/mod.rs
index 2c9684dd5a..2b60cc4b1e 100644
--- a/router/src/matching/horizontal/mod.rs
+++ b/router/src/matching/horizontal/mod.rs
@@ -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- , String)>;
+ const OPTIONAL: bool = false;
- fn test<'a>(
- &self,
- path: &'a str,
- ) -> Option>;
+ fn test<'a>(&self, path: &'a str) -> Option>;
fn generate_path(&self, path: &mut Vec);
}
diff --git a/router/src/matching/horizontal/param_segments.rs b/router/src/matching/horizontal/param_segments.rs
index c00947f168..35ead7c048 100644
--- a/router/src/matching/horizontal/param_segments.rs
+++ b/router/src/matching/horizontal/param_segments.rs
@@ -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> {
+ fn test<'a>(&self, path: &'a str) -> Option> {
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> {
+ fn test<'a>(&self, path: &'a str) -> Option> {
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) {
@@ -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> {
+ 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) {
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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()));
+ }
}
diff --git a/router/src/matching/horizontal/static_segment.rs b/router/src/matching/horizontal/static_segment.rs
index c9c22e8bae..5179efecfd 100644
--- a/router/src/matching/horizontal/static_segment.rs
+++ b/router/src/matching/horizontal/static_segment.rs
@@ -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> {
- Some(PartialPathMatch::new(path, iter::empty(), ""))
+ fn test<'a>(&self, path: &'a str) -> Option> {
+ Some(PartialPathMatch::new(path, vec![], ""))
}
fn generate_path(&self, _path: &mut Vec) {}
@@ -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(pub T);
impl PossibleRouteMatch for StaticSegment {
- type ParamsIter = iter::Empty<(Cow<'static, str>, String)>;
-
- fn test<'a>(
- &self,
- path: &'a str,
- ) -> Option> {
+ fn test<'a>(&self, path: &'a str) -> Option> {
let mut matched_len = 0;
let mut test = path.chars().peekable();
let mut this = self.0.as_path().chars();
@@ -113,8 +102,7 @@ impl PossibleRouteMatch for StaticSegment {
// 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) {
@@ -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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ let params = matched.params();
assert!(params.is_empty());
}
}
diff --git a/router/src/matching/horizontal/tuples.rs b/router/src/matching/horizontal/tuples.rs
index 1af93db1f0..a4879fb1f0 100644
--- a/router/src/matching/horizontal/tuples.rs
+++ b/router/src/matching/horizontal/tuples.rs
@@ -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> {
+ // 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> {
- 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) {
@@ -74,12 +90,7 @@ where
Self: core::fmt::Debug,
A: PossibleRouteMatch,
{
- type ParamsIter = A::ParamsIter;
-
- fn test<'a>(
- &self,
- path: &'a str,
- ) -> Option> {
+ fn test<'a>(&self, path: &'a str) -> Option> {
let remaining = path;
let PartialPathMatch {
remaining,
diff --git a/router/src/matching/mod.rs b/router/src/matching/mod.rs
index e37764c7aa..7e61d335a1 100644
--- a/router/src/matching/mod.rs
+++ b/router/src/matching/mod.rs
@@ -103,9 +103,7 @@ pub trait MatchInterface {
}
pub trait MatchParams {
- type Params: IntoIterator
- , 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::>();
+ let params = matched.to_params();
assert!(params.is_empty());
let matched = routes.match_route("/blog").unwrap();
- let params = matched.to_params().collect::>();
+ let params = matched.to_params();
assert!(params.is_empty());
let matched = routes.match_route("/blog/post/42").unwrap();
- let params = matched.to_params().collect::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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
}
diff --git a/router/src/matching/nested/mod.rs b/router/src/matching/nested/mod.rs
index 83028d5da7..4277d6142d 100644
--- a/router/src/matching/nested/mod.rs
+++ b/router/src/matching/nested/mod.rs
@@ -96,21 +96,19 @@ impl NestedRoute {
}
#[derive(PartialEq, Eq)]
-pub struct NestedMatch {
+pub struct NestedMatch {
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,
view_fn: View,
}
-impl fmt::Debug
- for NestedMatch
+impl fmt::Debug for NestedMatch
where
- ParamsIter: fmt::Debug,
Child: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -122,21 +120,14 @@ where
}
}
-impl MatchParams
- for NestedMatch
-where
- ParamsIter: IntoIterator
- , String)> + Clone,
-{
- type Params = ParamsIter;
-
+impl MatchParams for NestedMatch {
#[inline(always)]
- fn to_params(&self) -> Self::Params {
+ fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
self.params.clone()
}
}
-impl MatchInterface
- for NestedMatch
+impl MatchInterface for NestedMatch
where
Child: MatchInterface + MatchParams + 'static,
View: ChooseView,
@@ -161,21 +152,13 @@ impl MatchNestedRoutes
where
Self: 'static,
Segments: PossibleRouteMatch + std::fmt::Debug,
- <::ParamsIter as IntoIterator>::IntoIter: Clone,
Children: MatchNestedRoutes,
- <<::Match as MatchParams>::Params as IntoIterator>::IntoIter: Clone,
- Children::Match: MatchParams,
- Children: 'static,
- ::Params: Clone,
+ Children::Match: MatchParams,
+ Children: 'static,
View: ChooseView + Clone,
{
type Data = Data;
- type Match = NestedMatch::IntoIter,
- Either, String)
- >, <::Params as IntoIterator>::IntoIter>
- >, Children::Match, View>;
+ type Match = NestedMatch;
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
- }
- } else {
- GeneratedRouteData {
- segments,
- ssr_mode: ssr_mode.clone(), 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,
+ }
}
- }
- }))
+ },
+ ))
}
}
}
diff --git a/router/src/matching/nested/tuples.rs b/router/src/matching/nested/tuples.rs
index ea6378efaf..cdce0a47ce 100644
--- a/router/src/matching/nested/tuples.rs
+++ b/router/src/matching/nested/tuples.rs
@@ -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 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<
- ::IntoIter,
- ::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(),)*
}
}
}
diff --git a/router/src/matching/path_segment.rs b/router/src/matching/path_segment.rs
index 20f4ca7093..02c695a30b 100644
--- a/router/src/matching/path_segment.rs
+++ b/router/src/matching/path_segment.rs
@@ -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>;
+}
+
+impl ExpandOptionals for Vec {
+ fn expand_optionals(&self) -> Vec> {
+ 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())]
+ ]
+ );
+ }
+}
diff --git a/router/src/matching/vertical/mod.rs b/router/src/matching/vertical/mod.rs
index 2ea88b0ecd..4f4da11dd1 100644
--- a/router/src/matching/vertical/mod.rs
+++ b/router/src/matching/vertical/mod.rs
@@ -1,10 +1,5 @@
use super::PartialPathMatch;
pub trait ChooseRoute {
- fn choose_route<'a>(
- &self,
- path: &'a str,
- ) -> Option<
- PartialPathMatch<'a, impl IntoIterator
- >,
- >;
+ fn choose_route<'a>(&self, path: &'a str) -> Option>;
}
diff --git a/router/src/static_routes.rs b/router/src/static_routes.rs
index 8d363180f0..8329a4f3d3 100644
--- a/router/src/static_routes.rs
+++ b/router/src/static_routes.rs
@@ -247,6 +247,7 @@ impl StaticPath {
}
paths = new_paths;
}
+ OptionalParam(_) => todo!(),
}
}
paths
diff --git a/router_macro/Cargo.toml b/router_macro/Cargo.toml
index 118e225e51..f36eeed9ac 100644
--- a/router_macro/Cargo.toml
+++ b/router_macro/Cargo.toml
@@ -18,4 +18,4 @@ proc-macro2 = "1.0"
quote = "1.0"
[dev-dependencies]
-leptos_router = { version = "0.7.0-beta" }
+leptos_router = { path = "../router" }
diff --git a/router_macro/src/lib.rs b/router_macro/src/lib.rs
index 03451ffad4..3863f3b430 100644
--- a/router_macro/src/lib.rs
+++ b/router_macro/src/lib.rs
@@ -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);
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) });
+ }
}
}
}
diff --git a/router_macro/tests/path.rs b/router_macro/tests/path.rs
index 2110629f8b..c88b95a534 100644
--- a/router_macro/tests/path.rs
+++ b/router_macro/tests/path.rs
@@ -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"),
)
);