|
@@ -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,100 +171,137 @@ 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;
|
|
|
|
|
|
- if from_route {
|
|
|
- quote! {#name}
|
|
|
- } else {
|
|
|
- quote! {#name: Default::default()}
|
|
|
- }
|
|
|
- });
|
|
|
- let name = &self.route_name;
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- 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 },
|
|
|
+}
|