mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
reorganize
This commit is contained in:
parent
dbd9951a85
commit
c3b9932172
15 changed files with 534 additions and 409 deletions
|
@ -8,5 +8,6 @@ mod static_route;
|
|||
pub use generate_route_list::*;
|
||||
pub use method::*;
|
||||
pub use router::*;
|
||||
pub use routing_utils::*;
|
||||
pub use ssr_mode::*;
|
||||
pub use static_route::*;
|
||||
|
|
|
@ -3,21 +3,25 @@ use core::marker::PhantomData;
|
|||
use either_of::Either;
|
||||
use routing_utils::{
|
||||
location::Location,
|
||||
matching::{MatchNestedRoutes, PossibleRouteMatch, Routes},
|
||||
matching::{MatchInterface, MatchNestedRoutes, PossibleRouteMatch, Routes},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{either::EitherState, Position, PositionState, Render, RenderHtml},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, either::EitherState, Position, PositionState,
|
||||
Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Router<Rndr, Loc, Children, FallbackFn> {
|
||||
base: Option<Cow<'static, str>>,
|
||||
location: Loc,
|
||||
routes: Routes<Children>,
|
||||
pub routes: Routes<Children>,
|
||||
fallback: FallbackFn,
|
||||
rndr: PhantomData<Rndr>,
|
||||
}
|
||||
|
@ -74,21 +78,52 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<Rndr, Loc, FallbackFn, Fallback, Children> Render<Rndr>
|
||||
trait ChooseView {
|
||||
type Output;
|
||||
|
||||
fn choose(self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<F, View> ChooseView for F
|
||||
where
|
||||
F: Fn() -> View,
|
||||
{
|
||||
type Output = View;
|
||||
|
||||
fn choose(self) -> Self::Output {
|
||||
self()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, FnA, B, FnB> ChooseView for Either<FnA, FnB>
|
||||
where
|
||||
FnA: Fn() -> A,
|
||||
FnB: Fn() -> B,
|
||||
{
|
||||
type Output = Either<A, B>;
|
||||
|
||||
fn choose(self) -> Self::Output {
|
||||
match self {
|
||||
Either::Left(f) => Either::Left(f()),
|
||||
Either::Right(f) => Either::Right(f()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> Render<Rndr>
|
||||
for Router<Rndr, Loc, Children, FallbackFn>
|
||||
where
|
||||
Loc: Location,
|
||||
FallbackFn: Fn() -> Fallback,
|
||||
Fallback: Render<Rndr>,
|
||||
for<'a> Children: MatchNestedRoutes<'a>,
|
||||
for<'a> <Children as MatchNestedRoutes<'a>>::Match: core::fmt::Debug,
|
||||
for<'a> <<Children as MatchNestedRoutes<'a>>::Match as MatchInterface<'a>>::View:
|
||||
ChooseView<Output = View>,
|
||||
View: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
{
|
||||
type State = EitherState<
|
||||
<String as Render<Rndr>>::State,
|
||||
<Fallback as Render<Rndr>>::State,
|
||||
Rndr,
|
||||
>;
|
||||
type State =
|
||||
EitherState<View::State, <Fallback as Render<Rndr>>::State, Rndr>;
|
||||
type FallibleState = ();
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
|
@ -98,14 +133,31 @@ where
|
|||
.as_ref()
|
||||
.map(|url| self.routes.match_route(url.path()))
|
||||
{
|
||||
Ok(Some(matched)) => Either::Left(format!("{matched:#?}")),
|
||||
Ok(Some(matched)) => {
|
||||
let view = matched.to_view();
|
||||
let view = view.choose();
|
||||
Either::Left(view)
|
||||
}
|
||||
_ => Either::Right((self.fallback)()),
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
todo!()
|
||||
let new = match self
|
||||
.location
|
||||
.current()
|
||||
.as_ref()
|
||||
.map(|url| self.routes.match_route(url.path()))
|
||||
{
|
||||
Ok(Some(matched)) => {
|
||||
let view = matched.to_view();
|
||||
let view = view.choose();
|
||||
Either::Left(view)
|
||||
}
|
||||
_ => Either::Right((self.fallback)()),
|
||||
};
|
||||
new.rebuild(state);
|
||||
}
|
||||
|
||||
fn try_build(self) -> tachys::error::Result<Self::FallibleState> {
|
||||
|
@ -119,6 +171,69 @@ where
|
|||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> RenderHtml<Rndr>
|
||||
for Router<Rndr, Loc, Children, FallbackFn>
|
||||
where
|
||||
Loc: Location,
|
||||
FallbackFn: Fn() -> Fallback,
|
||||
Fallback: RenderHtml<Rndr>,
|
||||
for<'a> Children: MatchNestedRoutes<'a>,
|
||||
for<'a> <<Children as MatchNestedRoutes<'a>>::Match as MatchInterface<'a>>::View:
|
||||
ChooseView<Output = View>,
|
||||
View: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
{
|
||||
// TODO probably pick a max length here
|
||||
const MIN_LENGTH: usize = Fallback::MIN_LENGTH;
|
||||
|
||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> AddAnyAttr<Rndr>
|
||||
for Router<Rndr, Loc, Children, FallbackFn>
|
||||
where
|
||||
Loc: Location,
|
||||
FallbackFn: Fn() -> Fallback,
|
||||
Fallback: Render<Rndr>,
|
||||
for<'a> Children: MatchNestedRoutes<'a>,
|
||||
for<'a> <<Children as MatchNestedRoutes<'a>>::Match as MatchInterface<'a>>::View:
|
||||
ChooseView<Output = View>,
|
||||
Rndr: Renderer,
|
||||
Router<Rndr, Loc, Children, FallbackFn>: RenderHtml<Rndr>,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = Self;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
fn add_any_attr_by_ref<NewAttr: Attribute<Rndr>>(
|
||||
self,
|
||||
attr: &NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
/*
|
||||
impl<Rndr, Loc, Fal, Children> RenderHtml<Rndr>
|
||||
for Router<Rndr, Loc, Children, Fal>
|
||||
|
|
|
@ -85,7 +85,8 @@ impl PossibleRouteMatch for WildcardSegment {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PossibleRouteMatch;
|
||||
use crate::matching::{ParamSegment, StaticSegment, WildcardSegment};
|
||||
use crate::{ParamSegment, StaticSegment, WildcardSegment};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[test]
|
||||
fn single_param_match() {
|
|
@ -78,6 +78,7 @@ impl PossibleRouteMatch for StaticSegment {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PossibleRouteMatch, StaticSegment};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[test]
|
||||
fn single_static_match() {
|
|
@ -1,4 +1,5 @@
|
|||
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
|
||||
use alloc::vec::Vec;
|
||||
use core::iter::Chain;
|
||||
|
||||
macro_rules! chain_types {
|
|
@ -1,10 +1,388 @@
|
|||
//#![no_std]
|
||||
#![no_std]
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
pub mod location;
|
||||
pub mod matching;
|
||||
pub mod params;
|
||||
mod path_segment;
|
||||
use alloc::vec::Vec;
|
||||
pub use path_segment::*;
|
||||
mod horizontal;
|
||||
mod nested;
|
||||
mod vertical;
|
||||
use crate::PathSegment;
|
||||
use alloc::borrow::Cow;
|
||||
pub use horizontal::*;
|
||||
pub use nested::*;
|
||||
pub use vertical::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Routes<Children> {
|
||||
base: Option<Cow<'static, str>>,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl<Children> Routes<Children> {
|
||||
pub fn new(children: Children) -> Self {
|
||||
Self {
|
||||
base: None,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_base(
|
||||
children: Children,
|
||||
base: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base: Some(base.into()),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Children> Routes<Children>
|
||||
where
|
||||
Children: MatchNestedRoutes<'a>,
|
||||
{
|
||||
pub fn match_route(&'a self, path: &'a str) -> Option<Children::Match> {
|
||||
let path = match &self.base {
|
||||
None => path,
|
||||
Some(base) => {
|
||||
let (base, path) = if base.starts_with('/') {
|
||||
(base.trim_start_matches('/'), path.trim_start_matches('/'))
|
||||
} else {
|
||||
(base.as_ref(), path)
|
||||
};
|
||||
match path.strip_prefix(base) {
|
||||
Some(path) => path,
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (matched, remaining) = self.children.match_nested(path);
|
||||
let matched = matched?;
|
||||
|
||||
if !remaining.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(matched)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_routes(
|
||||
&'a self,
|
||||
) -> (
|
||||
Option<&str>,
|
||||
impl IntoIterator<Item = Vec<PathSegment>> + 'a,
|
||||
) {
|
||||
(self.base.as_deref(), self.children.generate_routes())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MatchInterface<'a> {
|
||||
type Params: IntoIterator<Item = (&'a str, &'a str)>;
|
||||
type Child;
|
||||
type View;
|
||||
|
||||
fn to_params(&self) -> Self::Params;
|
||||
|
||||
fn to_child(&'a self) -> Self::Child;
|
||||
|
||||
fn to_view(&self) -> Self::View;
|
||||
}
|
||||
|
||||
pub trait MatchNestedRoutes<'a> {
|
||||
type Data;
|
||||
type Match: MatchInterface<'a>;
|
||||
|
||||
fn match_nested(&'a self, path: &'a str) -> (Option<Self::Match>, &'a str);
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{NestedRoute, ParamSegment, Routes};
|
||||
use crate::{MatchInterface, PathSegment, StaticSegment, WildcardSegment};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[test]
|
||||
pub fn matches_single_root_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
});
|
||||
let matched = routes.match_route("/");
|
||||
assert!(matched.is_some());
|
||||
let matched = routes.match_route("");
|
||||
assert!(matched.is_some());
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(paths, vec![vec![PathSegment::Static("/".into())]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn matches_nested_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: NestedRoute {
|
||||
segments: (StaticSegment("author"), StaticSegment("contact")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: "Contact Me",
|
||||
},
|
||||
data: (),
|
||||
view: "Home",
|
||||
});
|
||||
|
||||
// route generation
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![vec![
|
||||
PathSegment::Static("".into()),
|
||||
PathSegment::Static("author".into()),
|
||||
PathSegment::Static("contact".into())
|
||||
]]
|
||||
);
|
||||
|
||||
let matched = routes.match_route("/author/contact").unwrap();
|
||||
assert_eq!(matched.matched(), "");
|
||||
assert_eq!(matched.to_child().matched(), "/author/contact");
|
||||
|
||||
let view = matched.to_view();
|
||||
assert_eq!(*view, "Home");
|
||||
assert_eq!(*matched.to_child().to_view(), "Contact Me");
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn does_not_match_incomplete_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: NestedRoute {
|
||||
segments: (StaticSegment("author"), StaticSegment("contact")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: "Contact Me",
|
||||
},
|
||||
data: (),
|
||||
view: "Home",
|
||||
});
|
||||
let matched = routes.match_route("/");
|
||||
assert!(matched.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn chooses_between_nested_routes() {
|
||||
let routes = Routes::new((
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("about"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/blog"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: (StaticSegment("post"), ParamSegment("id")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
));
|
||||
|
||||
// generates routes correctly
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
vec![
|
||||
PathSegment::Static("/".into()),
|
||||
PathSegment::Static("".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/".into()),
|
||||
PathSegment::Static("about".into())
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/blog".into()),
|
||||
PathSegment::Static("".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/blog".into()),
|
||||
PathSegment::Static("post".into()),
|
||||
PathSegment::Param("id".into())
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
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 arbitrary_nested_routes() {
|
||||
let routes = Routes::new_with_base(
|
||||
(
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("about"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
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: || (),
|
||||
},
|
||||
),
|
||||
"/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)]
|
||||
pub struct PartialPathMatch<'a, ParamsIter> {
|
||||
pub(crate) remaining: &'a str,
|
||||
pub(crate) params: ParamsIter,
|
||||
pub(crate) matched: &'a str,
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
|
||||
pub fn new(
|
||||
remaining: &'a str,
|
||||
params: ParamsIter,
|
||||
matched: &'a str,
|
||||
) -> Self {
|
||||
Self {
|
||||
remaining,
|
||||
params,
|
||||
matched,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.remaining.is_empty() || self.remaining == "/"
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> &str {
|
||||
self.remaining
|
||||
}
|
||||
|
||||
pub fn params(self) -> ParamsIter {
|
||||
self.params
|
||||
}
|
||||
|
||||
pub fn matched(&self) -> &str {
|
||||
self.matched
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,382 +0,0 @@
|
|||
mod horizontal;
|
||||
mod nested;
|
||||
mod vertical;
|
||||
use crate::PathSegment;
|
||||
use alloc::borrow::Cow;
|
||||
pub use horizontal::*;
|
||||
pub use nested::*;
|
||||
pub use vertical::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Routes<Children> {
|
||||
base: Option<Cow<'static, str>>,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl<Children> Routes<Children> {
|
||||
pub fn new(children: Children) -> Self {
|
||||
Self {
|
||||
base: None,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_base(
|
||||
children: Children,
|
||||
base: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base: Some(base.into()),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Children> Routes<Children>
|
||||
where
|
||||
Children: MatchNestedRoutes<'a>,
|
||||
{
|
||||
pub fn match_route(&'a self, path: &'a str) -> Option<Children::Match> {
|
||||
let path = match &self.base {
|
||||
None => path,
|
||||
Some(base) => {
|
||||
let (base, path) = if base.starts_with('/') {
|
||||
(base.trim_start_matches('/'), path.trim_start_matches('/'))
|
||||
} else {
|
||||
(base.as_ref(), path)
|
||||
};
|
||||
match path.strip_prefix(base) {
|
||||
Some(path) => path,
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (matched, remaining) = self.children.match_nested(path);
|
||||
let matched = matched?;
|
||||
|
||||
if !remaining.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(matched)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_routes(
|
||||
&'a self,
|
||||
) -> (
|
||||
Option<&str>,
|
||||
impl IntoIterator<Item = Vec<PathSegment>> + 'a,
|
||||
) {
|
||||
(self.base.as_deref(), self.children.generate_routes())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MatchInterface<'a> {
|
||||
type Params: IntoIterator<Item = (&'a str, &'a str)>;
|
||||
type Child;
|
||||
type View;
|
||||
|
||||
fn to_params(&self) -> Self::Params;
|
||||
|
||||
fn to_child(&'a self) -> Self::Child;
|
||||
|
||||
fn to_view(&self) -> Self::View;
|
||||
}
|
||||
|
||||
pub trait MatchNestedRoutes<'a> {
|
||||
type Data;
|
||||
type Match: MatchInterface<'a>;
|
||||
|
||||
fn match_nested(&'a self, path: &'a str) -> (Option<Self::Match>, &'a str);
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{NestedRoute, ParamSegment, Routes};
|
||||
use crate::{
|
||||
matching::{MatchInterface, StaticSegment, WildcardSegment},
|
||||
PathSegment,
|
||||
};
|
||||
|
||||
#[test]
|
||||
pub fn matches_single_root_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
});
|
||||
let matched = routes.match_route("/");
|
||||
assert!(matched.is_some());
|
||||
let matched = routes.match_route("");
|
||||
assert!(matched.is_some());
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(paths, vec![vec![PathSegment::Static("/".into())]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn matches_nested_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: NestedRoute {
|
||||
segments: (StaticSegment("author"), StaticSegment("contact")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: "Contact Me",
|
||||
},
|
||||
data: (),
|
||||
view: "Home",
|
||||
});
|
||||
|
||||
// route generation
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![vec![
|
||||
PathSegment::Static("".into()),
|
||||
PathSegment::Static("author".into()),
|
||||
PathSegment::Static("contact".into())
|
||||
]]
|
||||
);
|
||||
|
||||
let matched = routes.match_route("/author/contact").unwrap();
|
||||
assert_eq!(matched.matched(), "");
|
||||
assert_eq!(matched.to_child().matched(), "/author/contact");
|
||||
|
||||
let view = matched.to_view();
|
||||
assert_eq!(*view, "Home");
|
||||
assert_eq!(*matched.to_child().to_view(), "Contact Me");
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn does_not_match_incomplete_route() {
|
||||
let routes = Routes::new(NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: NestedRoute {
|
||||
segments: (StaticSegment("author"), StaticSegment("contact")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: "Contact Me",
|
||||
},
|
||||
data: (),
|
||||
view: "Home",
|
||||
});
|
||||
let matched = routes.match_route("/");
|
||||
assert!(matched.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn chooses_between_nested_routes() {
|
||||
let routes = Routes::new((
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("about"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/blog"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment(""),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: (StaticSegment("post"), ParamSegment("id")),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
));
|
||||
|
||||
// generates routes correctly
|
||||
let (base, paths) = routes.generate_routes();
|
||||
assert_eq!(base, None);
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
vec![
|
||||
PathSegment::Static("/".into()),
|
||||
PathSegment::Static("".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/".into()),
|
||||
PathSegment::Static("about".into())
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/blog".into()),
|
||||
PathSegment::Static("".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Static("/blog".into()),
|
||||
PathSegment::Static("post".into()),
|
||||
PathSegment::Param("id".into())
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
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 arbitrary_nested_routes() {
|
||||
let routes = Routes::new_with_base(
|
||||
(
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (
|
||||
NestedRoute {
|
||||
segments: StaticSegment("/"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("about"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
),
|
||||
data: (),
|
||||
view: || (),
|
||||
},
|
||||
NestedRoute {
|
||||
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: || (),
|
||||
},
|
||||
),
|
||||
"/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)]
|
||||
pub struct PartialPathMatch<'a, ParamsIter> {
|
||||
pub(crate) remaining: &'a str,
|
||||
pub(crate) params: ParamsIter,
|
||||
pub(crate) matched: &'a str,
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
|
||||
pub fn new(
|
||||
remaining: &'a str,
|
||||
params: ParamsIter,
|
||||
matched: &'a str,
|
||||
) -> Self {
|
||||
Self {
|
||||
remaining,
|
||||
params,
|
||||
matched,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.remaining.is_empty() || self.remaining == "/"
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> &str {
|
||||
self.remaining
|
||||
}
|
||||
|
||||
pub fn params(self) -> ParamsIter {
|
||||
self.params
|
||||
}
|
||||
|
||||
pub fn matched(&self) -> &str {
|
||||
self.matched
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@ use super::{
|
|||
MatchInterface, MatchNestedRoutes, PartialPathMatch, PossibleRouteMatch,
|
||||
};
|
||||
use crate::PathSegment;
|
||||
use core::iter;
|
||||
use alloc::vec::Vec;
|
||||
use core::{fmt, iter};
|
||||
|
||||
mod tuples;
|
||||
|
||||
|
@ -14,7 +15,7 @@ pub struct NestedRoute<Segments, Children, Data, View> {
|
|||
pub view: View,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct NestedMatch<'a, ParamsIter, Child, View> {
|
||||
/// The portion of the full path matched only by this nested route.
|
||||
matched: &'a str,
|
||||
|
@ -25,6 +26,21 @@ pub struct NestedMatch<'a, ParamsIter, Child, View> {
|
|||
view: &'a View,
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter, Child, View> fmt::Debug
|
||||
for NestedMatch<'a, ParamsIter, Child, View>
|
||||
where
|
||||
ParamsIter: fmt::Debug,
|
||||
Child: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NestedMatch")
|
||||
.field("matched", &self.matched)
|
||||
.field("params", &self.params)
|
||||
.field("child", &self.child)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter, Child, View> MatchInterface<'a>
|
||||
for NestedMatch<'a, ParamsIter, Child, View>
|
||||
where
|
|
@ -1,7 +1,5 @@
|
|||
use crate::{
|
||||
matching::{MatchInterface, MatchNestedRoutes},
|
||||
PathSegment,
|
||||
};
|
||||
use crate::{MatchInterface, MatchNestedRoutes, PathSegment};
|
||||
use alloc::vec::Vec;
|
||||
use core::iter;
|
||||
use either_of::*;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
extern crate alloc;
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
pub(crate) type Params<K> = Vec<(K, String)>;
|
Loading…
Reference in a new issue