From a73873a5715c7f46a6b67a3472bb32056f87fe99 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 14 May 2023 17:39:42 -0500 Subject: [PATCH] add static nesting --- packages/router-core/tests/macro.rs | 12 +-- packages/router-macro/src/lib.rs | 17 +++- packages/router-macro/src/nest.rs | 81 ++++++++++++++++++- packages/router-macro/src/route.rs | 116 +------------------------- packages/router-macro/src/segment.rs | 117 ++++++++++++++++++++++++++- 5 files changed, 218 insertions(+), 125 deletions(-) diff --git a/packages/router-core/tests/macro.rs b/packages/router-core/tests/macro.rs index f0709435c..5abcb63a0 100644 --- a/packages/router-core/tests/macro.rs +++ b/packages/router-core/tests/macro.rs @@ -64,11 +64,13 @@ fn Route6(cx: Scope, extra: Vec) -> 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)] diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index fb63a745a..a707aba36 100644 --- a/packages/router-macro/src/lib.rs +++ b/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); } diff --git a/packages/router-macro/src/nest.rs b/packages/router-macro/src/nest.rs index 882b1d715..80a2af058 100644 --- a/packages/router-macro/src/nest.rs +++ b/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 { + // First parse the route + let route: LitStr = input.parse()?; + + if route.value().contains('(') { + // Then parse the layout name + let _ = input.parse::(); + let layout_name: Ident = input.parse()?; + + // Then parse the component name + let _ = input.parse::(); + let comp: Variant = input.parse()?; + + // Then parse the props name + let _ = input.parse::(); + 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, + 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 }, +// } diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 0b956942e..a30ebf377 100644 --- a/packages/router-macro/src/route.rs +++ b/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, Option)> { - 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)) -} diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index e0aeda234..4a146db0e 100644 --- a/packages/router-macro/src/segment.rs +++ b/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, Option)> { + 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)) +}