implement spread segments

This commit is contained in:
Evan Almloff 2023-05-13 17:49:10 -05:00
parent 2aadeb8046
commit ee763d52e1
5 changed files with 238 additions and 123 deletions

View file

@ -67,20 +67,36 @@ where
} }
pub trait ToRouteSegments { pub trait ToRouteSegments {
fn to_route_segments(&self) -> Vec<String>; fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
impl<I, T: std::fmt::Display> ToRouteSegments for I
where
I: IntoIterator<Item = T>,
{
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for segment in self {
write!(f, "/")?;
write!(f, "{}", segment)?;
}
Ok(())
}
} }
pub trait FromRouteSegments: Sized { pub trait FromRouteSegments: Sized {
type Err; type Err;
fn from_route_segments(segments: &[&str], query: &str) -> Result<Self, Self::Err>; fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>;
} }
impl<T: FromRouteSegment> FromRouteSegments for Vec<T> { impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
type Err = <T as FromRouteSegment>::Err; type Err = <String as FromRouteSegment>::Err;
fn from_route_segments(segments: &[&str], query: &str) -> Result<Self, Self::Err> { fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err> {
segments.iter().map(|s| T::from_route_segment(s)).collect() segments
.iter()
.map(|s| String::from_route_segment(s))
.collect()
} }
} }

View file

@ -1,6 +1,6 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_router_macro::*;
use dioxus_router_core::*; use dioxus_router_core::*;
use dioxus_router_macro::*;
use std::str::FromStr; use std::str::FromStr;
#[inline_props] #[inline_props]
@ -43,7 +43,16 @@ fn Route4(cx: Scope, number1: u32, number2: u32) -> Element {
fn Route5(cx: Scope, query: String) -> Element { fn Route5(cx: Scope, query: String) -> Element {
render! { render! {
div{ div{
"Route5" "Route5: {query}"
}
}
}
#[inline_props]
fn Route6(cx: Scope, extra: Vec<String>) -> Element {
render! {
div{
"Route5: {extra:?}"
} }
} }
} }
@ -60,9 +69,9 @@ enum Route {
#[route("/(number1)/(number2)" Route4)] #[route("/(number1)/(number2)" Route4)]
Route4 { number1: u32, number2: u32 }, Route4 { number1: u32, number2: u32 },
#[route("/?(query)" Route5)] #[route("/?(query)" Route5)]
Route5 { Route5 { query: String },
query: String, #[route("/(...extra)" Route6)]
}, Route6 { extra: Vec<String> },
} }
#[test] #[test]
@ -82,6 +91,25 @@ fn display_works() {
}; };
assert_eq!(route.to_string(), "/hello_world2"); assert_eq!(route.to_string(), "/hello_world2");
let route = Route::Route4 {
number1: 1234,
number2: 5678,
};
assert_eq!(route.to_string(), "/1234/5678");
let route = Route::Route5 {
query: "hello".to_string(),
};
assert_eq!(route.to_string(), "/?hello");
let route = Route::Route6 {
extra: vec!["hello".to_string(), "world".to_string()],
};
assert_eq!(route.to_string(), "/hello/world");
} }
#[test] #[test]
@ -114,12 +142,6 @@ fn from_string_works() {
}) })
); );
let w = "/hello_world/-1";
match Route::from_str(w) {
Ok(r) => panic!("should not parse {r:?}"),
Err(err) => println!("{err}"),
}
let w = "/?x=1234&y=hello"; let w = "/?x=1234&y=hello";
assert_eq!( assert_eq!(
Route::from_str(w), Route::from_str(w),
@ -127,6 +149,18 @@ fn from_string_works() {
query: "x=1234&y=hello".to_string() query: "x=1234&y=hello".to_string()
}) })
); );
let w = "/hello_world/hello_world/hello_world";
assert_eq!(
Route::from_str(w),
Ok(Route::Route6 {
extra: vec![
"hello_world".to_string(),
"hello_world".to_string(),
"hello_world".to_string()
]
})
);
} }
#[test] #[test]
@ -161,4 +195,16 @@ fn round_trip() {
query: string.to_string(), query: string.to_string(),
}; };
assert_eq!(Route::from_str(&route.to_string()), Ok(route)); assert_eq!(Route::from_str(&route.to_string()), Ok(route));
// Route5
let route = Route::Route6 {
extra: vec![
"hello_world".to_string(),
"hello_world".to_string(),
"hello_world".to_string(),
],
};
assert_eq!(Route::from_str(&route.to_string()), Ok(route));
} }
fn main() {}

View file

@ -17,7 +17,7 @@ pub fn derive_routable(input: TokenStream) -> TokenStream {
let route_enum = match RouteEnum::parse(routes_enum) { let route_enum = match RouteEnum::parse(routes_enum) {
Ok(route_enum) => route_enum, Ok(route_enum) => route_enum,
Err(err) => return TokenStream2::from(err.to_compile_error()).into(), Err(err) => return err.to_compile_error().into(),
}; };
let error_type = route_enum.error_type(); let error_type = route_enum.error_type();
@ -118,9 +118,7 @@ impl RouteEnum {
let mut segments = route.split('/'); let mut segments = route.split('/');
let mut errors = Vec::new(); let mut errors = Vec::new();
if let Some(segment) = segments.next() {
#(#tokens)* #(#tokens)*
}
Err(RouteParseError { Err(RouteParseError {
attempted_routes: errors, attempted_routes: errors,

View file

@ -93,8 +93,7 @@ impl Route {
pub fn routable_match(&self) -> TokenStream2 { pub fn routable_match(&self) -> TokenStream2 {
let name = &self.route_name; let name = &self.route_name;
let dynamic_segments: Vec<_> = self let dynamic_segments: Vec<_> = self.dynamic_segments().collect();
.dynamic_segments().collect();
let props_name = &self.props_name; let props_name = &self.props_name;
let comp_name = &self.comp_name; let comp_name = &self.comp_name;
@ -110,7 +109,7 @@ impl Route {
} }
} }
fn dynamic_segments(&self)-> impl Iterator<Item = TokenStream2> +'_{ fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
let segments = self.route_segments.iter().filter_map(|seg| { let segments = self.route_segments.iter().filter_map(|seg| {
seg.name().map(|name| { seg.name().map(|name| {
quote! { quote! {
@ -118,12 +117,16 @@ impl Route {
} }
}) })
}); });
let query = self.query.as_ref().map(|q| { let query = self
.query
.as_ref()
.map(|q| {
let name = q.name(); let name = q.name();
quote! { quote! {
#name #name
} }
}).into_iter(); })
.into_iter();
segments.chain(query) segments.chain(query)
} }
@ -157,11 +160,14 @@ impl Route {
display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? }); display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? });
} }
RouteSegment::Dynamic(ident, ty) => { RouteSegment::Dynamic(ident, ty) => {
error_variants.push(quote! { #error_name(<#ty as std::str::FromStr>::Err) }); let missing_error = segment.missing_error_name().unwrap();
error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) });
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
error_variants.push(quote! { #missing_error });
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
} }
RouteSegment::CatchAll(ident, ty) => { RouteSegment::CatchAll(ident, ty) => {
error_variants.push(quote! { #error_name(<#ty as std::str::FromStr>::Err) }); error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegments>::Err) });
display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
} }
} }
@ -195,6 +201,13 @@ impl Route {
None => quote! {}, None => quote! {},
} }
} }
pub fn ends_with_catch_all(&self) -> bool {
self.route_segments
.last()
.map(|seg| matches!(seg, RouteSegment::CatchAll(..)))
.unwrap_or(false)
}
} }
impl ToTokens for Route { impl ToTokens for Route {
@ -211,7 +224,7 @@ impl ToTokens for Route {
let route = dir.join("src").join("pages").join(with_extension.clone()); let route = dir.join("src").join("pages").join(with_extension.clone());
// check if the route exists or if not use the index route // check if the route exists or if not use the index route
let route = if route.exists() && without_leading_slash != "" { let route = if route.exists() && !without_leading_slash.is_empty() {
with_extension.to_str().unwrap().to_string() with_extension.to_str().unwrap().to_string()
} else { } else {
route_path.join("index.rs").to_str().unwrap().to_string() route_path.join("index.rs").to_str().unwrap().to_string()
@ -259,13 +272,13 @@ fn parse_route_segments(
let spread = segment.starts_with("(..."); let spread = segment.starts_with("(...");
let ident = if spread { let ident = if spread {
segment[3..segment.len() - 1].to_string() segment[4..segment.len() - 1].to_string()
} else { } else {
segment[1..segment.len() - 1].to_string() segment[1..segment.len() - 1].to_string()
}; };
let field = varient.fields.iter().find(|field| match field.ident { let field = varient.fields.iter().find(|field| match field.ident {
Some(ref field_ident) => field_ident.to_string() == ident, Some(ref field_ident) => *field_ident == ident,
None => false, None => false,
}); });
@ -361,7 +374,7 @@ impl RouteSegment {
match self { match self {
Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; }, Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; },
Self::Dynamic(ident, _) => quote! { write!(f, "/{}", #ident)?; }, Self::Dynamic(ident, _) => quote! { write!(f, "/{}", #ident)?; },
Self::CatchAll(ident, _) => quote! { write!(f, "/{}", #ident)?; }, Self::CatchAll(ident, _) => quote! { #ident.display_route_segements(f)?; },
} }
} }
@ -373,32 +386,81 @@ impl RouteSegment {
} }
} }
fn missing_error_name(&self) -> Option<Ident> {
match self {
Self::Dynamic(ident, _) => Some(format_ident!("{}MissingError", ident)),
_ => None,
}
}
pub fn try_parse( pub fn try_parse(
&self, &self,
idx: usize, idx: usize,
error_enum_name: &Ident, error_enum_name: &Ident,
error_enum_varient: &Ident, error_enum_varient: &Ident,
inner_parse_enum: &Ident, inner_parse_enum: &Ident,
parse_children: TokenStream2,
) -> TokenStream2 { ) -> TokenStream2 {
let error_name = self.error_name(idx); let error_name = self.error_name(idx);
match self { match self {
Self::Static(segment) => { Self::Static(segment) => {
quote! { quote! {
let parsed = if segment == #segment { {
let mut segments = segments.clone();
let parsed = if let Some(#segment) = segments.next() {
Ok(()) Ok(())
} else { } else {
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name)) Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name))
}; };
match parsed {
Ok(_) => {
#parse_children
}
Err(err) => {
errors.push(err);
} }
} }
Self::Dynamic(_, ty) => { }
}
}
Self::Dynamic(name, ty) => {
let missing_error_name = self.missing_error_name().unwrap();
quote! { quote! {
let parsed = <#ty as dioxus_router_core::router::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))); {
let mut segments = segments.clone();
let parsed = if let Some(segment) = segments.next() {
<#ty as dioxus_router_core::router::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
} else {
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name))
};
match parsed {
Ok(#name) => {
#parse_children
}
Err(err) => {
errors.push(err);
} }
} }
Self::CatchAll(_, ty) => { }
}
}
Self::CatchAll(name, ty) => {
quote! { quote! {
let parsed = <#ty as dioxus_router_core::router::FromRouteSegments>::from_route_segments(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))); {
let parsed = {
let mut segments = segments.clone();
let segments: Vec<_> = segments.collect();
<#ty as dioxus_router_core::router::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
};
match parsed {
Ok(#name) => {
#parse_children
}
Err(err) => {
errors.push(err);
}
}
}
} }
} }
} }

View file

@ -18,8 +18,8 @@ pub enum RouteTreeSegment<'a> {
} }
impl<'a> RouteTreeSegment<'a> { impl<'a> RouteTreeSegment<'a> {
pub fn build(routes: &'a Vec<Route>) -> Vec<RouteTreeSegment<'a>> { pub fn build(routes: &'a [Route]) -> Vec<RouteTreeSegment<'a>> {
let routes = routes.into_iter().map(PartialRoute::new).collect(); let routes = routes.iter().map(PartialRoute::new).collect();
Self::construct(routes) Self::construct(routes)
} }
@ -37,7 +37,7 @@ impl<'a> RouteTreeSegment<'a> {
segment: s, segment: s,
children, children,
.. ..
} => (s == &segment).then(|| children), } => (s == &segment).then_some(children),
_ => None, _ => None,
}); });
@ -89,31 +89,24 @@ impl<'a> RouteTreeSegment<'a> {
let enum_varient = &from_route.route_name; let enum_varient = &from_route.route_name;
let error_ident = static_segment_idx(*index); let error_ident = static_segment_idx(*index);
let children_with_next_segment = children.iter().filter_map(|child| match child { let children = children
RouteTreeSegment::StaticEnd { .. } => None, .iter()
_ => Some(child.to_tokens(enum_name.clone(), error_enum_name.clone())), .map(|child| child.to_tokens(enum_name.clone(), error_enum_name.clone()));
});
let children_without_next_segment =
children.iter().filter_map(|child| match child {
RouteTreeSegment::StaticEnd { .. } => {
Some(child.to_tokens(enum_name.clone(), error_enum_name.clone()))
}
_ => None,
});
quote! { quote! {
if #segment == segment { {
let mut segments = segments.clone(); let mut segments = segments.clone();
#(#children_without_next_segment)*
if let Some(segment) = segments.next() { if let Some(segment) = segments.next() {
#(#children_with_next_segment)* if #segment == segment {
} #(#children)*
} }
else { else {
errors.push(#error_enum_name::#enum_varient(#varient_parse_error::#error_ident)) errors.push(#error_enum_name::#enum_varient(#varient_parse_error::#error_ident))
} }
} }
} }
}
}
RouteTreeSegment::Dynamic(route) => { RouteTreeSegment::Dynamic(route) => {
// At this point, we have matched all static segments, so we can just check if the remaining segments match the route // At this point, we have matched all static segments, so we can just check if the remaining segments match the route
let varient_parse_error = route.error_ident(); let varient_parse_error = route.error_ident();
@ -123,50 +116,31 @@ impl<'a> RouteTreeSegment<'a> {
.route_segments .route_segments
.iter() .iter()
.enumerate() .enumerate()
.skip_while(|(_, seg)| match seg { .skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_)));
RouteSegment::Static(_) => true,
_ => false,
})
.map(|(i, seg)| {
(
seg.name(),
seg.try_parse(i, &error_enum_name, enum_varient, &varient_parse_error),
)
});
fn print_route_segment<I: Iterator<Item = (Option<Ident>, TokenStream)>>( fn print_route_segment<'a, I: Iterator<Item = (usize, &'a RouteSegment)>>(
mut s: std::iter::Peekable<I>, mut s: std::iter::Peekable<I>,
sucess_tokens: TokenStream, sucess_tokens: TokenStream,
error_enum_name: &Ident,
enum_varient: &Ident,
varient_parse_error: &Ident,
) -> TokenStream { ) -> TokenStream {
if let Some((name, first)) = s.next() { if let Some((i, route)) = s.next() {
let has_next = s.peek().is_some(); let children = print_route_segment(
let children = print_route_segment(s, sucess_tokens); s,
let name = name sucess_tokens,
.map(|name| quote! {#name}) error_enum_name,
.unwrap_or_else(|| quote! {_}); enum_varient,
varient_parse_error,
);
let sucess = if has_next { route.try_parse(
quote! { i,
let mut segments = segments.clone(); error_enum_name,
if let Some(segment) = segments.next() { enum_varient,
#children varient_parse_error,
} children,
} )
} else {
children
};
quote! {
#first
match parsed {
Ok(#name) => {
#sucess
}
Err(err) => {
errors.push(err);
}
}
}
} else { } else {
quote! { quote! {
#sucess_tokens #sucess_tokens
@ -177,15 +151,25 @@ impl<'a> RouteTreeSegment<'a> {
let construct_variant = route.construct(enum_name); let construct_variant = route.construct(enum_name);
let parse_query = route.parse_query(); let parse_query = route.parse_query();
let insure_not_trailing = route
.route_segments
.last()
.map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _)))
.unwrap_or(true);
print_route_segment( print_route_segment(
route_segments.peekable(), route_segments.peekable(),
return_constructed( return_constructed(
insure_not_trailing,
construct_variant, construct_variant,
&error_enum_name, &error_enum_name,
enum_varient, enum_varient,
&varient_parse_error, &varient_parse_error,
parse_query, parse_query,
), ),
&error_enum_name,
enum_varient,
&varient_parse_error,
) )
} }
Self::StaticEnd(route) => { Self::StaticEnd(route) => {
@ -195,6 +179,7 @@ impl<'a> RouteTreeSegment<'a> {
let parse_query = route.parse_query(); let parse_query = route.parse_query();
return_constructed( return_constructed(
true,
construct_variant, construct_variant,
&error_enum_name, &error_enum_name,
enum_varient, enum_varient,
@ -207,12 +192,14 @@ impl<'a> RouteTreeSegment<'a> {
} }
fn return_constructed( fn return_constructed(
insure_not_trailing: bool,
construct_variant: TokenStream, construct_variant: TokenStream,
error_enum_name: &Ident, error_enum_name: &Ident,
enum_varient: &Ident, enum_varient: &Ident,
varient_parse_error: &Ident, varient_parse_error: &Ident,
parse_query: TokenStream, parse_query: TokenStream,
) -> TokenStream { ) -> TokenStream {
if insure_not_trailing {
quote! { quote! {
let remaining_segments = segments.clone(); let remaining_segments = segments.clone();
let mut segments_clone = segments.clone(); let mut segments_clone = segments.clone();
@ -235,6 +222,12 @@ fn return_constructed(
} }
} }
} }
} else {
quote! {
#parse_query
return Ok(#construct_variant);
}
}
} }
struct PartialRoute<'a> { struct PartialRoute<'a> {