diff --git a/packages/router-core/src/router.rs b/packages/router-core/src/router.rs index d84632a6f..561f7a133 100644 --- a/packages/router-core/src/router.rs +++ b/packages/router-core/src/router.rs @@ -67,20 +67,36 @@ where } pub trait ToRouteSegments { - fn to_route_segments(&self) -> Vec; + fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; +} + +impl ToRouteSegments for I +where + I: IntoIterator, +{ + 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 { type Err; - fn from_route_segments(segments: &[&str], query: &str) -> Result; + fn from_route_segments(segments: &[&str]) -> Result; } -impl FromRouteSegments for Vec { - type Err = ::Err; +impl> FromRouteSegments for I { + type Err = ::Err; - fn from_route_segments(segments: &[&str], query: &str) -> Result { - segments.iter().map(|s| T::from_route_segment(s)).collect() + fn from_route_segments(segments: &[&str]) -> Result { + segments + .iter() + .map(|s| String::from_route_segment(s)) + .collect() } } diff --git a/packages/router-core/tests/macro.rs b/packages/router-core/tests/macro.rs index 244719684..def7e3e6d 100644 --- a/packages/router-core/tests/macro.rs +++ b/packages/router-core/tests/macro.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; -use dioxus_router_macro::*; use dioxus_router_core::*; +use dioxus_router_macro::*; use std::str::FromStr; #[inline_props] @@ -43,7 +43,16 @@ fn Route4(cx: Scope, number1: u32, number2: u32) -> Element { fn Route5(cx: Scope, query: String) -> Element { render! { div{ - "Route5" + "Route5: {query}" + } + } +} + +#[inline_props] +fn Route6(cx: Scope, extra: Vec) -> Element { + render! { + div{ + "Route5: {extra:?}" } } } @@ -60,9 +69,9 @@ enum Route { #[route("/(number1)/(number2)" Route4)] Route4 { number1: u32, number2: u32 }, #[route("/?(query)" Route5)] - Route5 { - query: String, - }, + Route5 { query: String }, + #[route("/(...extra)" Route6)] + Route6 { extra: Vec }, } #[test] @@ -82,6 +91,25 @@ fn display_works() { }; 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] @@ -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"; assert_eq!( Route::from_str(w), @@ -127,6 +149,18 @@ fn from_string_works() { 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] @@ -161,4 +195,16 @@ fn round_trip() { query: string.to_string(), }; 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() {} diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index c1e15748c..5549abf53 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -17,7 +17,7 @@ pub fn derive_routable(input: TokenStream) -> TokenStream { let route_enum = match RouteEnum::parse(routes_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(); @@ -118,9 +118,7 @@ impl RouteEnum { let mut segments = route.split('/'); let mut errors = Vec::new(); - if let Some(segment) = segments.next() { - #(#tokens)* - } + #(#tokens)* Err(RouteParseError { attempted_routes: errors, diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 792696ea8..5f4d6b027 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -93,8 +93,7 @@ impl Route { pub fn routable_match(&self) -> TokenStream2 { let name = &self.route_name; - let dynamic_segments: Vec<_> = self - .dynamic_segments().collect(); + let dynamic_segments: Vec<_> = self.dynamic_segments().collect(); let props_name = &self.props_name; let comp_name = &self.comp_name; @@ -110,7 +109,7 @@ impl Route { } } - fn dynamic_segments(&self)-> impl Iterator +'_{ + fn dynamic_segments(&self) -> impl Iterator + '_ { let segments = self.route_segments.iter().filter_map(|seg| { seg.name().map(|name| { quote! { @@ -118,18 +117,22 @@ impl Route { } }) }); - let query = self.query.as_ref().map(|q| { - let name = q.name(); - quote! { - #name - } - }).into_iter(); + let query = self + .query + .as_ref() + .map(|q| { + let name = q.name(); + quote! { + #name + } + }) + .into_iter(); segments.chain(query) } pub fn construct(&self, enum_name: Ident) -> TokenStream2 { - let segments = self.dynamic_segments(); + let segments = self.dynamic_segments(); let name = &self.route_name; quote! { @@ -157,11 +160,14 @@ impl Route { display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? }); } 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)? }); + 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) => { - 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)? }); } } @@ -195,6 +201,13 @@ impl Route { 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 { @@ -211,7 +224,7 @@ impl ToTokens for Route { let route = dir.join("src").join("pages").join(with_extension.clone()); // 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() } else { route_path.join("index.rs").to_str().unwrap().to_string() @@ -259,13 +272,13 @@ fn parse_route_segments( let spread = segment.starts_with("(..."); let ident = if spread { - segment[3..segment.len() - 1].to_string() + segment[4..segment.len() - 1].to_string() } else { segment[1..segment.len() - 1].to_string() }; 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, }); @@ -361,7 +374,7 @@ impl RouteSegment { match self { Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; }, 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 { + match self { + Self::Dynamic(ident, _) => Some(format_ident!("{}MissingError", ident)), + _ => None, + } + } + pub fn try_parse( &self, idx: usize, error_enum_name: &Ident, error_enum_varient: &Ident, inner_parse_enum: &Ident, + parse_children: TokenStream2, ) -> TokenStream2 { let error_name = self.error_name(idx); match self { Self::Static(segment) => { quote! { - let parsed = if segment == #segment { - Ok(()) - } else { - Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name)) - }; + { + let mut segments = segments.clone(); + let parsed = if let Some(#segment) = segments.next() { + Ok(()) + } else { + 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! { - 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! { - 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); + } + } + } } } } diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs index 9fa1b2a1f..fcfa43a8d 100644 --- a/packages/router-macro/src/route_tree.rs +++ b/packages/router-macro/src/route_tree.rs @@ -18,8 +18,8 @@ pub enum RouteTreeSegment<'a> { } impl<'a> RouteTreeSegment<'a> { - pub fn build(routes: &'a Vec) -> Vec> { - let routes = routes.into_iter().map(PartialRoute::new).collect(); + pub fn build(routes: &'a [Route]) -> Vec> { + let routes = routes.iter().map(PartialRoute::new).collect(); Self::construct(routes) } @@ -37,7 +37,7 @@ impl<'a> RouteTreeSegment<'a> { segment: s, children, .. - } => (s == &segment).then(|| children), + } => (s == &segment).then_some(children), _ => None, }); @@ -89,29 +89,22 @@ impl<'a> RouteTreeSegment<'a> { let enum_varient = &from_route.route_name; let error_ident = static_segment_idx(*index); - let children_with_next_segment = children.iter().filter_map(|child| match child { - RouteTreeSegment::StaticEnd { .. } => None, - _ => Some(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, - }); + let children = children + .iter() + .map(|child| child.to_tokens(enum_name.clone(), error_enum_name.clone())); quote! { - if #segment == segment { + { let mut segments = segments.clone(); - #(#children_without_next_segment)* if let Some(segment) = segments.next() { - #(#children_with_next_segment)* + if #segment == segment { + #(#children)* + } + else { + errors.push(#error_enum_name::#enum_varient(#varient_parse_error::#error_ident)) + } } } - else { - errors.push(#error_enum_name::#enum_varient(#varient_parse_error::#error_ident)) - } } } RouteTreeSegment::Dynamic(route) => { @@ -123,50 +116,31 @@ impl<'a> RouteTreeSegment<'a> { .route_segments .iter() .enumerate() - .skip_while(|(_, seg)| match seg { - RouteSegment::Static(_) => true, - _ => false, - }) - .map(|(i, seg)| { - ( - seg.name(), - seg.try_parse(i, &error_enum_name, enum_varient, &varient_parse_error), - ) - }); + .skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_))); - fn print_route_segment, TokenStream)>>( + fn print_route_segment<'a, I: Iterator>( mut s: std::iter::Peekable, sucess_tokens: TokenStream, + error_enum_name: &Ident, + enum_varient: &Ident, + varient_parse_error: &Ident, ) -> TokenStream { - if let Some((name, first)) = s.next() { - let has_next = s.peek().is_some(); - let children = print_route_segment(s, sucess_tokens); - let name = name - .map(|name| quote! {#name}) - .unwrap_or_else(|| quote! {_}); + if let Some((i, route)) = s.next() { + let children = print_route_segment( + s, + sucess_tokens, + error_enum_name, + enum_varient, + varient_parse_error, + ); - let sucess = if has_next { - quote! { - let mut segments = segments.clone(); - if let Some(segment) = segments.next() { - #children - } - } - } else { - children - }; - - quote! { - #first - match parsed { - Ok(#name) => { - #sucess - } - Err(err) => { - errors.push(err); - } - } - } + route.try_parse( + i, + error_enum_name, + enum_varient, + varient_parse_error, + children, + ) } else { quote! { #sucess_tokens @@ -177,15 +151,25 @@ impl<'a> RouteTreeSegment<'a> { let construct_variant = route.construct(enum_name); 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( route_segments.peekable(), return_constructed( + insure_not_trailing, construct_variant, &error_enum_name, enum_varient, &varient_parse_error, parse_query, ), + &error_enum_name, + enum_varient, + &varient_parse_error, ) } Self::StaticEnd(route) => { @@ -195,6 +179,7 @@ impl<'a> RouteTreeSegment<'a> { let parse_query = route.parse_query(); return_constructed( + true, construct_variant, &error_enum_name, enum_varient, @@ -207,33 +192,41 @@ impl<'a> RouteTreeSegment<'a> { } fn return_constructed( + insure_not_trailing: bool, construct_variant: TokenStream, error_enum_name: &Ident, enum_varient: &Ident, varient_parse_error: &Ident, parse_query: TokenStream, ) -> TokenStream { - quote! { - let remaining_segments = segments.clone(); - let mut segments_clone = segments.clone(); - let next_segment = segments_clone.next(); - let segment_after_next = segments_clone.next(); - match (next_segment, segment_after_next) { - // This is the last segment, return the parsed route - (None, _) | (Some(""), None) => { - #parse_query - return Ok(#construct_variant); - } - _ => { - let mut trailing = String::new(); - for seg in remaining_segments { - trailing += seg; - trailing += "/"; + if insure_not_trailing { + quote! { + let remaining_segments = segments.clone(); + let mut segments_clone = segments.clone(); + let next_segment = segments_clone.next(); + let segment_after_next = segments_clone.next(); + match (next_segment, segment_after_next) { + // This is the last segment, return the parsed route + (None, _) | (Some(""), None) => { + #parse_query + return Ok(#construct_variant); + } + _ => { + let mut trailing = String::new(); + for seg in remaining_segments { + trailing += seg; + trailing += "/"; + } + trailing.pop(); + errors.push(#error_enum_name::#enum_varient(#varient_parse_error::ExtraSegments(trailing))) } - trailing.pop(); - errors.push(#error_enum_name::#enum_varient(#varient_parse_error::ExtraSegments(trailing))) } } + } else { + quote! { + #parse_query + return Ok(#construct_variant); + } } }