Evan Almloff 2 роки тому
батько
коміт
a73873a571

+ 7 - 5
packages/router-core/tests/macro.rs

@@ -64,11 +64,13 @@ fn Route6(cx: Scope, extra: Vec<String>) -> Element {
 enum Route {
     #[route("/(dynamic)" Route1)]
     Route1 { dynamic: String },
-    #[route("/hello_world" Route2)]
-    Route2 {},
-    // #[redirect("/(dynamic)/hello_world")]
-    #[route("/hello_world/(dynamic)" Route3)]
-    Route3 { dynamic: u32 },
+    #[nest("/hello_world")]
+        #[route("/" Route2)]
+        Route2 {},
+        // #[redirect("/(dynamic)/hello_world")]
+        #[route("/(dynamic)" Route3)]
+        Route3 { dynamic: u32 },
+    #[end_nest]
     #[route("/(number1)/(number2)" Route4)]
     Route4 { number1: u32, number2: u32 },
     #[route("/?(query)" Route5)]

+ 13 - 4
packages/router-macro/src/lib.rs

@@ -1,5 +1,6 @@
 extern crate proc_macro;
 
+use nest::Nest;
 use proc_macro::TokenStream;
 use quote::{__private::Span, format_ident, quote, ToTokens};
 use route::Route;
@@ -14,7 +15,7 @@ mod route;
 mod route_tree;
 mod segment;
 
-#[proc_macro_derive(Routable, attributes(route))]
+#[proc_macro_derive(Routable, attributes(route, nest, end_nest))]
 pub fn derive_routable(input: TokenStream) -> TokenStream {
     let routes_enum = parse_macro_input!(input as syn::DeriveInput);
 
@@ -54,15 +55,23 @@ impl RouteEnum {
         if let syn::Data::Enum(data) = input.data {
             let mut routes = Vec::new();
 
-            let mut current_base_route = String::new();
+            let mut current_base_route = Vec::new();
 
             for variant in data.variants {
                 // Apply the any nesting attributes in order
                 for attr in &variant.attrs {
-                    if attr.path.is_ident("nest") {}
+                    if attr.path.is_ident("nest") {
+                        let nest: Nest = attr.parse_args()?;
+                        match nest {
+                            Nest::Static(s) => current_base_route.push(s),
+                            _ => todo!(),
+                        }
+                    } else if attr.path.is_ident("end_nest") {
+                        current_base_route.pop();
+                    }
                 }
 
-                let route = Route::parse(current_base_route.clone(), variant)?;
+                let route = Route::parse(current_base_route.join("/"), variant)?;
                 routes.push(route);
             }
 

+ 80 - 1
packages/router-macro/src/nest.rs

@@ -1 +1,80 @@
-struct Nest {}
+use quote::format_ident;
+use syn::{parse::Parse, Ident, LitStr, Variant};
+
+use crate::segment::RouteSegment;
+
+pub enum Nest {
+    Static(String),
+    Layout(Layout),
+}
+
+impl Parse for Nest {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        // First parse the route
+        let route: LitStr = input.parse()?;
+
+        if route.value().contains('(') {
+            // Then parse the layout name
+            let _ = input.parse::<syn::Token![,]>();
+            let layout_name: Ident = input.parse()?;
+
+            // Then parse the component name
+            let _ = input.parse::<syn::Token![,]>();
+            let comp: Variant = input.parse()?;
+
+            // Then parse the props name
+            let _ = input.parse::<syn::Token![,]>();
+            let props_name: Ident = input
+                .parse()
+                .unwrap_or_else(|_| format_ident!("{}Props", comp.ident.to_string()));
+
+            Ok(Self::Layout(Layout {
+                route: route.value(),
+                route_segments: Vec::new(),
+                layout_name,
+                comp,
+                props_name,
+            }))
+        } else {
+            Ok(Self::Static(route.value()))
+        }
+    }
+}
+
+struct Layout {
+    pub route: String,
+    pub route_segments: Vec<RouteSegment>,
+    pub layout_name: Ident,
+    pub comp: Variant,
+    pub props_name: Ident,
+}
+
+// #[derive(Clone, Debug, PartialEq, Routable)]
+// enum Route {
+//     // Each Variant is a route with a linked component, dynamic segments are defined with the syntax: (name) and the type is inferred from the field type. The type must implement FromStr
+//     #[route("/(dynamic)" Component1)]
+//     Route1 { dynamic: usize },
+//     // You can nest routes which makes all routes in the block relative to a parent route. Nested routes are flattened into the parent enum
+//     // Nest accepts a optional layout component. The layout component that wraps all children and renders them where the Outlet component is found. It can accept parameters from the nested route, just like a normal route
+//     #[nest("/(dynamic)" root_dynamic_segment Component { dynamic: String })]
+//         // If the component is not specified, the component is assumed to be at the path of the route (in this case /pages/hello_world.rs or /pages/hello_world/index.rs)
+//         #[route("/")]
+//         // You can opt out of a parent Layout
+//         #[layout(!Component)]
+//         Route2 {
+//             // implicitly adds
+//             // root_dynamic_segment: ComponentProps,
+//         },
+//     #[end_nest]
+//     // Queries are defined with the syntax: ?(name) and the type is inferred from the field type. The type must implement From<&str> (not FromStr because the query parsing must be infallible). The query part of the url is not included in the route path for file based routing. (in this case /pages/takes_query.rs or /pages/takes_query/index.rs)
+//     #[route("/takes_query?(dynamic)")]
+//     Route3 { dynamic: u32 },
+//     // Redirects are defined with the redirect attribute
+//     #[redirect("/old_hello_world/(dynamic)")]
+//     #[route("/hello_world/(dynamic)")]
+//     Route4 { dynamic: u32 },
+//     // members that can be parsed from all trailing segments are defined with the syntax: (...name) and the type is inferred from the field type. The type must implement FromSegments.
+//     // Because this route is defined after Route3, it will only be matched if Route3 does not match and it will act as a fallback
+//     #[route("/(...number2)")]
+//     Route5 { number1: u32, number2: u32 },
+// }

+ 3 - 113
packages/router-macro/src/route.rs

@@ -1,11 +1,12 @@
-use quote::{__private::Span, format_ident, quote, ToTokens};
+use quote::{format_ident, quote, ToTokens};
 use syn::parse::Parse;
 use syn::parse::ParseStream;
-use syn::{Ident, LitStr, Variant};
+use syn::{Ident, LitStr};
 
 use proc_macro2::TokenStream as TokenStream2;
 
 use crate::query::QuerySegment;
+use crate::segment::parse_route_segments;
 use crate::segment::RouteSegment;
 
 struct RouteArgs {
@@ -237,114 +238,3 @@ impl ToTokens for Route {
         ));
     }
 }
-
-fn parse_route_segments(
-    varient: &Variant,
-    route: &str,
-) -> syn::Result<(Vec<RouteSegment>, Option<QuerySegment>)> {
-    let mut route_segments = Vec::new();
-
-    let (route_string, query) = match route.rsplit_once('?') {
-        Some((route, query)) => (route, Some(query)),
-        None => (route, None),
-    };
-    let mut iterator = route_string.split('/');
-
-    // skip the first empty segment
-    let first = iterator.next();
-    if first != Some("") {
-        return Err(syn::Error::new_spanned(
-            varient,
-            format!(
-                "Routes should start with /. Error found in the route '{}'",
-                route
-            ),
-        ));
-    }
-
-    while let Some(segment) = iterator.next() {
-        if segment.starts_with('(') && segment.ends_with(')') {
-            let spread = segment.starts_with("(...");
-
-            let ident = if spread {
-                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 == ident,
-                None => false,
-            });
-
-            let ty = if let Some(field) = field {
-                field.ty.clone()
-            } else {
-                return Err(syn::Error::new_spanned(
-                    varient,
-                    format!(
-                        "Could not find a field with the name '{}' in the variant '{}'",
-                        ident, varient.ident
-                    ),
-                ));
-            };
-            if spread {
-                route_segments.push(RouteSegment::CatchAll(
-                    Ident::new(&ident, Span::call_site()),
-                    ty,
-                ));
-
-                if iterator.next().is_some() {
-                    return Err(syn::Error::new_spanned(
-                        route,
-                        "Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.",
-                    ));
-                } else {
-                    break;
-                }
-            } else {
-                route_segments.push(RouteSegment::Dynamic(
-                    Ident::new(&ident, Span::call_site()),
-                    ty,
-                ));
-            }
-        } else {
-            route_segments.push(RouteSegment::Static(segment.to_string()));
-        }
-    }
-
-    // check if the route has a query string
-    let parsed_query = match query {
-        Some(query) => {
-            if query.starts_with('(') && query.ends_with(')') {
-                let query_ident = Ident::new(&query[1..query.len() - 1], Span::call_site());
-                let field = varient.fields.iter().find(|field| match field.ident {
-                    Some(ref field_ident) => field_ident == &query_ident,
-                    None => false,
-                });
-
-                let ty = if let Some(field) = field {
-                    field.ty.clone()
-                } else {
-                    return Err(syn::Error::new_spanned(
-                        varient,
-                        format!(
-                            "Could not find a field with the name '{}' in the variant '{}'",
-                            query_ident, varient.ident
-                        ),
-                    ));
-                };
-
-                Some(QuerySegment {
-                    ident: query_ident,
-                    ty,
-                })
-            } else {
-                None
-            }
-        }
-        None => None,
-    };
-
-    Ok((route_segments, parsed_query))
-}

+ 115 - 2
packages/router-macro/src/segment.rs

@@ -1,7 +1,9 @@
 use quote::{format_ident, quote};
-use syn::{Ident, Type};
+use syn::{Ident, Type, Variant};
 
-use proc_macro2::TokenStream as TokenStream2;
+use proc_macro2::{Span, TokenStream as TokenStream2};
+
+use crate::query::QuerySegment;
 
 #[derive(Debug)]
 pub enum RouteSegment {
@@ -119,3 +121,114 @@ impl RouteSegment {
 pub fn static_segment_idx(idx: usize) -> Ident {
     format_ident!("StaticSegment{}ParseError", idx)
 }
+
+pub fn parse_route_segments(
+    varient: &Variant,
+    route: &str,
+) -> syn::Result<(Vec<RouteSegment>, Option<QuerySegment>)> {
+    let mut route_segments = Vec::new();
+
+    let (route_string, query) = match route.rsplit_once('?') {
+        Some((route, query)) => (route, Some(query)),
+        None => (route, None),
+    };
+    let mut iterator = route_string.split('/');
+
+    // skip the first empty segment
+    let first = iterator.next();
+    if first != Some("") {
+        return Err(syn::Error::new_spanned(
+            varient,
+            format!(
+                "Routes should start with /. Error found in the route '{}'",
+                route
+            ),
+        ));
+    }
+
+    while let Some(segment) = iterator.next() {
+        if segment.starts_with('(') && segment.ends_with(')') {
+            let spread = segment.starts_with("(...");
+
+            let ident = if spread {
+                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 == ident,
+                None => false,
+            });
+
+            let ty = if let Some(field) = field {
+                field.ty.clone()
+            } else {
+                return Err(syn::Error::new_spanned(
+                    varient,
+                    format!(
+                        "Could not find a field with the name '{}' in the variant '{}'",
+                        ident, varient.ident
+                    ),
+                ));
+            };
+            if spread {
+                route_segments.push(RouteSegment::CatchAll(
+                    Ident::new(&ident, Span::call_site()),
+                    ty,
+                ));
+
+                if iterator.next().is_some() {
+                    return Err(syn::Error::new_spanned(
+                        route,
+                        "Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.",
+                    ));
+                } else {
+                    break;
+                }
+            } else {
+                route_segments.push(RouteSegment::Dynamic(
+                    Ident::new(&ident, Span::call_site()),
+                    ty,
+                ));
+            }
+        } else {
+            route_segments.push(RouteSegment::Static(segment.to_string()));
+        }
+    }
+
+    // check if the route has a query string
+    let parsed_query = match query {
+        Some(query) => {
+            if query.starts_with('(') && query.ends_with(')') {
+                let query_ident = Ident::new(&query[1..query.len() - 1], Span::call_site());
+                let field = varient.fields.iter().find(|field| match field.ident {
+                    Some(ref field_ident) => field_ident == &query_ident,
+                    None => false,
+                });
+
+                let ty = if let Some(field) = field {
+                    field.ty.clone()
+                } else {
+                    return Err(syn::Error::new_spanned(
+                        varient,
+                        format!(
+                            "Could not find a field with the name '{}' in the variant '{}'",
+                            query_ident, varient.ident
+                        ),
+                    ));
+                };
+
+                Some(QuerySegment {
+                    ident: query_ident,
+                    ty,
+                })
+            } else {
+                None
+            }
+        }
+        None => None,
+    };
+
+    Ok((route_segments, parsed_query))
+}