* move history providers into a separate crate * start moving route providers into the renderers * clean up intoroutable * remove into routable * fix router tests * Provide history providers in each renderer * implement nested routers * move the lens out of the history crate * re-export dioxus history trait in the prelude * also re-export the history function * fix history doctests * some light cleanups --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
380 lines
13 KiB
Rust
380 lines
13 KiB
Rust
use quote::{format_ident, quote};
|
|
use syn::parse::Parse;
|
|
use syn::parse::ParseStream;
|
|
use syn::parse_quote;
|
|
use syn::Field;
|
|
use syn::Path;
|
|
use syn::Type;
|
|
use syn::{Ident, LitStr};
|
|
|
|
use proc_macro2::TokenStream as TokenStream2;
|
|
|
|
use crate::hash::HashFragment;
|
|
use crate::layout::Layout;
|
|
use crate::layout::LayoutId;
|
|
use crate::nest::Nest;
|
|
use crate::nest::NestId;
|
|
use crate::query::QuerySegment;
|
|
use crate::segment::create_error_type;
|
|
use crate::segment::parse_route_segments;
|
|
use crate::segment::RouteSegment;
|
|
|
|
struct RouteArgs {
|
|
route: LitStr,
|
|
comp_name: Option<Path>,
|
|
}
|
|
|
|
impl Parse for RouteArgs {
|
|
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
|
|
let route = input.parse::<LitStr>()?;
|
|
|
|
Ok(RouteArgs {
|
|
route,
|
|
comp_name: {
|
|
let _ = input.parse::<syn::Token![,]>();
|
|
input.parse().ok()
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
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(crate) struct Route {
|
|
pub route_name: Ident,
|
|
pub ty: RouteType,
|
|
pub route: String,
|
|
pub segments: Vec<RouteSegment>,
|
|
pub query: Option<QuerySegment>,
|
|
pub hash: Option<HashFragment>,
|
|
pub nests: Vec<NestId>,
|
|
pub layouts: Vec<LayoutId>,
|
|
fields: Vec<(Ident, Type)>,
|
|
}
|
|
|
|
impl Route {
|
|
pub fn parse(
|
|
nests: Vec<NestId>,
|
|
layouts: Vec<LayoutId>,
|
|
variant: syn::Variant,
|
|
) -> syn::Result<Self> {
|
|
let route_attr = variant
|
|
.attrs
|
|
.iter()
|
|
.find(|attr| attr.path().is_ident("route"));
|
|
let route;
|
|
let ty;
|
|
let route_name = variant.ident.clone();
|
|
match route_attr {
|
|
Some(attr) => {
|
|
let args = attr.parse_args::<RouteArgs>()?;
|
|
let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
|
|
ty = RouteType::Leaf {
|
|
component: comp_name,
|
|
};
|
|
route = args.route.value();
|
|
}
|
|
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::Named(fields) => {
|
|
// find either a field with #[child] or a field named "child"
|
|
let child_field = fields.named.iter().find(|f| {
|
|
f.attrs
|
|
.iter()
|
|
.any(|attr| attr.path().is_ident("child"))
|
|
|| *f.ident.as_ref().unwrap() == "child"
|
|
});
|
|
match child_field{
|
|
Some(child) => {
|
|
ty = RouteType::Child(child.clone());
|
|
}
|
|
None => {
|
|
return Err(syn::Error::new_spanned(
|
|
variant.clone(),
|
|
"Routable variants with a #[child(..)] attribute must have a field named \"child\" or a field with a #[child] attribute",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(syn::Error::new_spanned(
|
|
variant.clone(),
|
|
"Routable variants with a #[child(..)] attribute must have named fields",
|
|
))
|
|
}
|
|
}
|
|
} else {
|
|
return Err(syn::Error::new_spanned(
|
|
variant.clone(),
|
|
"Routable variants must either have a #[route(..)] attribute or a #[child(..)] attribute",
|
|
));
|
|
}
|
|
}
|
|
};
|
|
|
|
let fields = match &variant.fields {
|
|
syn::Fields::Named(fields) => fields
|
|
.named
|
|
.iter()
|
|
.filter_map(|f| {
|
|
if let RouteType::Child(child) = &ty {
|
|
if f.ident == child.ident {
|
|
return None;
|
|
}
|
|
}
|
|
Some((f.ident.clone().unwrap(), f.ty.clone()))
|
|
})
|
|
.collect(),
|
|
_ => Vec::new(),
|
|
};
|
|
|
|
let (route_segments, query, hash) = {
|
|
parse_route_segments(
|
|
variant.ident.span(),
|
|
fields.iter().map(|f| (&f.0, &f.1)),
|
|
&route,
|
|
)?
|
|
};
|
|
|
|
Ok(Self {
|
|
ty,
|
|
route_name,
|
|
segments: route_segments,
|
|
route,
|
|
query,
|
|
hash,
|
|
nests,
|
|
layouts,
|
|
fields,
|
|
})
|
|
}
|
|
|
|
pub fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
|
|
let name = &self.route_name;
|
|
let dynamic_segments = self.dynamic_segments();
|
|
let write_query: Option<TokenStream2> = self.query.as_ref().map(|q| q.write());
|
|
let write_hash = self.hash.as_ref().map(|q| q.write());
|
|
|
|
match &self.ty {
|
|
RouteType::Child(field) => {
|
|
let write_nests = self.nests.iter().map(|id| nests[id.0].write());
|
|
let write_segments = self.segments.iter().map(|s| s.write_segment());
|
|
let child = field.ident.as_ref().unwrap();
|
|
quote! {
|
|
Self::#name { #(#dynamic_segments,)* #child } => {
|
|
use std::fmt::Display;
|
|
use std::fmt::Write;
|
|
let mut route = String::new();
|
|
{
|
|
let f = &mut route;
|
|
#(#write_nests)*
|
|
#(#write_segments)*
|
|
}
|
|
if route.ends_with('/') {
|
|
route.pop();
|
|
}
|
|
f.write_str(&route)?;
|
|
#child.fmt(f)?;
|
|
}
|
|
}
|
|
}
|
|
RouteType::Leaf { .. } => {
|
|
let write_nests = self.nests.iter().map(|id| nests[id.0].write());
|
|
let write_segments = self.segments.iter().map(|s| s.write_segment());
|
|
quote! {
|
|
Self::#name { #(#dynamic_segments,)* } => {
|
|
#(#write_nests)*
|
|
#(#write_segments)*
|
|
#write_query
|
|
#write_hash
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 {
|
|
let name = &self.route_name;
|
|
|
|
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();
|
|
let mut field_name = None;
|
|
if let RouteType::Child(field) = &self.ty {
|
|
field_name = field.ident.as_ref();
|
|
}
|
|
let field_name = field_name.map(|f| quote!(#f,));
|
|
// This is a layout
|
|
tokens.extend(quote! {
|
|
#[allow(unused)]
|
|
(#idx, Self::#name { #(#dynamic_segments,)* #field_name .. }) => {
|
|
#render_layout
|
|
}
|
|
});
|
|
}
|
|
|
|
// Then match the route
|
|
let last_index = self.layouts.len();
|
|
tokens.extend(match &self.ty {
|
|
RouteType::Child(field) => {
|
|
let field_name = field.ident.as_ref().unwrap();
|
|
quote! {
|
|
#[allow(unused)]
|
|
(#last_index.., Self::#name { #field_name, .. }) => {
|
|
rsx! {
|
|
dioxus_router::components::child_router::ChildRouter {
|
|
route: #field_name,
|
|
// Try to parse the current route as a parent route, and then match it as a child route
|
|
parse_route_from_root_route: |__route| if let Ok(__route) = __route.parse() {
|
|
if let Self::#name { #field_name, .. } = __route {
|
|
Some(#field_name)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
},
|
|
// Try to parse the child route and turn it into a parent route
|
|
format_route_as_root_route: |#field_name| Self::#name { #field_name: #field_name }.to_string(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
RouteType::Leaf { component } => {
|
|
let dynamic_segments = self.dynamic_segments();
|
|
let dynamic_segments_from_route = self.dynamic_segments();
|
|
quote! {
|
|
#[allow(unused)]
|
|
(#last_index, Self::#name { #(#dynamic_segments,)* }) => {
|
|
rsx! {
|
|
#component {
|
|
#(#dynamic_segments_from_route: #dynamic_segments_from_route,)*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
tokens
|
|
}
|
|
|
|
fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
|
|
self.fields.iter().map(|(name, _)| {
|
|
quote! {#name}
|
|
})
|
|
}
|
|
|
|
pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
|
|
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 segment.name().as_ref() == Some(name) {
|
|
from_route = true
|
|
}
|
|
}
|
|
if let Some(query) = &self.query {
|
|
if query.contains_ident(name) {
|
|
from_route = true
|
|
}
|
|
}
|
|
if let Some(hash) = &self.hash {
|
|
if hash.contains_ident(name) {
|
|
from_route = true
|
|
}
|
|
}
|
|
|
|
if from_route {
|
|
quote! {#name}
|
|
} else {
|
|
quote! {#name: Default::default()}
|
|
}
|
|
});
|
|
match &self.ty {
|
|
RouteType::Child(field) => {
|
|
let name = &self.route_name;
|
|
let child_name = field.ident.as_ref().unwrap();
|
|
|
|
quote! {
|
|
#enum_name::#name {
|
|
#child_name,
|
|
#(#segments,)*
|
|
}
|
|
}
|
|
}
|
|
RouteType::Leaf { .. } => {
|
|
let name = &self.route_name;
|
|
|
|
quote! {
|
|
#enum_name::#name {
|
|
#(#segments,)*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn error_ident(&self) -> Ident {
|
|
format_ident!("{}ParseError", self.route_name)
|
|
}
|
|
|
|
pub fn error_type(&self) -> TokenStream2 {
|
|
let error_name = self.error_ident();
|
|
let child_type = match &self.ty {
|
|
RouteType::Child(field) => Some(&field.ty),
|
|
RouteType::Leaf { .. } => None,
|
|
};
|
|
|
|
create_error_type(&self.route, error_name, &self.segments, child_type)
|
|
}
|
|
|
|
pub fn parse_query(&self) -> TokenStream2 {
|
|
match &self.query {
|
|
Some(query) => query.parse(),
|
|
None => quote! {},
|
|
}
|
|
}
|
|
|
|
pub fn parse_hash(&self) -> TokenStream2 {
|
|
match &self.hash {
|
|
Some(hash) => hash.parse(),
|
|
None => quote! {},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum RouteType {
|
|
Child(Field),
|
|
Leaf { component: Path },
|
|
}
|