From 58b74c115544d2dd54f5eda53f7f2006df6da711 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 31 May 2023 12:11:11 -0500 Subject: [PATCH] create redirects --- packages/router-macro/src/lib.rs | 34 ++- packages/router-macro/src/macro2.rs | 286 ---------------------- packages/router-macro/src/nest.rs | 45 +--- packages/router-macro/src/redirect.rs | 91 +++++++ packages/router-macro/src/route.rs | 56 +---- packages/router-macro/src/route_tree.rs | 123 ++++++++-- packages/router-macro/src/segment.rs | 67 ++++- packages/router/examples/simple_routes.rs | 12 +- packages/router/src/history/web.rs | 12 +- 9 files changed, 307 insertions(+), 419 deletions(-) delete mode 100644 packages/router-macro/src/macro2.rs create mode 100644 packages/router-macro/src/redirect.rs diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index de250f9c5..bde212e42 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -4,6 +4,7 @@ use layout::Layout; use nest::{Nest, NestId}; use proc_macro::TokenStream; use quote::{__private::Span, format_ident, quote, ToTokens}; +use redirect::Redirect; use route::Route; use segment::RouteSegment; use syn::{parse::ParseStream, parse_macro_input, Ident, Token}; @@ -13,14 +14,17 @@ use proc_macro2::TokenStream as TokenStream2; use crate::{layout::LayoutId, route_tree::RouteTree}; mod layout; -mod macro2; mod nest; mod query; +mod redirect; mod route; mod route_tree; mod segment; -#[proc_macro_derive(Routable)] +#[proc_macro_derive( + Routable, + attributes(route, nest, end_nest, layout, end_layout, redirect) +)] pub fn routable(input: TokenStream) -> TokenStream { let routes_enum = parse_macro_input!(input as syn::ItemEnum); @@ -69,6 +73,7 @@ pub fn routable(input: TokenStream) -> TokenStream { struct RouteEnum { vis: syn::Visibility, name: Ident, + redirects: Vec, routes: Vec, nests: Vec, layouts: Vec, @@ -85,6 +90,8 @@ impl RouteEnum { let mut routes = Vec::new(); + let mut redirects = Vec::new(); + let mut layouts: Vec = Vec::new(); let mut layout_stack = Vec::new(); @@ -197,6 +204,16 @@ impl RouteEnum { } } else if attr.path.is_ident("end_layout") { layout_stack.pop(); + } else if attr.path.is_ident("redirect") { + let parser = |input: ParseStream| { + Redirect::parse( + input, + nest_stack.iter().rev().cloned().collect(), + redirects.len(), + ) + }; + let redirect = attr.parse_args_with(parser)?; + redirects.push(redirect); } } @@ -225,6 +242,7 @@ impl RouteEnum { vis: vis.clone(), name: name.clone(), routes, + redirects, nests, layouts, site_map, @@ -256,7 +274,7 @@ impl RouteEnum { } fn parse_impl(&self) -> TokenStream2 { - let tree = RouteTree::new(&self.routes, &self.nests); + let tree = RouteTree::new(&self.routes, &self.nests, &self.redirects); let name = &self.name; let error_name = format_ident!("{}MatchError", self.name); @@ -325,6 +343,16 @@ impl RouteEnum { type_defs.push(nest.error_type()); } + for redirect in &self.redirects { + let error_variant = redirect.error_variant(); + let error_name = redirect.error_ident(); + let route_str = &redirect.route; + + error_variants.push(quote! { #error_variant(#error_name) }); + display_match.push(quote! { Self::#error_variant(err) => write!(f, "Redirect '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? }); + type_defs.push(redirect.error_type()); + } + quote! { #(#type_defs)* diff --git a/packages/router-macro/src/macro2.rs b/packages/router-macro/src/macro2.rs deleted file mode 100644 index 5d7ad7cce..000000000 --- a/packages/router-macro/src/macro2.rs +++ /dev/null @@ -1,286 +0,0 @@ -use syn::{braced, parenthesized, parse::Parse, Expr, Ident, LitStr, Path, Token}; - -#[test] -fn parses() { - use quote::quote; - - let tokens = quote! { - // The name of the enum - Route, - // All nests that have dynamic segments must have a name used to generate the enum - route(User, "user" / user_id: usize) { - route(Product, "product" / product_id: usize / dynamic: usize ) { - // Render creates a new route (that will be included in the enum) and is rendered with the given component - // The component uses the struct of the parent route as a prop (in this case, Product) - render(Other) - } - - // You can nest routes inside a layout to wrap them in a component that accepts the struct of the parent route as a prop (in this case, User) - layout(UserFrame) { - route(Route1Props, "hello_world" / dynamic: usize ) { - // (Accepts Route1Props as a prop) - render(Route1) - } - - // You can opt out of the layout by using !layout - !layout(UserFrame) { - route(Route2Props, "hello_world" / dynamic: usize ) { - // (Accepts Route2Props as a prop) - render(Route2) - } - } - } - } - - route(Route3Props, "hello_world" / dynamic: usize ) { - // (Accepts Route3Props as a prop) - render(Route3) - } - - route(RedirectData, dynamic: usize / extra: String) { - // Redirects accept a function that receives the struct of the parent route and returns the new route - redirect(|data: RedirectData| todo!() ) - } - }; - - let _ = syn::parse2::(tokens).unwrap(); -} - -struct RouteTree { - name: Ident, - roots: Vec, -} - -impl Parse for RouteTree { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let name = input.parse()?; - let _ = input.parse::(); - - let mut roots = Vec::new(); - while !input.is_empty() { - roots.push(input.parse()?); - } - Ok(Self { name, roots }) - } -} - -enum RouteSegment { - Route(Route), - Layout(Layout), - Render(Render), - Redirect(Redirect), -} - -impl Parse for RouteSegment { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let lookahead = input.lookahead1(); - if lookahead.peek(syn::Token![!]) { - input.parse::()?; - let ident: Ident = input.parse()?; - if ident == "layout" { - let mut layout: Layout = input.parse()?; - layout.opt_out = true; - Ok(RouteSegment::Layout(layout)) - } else { - Err(lookahead.error()) - } - } else if lookahead.peek(syn::Ident) { - let ident: Ident = input.parse()?; - if ident == "route" { - let route = input.parse()?; - Ok(RouteSegment::Route(route)) - } else if ident == "layout" { - let layout = input.parse()?; - Ok(RouteSegment::Layout(layout)) - } else if ident == "render" { - let render = input.parse()?; - Ok(RouteSegment::Render(render)) - } else if ident == "redirect" { - let redirect = input.parse()?; - Ok(RouteSegment::Redirect(redirect)) - } else { - Err(lookahead.error()) - } - } else { - Err(lookahead.error()) - } - } -} - -struct Render { - component: Path, -} - -impl Parse for Render { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let inner; - parenthesized!(inner in input); - let component = inner.parse()?; - - Ok(Self { component }) - } -} - -struct Layout { - opt_out: bool, - component: Path, - children: Vec, -} - -impl Parse for Layout { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let inner; - parenthesized!(inner in input); - let component = inner.parse()?; - - let content; - braced!(content in input); - let mut children = Vec::new(); - while !content.is_empty() { - children.push(content.parse()?); - } - - Ok(Self { - opt_out: false, - component, - children, - }) - } -} - -struct Route { - name: Ident, - path: RoutePath, - children: Vec, -} - -impl Parse for Route { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let inner; - parenthesized!(inner in input); - let name = inner.parse()?; - inner.parse::()?; - let path = inner.parse()?; - - let content; - braced!(content in input); - let mut children = Vec::new(); - while !content.is_empty() { - children.push(content.parse()?); - } - - Ok(Self { - name, - path, - children, - }) - } -} - -struct RoutePath { - segments: Vec, - query: Option, -} - -impl Parse for RoutePath { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - // parse all segments first - let mut segments = Vec::new(); - // remove any leading slash - if input.peek(syn::Token![/]) { - input.parse::()?; - } - - while !input.is_empty() { - let peak = input.lookahead1(); - // check if the next segment is a query - if peak.peek(syn::Token![?]) { - break; - } else if peak.peek(syn::Token![/]) { - input.parse::()?; - } else if peak.peek(syn::Ident) || peak.peek(syn::Token![...]) || peak.peek(syn::LitStr) - { - // parse the segment - segments.push(input.parse()?); - } else { - return Err(peak.error()); - } - } - // then parse the query - let query = if input.peek(syn::Token![?]) { - Some(input.parse()?) - } else { - None - }; - Ok(Self { segments, query }) - } -} - -enum RoutePathSegment { - Static(String), - Dynamic(Ident, Path), - CatchAll(Ident, Path), -} - -impl Parse for RoutePathSegment { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let lookahead = input.lookahead1(); - if lookahead.peek(Token![...]) { - input.parse::()?; - let name: Ident = input.parse()?; - input.parse::()?; - let type_: Path = input.parse()?; - - // parse the / - let _ = input.parse::(); - - Ok(RoutePathSegment::CatchAll(name, type_)) - } else if lookahead.peek(LitStr) { - let lit: LitStr = input.parse()?; - - // parse the / - let _ = input.parse::(); - - Ok(RoutePathSegment::Static(lit.value())) - } else if lookahead.peek(Ident) { - let ident: Ident = input.parse()?; - input.parse::()?; - let type_: Path = input.parse()?; - - // parse the / - let _ = input.parse::(); - - Ok(RoutePathSegment::Dynamic(ident, type_)) - } else { - Err(lookahead.error()) - } - } -} - -struct QuerySegment { - name: Ident, - type_: Path, -} - -impl Parse for QuerySegment { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - input.parse::()?; - let name = input.parse()?; - input.parse::()?; - let type_ = input.parse()?; - Ok(Self { name, type_ }) - } -} - -struct Redirect { - function: Expr, -} - -impl Parse for Redirect { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let inner; - parenthesized!(inner in input); - let function = inner.parse()?; - Ok(Self { function }) - } -} diff --git a/packages/router-macro/src/nest.rs b/packages/router-macro/src/nest.rs index 4a185f6b1..1aaff68f4 100644 --- a/packages/router-macro/src/nest.rs +++ b/packages/router-macro/src/nest.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, LitStr}; -use crate::segment::{parse_route_segments, RouteSegment}; +use crate::segment::{create_error_type, parse_route_segments, RouteSegment}; #[derive(Debug, Clone, Copy)] pub struct NestId(pub usize); @@ -25,7 +25,10 @@ impl Nest { let route_segments = parse_route_segments( route.span(), - children_routes.iter().flat_map(|f| f.named.iter()), + children_routes + .iter() + .flat_map(|f| f.named.iter()) + .map(|f| (f.ident.as_ref().unwrap(), &f.ty)), &route.value(), )? .0; @@ -79,42 +82,6 @@ impl Nest { pub fn error_type(&self) -> TokenStream { let error_name = self.error_ident(); - let mut error_variants = Vec::new(); - let mut display_match = Vec::new(); - - for (i, segment) in self.segments.iter().enumerate() { - let error_name = segment.error_name(i); - match segment { - RouteSegment::Static(index) => { - error_variants.push(quote! { #error_name }); - display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? }); - } - RouteSegment::Dynamic(ident, ty) => { - let missing_error = segment.missing_error_name().unwrap(); - error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::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))? }); - } - _ => todo!(), - } - } - - quote! { - #[allow(non_camel_case_types)] - #[derive(Debug, PartialEq)] - pub enum #error_name { - #(#error_variants,)* - } - - impl std::fmt::Display for #error_name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - #(#display_match,)* - } - Ok(()) - } - } - } + create_error_type(error_name, &self.segments) } } diff --git a/packages/router-macro/src/redirect.rs b/packages/router-macro/src/redirect.rs new file mode 100644 index 000000000..5978e2cb8 --- /dev/null +++ b/packages/router-macro/src/redirect.rs @@ -0,0 +1,91 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; +use syn::LitStr; + +use crate::{ + nest::NestId, + query::QuerySegment, + segment::{create_error_type, parse_route_segments, RouteSegment}, +}; + +#[derive(Debug)] +pub(crate) struct Redirect { + pub route: LitStr, + pub nests: Vec, + pub segments: Vec, + pub query: Option, + pub function: syn::ExprClosure, + pub index: usize, +} + +impl Redirect { + pub fn error_ident(&self) -> Ident { + format_ident!("Redirect{}ParseError", self.index) + } + + pub fn error_variant(&self) -> Ident { + format_ident!("Redirect{}", self.index) + } + + pub fn error_type(&self) -> TokenStream { + let error_name = self.error_ident(); + + create_error_type(error_name, &self.segments) + } + + pub fn parse_query(&self) -> TokenStream { + match &self.query { + Some(query) => query.parse(), + None => quote! {}, + } + } + + pub fn parse( + input: syn::parse::ParseStream, + active_nests: Vec, + index: usize, + ) -> syn::Result { + let path = input.parse::()?; + + let _ = input.parse::(); + let function = input.parse::()?; + + let mut closure_arguments = Vec::new(); + for arg in function.inputs.iter() { + match arg { + syn::Pat::Type(pat) => match &*pat.pat { + syn::Pat::Ident(ident) => { + closure_arguments.push((ident.ident.clone(), (*pat.ty).clone())); + } + _ => { + return Err(syn::Error::new_spanned( + arg, + "Expected closure argument to be a typed pattern", + )) + } + }, + _ => { + return Err(syn::Error::new_spanned( + arg, + "Expected closure argument to be a typed pattern", + )) + } + } + } + + let (segments, query) = parse_route_segments( + path.span(), + closure_arguments.iter().map(|(name, ty)| (name, ty)), + &path.value(), + )?; + + Ok(Redirect { + route: path, + nests: active_nests, + segments, + query, + function, + index, + }) + } +} diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 642423777..c7ad4b0d1 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -10,6 +10,7 @@ use crate::layout::LayoutId; use crate::nest::Nest; use crate::nest::NestId; use crate::query::QuerySegment; +use crate::segment::create_error_type; use crate::segment::parse_route_segments; use crate::segment::RouteSegment; @@ -90,8 +91,14 @@ impl Route { } }; - let (route_segments, query) = - parse_route_segments(variant.ident.span(), named_fields.named.iter(), &route)?; + let (route_segments, query) = parse_route_segments( + variant.ident.span(), + named_fields + .named + .iter() + .map(|f| (f.ident.as_ref().unwrap(), &f.ty)), + &route, + )?; Ok(Self { comp_name, @@ -217,50 +224,7 @@ impl Route { pub fn error_type(&self) -> TokenStream2 { let error_name = self.error_ident(); - let mut error_variants = Vec::new(); - let mut display_match = Vec::new(); - - for (i, segment) in self.segments.iter().enumerate() { - let error_name = segment.error_name(i); - match segment { - RouteSegment::Static(index) => { - error_variants.push(quote! { #error_name }); - display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? }); - } - RouteSegment::Dynamic(ident, ty) => { - let missing_error = segment.missing_error_name().unwrap(); - error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::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 dioxus_router::routable::FromRouteSegments>::Err) }); - display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); - } - } - } - - quote! { - #[allow(non_camel_case_types)] - #[derive(Debug, PartialEq)] - pub enum #error_name { - ExtraSegments(String), - #(#error_variants,)* - } - - impl std::fmt::Display for #error_name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ExtraSegments(segments) => { - write!(f, "Found additional trailing segments: {}", segments)? - } - #(#display_match,)* - } - Ok(()) - } - } - } + create_error_type(error_name, &self.segments) } pub fn parse_query(&self) -> TokenStream2 { diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs index dadd35370..5a05d3a06 100644 --- a/packages/router-macro/src/route_tree.rs +++ b/packages/router-macro/src/route_tree.rs @@ -4,13 +4,14 @@ use slab::Slab; use syn::Ident; use crate::{ - nest::Nest, + nest::{Nest, NestId}, + redirect::Redirect, route::Route, segment::{static_segment_idx, RouteSegment}, }; #[derive(Debug, Clone, Default)] -pub struct RouteTree<'a> { +pub(crate) struct RouteTree<'a> { pub roots: Vec, entries: Slab>, } @@ -47,6 +48,13 @@ impl<'a> RouteTree<'a> { _ => 1, } } + RouteTreeSegmentData::Redirect(redirect) => { + // Routes that end in a catch all segment should be checked last + match redirect.segments.last() { + Some(RouteSegment::CatchAll(..)) => 2, + _ => 1, + } + } } }); } @@ -89,10 +97,15 @@ impl<'a> RouteTree<'a> { .expect("Cannot get children of non static or nest segment") } - pub fn new(routes: &'a [Route], nests: &'a [Nest]) -> Self { + pub(crate) fn new(routes: &'a [Route], nests: &'a [Nest], redirects: &'a [Redirect]) -> Self { let routes = routes .iter() - .map(|route| RouteIter::new(route, nests)) + .map(|route| PathIter::new_route(route, nests)) + .chain( + redirects + .iter() + .map(|redirect| PathIter::new_redirect(redirect, nests)), + ) .collect::>(); let mut myself = Self::default(); @@ -102,7 +115,7 @@ impl<'a> RouteTree<'a> { myself } - pub fn construct(&mut self, routes: Vec>) -> Vec { + pub fn construct(&mut self, routes: Vec>) -> Vec { let mut segments = Vec::new(); // Add all routes to the tree @@ -127,7 +140,7 @@ impl<'a> RouteTree<'a> { for &seg_id in segments.iter() { let seg = self.get(seg_id).unwrap(); if let RouteTreeSegmentData::Static { segment: s, .. } = seg { - if s == segment { + if *s == segment { // If it does, just update the current route current_route = Some(seg_id); continue 'o; @@ -223,9 +236,7 @@ impl<'a> RouteTree<'a> { } // If there is no static segment, add the route to the current_route None => { - let id = self - .entries - .insert(RouteTreeSegmentData::Route(route.route)); + let id = self.entries.insert(route.final_segment); let current_children_mut = current_route .map(|id| self.children_mut(id)) .unwrap_or_else(|| &mut segments); @@ -246,7 +257,7 @@ pub struct StaticErrorVariant { // First deduplicate the routes by the static part of the route #[derive(Debug, Clone)] -pub enum RouteTreeSegmentData<'a> { +pub(crate) enum RouteTreeSegmentData<'a> { Static { segment: &'a str, error_variant: StaticErrorVariant, @@ -258,6 +269,7 @@ pub enum RouteTreeSegmentData<'a> { children: Vec, }, Route(&'a Route), + Redirect(&'a Redirect), } impl<'a> RouteTreeSegmentData<'a> { @@ -362,6 +374,52 @@ impl<'a> RouteTreeSegmentData<'a> { &varient_parse_error, ) } + Self::Redirect(redirect) => { + // 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 = redirect.error_ident(); + let enum_varient = &redirect.error_variant(); + + let route_segments = redirect + .segments + .iter() + .enumerate() + .skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_))); + + let parse_query = redirect.parse_query(); + + let insure_not_trailing = redirect + .segments + .last() + .map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _))) + .unwrap_or(true); + + let redirect_function = &redirect.function; + let args = redirect_function.inputs.iter().map(|pat| match pat { + syn::Pat::Type(ident) => { + let name = &ident.pat; + quote! {#name} + } + _ => panic!("Expected closure argument to be a typed pattern"), + }); + let return_redirect = quote! { + (#redirect_function)(#(#args,)*) + }; + + print_route_segment( + route_segments.peekable(), + return_constructed( + insure_not_trailing, + return_redirect, + &error_enum_name, + enum_varient, + &varient_parse_error, + parse_query, + ), + &error_enum_name, + enum_varient, + &varient_parse_error, + ) + } } } } @@ -435,18 +493,39 @@ fn return_constructed( } } -pub struct RouteIter<'a> { - route: &'a Route, - nests: &'a [Nest], +pub struct PathIter<'a> { + final_segment: RouteTreeSegmentData<'a>, + active_nests: &'a [NestId], + all_nests: &'a [Nest], + segments: &'a [RouteSegment], + error_ident: Ident, + error_variant: Ident, nest_index: usize, static_segment_index: usize, } -impl<'a> RouteIter<'a> { - fn new(route: &'a Route, nests: &'a [Nest]) -> Self { +impl<'a> PathIter<'a> { + fn new_route(route: &'a Route, nests: &'a [Nest]) -> Self { Self { - route, - nests, + final_segment: RouteTreeSegmentData::Route(route), + active_nests: &*route.nests, + segments: &*route.segments, + error_ident: route.error_ident(), + error_variant: route.route_name.clone(), + all_nests: nests, + nest_index: 0, + static_segment_index: 0, + } + } + + fn new_redirect(redirect: &'a Redirect, nests: &'a [Nest]) -> Self { + Self { + final_segment: RouteTreeSegmentData::Redirect(redirect), + active_nests: &*redirect.nests, + segments: &*redirect.segments, + error_ident: redirect.error_ident(), + error_variant: redirect.error_variant(), + all_nests: nests, nest_index: 0, static_segment_index: 0, } @@ -454,15 +533,15 @@ impl<'a> RouteIter<'a> { fn next_nest(&mut self) -> Option<&'a Nest> { let idx = self.nest_index; - let nest_index = self.route.nests.get(idx)?; - let nest = &self.nests[nest_index.0]; + let nest_index = self.active_nests.get(idx)?; + let nest = &self.all_nests[nest_index.0]; self.nest_index += 1; Some(nest) } fn next_static_segment(&mut self) -> Option<(usize, &'a str)> { let idx = self.static_segment_index; - let segment = self.route.segments.get(idx)?; + let segment = self.segments.get(idx)?; match segment { RouteSegment::Static(segment) => { self.static_segment_index += 1; @@ -474,8 +553,8 @@ impl<'a> RouteIter<'a> { fn error_variant(&self) -> StaticErrorVariant { StaticErrorVariant { - varient_parse_error: self.route.error_ident(), - enum_varient: self.route.route_name.clone(), + varient_parse_error: self.error_ident.clone(), + enum_varient: self.error_variant.clone(), } } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index e81a4e433..6295a8a26 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -124,7 +124,7 @@ pub fn static_segment_idx(idx: usize) -> Ident { pub fn parse_route_segments<'a>( route_span: Span, - mut fields: impl Iterator, + mut fields: impl Iterator, route: &str, ) -> syn::Result<(Vec, Option)> { let mut route_segments = Vec::new(); @@ -157,13 +157,10 @@ pub fn parse_route_segments<'a>( segment.to_string() }; - let field = fields.find(|field| match field.ident { - Some(ref field_ident) => *field_ident == ident, - None => false, - }); + let field = fields.find(|(name, _)| **name == ident); let ty = if let Some(field) = field { - field.ty.clone() + field.1.clone() } else { return Err(syn::Error::new( route_span, @@ -200,13 +197,10 @@ pub fn parse_route_segments<'a>( Some(query) => { if let Some(query) = query.strip_prefix(':') { let query_ident = Ident::new(query, Span::call_site()); - let field = fields.find(|field| match field.ident { - Some(ref field_ident) => field_ident == &query_ident, - None => false, - }); + let field = fields.find(|(name, _)| *name == &query_ident); - let ty = if let Some(field) = field { - field.ty.clone() + let ty = if let Some((_, ty)) = field { + ty.clone() } else { return Err(syn::Error::new( route_span, @@ -227,3 +221,52 @@ pub fn parse_route_segments<'a>( Ok((route_segments, parsed_query)) } + +pub(crate) fn create_error_type(error_name: Ident, segments: &[RouteSegment]) -> TokenStream2 { + let mut error_variants = Vec::new(); + let mut display_match = Vec::new(); + + for (i, segment) in segments.iter().enumerate() { + let error_name = segment.error_name(i); + match segment { + RouteSegment::Static(index) => { + error_variants.push(quote! { #error_name }); + display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? }); + } + RouteSegment::Dynamic(ident, ty) => { + let missing_error = segment.missing_error_name().unwrap(); + error_variants.push( + quote! { #error_name(<#ty as dioxus_router::routable::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 dioxus_router::routable::FromRouteSegments>::Err) }); + display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); + } + } + } + + quote! { + #[allow(non_camel_case_types)] + #[derive(Debug, PartialEq)] + pub enum #error_name { + ExtraSegments(String), + #(#error_variants,)* + } + + impl std::fmt::Display for #error_name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ExtraSegments(segments) => { + write!(f, "Found additional trailing segments: {}", segments)? + } + #(#display_match,)* + } + Ok(()) + } + } + } +} diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index 380f5ce8d..7881bd1ef 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -45,13 +45,13 @@ fn UserFrame(cx: Scope, user_id: usize) -> Element { } #[inline_props] -fn Route1(cx: Scope, user_id: usize, dynamic: usize, extra: String) -> Element { +fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: String) -> Element { render! { pre { - "Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\textra:{extra}\n}}" + "Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query},\n\textra:{extra}\n}}" } Link { - target: Route::Route1 { user_id: *user_id, dynamic: *dynamic, extra: extra.clone() + "." }, + target: Route::Route1 { user_id: *user_id, dynamic: *dynamic, query: String::new(), extra: extra.clone() + "." }, "Route1 with extra+\".\"" } p { "Footer" } @@ -131,12 +131,13 @@ enum Route { // Everything inside the nest has the added parameter `user_id: String` // UserFrame is a layout component that will receive the `user_id: String` parameter #[layout(UserFrame)] - // Route1 is a non-layout component that will receive the `user_id: String` and `dynamic: String` parameters - #[route("/:dynamic", Route1)] + // Route1 is a non-layout component that will receive the `user_id: String` and `dynamic: String` parameters + #[route("/:dynamic?:query", Route1)] Route1 { // The type is taken from the first instance of the dynamic parameter user_id: usize, dynamic: usize, + query: String, extra: String, }, // Route2 is a non-layout component that will receive the `user_id: String` parameter @@ -146,6 +147,7 @@ enum Route { Route2 { user_id: usize }, #[end_layout] #[end_nest] + #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})] #[route("/:dynamic", Route3)] Route3 { dynamic: String }, } diff --git a/packages/router/src/history/web.rs b/packages/router/src/history/web.rs index b17a18216..ec085f88c 100644 --- a/packages/router/src/history/web.rs +++ b/packages/router/src/history/web.rs @@ -12,7 +12,7 @@ use super::{ HistoryProvider, }; -fn update_scroll(window: &Window, history: &History) { +fn update_scroll(window: &Window, history: &History) { if let Some(WebHistoryState { state, .. }) = get_current::>(history) { let scroll = ScrollPosition::of_window(window); let state = WebHistoryState { state, scroll }; @@ -42,7 +42,7 @@ struct WebHistoryState { /// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added. /// /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API -pub struct WebHistory { +pub struct WebHistory { do_scroll_restoration: bool, history: History, listener_navigation: Option, @@ -102,10 +102,10 @@ impl WebHistory { phantom: Default::default(), }; - let state = myself.current_route(); - - let state = myself.create_state(state); - let _ = replace_state_with_url(&myself.history, &state, None); + let current_route = myself.current_route(); + let current_url = current_route.to_string(); + let state = myself.create_state(current_route); + let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url)); myself }