Selaa lähdekoodia

allow nested routes

Evan Almloff 2 vuotta sitten
vanhempi
commit
2da1f7faa7

+ 61 - 43
packages/router-macro/src/lib.rs

@@ -5,9 +5,9 @@ use nest::{Nest, NestId};
 use proc_macro::TokenStream;
 use quote::{__private::Span, format_ident, quote, ToTokens};
 use redirect::Redirect;
-use route::Route;
+use route::{Route, RouteType};
 use segment::RouteSegment;
-use syn::{parse::ParseStream, parse_macro_input, Ident, Token};
+use syn::{parse::ParseStream, parse_macro_input, Ident, Token, Type};
 
 use proc_macro2::TokenStream as TokenStream2;
 
@@ -73,11 +73,13 @@ mod segment;
 ///     #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
 ///     #[route("/:dynamic")]
 ///     Route3 { dynamic: String },
+///     #[child]
+///     NestedRoute(NestedRoute),
 /// }
 /// ```
 #[proc_macro_derive(
     Routable,
-    attributes(route, nest, end_nest, layout, end_layout, redirect)
+    attributes(route, nest, end_nest, layout, end_layout, redirect, child)
 )]
 pub fn routable(input: TokenStream) -> TokenStream {
     let routes_enum = parse_macro_input!(input as syn::ItemEnum);
@@ -124,12 +126,12 @@ pub fn routable(input: TokenStream) -> TokenStream {
         }
 
         #error_type
-
-        #parse_impl
-
+        
         #display_impl
-
+        
         #routable_impl
+
+        #parse_impl
     }
     .into()
 }
@@ -288,7 +290,33 @@ impl RouteEnum {
             let route = Route::parse(active_nests, active_layouts, variant.clone())?;
 
             // add the route to the site map
-            if let Some(segment) = SiteMapSegment::new(&route.segments) {
+            let mut segment = SiteMapSegment::new(&route.segments);
+            if let RouteType::Child(child) = &route.ty {
+                let new_segment = SiteMapSegment {
+                    segment_type: SegmentType::Child(child.clone()),
+                    children: Vec::new(),
+                };
+                match &mut segment {
+                    Some(segment) => {
+                        fn set_last_child_to(
+                            segment: &mut SiteMapSegment,
+                            new_segment: SiteMapSegment,
+                        ) {
+                            if let Some(last) = segment.children.last_mut() {
+                                set_last_child_to(last, new_segment);
+                            } else {
+                                segment.children = vec![new_segment];
+                            }
+                        }
+                        set_last_child_to(segment, new_segment);
+                    }
+                    None => {
+                        segment = Some(new_segment);
+                    }
+                }
+            }
+
+            if let Some(segment) = segment {
                 let parent = site_map_stack.last_mut();
                 let children = match parent {
                     Some(parent) => &mut parent.last_mut().unwrap().children,
@@ -443,31 +471,13 @@ impl RouteEnum {
         let name = &self.name;
         let site_map = &self.site_map;
 
-        let mut layers = Vec::new();
-
-        loop {
-            let index = layers.len();
-            let mut routable_match = Vec::new();
+        let mut matches = Vec::new();
 
-            // Collect all routes that match the current layer
-            for route in &self.routes {
-                if let Some(matched) = route.routable_match(&self.layouts, &self.nests, index) {
-                    routable_match.push(matched);
-                }
-            }
-
-            // All routes are exhausted
-            if routable_match.is_empty() {
-                break;
-            }
-
-            layers.push(quote! {
-                #(#routable_match)*
-            });
+        // Collect all routes matches
+        for route in &self.routes {
+            matches.push(route.routable_match(&self.layouts, &self.nests));
         }
 
-        let index_iter = 0..layers.len();
-
         quote! {
             impl dioxus_router::routable::Routable for #name where Self: Clone {
                 const SITE_MAP: &'static [dioxus_router::routable::SiteMapSegment] = &[
@@ -476,15 +486,8 @@ impl RouteEnum {
 
                 fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
                     let myself = self.clone();
-                    match level {
-                        #(
-                            #index_iter => {
-                                match myself {
-                                    #layers
-                                    _ => None
-                                }
-                            },
-                        )*
+                    match (level, myself) {
+                        #(#matches)*
                         _ => None
                     }
                 }
@@ -521,14 +524,25 @@ impl SiteMapSegment {
 impl ToTokens for SiteMapSegment {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
         let segment_type = &self.segment_type;
-        let children = &self.children;
+        let children = if let SegmentType::Child(ty) = &self.segment_type {
+            quote! { #ty::SITE_MAP }
+        } else {
+            let children = self
+                .children
+                .iter()
+                .map(|child| child.to_token_stream())
+                .collect::<Vec<_>>();
+            quote! {
+                &[
+                    #(#children,)*
+                ]
+            }
+        };
 
         tokens.extend(quote! {
             dioxus_router::routable::SiteMapSegment {
                 segment_type: #segment_type,
-                children: &[
-                    #(#children,)*
-                ]
+                children: #children,
             }
         });
     }
@@ -538,6 +552,7 @@ enum SegmentType {
     Static(String),
     Dynamic(String),
     CatchAll(String),
+    Child(Type),
 }
 
 impl ToTokens for SegmentType {
@@ -552,6 +567,9 @@ impl ToTokens for SegmentType {
             SegmentType::CatchAll(s) => {
                 tokens.extend(quote! { dioxus_router::routable::SegmentType::CatchAll(#s) })
             }
+            SegmentType::Child(_) => {
+                tokens.extend(quote! { dioxus_router::routable::SegmentType::Child })
+            }
         }
     }
 }

+ 194 - 109
packages/router-macro/src/route.rs

@@ -3,6 +3,7 @@ use syn::parse::Parse;
 use syn::parse::ParseStream;
 use syn::parse_quote;
 use syn::Path;
+use syn::Type;
 use syn::{Ident, LitStr};
 
 use proc_macro2::TokenStream as TokenStream2;
@@ -40,18 +41,28 @@ impl Parse for RouteArgs {
     }
 }
 
+struct ChildArgs {
+    route: LitStr,
+}
+
+impl Parse for ChildArgs {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        let route = input.parse::<LitStr>()?;
+
+        Ok(ChildArgs { route })
+    }
+}
+
 #[derive(Debug)]
-pub struct Route {
+pub(crate) struct Route {
     pub route_name: Ident,
-    pub comp_name: Path,
-    pub props_name: Path,
+    pub ty: RouteType,
     pub route: String,
     pub segments: Vec<RouteSegment>,
     pub query: Option<QuerySegment>,
     pub nests: Vec<NestId>,
     pub layouts: Vec<LayoutId>,
-    pub variant: syn::Variant,
-    fields: syn::FieldsNamed,
+    fields: Vec<(Ident, Type)>,
 }
 
 impl Route {
@@ -63,62 +74,93 @@ impl Route {
         let route_attr = variant
             .attrs
             .iter()
-            .find(|attr| attr.path.is_ident("route"))
-            .ok_or_else(|| {
-                syn::Error::new_spanned(
-                    variant.clone(),
-                    "Routable variants must have a #[route(...)] attribute",
-                )
-            })?;
-
+            .find(|attr| attr.path.is_ident("route"));
+        let route;
+        let ty;
         let route_name = variant.ident.clone();
-        let args = route_attr.parse_args::<RouteArgs>()?;
-        let route = args.route.value();
-        let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
-        let props_name = args.props_name.unwrap_or_else(|| {
-            let last = format_ident!(
-                "{}Props",
-                comp_name.segments.last().unwrap().ident.to_string()
-            );
-            let mut segments = comp_name.segments.clone();
-            segments.pop();
-            segments.push(last.into());
-            Path {
-                leading_colon: None,
-                segments,
+        match route_attr {
+            Some(attr) => {
+                let args = attr.parse_args::<RouteArgs>()?;
+                let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
+                let props_name = args.props_name.unwrap_or_else(|| {
+                    let last = format_ident!(
+                        "{}Props",
+                        comp_name.segments.last().unwrap().ident.to_string()
+                    );
+                    let mut segments = comp_name.segments.clone();
+                    segments.pop();
+                    segments.push(last.into());
+                    Path {
+                        leading_colon: None,
+                        segments,
+                    }
+                });
+                ty = RouteType::Leaf {
+                    component: comp_name,
+                    props: props_name,
+                };
+                route = args.route.value();
             }
-        });
-
-        let named_fields = match &variant.fields {
-            syn::Fields::Named(fields) => fields,
-            _ => {
-                return Err(syn::Error::new_spanned(
-                    variant.clone(),
-                    "Routable variants must have named fields",
-                ))
+            None => {
+                if let Some(route_attr) = variant
+                    .attrs
+                    .iter()
+                    .find(|attr| attr.path.is_ident("child"))
+                {
+                    let args = route_attr.parse_args::<ChildArgs>()?;
+                    route = args.route.value();
+                    match &variant.fields {
+                        syn::Fields::Unnamed(fields) => {
+                            if fields.unnamed.len() != 1 {
+                                return Err(syn::Error::new_spanned(
+                                    variant.clone(),
+                                    "Routable variants with a #[child(...)] attribute must have exactly one field",
+                                ));
+                            }
+                            ty = RouteType::Child(fields.unnamed[0].ty.clone());
+                        }
+                        _ => {
+                            return Err(syn::Error::new_spanned(
+                                variant.clone(),
+                                "Routable variants with a #[child(...)] attribute must have exactly one field",
+                            ))
+                        }
+                    }
+                } else {
+                    return Err(syn::Error::new_spanned(
+                            variant.clone(),
+                            "Routable variants must either have a #[route(...)] attribute or a #[child(...)] attribute",
+                        ));
+                }
             }
         };
 
-        let (route_segments, query) = parse_route_segments(
-            variant.ident.span(),
-            named_fields
+        let fields = match &variant.fields {
+            syn::Fields::Named(fields) => fields
                 .named
                 .iter()
-                .map(|f| (f.ident.as_ref().unwrap(), &f.ty)),
-            &route,
-        )?;
+                .map(|f| (f.ident.clone().unwrap(), f.ty.clone()))
+                .collect(),
+            _ => Vec::new(),
+        };
+
+        let (route_segments, query) = {
+            parse_route_segments(
+                variant.ident.span(),
+                fields.iter().map(|f| (&f.0, &f.1)),
+                &route,
+            )?
+        };
 
         Ok(Self {
-            comp_name,
-            props_name,
+            ty,
             route_name,
             segments: route_segments,
             route,
             query,
             nests,
             layouts,
-            fields: named_fields.clone(),
-            variant,
+            fields,
         })
     }
 
@@ -129,100 +171,137 @@ impl Route {
         let write_segments = self.segments.iter().map(|s| s.write_segment());
         let write_query = self.query.as_ref().map(|q| q.write());
 
-        quote! {
-            Self::#name { #(#dynamic_segments,)* } => {
-                #(#write_layouts)*
-                #(#write_segments)*
-                #write_query
+        match &self.ty {
+            RouteType::Child(_) => {
+                quote! {
+                    Self::#name(child) => {
+                        use std::fmt::Display;
+                        #(#write_layouts)*
+                        #(#write_segments)*
+                        child.fmt(f);
+                    }
+                }
+            }
+            RouteType::Leaf { .. } => {
+                quote! {
+                    Self::#name { #(#dynamic_segments,)* } => {
+                        #(#write_layouts)*
+                        #(#write_segments)*
+                        #write_query
+                    }
+                }
             }
         }
     }
 
-    pub fn routable_match(
-        &self,
-        layouts: &[Layout],
-        nests: &[Nest],
-        index: usize,
-    ) -> Option<TokenStream2> {
+    pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 {
         let name = &self.route_name;
         let name_str = name.to_string();
-        let dynamic_segments = self.dynamic_segments();
 
-        match index.cmp(&self.layouts.len()) {
-            std::cmp::Ordering::Less => {
-                let layout = self.layouts[index];
-                let render_layout = layouts[layout.0].routable_match(nests);
-                // This is a layout
-                Some(quote! {
+        let mut tokens = TokenStream2::new();
+
+        // First match all layouts
+        for (idx, layout_id) in self.layouts.iter().copied().enumerate() {
+            let render_layout = layouts[layout_id.0].routable_match(nests);
+            let dynamic_segments = self.dynamic_segments();
+            // This is a layout
+            tokens.extend(quote! {
+                #[allow(unused)]
+                (#idx, Self::#name { #(#dynamic_segments,)* }) => {
+                    #render_layout
+                }
+            });
+        }
+
+        // Then match the route
+        let last_index = self.layouts.len();
+        tokens.extend(match &self.ty {
+            RouteType::Child(_) => {
+                quote! {
                     #[allow(unused)]
-                    Self::#name { #(#dynamic_segments,)* } => {
-                        #render_layout
+                    (#last_index.., Self::#name(child_route)) => {
+                        child_route.render(cx, level - #last_index)
                     }
-                })
+                }
             }
-            std::cmp::Ordering::Equal => {
+            RouteType::Leaf { component, props } => {
+                let dynamic_segments = self.dynamic_segments();
                 let dynamic_segments_from_route = self.dynamic_segments();
-                let props_name = &self.props_name;
-                let comp_name = &self.comp_name;
-                // This is the final route
-                Some(quote! {
+                quote! {
                     #[allow(unused)]
-                    Self::#name { #(#dynamic_segments,)* } => {
-                        let comp = #props_name { #(#dynamic_segments_from_route,)* };
-                        let dynamic = cx.component(#comp_name, comp, #name_str);
+                    (#last_index, Self::#name { #(#dynamic_segments,)* }) => {
+                        let comp = #props { #(#dynamic_segments_from_route,)* };
+                        let dynamic = cx.component(#component, comp, #name_str);
                         render! {
                             dynamic
                         }
                     }
-                })
+                }
             }
-            _ => None,
-        }
+        });
+
+        tokens
     }
 
     fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
-        self.fields.named.iter().map(|f| {
-            let name = f.ident.as_ref().unwrap();
+        self.fields.iter().map(|(name, _)| {
             quote! {#name}
         })
     }
 
     pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
-        let segments = self.fields.named.iter().map(|f| {
-            let name = f.ident.as_ref().unwrap();
+        match &self.ty {
+            RouteType::Child(ty) => {
+                let name = &self.route_name;
 
-            let mut from_route = false;
-
-            for id in &self.nests {
-                let nest = &nests[id.0];
-                if nest.dynamic_segments_names().any(|i| &i == name) {
-                    from_route = true
-                }
-            }
-            for segment in &self.segments {
-                if let RouteSegment::Dynamic(other, ..) = segment {
-                    if other == name {
-                        from_route = true
+                quote! {
+                    {
+                        let mut trailing = String::new();
+                        for seg in segments {
+                            trailing += seg;
+                            trailing += "/";
+                        }
+                        trailing.pop();
+                        #enum_name::#name(#ty::from_str(&trailing).unwrap())
                     }
                 }
             }
-            if let Some(query) = &self.query {
-                if &query.ident == name {
-                    from_route = true
-                }
-            }
+            RouteType::Leaf { .. } => {
+                let segments = self.fields.iter().map(|(name, _)| {
+                    let mut from_route = false;
 
-            if from_route {
-                quote! {#name}
-            } else {
-                quote! {#name: Default::default()}
-            }
-        });
-        let name = &self.route_name;
+                    for id in &self.nests {
+                        let nest = &nests[id.0];
+                        if nest.dynamic_segments_names().any(|i| &i == name) {
+                            from_route = true
+                        }
+                    }
+                    for segment in &self.segments {
+                        if let RouteSegment::Dynamic(other, ..) = segment {
+                            if other == name {
+                                from_route = true
+                            }
+                        }
+                    }
+                    if let Some(query) = &self.query {
+                        if &query.ident == name {
+                            from_route = true
+                        }
+                    }
 
-        quote! {
-            #enum_name::#name {
-                #(#segments,)*
+                    if from_route {
+                        quote! {#name}
+                    } else {
+                        quote! {#name: Default::default()}
+                    }
+                });
+                let name = &self.route_name;
+
+                quote! {
+                    #enum_name::#name {
+                        #(#segments,)*
+                    }
+                }
             }
         }
     }
@@ -244,3 +323,9 @@ impl Route {
         }
     }
 }
+
+#[derive(Debug)]
+pub(crate) enum RouteType {
+    Child(Type),
+    Leaf { component: Path, props: Path },
+}

+ 9 - 6
packages/router-macro/src/route_tree.rs

@@ -6,7 +6,7 @@ use syn::Ident;
 use crate::{
     nest::{Nest, NestId},
     redirect::Redirect,
-    route::Route,
+    route::{Route, RouteType},
     segment::{static_segment_idx, RouteSegment},
 };
 
@@ -334,11 +334,14 @@ impl<'a> RouteTreeSegmentData<'a> {
                 let construct_variant = route.construct(nests, enum_name);
                 let parse_query = route.parse_query();
 
-                let insure_not_trailing = route
-                    .segments
-                    .last()
-                    .map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _)))
-                    .unwrap_or(true);
+                let insure_not_trailing = match route.ty {
+                    RouteType::Leaf { .. } => route
+                        .segments
+                        .last()
+                        .map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _)))
+                        .unwrap_or(true),
+                    RouteType::Child(_) => false,
+                };
 
                 print_route_segment(
                     route_segments.peekable(),

+ 76 - 13
packages/router/examples/static_generation.rs

@@ -28,22 +28,25 @@ async fn main() {
     .invalidate_after(Duration::from_secs(10))
     .build();
 
+    println!(
+        "SITE MAP:\n{}",
+        Route::SITE_MAP
+            .iter()
+            .flat_map(|route| route.flatten().into_iter())
+            .map(|route| {
+                route
+                    .iter()
+                    .map(|segment| segment.to_string())
+                    .collect::<Vec<_>>()
+                    .join("")
+            })
+            .collect::<Vec<_>>()
+            .join("\n")
+    );
+
     pre_cache_static_routes::<Route, _>(&mut renderer)
         .await
         .unwrap();
-
-    for _ in 0..1_000_000 {
-        for id in 0..10 {
-            render_route(
-                &mut renderer,
-                Route::Post { id },
-                &mut tokio::io::sink(),
-                |_| {},
-            )
-            .await
-            .unwrap();
-        }
-    }
 }
 
 #[inline_props]
@@ -82,6 +85,64 @@ fn Home(cx: Scope) -> Element {
     }
 }
 
+mod inner {
+    use dioxus::prelude::*;
+    use dioxus_router::prelude::*;
+
+    #[rustfmt::skip]
+    #[derive(Clone, Debug, PartialEq, Routable)]
+    pub enum Route {
+        #[nest("/blog")]
+            #[route("/")]
+            Blog {},
+            #[route("/post/index")]
+            PostHome {},
+            #[route("/post/:id")]
+            Post {
+                id: usize,
+            },
+        #[end_nest]
+        #[route("/")]
+        Home {},
+    }
+
+    #[inline_props]
+    fn Blog(cx: Scope) -> Element {
+        render! {
+            div {
+                "Blog"
+            }
+        }
+    }
+
+    #[inline_props]
+    fn Post(cx: Scope, id: usize) -> Element {
+        render! {
+            div {
+                "PostId: {id}"
+            }
+        }
+    }
+
+    #[inline_props]
+    fn PostHome(cx: Scope) -> Element {
+        render! {
+            div {
+                "Post"
+            }
+        }
+    }
+
+    #[inline_props]
+    fn Home(cx: Scope) -> Element {
+        render! {
+            div {
+                "Home"
+            }
+        }
+    }
+}
+
 #[rustfmt::skip]
 #[derive(Clone, Debug, PartialEq, Routable)]
 enum Route {
@@ -97,4 +158,6 @@ enum Route {
     #[end_nest]
     #[route("/")]
     Home {},
+    #[child("/hello_world")]
+    HelloWorldRoute(inner::Route)
 }

+ 2 - 0
packages/router/src/incremental.rs

@@ -25,6 +25,7 @@ where
         let mut full_path = String::new();
         for segment in &route {
             match segment {
+                SegmentType::Child => {}
                 SegmentType::Static(s) => {
                     full_path += "/";
                     full_path += s;
@@ -43,6 +44,7 @@ where
                     render_route(renderer, route, &mut tokio::io::sink(), |_| {}).await?;
                 }
                 Err(e) => {
+                    log::info!("@ route: {}", full_path);
                     log::error!("Error pre-caching static route: {}", e);
                 }
             }

+ 3 - 0
packages/router/src/routable.rs

@@ -172,12 +172,15 @@ pub enum SegmentType {
     Dynamic(&'static str),
     /// A catch all route segment
     CatchAll(&'static str),
+    /// A child router
+    Child,
 }
 
 impl Display for SegmentType {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match &self {
             SegmentType::Static(s) => write!(f, "/{}", s),
+            SegmentType::Child => Ok(()),
             SegmentType::Dynamic(s) => write!(f, "/:{}", s),
             SegmentType::CatchAll(s) => write!(f, "/:...{}", s),
         }