allow nested routes

This commit is contained in:
Evan Almloff committed 2023-06-26 16:08:53 -07:00
1 parent 6569827b97
commit 2da1f7faa7
6 files changed
+344 -170

No files matched your search

+59 -41
View File
@@ -5,9 +5,9 @@ use nest::{Nest, NestId};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{__private::Span, format_ident, quote, ToTokens}; use quote::{__private::Span, format_ident, quote, ToTokens};
use redirect::Redirect; use redirect::Redirect;
use route::Route; use route::{Route, RouteType};
use segment::RouteSegment; 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; use proc_macro2::TokenStream as TokenStream2;
@@ -73,11 +73,13 @@ mod segment;
/// #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})] /// #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
/// #[route("/:dynamic")] /// #[route("/:dynamic")]
/// Route3 { dynamic: String }, /// Route3 { dynamic: String },
/// #[child]
/// NestedRoute(NestedRoute),
/// } /// }
/// ``` /// ```
#[proc_macro_derive( #[proc_macro_derive(
Routable, 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 { pub fn routable(input: TokenStream) -> TokenStream {
let routes_enum = parse_macro_input!(input as syn::ItemEnum); let routes_enum = parse_macro_input!(input as syn::ItemEnum);
@@ -125,11 +127,11 @@ pub fn routable(input: TokenStream) -> TokenStream {
#error_type #error_type
#parse_impl
#display_impl #display_impl
#routable_impl #routable_impl
#parse_impl
} }
.into() .into()
} }
@@ -288,7 +290,33 @@ impl RouteEnum {
let route = Route::parse(active_nests, active_layouts, variant.clone())?; let route = Route::parse(active_nests, active_layouts, variant.clone())?;
// add the route to the site map // 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 parent = site_map_stack.last_mut();
let children = match parent { let children = match parent {
Some(parent) => &mut parent.last_mut().unwrap().children, Some(parent) => &mut parent.last_mut().unwrap().children,
@@ -443,31 +471,13 @@ impl RouteEnum {
let name = &self.name; let name = &self.name;
let site_map = &self.site_map; let site_map = &self.site_map;
let mut layers = Vec::new(); let mut matches = Vec::new();
loop { // Collect all routes matches
let index = layers.len(); for route in &self.routes {
let mut routable_match = Vec::new(); matches.push(route.routable_match(&self.layouts, &self.nests));
// 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)*
});
} }
let index_iter = 0..layers.len();
quote! { quote! {
impl dioxus_router::routable::Routable for #name where Self: Clone { impl dioxus_router::routable::Routable for #name where Self: Clone {
const SITE_MAP: &'static [dioxus_router::routable::SiteMapSegment] = &[ 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> { fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
let myself = self.clone(); let myself = self.clone();
match level { match (level, myself) {
#( #(#matches)*
#index_iter => {
match myself {
#layers
_ => None
}
},
)*
_ => None _ => None
} }
} }
@@ -521,14 +524,25 @@ impl SiteMapSegment {
impl ToTokens for SiteMapSegment { impl ToTokens for SiteMapSegment {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
let segment_type = &self.segment_type; 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! { tokens.extend(quote! {
dioxus_router::routable::SiteMapSegment { dioxus_router::routable::SiteMapSegment {
segment_type: #segment_type, segment_type: #segment_type,
children: &[ children: #children,
#(#children,)*
]
} }
}); });
} }
@@ -538,6 +552,7 @@ enum SegmentType {
Static(String), Static(String),
Dynamic(String), Dynamic(String),
CatchAll(String), CatchAll(String),
Child(Type),
} }
impl ToTokens for SegmentType { impl ToTokens for SegmentType {
@@ -552,6 +567,9 @@ impl ToTokens for SegmentType {
SegmentType::CatchAll(s) => { SegmentType::CatchAll(s) => {
tokens.extend(quote! { dioxus_router::routable::SegmentType::CatchAll(#s) }) tokens.extend(quote! { dioxus_router::routable::SegmentType::CatchAll(#s) })
} }
SegmentType::Child(_) => {
tokens.extend(quote! { dioxus_router::routable::SegmentType::Child })
}
} }
} }
} }
+195 -110
View File
@@ -3,6 +3,7 @@ use syn::parse::Parse;
use syn::parse::ParseStream; use syn::parse::ParseStream;
use syn::parse_quote; use syn::parse_quote;
use syn::Path; use syn::Path;
use syn::Type;
use syn::{Ident, LitStr}; use syn::{Ident, LitStr};
use proc_macro2::TokenStream as TokenStream2; 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)] #[derive(Debug)]
pub struct Route { pub(crate) struct Route {
pub route_name: Ident, pub route_name: Ident,
pub comp_name: Path, pub ty: RouteType,
pub props_name: Path,
pub route: String, pub route: String,
pub segments: Vec<RouteSegment>, pub segments: Vec<RouteSegment>,
pub query: Option<QuerySegment>, pub query: Option<QuerySegment>,
pub nests: Vec<NestId>, pub nests: Vec<NestId>,
pub layouts: Vec<LayoutId>, pub layouts: Vec<LayoutId>,
pub variant: syn::Variant, fields: Vec<(Ident, Type)>,
fields: syn::FieldsNamed,
} }
impl Route { impl Route {
@@ -63,62 +74,93 @@ impl Route {
let route_attr = variant let route_attr = variant
.attrs .attrs
.iter() .iter()
.find(|attr| attr.path.is_ident("route")) .find(|attr| attr.path.is_ident("route"));
.ok_or_else(|| { let route;
syn::Error::new_spanned( let ty;
variant.clone(),
"Routable variants must have a #[route(...)] attribute",
)
})?;
let route_name = variant.ident.clone(); let route_name = variant.ident.clone();
let args = route_attr.parse_args::<RouteArgs>()?; match route_attr {
let route = args.route.value(); Some(attr) => {
let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name)); let args = attr.parse_args::<RouteArgs>()?;
let props_name = args.props_name.unwrap_or_else(|| { let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
let last = format_ident!( let props_name = args.props_name.unwrap_or_else(|| {
"{}Props", let last = format_ident!(
comp_name.segments.last().unwrap().ident.to_string() "{}Props",
); comp_name.segments.last().unwrap().ident.to_string()
let mut segments = comp_name.segments.clone(); );
segments.pop(); let mut segments = comp_name.segments.clone();
segments.push(last.into()); segments.pop();
Path { segments.push(last.into());
leading_colon: None, Path {
segments, leading_colon: None,
segments,
}
});
ty = RouteType::Leaf {
component: comp_name,
props: props_name,
};
route = args.route.value();
} }
}); None => {
if let Some(route_attr) = variant
let named_fields = match &variant.fields { .attrs
syn::Fields::Named(fields) => fields, .iter()
_ => { .find(|attr| attr.path.is_ident("child"))
return Err(syn::Error::new_spanned( {
variant.clone(), let args = route_attr.parse_args::<ChildArgs>()?;
"Routable variants must have named fields", 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( let fields = match &variant.fields {
variant.ident.span(), syn::Fields::Named(fields) => fields
named_fields
.named .named
.iter() .iter()
.map(|f| (f.ident.as_ref().unwrap(), &f.ty)), .map(|f| (f.ident.clone().unwrap(), f.ty.clone()))
&route, .collect(),
)?; _ => Vec::new(),
};
let (route_segments, query) = {
parse_route_segments(
variant.ident.span(),
fields.iter().map(|f| (&f.0, &f.1)),
&route,
)?
};
Ok(Self { Ok(Self {
comp_name, ty,
props_name,
route_name, route_name,
segments: route_segments, segments: route_segments,
route, route,
query, query,
nests, nests,
layouts, layouts,
fields: named_fields.clone(), fields,
variant,
}) })
} }
@@ -129,101 +171,138 @@ impl Route {
let write_segments = self.segments.iter().map(|s| s.write_segment()); let write_segments = self.segments.iter().map(|s| s.write_segment());
let write_query = self.query.as_ref().map(|q| q.write()); let write_query = self.query.as_ref().map(|q| q.write());
quote! { match &self.ty {
Self::#name { #(#dynamic_segments,)* } => { RouteType::Child(_) => {
#(#write_layouts)* quote! {
#(#write_segments)* Self::#name(child) => {
#write_query 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( pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 {
&self,
layouts: &[Layout],
nests: &[Nest],
index: usize,
) -> Option<TokenStream2> {
let name = &self.route_name; let name = &self.route_name;
let name_str = name.to_string(); let name_str = name.to_string();
let dynamic_segments = self.dynamic_segments();
match index.cmp(&self.layouts.len()) { let mut tokens = TokenStream2::new();
std::cmp::Ordering::Less => {
let layout = self.layouts[index]; // First match all layouts
let render_layout = layouts[layout.0].routable_match(nests); for (idx, layout_id) in self.layouts.iter().copied().enumerate() {
// This is a layout let render_layout = layouts[layout_id.0].routable_match(nests);
Some(quote! { 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)] #[allow(unused)]
Self::#name { #(#dynamic_segments,)* } => { (#last_index.., Self::#name(child_route)) => {
#render_layout 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 dynamic_segments_from_route = self.dynamic_segments();
let props_name = &self.props_name; quote! {
let comp_name = &self.comp_name;
// This is the final route
Some(quote! {
#[allow(unused)] #[allow(unused)]
Self::#name { #(#dynamic_segments,)* } => { (#last_index, Self::#name { #(#dynamic_segments,)* }) => {
let comp = #props_name { #(#dynamic_segments_from_route,)* }; let comp = #props { #(#dynamic_segments_from_route,)* };
let dynamic = cx.component(#comp_name, comp, #name_str); let dynamic = cx.component(#component, comp, #name_str);
render! { render! {
dynamic dynamic
} }
} }
}) }
} }
_ => None, });
}
tokens
} }
fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ { fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
self.fields.named.iter().map(|f| { self.fields.iter().map(|(name, _)| {
let name = f.ident.as_ref().unwrap();
quote! {#name} quote! {#name}
}) })
} }
pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 { pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
let segments = self.fields.named.iter().map(|f| { match &self.ty {
let name = f.ident.as_ref().unwrap(); RouteType::Child(ty) => {
let name = &self.route_name;
let mut from_route = false; quote! {
{
for id in &self.nests { let mut trailing = String::new();
let nest = &nests[id.0]; for seg in segments {
if nest.dynamic_segments_names().any(|i| &i == name) { trailing += seg;
from_route = true trailing += "/";
} }
} trailing.pop();
for segment in &self.segments { #enum_name::#name(#ty::from_str(&trailing).unwrap())
if let RouteSegment::Dynamic(other, ..) = segment {
if other == name {
from_route = true
} }
} }
} }
if let Some(query) = &self.query { RouteType::Leaf { .. } => {
if &query.ident == name { let segments = self.fields.iter().map(|(name, _)| {
from_route = true 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
}
}
}
if let Some(query) = &self.query {
if &query.ident == name {
from_route = true
}
}
if from_route {
quote! {#name}
} else {
quote! {#name: Default::default()}
}
});
let name = &self.route_name;
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
View File
@@ -6,7 +6,7 @@ use syn::Ident;
use crate::{ use crate::{
nest::{Nest, NestId}, nest::{Nest, NestId},
redirect::Redirect, redirect::Redirect,
route::Route, route::{Route, RouteType},
segment::{static_segment_idx, RouteSegment}, segment::{static_segment_idx, RouteSegment},
}; };
@@ -334,11 +334,14 @@ impl<'a> RouteTreeSegmentData<'a> {
let construct_variant = route.construct(nests, enum_name); let construct_variant = route.construct(nests, enum_name);
let parse_query = route.parse_query(); let parse_query = route.parse_query();
let insure_not_trailing = route let insure_not_trailing = match route.ty {
.segments RouteType::Leaf { .. } => route
.last() .segments
.map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _))) .last()
.unwrap_or(true); .map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _)))
.unwrap_or(true),
RouteType::Child(_) => false,
};
print_route_segment( print_route_segment(
route_segments.peekable(), route_segments.peekable(),
+76 -13
View File
@@ -28,22 +28,25 @@ async fn main() {
.invalidate_after(Duration::from_secs(10)) .invalidate_after(Duration::from_secs(10))
.build(); .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) pre_cache_static_routes::<Route, _>(&mut renderer)
.await .await
.unwrap(); .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] #[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] #[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)] #[derive(Clone, Debug, PartialEq, Routable)]
enum Route { enum Route {
@@ -97,4 +158,6 @@ enum Route {
#[end_nest] #[end_nest]
#[route("/")] #[route("/")]
Home {}, Home {},
#[child("/hello_world")]
HelloWorldRoute(inner::Route)
} }
+2
View File
@@ -25,6 +25,7 @@ where
let mut full_path = String::new(); let mut full_path = String::new();
for segment in &route { for segment in &route {
match segment { match segment {
SegmentType::Child => {}
SegmentType::Static(s) => { SegmentType::Static(s) => {
full_path += "/"; full_path += "/";
full_path += s; full_path += s;
@@ -43,6 +44,7 @@ where
render_route(renderer, route, &mut tokio::io::sink(), |_| {}).await?; render_route(renderer, route, &mut tokio::io::sink(), |_| {}).await?;
} }
Err(e) => { Err(e) => {
log::info!("@ route: {}", full_path);
log::error!("Error pre-caching static route: {}", e); log::error!("Error pre-caching static route: {}", e);
} }
} }
+3
View File
@@ -172,12 +172,15 @@ pub enum SegmentType {
Dynamic(&'static str), Dynamic(&'static str),
/// A catch all route segment /// A catch all route segment
CatchAll(&'static str), CatchAll(&'static str),
/// A child router
Child,
} }
impl Display for SegmentType { impl Display for SegmentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self { match &self {
SegmentType::Static(s) => write!(f, "/{}", s), SegmentType::Static(s) => write!(f, "/{}", s),
SegmentType::Child => Ok(()),
SegmentType::Dynamic(s) => write!(f, "/:{}", s), SegmentType::Dynamic(s) => write!(f, "/:{}", s),
SegmentType::CatchAll(s) => write!(f, "/:...{}", s), SegmentType::CatchAll(s) => write!(f, "/:...{}", s),
} }