diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index e1dae23ad..de250f9c5 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -13,13 +13,14 @@ use proc_macro2::TokenStream as TokenStream2; use crate::{layout::LayoutId, route_tree::RouteTree}; mod layout; +mod macro2; mod nest; mod query; mod route; mod route_tree; mod segment; -#[proc_macro_derive(Routable, attributes(route, nest, end_nest, layout, end_layout))] +#[proc_macro_derive(Routable)] pub fn routable(input: TokenStream) -> TokenStream { let routes_enum = parse_macro_input!(input as syn::ItemEnum); diff --git a/packages/router-macro/src/macro2.rs b/packages/router-macro/src/macro2.rs new file mode 100644 index 000000000..5d7ad7cce --- /dev/null +++ b/packages/router-macro/src/macro2.rs @@ -0,0 +1,286 @@ +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 }) + } +}