From fbd333e334763f23fea2b90af71127cac89e4112 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 20 May 2023 16:32:48 -0500 Subject: [PATCH] implement layouts and outlets --- packages/router-core/src/router.rs | 144 ++++++-- packages/router-core/tests/macro.rs | 14 +- packages/router-core/tests/nested.rs | 35 ++ packages/router-macro/Cargo.toml | 1 + packages/router-macro/src/lib.rs | 205 +++++++++--- packages/router-macro/src/nest.rs | 169 +++++++--- packages/router-macro/src/query.rs | 4 + packages/router-macro/src/route.rs | 147 ++++++-- packages/router-macro/src/route_tree.rs | 423 ++++++++++++++++++------ packages/router-macro/src/segment.rs | 27 +- 10 files changed, 910 insertions(+), 259 deletions(-) create mode 100644 packages/router-core/tests/nested.rs diff --git a/packages/router-core/src/router.rs b/packages/router-core/src/router.rs index 561f7a133..6f8f5bbb8 100644 --- a/packages/router-core/src/router.rs +++ b/packages/router-core/src/router.rs @@ -1,7 +1,8 @@ +#![allow(non_snake_case)] use crate::history::HistoryProvider; use dioxus::prelude::*; -use std::str::FromStr; +use std::{cell::RefCell, rc::Rc, str::FromStr, sync::Arc}; #[derive(Debug, PartialEq)] pub struct RouteParseError { @@ -18,27 +19,38 @@ impl std::fmt::Display for RouteParseError { } } -struct Router -where - ::Err: std::fmt::Display, -{ - history: H, - route: R, +#[derive(Clone)] +pub struct Router { + subscribers: Rc>>, + update_any: Arc, + history: Rc, + route: Rc>>>, } -impl Router -where - ::Err: std::fmt::Display, -{ - fn new(history: H) -> Result { - let path = history.current_path(); - Ok(Self { - history, - route: R::from_str(path.as_str())?, - }) +impl Router { + fn set_route(&self, route: R) + where + R::Err: std::fmt::Display, + { + *self.route.borrow_mut() = Some(Rc::new(route)); + for subscriber in self.subscribers.borrow().iter() { + (self.update_any)(*subscriber); + } } } +fn use_router(cx: &ScopeState) -> &Router { + use_context(cx).unwrap() +} + +fn use_route(cx: &ScopeState) -> Rc { + let router = use_router(cx); + cx.use_hook(|| { + router.subscribers.borrow_mut().push(cx.scope_id()); + }); + router.route.borrow().clone().unwrap() +} + pub trait FromQuery { fn from_query(query: &str) -> Self; } @@ -104,25 +116,93 @@ impl> FromRouteSegments for I { pub struct RouterProps { pub current_route: String, } - -pub trait Routable: FromStr + std::fmt::Display + Clone +pub trait Routable: std::fmt::Display + std::str::FromStr + 'static where ::Err: std::fmt::Display, { - fn render(self, cx: &ScopeState) -> Element; + fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>; +} - fn comp(cx: Scope) -> Element - where - Self: 'static, - { - let router = Self::from_str(&cx.props.current_route); - match router { - Ok(router) => router.render(cx), - Err(err) => { - render! { - pre { - "{err}" - } +trait RoutableFactory { + type Err: std::fmt::Display; + type Routable: Routable + FromStr; +} + +impl RoutableFactory for R +where + ::Err: std::fmt::Display, +{ + type Err = ::Err; + type Routable = R; +} + +trait RouteRenderable: std::fmt::Display + 'static { + fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>; +} + +impl RouteRenderable for R +where + ::Err: std::fmt::Display, +{ + fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> { + self.render(cx, level) + } +} + +#[derive(Clone)] +struct OutletContext { + current_level: usize, +} + +fn use_outlet_context(cx: &ScopeState) -> &OutletContext { + let outlet_context = use_context(cx).unwrap(); + outlet_context +} + +impl OutletContext { + fn render(cx: &ScopeState) -> Element<'_> { + let outlet = use_outlet_context(cx); + let current_level = outlet.current_level; + cx.provide_context({ + OutletContext { + current_level: current_level + 1, + } + }); + + use_route(cx).render(cx, current_level) + } +} + +pub fn Outlet(cx: Scope) -> Element { + OutletContext::render(cx) +} + +pub fn Router( + cx: Scope, +) -> Element +where + ::Err: std::fmt::Display, +{ + let current_route = R::from_str(&cx.props.current_route); + let router = use_context_provider(cx, || Router { + subscribers: Rc::default(), + update_any: cx.schedule_update_any(), + history: Rc::::default(), + route: Rc::new(RefCell::new(None)), + }); + + use_context_provider(cx, || OutletContext { current_level: 1 }); + + match current_route { + Ok(current_route) => { + router.set_route(current_route); + + router.route.borrow().as_ref().unwrap().render(cx, 0) + } + Err(err) => { + render! { + pre { + "{err}" } } } diff --git a/packages/router-core/tests/macro.rs b/packages/router-core/tests/macro.rs index 5abcb63a0..a1450ebbf 100644 --- a/packages/router-core/tests/macro.rs +++ b/packages/router-core/tests/macro.rs @@ -59,12 +59,22 @@ fn Route6(cx: Scope, extra: Vec) -> Element { } } +#[inline_props] +fn Nested(cx: Scope, nested: String) -> Element { + render! { + div{ + "Nested: {nested:?}" + } + } +} + #[rustfmt::skip] -#[derive(Routable, Clone, Debug, PartialEq)] +#[routable] +#[derive(Clone, Debug, PartialEq)] enum Route { #[route("/(dynamic)" Route1)] Route1 { dynamic: String }, - #[nest("/hello_world")] + #[nest("/(nested)" nested { nested: String } Nested)] #[route("/" Route2)] Route2 {}, // #[redirect("/(dynamic)/hello_world")] diff --git a/packages/router-core/tests/nested.rs b/packages/router-core/tests/nested.rs new file mode 100644 index 000000000..a85d1a71f --- /dev/null +++ b/packages/router-core/tests/nested.rs @@ -0,0 +1,35 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_router_core::*; +use dioxus_router_macro::*; + +#[inline_props] +fn Route1(cx: Scope, dynamic: String) -> Element { + render! { + div{ + "Route1: {dynamic}" + } + } +} + +#[inline_props] +fn Nested(cx: Scope, nested: String) -> Element { + render! { + div{ + "Nested: {nested:?}" + } + } +} + +#[rustfmt::skip] +#[routable] +#[derive(Clone, Debug, PartialEq)] +enum Route { + #[nest("/(nested)" nested { nested: String } Nested)] + #[route("/(dynamic)" Route1)] + Route1 { dynamic: String }, + #[end_nest] + #[route("/(dynamic)" Route1)] + Route2 { dynamic: String }, +} diff --git a/packages/router-macro/Cargo.toml b/packages/router-macro/Cargo.toml index cae396da0..3b7a48aeb 100644 --- a/packages/router-macro/Cargo.toml +++ b/packages/router-macro/Cargo.toml @@ -18,6 +18,7 @@ proc-macro = true syn = { version = "1.0.11", features = ["extra-traits", "full"] } quote = "1.0" proc-macro2 = "1.0.56" +slab = "0.4" [features] default = [] diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index a707aba36..cfd2a3cc5 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -1,23 +1,25 @@ extern crate proc_macro; -use nest::Nest; +use nest::{Layout, Nest}; use proc_macro::TokenStream; use quote::{__private::Span, format_ident, quote, ToTokens}; use route::Route; -use route_tree::RouteTreeSegment; use syn::{parse_macro_input, Ident}; use proc_macro2::TokenStream as TokenStream2; +use crate::{nest::LayoutId, route_tree::RouteTree}; + mod nest; mod query; mod route; mod route_tree; mod segment; -#[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); +// #[proc_macro_derive(Routable, attributes(route, nest, end_nest))] +#[proc_macro_attribute] +pub fn routable(_: TokenStream, input: TokenStream) -> TokenStream { + let routes_enum = parse_macro_input!(input as syn::ItemEnum); let route_enum = match RouteEnum::parse(routes_enum) { Ok(route_enum) => route_enum, @@ -44,59 +46,104 @@ pub fn derive_routable(input: TokenStream) -> TokenStream { } struct RouteEnum { - route_name: Ident, + vis: syn::Visibility, + attrs: Vec, + name: Ident, routes: Vec, + layouts: Vec, } impl RouteEnum { - fn parse(input: syn::DeriveInput) -> syn::Result { - let name = &input.ident; + fn parse(data: syn::ItemEnum) -> syn::Result { + let name = &data.ident; - if let syn::Data::Enum(data) = input.data { - let mut routes = Vec::new(); + enum NestRef { + Static(String), + Dynamic { id: LayoutId }, + } - let mut current_base_route = Vec::new(); + let mut routes = 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") { - let nest: Nest = attr.parse_args()?; - match nest { - Nest::Static(s) => current_base_route.push(s), - _ => todo!(), + let mut layouts = Vec::new(); + + let mut nest_stack = 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") { + let nest: Nest = attr.parse_args()?; + let nest_ref = match nest { + Nest::Static(s) => NestRef::Static(s), + Nest::Layout(mut l) => { + // if there is a static nest before this, add it to the layout + let mut static_prefix = nest_stack + .iter() + // walk backwards and take all static nests + .rev() + .map_while(|nest| match nest { + NestRef::Static(s) => Some(s.clone()), + NestRef::Dynamic { .. } => None, + }) + .collect::>(); + // reverse the static prefix so it is in the correct order + static_prefix.reverse(); + + if !static_prefix.is_empty() { + l.add_static_prefix(&static_prefix.join("/")); + } + + let id = layouts.len(); + layouts.push(l); + NestRef::Dynamic { id: LayoutId(id) } } - } else if attr.path.is_ident("end_nest") { - current_base_route.pop(); - } + }; + nest_stack.push(nest_ref); + } else if attr.path.is_ident("end_nest") { + nest_stack.pop(); } - - let route = Route::parse(current_base_route.join("/"), variant)?; - routes.push(route); } - let myself = Self { - route_name: name.clone(), - routes, - }; + let mut trailing_static_route = nest_stack + .iter() + .rev() + .map_while(|nest| match nest { + NestRef::Static(s) => Some(s.clone()), + NestRef::Dynamic { .. } => None, + }) + .collect::>(); + trailing_static_route.reverse(); + let active_layouts = nest_stack + .iter() + .filter_map(|nest| match nest { + NestRef::Static(_) => None, + NestRef::Dynamic { id } => Some(*id), + }) + .collect::>(); - Ok(myself) - } else { - Err(syn::Error::new_spanned( - input.clone(), - "Routable can only be derived for enums", - )) + let route = Route::parse(trailing_static_route.join("/"), active_layouts, variant)?; + routes.push(route); } + + let myself = Self { + vis: data.vis, + attrs: data.attrs, + name: name.clone(), + routes, + layouts, + }; + + Ok(myself) } fn impl_display(&self) -> TokenStream2 { let mut display_match = Vec::new(); for route in &self.routes { - display_match.push(route.display_match()); + display_match.push(route.display_match(&self.layouts)); } - let name = &self.route_name; + let name = &self.name; quote! { impl std::fmt::Display for #name { @@ -111,13 +158,14 @@ impl RouteEnum { } fn parse_impl(&self) -> TokenStream2 { - let tree = RouteTreeSegment::build(&self.routes); - let name = &self.route_name; + let tree = RouteTree::new(&self.routes, &self.layouts); + let name = &self.name; - let error_name = format_ident!("{}MatchError", self.route_name); - let tokens = tree - .into_iter() - .map(|t| t.to_tokens(self.route_name.clone(), error_name.clone())); + let error_name = format_ident!("{}MatchError", self.name); + let tokens = tree.roots.iter().map(|&id| { + let route = tree.get(id).unwrap(); + route.to_tokens(&tree, self.name.clone(), error_name.clone(), &self.layouts) + }); quote! { impl<'a> TryFrom<&'a str> for #name { @@ -148,10 +196,7 @@ impl RouteEnum { } fn error_name(&self) -> Ident { - Ident::new( - &(self.route_name.to_string() + "MatchError"), - Span::call_site(), - ) + Ident::new(&(self.name.to_string() + "MatchError"), Span::call_site()) } fn error_type(&self) -> TokenStream2 { @@ -164,7 +209,7 @@ impl RouteEnum { for route in &self.routes { let route_name = &route.route_name; - let error_name = Ident::new(&format!("{}ParseError", route_name), Span::call_site()); + let error_name = route.error_ident(); let route_str = &route.route; error_variants.push(quote! { #route_name(#error_name) }); @@ -172,6 +217,17 @@ impl RouteEnum { type_defs.push(route.error_type()); } + for layout in &self.layouts { + let layout_name = &layout.layout_name; + + let error_name = layout.error_ident(); + let route_str = &layout.route; + + error_variants.push(quote! { #layout_name(#error_name) }); + display_match.push(quote! { Self::#layout_name(err) => write!(f, "Layout '{}' ('{}') did not match:\n{}", stringify!(#layout_name), #route_str, err)? }); + type_defs.push(layout.error_type()); + } + quote! { #(#type_defs)* @@ -192,17 +248,47 @@ impl RouteEnum { } fn routable_impl(&self) -> TokenStream2 { - let mut routable_match = Vec::new(); + let name = &self.name; - for route in &self.routes { - routable_match.push(route.routable_match()); + let mut layers = Vec::new(); + + loop { + let index = layers.len(); + let mut routable_match = Vec::new(); + + // Collect all routes that match the current layer + for route in &self.routes { + if let Some(matched) = route.routable_match(&self.layouts, index) { + routable_match.push(matched); + } + } + + // All routes are exhausted + if routable_match.is_empty() { + break; + } + + layers.push(quote! { + #(#routable_match)* + }); } + let index_iter = 0..layers.len(); + quote! { - impl Routable for Route { - fn render<'a>(self, cx: &'a ScopeState) -> Element<'a> { - match self { - #(#routable_match)* + impl Routable for #name where Self: Clone { + fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> { + let myself = self.clone(); + match level { + #( + #index_iter => { + match myself { + #layers + _ => panic!("Route::render called with invalid level {}", level), + } + }, + )* + _ => panic!("Route::render called with invalid level {}", level), } } } @@ -213,8 +299,17 @@ impl RouteEnum { impl ToTokens for RouteEnum { fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { let routes = &self.routes; + let vis = &self.vis; + let name = &self.name; + let attrs = &self.attrs; + let variants = routes.iter().map(|r| r.variant(&self.layouts)); tokens.extend(quote!( + #(#attrs)* + #vis enum #name { + #(#variants),* + } + #[path = "pages"] mod pages { #(#routes)* diff --git a/packages/router-macro/src/nest.rs b/packages/router-macro/src/nest.rs index 9e37a2c42..1dbe1ace4 100644 --- a/packages/router-macro/src/nest.rs +++ b/packages/router-macro/src/nest.rs @@ -1,7 +1,8 @@ -use quote::format_ident; -use syn::{parse::Parse, Ident, LitStr, Variant}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse::Parse, Ident, LitStr}; -use crate::segment::RouteSegment; +use crate::segment::{parse_route_segments, RouteSegment}; pub enum Nest { Static(String), @@ -12,28 +13,45 @@ impl Parse for Nest { fn parse(input: syn::parse::ParseStream) -> syn::Result { // First parse the route let route: LitStr = input.parse()?; + let is_dynamic = route.value().contains('('); - if route.value().contains('(') { + if !input.is_empty() || is_dynamic { // Then parse the layout name let _ = input.parse::(); - let layout_name: Ident = input.parse()?; + let layout_name: syn::Ident = input.parse()?; + let layout_fields: syn::FieldsNamed = input.parse()?; // Then parse the component name let _ = input.parse::(); - let comp: Variant = input.parse()?; + let comp: Ident = 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())); + .unwrap_or_else(|_| format_ident!("{}Props", comp.to_string())); + + let route_segments = + parse_route_segments(&layout_name, &layout_fields, &route.value())?.0; + for seg in &route_segments { + if let RouteSegment::CatchAll(name, _) = seg { + return Err(syn::Error::new_spanned( + name, + format!( + "Catch-all segments are not allowed in nested routes: {}", + route.value() + ), + )); + } + } Ok(Self::Layout(Layout { route: route.value(), - route_segments: Vec::new(), + segments: route_segments, layout_name, comp, props_name, + layout_fields, })) } else { Ok(Self::Static(route.value())) @@ -41,40 +59,111 @@ impl Parse for Nest { } } -struct Layout { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct LayoutId(pub usize); + +#[derive(Debug)] +pub struct Layout { pub route: String, - pub route_segments: Vec, + pub segments: Vec, pub layout_name: Ident, - pub comp: Variant, + pub layout_fields: syn::FieldsNamed, + pub comp: Ident, 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(!root_dynamic_segment)] -// 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 }, -// } +impl Layout { + pub fn add_static_prefix(&mut self, prefix: &str) { + self.route = format!("{}{}", prefix, self.route); + self.segments.push(RouteSegment::Static(prefix.to_string())); + } + + pub fn dynamic_segments(&self) -> impl Iterator + '_ { + self.segments + .iter() + .filter_map(|seg| seg.name()) + .map(|i| quote! {#i}) + } + + pub fn dynamic_segment_types(&self) -> impl Iterator + '_ { + self.segments + .iter() + .filter_map(|seg| seg.ty()) + .map(|ty| quote! {#ty}) + } + + pub fn write(&self) -> TokenStream { + let write_segments = self.segments.iter().map(|s| s.write_segment()); + + quote! { + { + #(#write_segments)* + } + } + } + + pub fn error_ident(&self) -> Ident { + format_ident!("{}LayoutParseError", self.layout_name) + } + + 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_core::router::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(()) + } + } + } + } + + pub fn routable_match(&self) -> TokenStream { + let props_name = &self.props_name; + let comp_name = &self.comp; + let dynamic_segments_from_route = self + .segments + .iter() + .filter_map(|seg| seg.name()) + .map(|seg| quote! { #seg }); + + quote! { + let comp = #props_name { #(#dynamic_segments_from_route,)* }; + let cx = cx.bump().alloc(Scoped { + props: cx.bump().alloc(comp), + scope: cx, + }); + #comp_name(cx) + } + } +} diff --git a/packages/router-macro/src/query.rs b/packages/router-macro/src/query.rs index e9683c553..87db30453 100644 --- a/packages/router-macro/src/query.rs +++ b/packages/router-macro/src/query.rs @@ -28,4 +28,8 @@ impl QuerySegment { pub fn name(&self) -> Ident { self.ident.clone() } + + pub fn ty(&self) -> &Type { + &self.ty + } } diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index a30ebf377..804f89c92 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -5,6 +5,8 @@ use syn::{Ident, LitStr}; use proc_macro2::TokenStream as TokenStream2; +use crate::nest::Layout; +use crate::nest::LayoutId; use crate::query::QuerySegment; use crate::segment::parse_route_segments; use crate::segment::RouteSegment; @@ -40,24 +42,30 @@ pub struct Route { pub comp_name: Ident, pub props_name: Ident, pub route: String, - pub route_segments: Vec, + pub segments: Vec, pub query: Option, + pub layouts: Vec, + pub variant: syn::Variant, } impl Route { - pub fn parse(root_route: String, input: syn::Variant) -> syn::Result { - let route_attr = input + pub fn parse( + root_route: String, + layouts: Vec, + variant: syn::Variant, + ) -> syn::Result { + let route_attr = variant .attrs .iter() .find(|attr| attr.path.is_ident("route")) .ok_or_else(|| { syn::Error::new_spanned( - input.clone(), + variant.clone(), "Routable variants must have a #[route(...)] attribute", ) })?; - let route_name = input.ident.clone(); + let route_name = variant.ident.clone(); let args = route_attr.parse_args::()?; let route = root_route + &args.route.value(); let file_based = args.comp_name.is_none(); @@ -68,53 +76,115 @@ impl Route { .props_name .unwrap_or_else(|| format_ident!("{}Props", comp_name)); - let (route_segments, query) = parse_route_segments(&input, &route)?; + 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", + )) + } + }; + + let (route_segments, query) = parse_route_segments(&variant.ident, named_fields, &route)?; Ok(Self { comp_name, props_name, route_name, - route_segments, + segments: route_segments, route, file_based, query, + layouts, + variant, }) } - pub fn display_match(&self) -> TokenStream2 { + pub fn display_match(&self, layouts: &[Layout]) -> TokenStream2 { let name = &self.route_name; - let dynamic_segments = self.dynamic_segments(); - let write_segments = self.route_segments.iter().map(|s| s.write_segment()); + let dynamic_segments = self.dynamic_segments(layouts); + let write_layouts = self.layouts.iter().map(|id| layouts[id.0].write()); + 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 } } } - pub fn routable_match(&self) -> TokenStream2 { + pub fn routable_match(&self, layouts: &[Layout], index: usize) -> Option { let name = &self.route_name; - let dynamic_segments: Vec<_> = self.dynamic_segments().collect(); - let props_name = &self.props_name; - let comp_name = &self.comp_name; + let dynamic_segments = self.dynamic_segments(layouts); - quote! { - Self::#name { #(#dynamic_segments,)* } => { - let comp = #props_name { #(#dynamic_segments,)* }; - let cx = cx.bump().alloc(Scoped { - props: cx.bump().alloc(comp), - scope: cx, - }); - #comp_name(cx) + match index.cmp(&self.layouts.len()) { + std::cmp::Ordering::Less => { + let layout = self.layouts[index]; + let render_layout = layouts[layout.0].routable_match(); + // This is a layout + Some(quote! { + #[allow(unused)] + Self::#name { #(#dynamic_segments,)* } => { + #render_layout + } + }) } + std::cmp::Ordering::Equal => { + let dynamic_segments_from_route = self.dynamic_segments_from_route(); + let props_name = &self.props_name; + let comp_name = &self.comp_name; + // This is the final route + Some(quote! { + #[allow(unused)] + Self::#name { #(#dynamic_segments,)* } => { + let comp = #props_name { #(#dynamic_segments_from_route,)* }; + let cx = cx.bump().alloc(Scoped { + props: cx.bump().alloc(comp), + scope: cx, + }); + #comp_name(cx) + } + }) + } + _ => None, } } - fn dynamic_segments(&self) -> impl Iterator + '_ { - let segments = self.route_segments.iter().filter_map(|seg| { + fn dynamic_segment_types<'a>( + &'a self, + layouts: &'a [Layout], + ) -> impl Iterator + 'a { + let layouts = self + .layouts + .iter() + .flat_map(|id| layouts[id.0].dynamic_segment_types()); + let segments = self.segments.iter().filter_map(|seg| { + let ty = seg.ty()?; + + Some(quote! { + #ty + }) + }); + let query = self + .query + .as_ref() + .map(|q| { + let ty = q.ty(); + quote! { + #ty + } + }) + .into_iter(); + + layouts.chain(segments.chain(query)) + } + + fn dynamic_segments_from_route(&self) -> impl Iterator + '_ { + let segments = self.segments.iter().filter_map(|seg| { seg.name().map(|name| { quote! { #name @@ -135,8 +205,21 @@ impl Route { segments.chain(query) } - pub fn construct(&self, enum_name: Ident) -> TokenStream2 { - let segments = self.dynamic_segments(); + fn dynamic_segments<'a>( + &'a self, + layouts: &'a [Layout], + ) -> impl Iterator + 'a { + let layouts = self + .layouts + .iter() + .flat_map(|id| layouts[id.0].dynamic_segments()); + let dynamic_segments = self.dynamic_segments_from_route(); + + layouts.chain(dynamic_segments) + } + + pub fn construct(&self, enum_name: Ident, layouts: &[Layout]) -> TokenStream2 { + let segments = self.dynamic_segments(layouts); let name = &self.route_name; quote! { @@ -156,7 +239,7 @@ impl Route { let mut error_variants = Vec::new(); let mut display_match = Vec::new(); - for (i, segment) in self.route_segments.iter().enumerate() { + for (i, segment) in self.segments.iter().enumerate() { let error_name = segment.error_name(i); match segment { RouteSegment::Static(index) => { @@ -205,6 +288,16 @@ impl Route { None => quote! {}, } } + + pub fn variant(&self, layouts: &[Layout]) -> TokenStream2 { + let name = &self.route_name; + let segments = self.dynamic_segments(layouts); + let types = self.dynamic_segment_types(layouts); + + quote! { + #name { #(#segments: #types,)* } + } + } } impl ToTokens for Route { diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs index 1a0fa1bdd..8edda2a37 100644 --- a/packages/router-macro/src/route_tree.rs +++ b/packages/router-macro/src/route_tree.rs @@ -1,94 +1,283 @@ use proc_macro2::TokenStream; use quote::quote; +use slab::Slab; use syn::Ident; use crate::{ + nest::Layout, route::Route, segment::{static_segment_idx, RouteSegment}, }; -// First deduplicate the routes by the static part of the route -#[derive(Debug)] -pub enum RouteTreeSegment<'a> { - Static { - index: usize, - segment: &'a str, - children: Vec>, - from_route: &'a Route, - }, - Dynamic(&'a Route), +#[derive(Debug, Clone, Default)] +pub struct RouteTree<'a> { + pub roots: Vec, + entries: Slab>, } -impl<'a> RouteTreeSegment<'a> { - pub fn build(routes: &'a [Route]) -> Vec> { - let routes = routes.iter().map(PartialRoute::new).collect(); - Self::construct(routes) +impl<'a> RouteTree<'a> { + pub fn get(&self, index: usize) -> Option<&RouteTreeSegmentData<'a>> { + self.entries.get(index) } - fn construct(routes: Vec>) -> Vec> { - let mut static_segments = Vec::new(); - let mut dyn_segments = Vec::new(); + pub fn get_mut(&mut self, element: usize) -> Option<&mut RouteTreeSegmentData<'a>> { + self.entries.get_mut(element) + } - // Add all routes we can to the tree + fn sort_children(&mut self) { + let mut old_roots = self.roots.clone(); + self.sort_ids(&mut old_roots); + self.roots = old_roots; + + for id in self.roots.clone() { + self.sort_children_of_id(id); + } + } + + fn sort_ids(&self, ids: &mut [usize]) { + ids.sort_by_key(|&seg| { + let seg = self.get(seg).unwrap(); + match seg { + RouteTreeSegmentData::Static { .. } => 0, + RouteTreeSegmentData::Layout { .. } => 1, + RouteTreeSegmentData::Route(_) => 1, + } + }); + } + + fn sort_children_of_id(&mut self, id: usize) { + // Sort segments so that all static routes are checked before dynamic routes + let mut children = self.children(id); + + self.sort_ids(&mut children); + + if let Some(old) = self.try_children_mut(id) { + old.clone_from(&children) + } + + for id in children { + self.sort_children_of_id(id); + } + } + + fn children(&self, element: usize) -> Vec { + let element = self.entries.get(element).unwrap(); + match element { + RouteTreeSegmentData::Static { children, .. } => children.clone(), + RouteTreeSegmentData::Layout { children, .. } => children.clone(), + _ => Vec::new(), + } + } + + fn try_children_mut(&mut self, element: usize) -> Option<&mut Vec> { + let element = self.entries.get_mut(element).unwrap(); + match element { + RouteTreeSegmentData::Static { children, .. } => Some(children), + RouteTreeSegmentData::Layout { children, .. } => Some(children), + _ => None, + } + } + + fn children_mut(&mut self, element: usize) -> &mut Vec { + self.try_children_mut(element) + .expect("Cannot get children of non static or layout segment") + } + + pub fn new(routes: &'a [Route], layouts: &'a [Layout]) -> Self { + let routes = routes + .iter() + .map(|route| RouteIter::new(route, layouts)) + .collect::>(); + + let mut myself = Self::default(); + myself.roots = myself.construct(routes); + myself.sort_children(); + + myself + } + + pub fn construct(&mut self, routes: Vec>) -> Vec { + let mut segments = Vec::new(); + + // Add all routes to the tree for mut route in routes { - match route.next_static_segment() { - // If there is a static segment, check if it already exists in the tree - Some((i, segment)) => { - let found = static_segments.iter_mut().find_map(|seg| match seg { - RouteTreeSegment::Static { - segment: s, - children, - .. - } => (s == &segment).then_some(children), - _ => None, - }); + let mut current_route: Option = None; - match found { - Some(children) => { - // If it does, add the route to the children of the segment - children.append(&mut RouteTreeSegment::construct(vec![route])) - } - None => { - // If it doesn't, add the route as a new segment - static_segments.push(RouteTreeSegment::Static { + // First add a layout if there is one + while let Some(layout) = route.next_layout() { + let segments_iter: std::slice::Iter = layout.segments.iter(); + + // Add all static segments of the layout + 'o: for (index, segment) in segments_iter.enumerate() { + match segment { + RouteSegment::Static(segment) => { + // Check if the segment already exists + { + // Either look for the segment in the current route or in the static segments + let segments = current_route + .map(|id| self.children(id)) + .unwrap_or_else(|| segments.clone()); + + for seg in segments.iter() { + let seg = self.get(*seg).unwrap(); + if let RouteTreeSegmentData::Static { + segment: s, + children, + .. + } = seg + { + if s == segment { + // If it does, just update the current route + current_route = children.last().cloned(); + continue 'o; + } + } + } + } + + let static_segment = RouteTreeSegmentData::Static { segment, - from_route: route.route, - children: RouteTreeSegment::construct(vec![route]), - index: i, - }) + children: Vec::new(), + error_variant: route.error_variant(), + index, + }; + + // If it doesn't, add the segment to the current route + let static_segment = self.entries.insert(static_segment); + + let current_children = current_route + .map(|id| self.children_mut(id)) + .unwrap_or_else(|| &mut segments); + current_children.push(static_segment); + } + // If there is a dynamic segment, stop adding static segments + RouteSegment::Dynamic(..) => break, + RouteSegment::CatchAll(..) => { + todo!("Catch all segments are not allowed in layouts") } } } - // If there is no static segment, add the route to the dynamic routes + + // Add the layout to the current route + let layout = RouteTreeSegmentData::Layout { + layout, + children: Vec::new(), + }; + + let layout = self.entries.insert(layout); + let segments = match current_route.and_then(|id| self.get_mut(id)) { + Some(RouteTreeSegmentData::Static { children, .. }) => children, + Some(_) => unreachable!(), + None => &mut segments, + }; + segments.push(layout); + + // Update the current route + current_route = segments.last().cloned(); + } + + match route.next_static_segment() { + // If there is a static segment, check if it already exists in the tree + Some((i, segment)) => { + let current_children = current_route + .map(|id| self.children(id)) + .unwrap_or_else(|| segments.clone()); + let found = current_children.iter().find_map(|&id| { + let seg = self.get(id).unwrap(); + match seg { + RouteTreeSegmentData::Static { segment: s, .. } => { + (s == &segment).then_some(id) + } + _ => None, + } + }); + + match found { + Some(id) => { + // If it exists, add the route to the children of the segment + let new_children = self.construct(vec![route]); + self.children_mut(id).extend(new_children.into_iter()); + } + None => { + // If it doesn't exist, add the route as a new segment + let data = RouteTreeSegmentData::Static { + segment, + error_variant: route.error_variant(), + children: self.construct(vec![route]), + index: i, + }; + let id = self.entries.insert(data); + let current_children_mut = current_route + .map(|id| self.children_mut(id)) + .unwrap_or_else(|| &mut segments); + current_children_mut.push(id); + } + } + } + // If there is no static segment, add the route to the current_route None => { - dyn_segments.push(RouteTreeSegment::Dynamic(route.route)); + let id = self + .entries + .insert(RouteTreeSegmentData::Route(route.route)); + let current_children_mut = current_route + .map(|id| self.children_mut(id)) + .unwrap_or_else(|| &mut segments); + current_children_mut.push(id); } } } - // All static routes are checked before dynamic routes - static_segments.append(&mut dyn_segments); - - static_segments + segments } } -impl<'a> RouteTreeSegment<'a> { - pub fn to_tokens(&self, enum_name: syn::Ident, error_enum_name: syn::Ident) -> TokenStream { +#[derive(Debug, Clone)] +pub struct StaticErrorVariant { + varient_parse_error: Ident, + enum_varient: Ident, +} + +// First deduplicate the routes by the static part of the route +#[derive(Debug, Clone)] +pub enum RouteTreeSegmentData<'a> { + Static { + segment: &'a str, + error_variant: StaticErrorVariant, + index: usize, + children: Vec, + }, + Layout { + layout: &'a Layout, + children: Vec, + }, + Route(&'a Route), +} + +impl<'a> RouteTreeSegmentData<'a> { + pub fn to_tokens( + &self, + tree: &RouteTree, + enum_name: syn::Ident, + error_enum_name: syn::Ident, + layouts: &[Layout], + ) -> TokenStream { match self { - RouteTreeSegment::Static { + RouteTreeSegmentData::Static { segment, children, index, - from_route, + error_variant: + StaticErrorVariant { + varient_parse_error, + enum_varient, + }, } => { - let varient_parse_error = from_route.error_ident(); - let enum_varient = &from_route.route_name; let error_ident = static_segment_idx(*index); - let children = children - .iter() - .map(|child| child.to_tokens(enum_name.clone(), error_enum_name.clone())); + let children = children.iter().map(|child| { + let child = tree.get(*child).unwrap(); + child.to_tokens(tree, enum_name.clone(), error_enum_name.clone(), layouts) + }); quote! { { @@ -104,52 +293,22 @@ impl<'a> RouteTreeSegment<'a> { } } } - RouteTreeSegment::Dynamic(route) => { + RouteTreeSegmentData::Route(route) => { // 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 = route.error_ident(); let enum_varient = &route.route_name; let route_segments = route - .route_segments + .segments .iter() .enumerate() .skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_))); - fn print_route_segment<'a, I: Iterator>( - mut s: std::iter::Peekable, - sucess_tokens: TokenStream, - error_enum_name: &Ident, - enum_varient: &Ident, - varient_parse_error: &Ident, - ) -> TokenStream { - if let Some((i, route)) = s.next() { - let children = print_route_segment( - s, - sucess_tokens, - error_enum_name, - enum_varient, - varient_parse_error, - ); - - route.try_parse( - i, - error_enum_name, - enum_varient, - varient_parse_error, - children, - ) - } else { - quote! { - #sucess_tokens - } - } - } - - let construct_variant = route.construct(enum_name); + let construct_variant = route.construct(enum_name, layouts); let parse_query = route.parse_query(); let insure_not_trailing = route - .route_segments + .segments .last() .map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _))) .unwrap_or(true); @@ -169,6 +328,63 @@ impl<'a> RouteTreeSegment<'a> { &varient_parse_error, ) } + Self::Layout { layout, children } => { + // 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: Ident = layout.error_ident(); + let enum_varient = &layout.layout_name; + + let route_segments = layout + .segments + .iter() + .enumerate() + .skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_))); + + let parse_children = children + .iter() + .map(|child| { + let child = tree.get(*child).unwrap(); + child.to_tokens(tree, enum_name.clone(), error_enum_name.clone(), layouts) + }) + .collect(); + + print_route_segment( + route_segments.peekable(), + parse_children, + &error_enum_name, + enum_varient, + &varient_parse_error, + ) + } + } + } +} + +fn print_route_segment<'a, I: Iterator>( + mut s: std::iter::Peekable, + sucess_tokens: TokenStream, + error_enum_name: &Ident, + enum_varient: &Ident, + varient_parse_error: &Ident, +) -> TokenStream { + if let Some((i, route)) = s.next() { + let children = print_route_segment( + s, + sucess_tokens, + error_enum_name, + enum_varient, + varient_parse_error, + ); + + route.try_parse( + i, + error_enum_name, + enum_varient, + varient_parse_error, + children, + ) + } else { + quote! { + #sucess_tokens } } } @@ -212,22 +428,34 @@ fn return_constructed( } } -struct PartialRoute<'a> { +pub struct RouteIter<'a> { route: &'a Route, + layouts: &'a [Layout], + layout_index: usize, static_segment_index: usize, } -impl<'a> PartialRoute<'a> { - fn new(route: &'a Route) -> Self { +impl<'a> RouteIter<'a> { + fn new(route: &'a Route, layouts: &'a [Layout]) -> Self { Self { route, + layouts, + layout_index: 0, static_segment_index: 0, } } + fn next_layout(&mut self) -> Option<&'a Layout> { + let idx = self.layout_index; + let layout_index = self.route.layouts.get(idx)?; + let layout = &self.layouts[layout_index.0]; + self.layout_index += 1; + Some(layout) + } + fn next_static_segment(&mut self) -> Option<(usize, &'a str)> { let idx = self.static_segment_index; - let segment = self.route.route_segments.get(idx)?; + let segment = self.route.segments.get(idx)?; match segment { RouteSegment::Static(segment) => { self.static_segment_index += 1; @@ -236,4 +464,11 @@ impl<'a> PartialRoute<'a> { _ => None, } } + + fn error_variant(&self) -> StaticErrorVariant { + StaticErrorVariant { + varient_parse_error: self.route.error_ident(), + enum_varient: self.route.route_name.clone(), + } + } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 4a146db0e..8feedfe03 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -1,5 +1,5 @@ use quote::{format_ident, quote}; -use syn::{Ident, Type, Variant}; +use syn::{Ident, Type}; use proc_macro2::{Span, TokenStream as TokenStream2}; @@ -21,6 +21,14 @@ impl RouteSegment { } } + pub fn ty(&self) -> Option<&Type> { + match self { + Self::Static(_) => None, + Self::Dynamic(_, ty) => Some(ty), + Self::CatchAll(_, ty) => Some(ty), + } + } + pub fn write_segment(&self) -> TokenStream2 { match self { Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; }, @@ -123,7 +131,8 @@ pub fn static_segment_idx(idx: usize) -> Ident { } pub fn parse_route_segments( - varient: &Variant, + route_name: &Ident, + fields: &syn::FieldsNamed, route: &str, ) -> syn::Result<(Vec, Option)> { let mut route_segments = Vec::new(); @@ -138,7 +147,7 @@ pub fn parse_route_segments( let first = iterator.next(); if first != Some("") { return Err(syn::Error::new_spanned( - varient, + route_name, format!( "Routes should start with /. Error found in the route '{}'", route @@ -156,7 +165,7 @@ pub fn parse_route_segments( segment[1..segment.len() - 1].to_string() }; - let field = varient.fields.iter().find(|field| match field.ident { + let field = fields.named.iter().find(|field| match field.ident { Some(ref field_ident) => *field_ident == ident, None => false, }); @@ -165,10 +174,10 @@ pub fn parse_route_segments( field.ty.clone() } else { return Err(syn::Error::new_spanned( - varient, + route_name, format!( "Could not find a field with the name '{}' in the variant '{}'", - ident, varient.ident + ident, route_name ), )); }; @@ -202,7 +211,7 @@ pub fn parse_route_segments( 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 { + let field = fields.named.iter().find(|field| match field.ident { Some(ref field_ident) => field_ident == &query_ident, None => false, }); @@ -211,10 +220,10 @@ pub fn parse_route_segments( field.ty.clone() } else { return Err(syn::Error::new_spanned( - varient, + route_name, format!( "Could not find a field with the name '{}' in the variant '{}'", - query_ident, varient.ident + query_ident, route_name ), )); };