allow nested routes
This commit is contained in:
1 parent
6569827b97
commit
2da1f7faa7
6 files changed
+346
-172
No files matched your search
+195
-110
@@ -3,6 +3,7 @@ use syn::parse::Parse;
|
||||
use syn::parse::ParseStream;
|
||||
use syn::parse_quote;
|
||||
use syn::Path;
|
||||
use syn::Type;
|
||||
use syn::{Ident, LitStr};
|
||||
|
||||
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)]
|
||||
pub struct Route {
|
||||
pub(crate) struct Route {
|
||||
pub route_name: Ident,
|
||||
pub comp_name: Path,
|
||||
pub props_name: Path,
|
||||
pub ty: RouteType,
|
||||
pub route: String,
|
||||
pub segments: Vec<RouteSegment>,
|
||||
pub query: Option<QuerySegment>,
|
||||
pub nests: Vec<NestId>,
|
||||
pub layouts: Vec<LayoutId>,
|
||||
pub variant: syn::Variant,
|
||||
fields: syn::FieldsNamed,
|
||||
fields: Vec<(Ident, Type)>,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
@@ -63,62 +74,93 @@ impl Route {
|
||||
let route_attr = variant
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|attr| attr.path.is_ident("route"))
|
||||
.ok_or_else(|| {
|
||||
syn::Error::new_spanned(
|
||||
variant.clone(),
|
||||
"Routable variants must have a #[route(...)] attribute",
|
||||
)
|
||||
})?;
|
||||
|
||||
.find(|attr| attr.path.is_ident("route"));
|
||||
let route;
|
||||
let ty;
|
||||
let route_name = variant.ident.clone();
|
||||
let args = route_attr.parse_args::<RouteArgs>()?;
|
||||
let route = args.route.value();
|
||||
let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
|
||||
let props_name = args.props_name.unwrap_or_else(|| {
|
||||
let last = format_ident!(
|
||||
"{}Props",
|
||||
comp_name.segments.last().unwrap().ident.to_string()
|
||||
);
|
||||
let mut segments = comp_name.segments.clone();
|
||||
segments.pop();
|
||||
segments.push(last.into());
|
||||
Path {
|
||||
leading_colon: None,
|
||||
segments,
|
||||
match route_attr {
|
||||
Some(attr) => {
|
||||
let args = attr.parse_args::<RouteArgs>()?;
|
||||
let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
|
||||
let props_name = args.props_name.unwrap_or_else(|| {
|
||||
let last = format_ident!(
|
||||
"{}Props",
|
||||
comp_name.segments.last().unwrap().ident.to_string()
|
||||
);
|
||||
let mut segments = comp_name.segments.clone();
|
||||
segments.pop();
|
||||
segments.push(last.into());
|
||||
Path {
|
||||
leading_colon: None,
|
||||
segments,
|
||||
}
|
||||
});
|
||||
ty = RouteType::Leaf {
|
||||
component: comp_name,
|
||||
props: props_name,
|
||||
};
|
||||
route = args.route.value();
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
))
|
||||
None => {
|
||||
if let Some(route_attr) = variant
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|attr| attr.path.is_ident("child"))
|
||||
{
|
||||
let args = route_attr.parse_args::<ChildArgs>()?;
|
||||
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(
|
||||
variant.ident.span(),
|
||||
named_fields
|
||||
let fields = match &variant.fields {
|
||||
syn::Fields::Named(fields) => fields
|
||||
.named
|
||||
.iter()
|
||||
.map(|f| (f.ident.as_ref().unwrap(), &f.ty)),
|
||||
&route,
|
||||
)?;
|
||||
.map(|f| (f.ident.clone().unwrap(), f.ty.clone()))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let (route_segments, query) = {
|
||||
parse_route_segments(
|
||||
variant.ident.span(),
|
||||
fields.iter().map(|f| (&f.0, &f.1)),
|
||||
&route,
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
comp_name,
|
||||
props_name,
|
||||
ty,
|
||||
route_name,
|
||||
segments: route_segments,
|
||||
route,
|
||||
query,
|
||||
nests,
|
||||
layouts,
|
||||
fields: named_fields.clone(),
|
||||
variant,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -129,101 +171,138 @@ impl Route {
|
||||
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
|
||||
match &self.ty {
|
||||
RouteType::Child(_) => {
|
||||
quote! {
|
||||
Self::#name(child) => {
|
||||
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(
|
||||
&self,
|
||||
layouts: &[Layout],
|
||||
nests: &[Nest],
|
||||
index: usize,
|
||||
) -> Option<TokenStream2> {
|
||||
pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 {
|
||||
let name = &self.route_name;
|
||||
let name_str = name.to_string();
|
||||
let dynamic_segments = self.dynamic_segments();
|
||||
|
||||
match index.cmp(&self.layouts.len()) {
|
||||
std::cmp::Ordering::Less => {
|
||||
let layout = self.layouts[index];
|
||||
let render_layout = layouts[layout.0].routable_match(nests);
|
||||
// This is a layout
|
||||
Some(quote! {
|
||||
let mut tokens = TokenStream2::new();
|
||||
|
||||
// First match all layouts
|
||||
for (idx, layout_id) in self.layouts.iter().copied().enumerate() {
|
||||
let render_layout = layouts[layout_id.0].routable_match(nests);
|
||||
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)]
|
||||
Self::#name { #(#dynamic_segments,)* } => {
|
||||
#render_layout
|
||||
(#last_index.., Self::#name(child_route)) => {
|
||||
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 props_name = &self.props_name;
|
||||
let comp_name = &self.comp_name;
|
||||
// This is the final route
|
||||
Some(quote! {
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
Self::#name { #(#dynamic_segments,)* } => {
|
||||
let comp = #props_name { #(#dynamic_segments_from_route,)* };
|
||||
let dynamic = cx.component(#comp_name, comp, #name_str);
|
||||
(#last_index, Self::#name { #(#dynamic_segments,)* }) => {
|
||||
let comp = #props { #(#dynamic_segments_from_route,)* };
|
||||
let dynamic = cx.component(#component, comp, #name_str);
|
||||
render! {
|
||||
dynamic
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
|
||||
self.fields.named.iter().map(|f| {
|
||||
let name = f.ident.as_ref().unwrap();
|
||||
self.fields.iter().map(|(name, _)| {
|
||||
quote! {#name}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
|
||||
let segments = self.fields.named.iter().map(|f| {
|
||||
let name = f.ident.as_ref().unwrap();
|
||||
match &self.ty {
|
||||
RouteType::Child(ty) => {
|
||||
let name = &self.route_name;
|
||||
|
||||
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
|
||||
quote! {
|
||||
{
|
||||
let mut trailing = String::new();
|
||||
for seg in segments {
|
||||
trailing += seg;
|
||||
trailing += "/";
|
||||
}
|
||||
trailing.pop();
|
||||
#enum_name::#name(#ty::from_str(&trailing).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(query) = &self.query {
|
||||
if &query.ident == name {
|
||||
from_route = true
|
||||
RouteType::Leaf { .. } => {
|
||||
let segments = self.fields.iter().map(|(name, _)| {
|
||||
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 },
|
||||
}
|
||||
Reference in new issue
Block a user