wip: add router
This commit is contained in:
1 parent
9874f342e2
commit
d298b626d3
17 files changed
+617
-20
No files matched your search
@@ -5,6 +5,7 @@ use syn::parse_macro_input;
|
||||
pub(crate) mod htm;
|
||||
pub(crate) mod ifmt;
|
||||
pub(crate) mod props;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod rsx;
|
||||
|
||||
#[proc_macro]
|
||||
@@ -82,12 +83,9 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
||||
/// }}
|
||||
///
|
||||
/// // Matching
|
||||
/// // Matching will throw a Rust error about "no two closures are the same type"
|
||||
/// // To fix this, call "render" method or use the "in" syntax to produce VNodes.
|
||||
/// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
|
||||
/// {match true {
|
||||
/// true => rsx!(cx, h1 {"Top text"}),
|
||||
/// false => cx.render(rsx!( h1 {"Bottom text"}))
|
||||
/// true => rsx!(h1 {"Top text"}),
|
||||
/// false => rsx!(h1 {"Bottom text"})
|
||||
/// }}
|
||||
///
|
||||
/// // Conditional rendering
|
||||
@@ -95,11 +93,11 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
||||
/// // You can convert a bool condition to rsx! with .then and .or
|
||||
/// {true.then(|| rsx!(div {}))}
|
||||
///
|
||||
/// // True conditions need to be rendered (same reasons as matching)
|
||||
/// // True conditions
|
||||
/// {if true {
|
||||
/// rsx!(cx, h1 {"Top text"})
|
||||
/// rsx!(h1 {"Top text"})
|
||||
/// } else {
|
||||
/// rsx!(cx, h1 {"Bottom text"})
|
||||
/// rsx!(h1 {"Bottom text"})
|
||||
/// }}
|
||||
///
|
||||
/// // returning "None" is a bit noisy... but rare in practice
|
||||
@@ -186,3 +184,31 @@ pub fn rsx(s: TokenStream) -> TokenStream {
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive macro used to mark an enum as Routable.
|
||||
///
|
||||
/// This macro can only be used on enums. Every varient of the macro needs to be marked
|
||||
/// with the `at` attribute to specify the URL of the route. It generates an implementation of
|
||||
/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route`
|
||||
/// component.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use yew_router::Routable;
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Routable)]
|
||||
/// enum Routes {
|
||||
/// #[at("/")]
|
||||
/// Home,
|
||||
/// #[at("/secure")]
|
||||
/// Secure,
|
||||
/// #[at("/404")]
|
||||
/// NotFound,
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(Routable, attributes(at, not_found))]
|
||||
pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
use router::{routable_derive_impl, Routable};
|
||||
let input = parse_macro_input!(input as Routable);
|
||||
routable_derive_impl(input).into()
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
|
||||
|
||||
const AT_ATTR_IDENT: &str = "at";
|
||||
const NOT_FOUND_ATTR_IDENT: &str = "not_found";
|
||||
|
||||
pub struct Routable {
|
||||
ident: Ident,
|
||||
ats: Vec<LitStr>,
|
||||
variants: Punctuated<Variant, syn::token::Comma>,
|
||||
not_found_route: Option<Ident>,
|
||||
}
|
||||
|
||||
impl Parse for Routable {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let DeriveInput { ident, data, .. } = input.parse()?;
|
||||
|
||||
let data = match data {
|
||||
Data::Enum(data) => data,
|
||||
Data::Struct(s) => {
|
||||
return Err(syn::Error::new(
|
||||
s.struct_token.span(),
|
||||
"expected enum, found struct",
|
||||
))
|
||||
}
|
||||
Data::Union(u) => {
|
||||
return Err(syn::Error::new(
|
||||
u.union_token.span(),
|
||||
"expected enum, found union",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let (not_found_route, ats) = parse_variants_attributes(&data.variants)?;
|
||||
|
||||
Ok(Self {
|
||||
ident,
|
||||
variants: data.variants,
|
||||
ats,
|
||||
not_found_route,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_variants_attributes(
|
||||
variants: &Punctuated<Variant, syn::token::Comma>,
|
||||
) -> syn::Result<(Option<Ident>, Vec<LitStr>)> {
|
||||
let mut not_founds = vec![];
|
||||
let mut ats: Vec<LitStr> = vec![];
|
||||
|
||||
let mut not_found_attrs = vec![];
|
||||
|
||||
for variant in variants.iter() {
|
||||
if let Fields::Unnamed(ref field) = variant.fields {
|
||||
return Err(syn::Error::new(
|
||||
field.span(),
|
||||
"only named fields are supported",
|
||||
));
|
||||
}
|
||||
|
||||
let attrs = &variant.attrs;
|
||||
let at_attrs = attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path.is_ident(AT_ATTR_IDENT))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let attr = match at_attrs.len() {
|
||||
1 => *at_attrs.first().unwrap(),
|
||||
0 => {
|
||||
return Err(syn::Error::new(
|
||||
variant.span(),
|
||||
format!(
|
||||
"{} attribute must be present on every variant",
|
||||
AT_ATTR_IDENT
|
||||
),
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
quote! { #(#at_attrs)* },
|
||||
format!("only one {} attribute must be present", AT_ATTR_IDENT),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let lit = attr.parse_args::<LitStr>()?;
|
||||
ats.push(lit);
|
||||
|
||||
for attr in attrs.iter() {
|
||||
if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) {
|
||||
not_found_attrs.push(attr);
|
||||
not_founds.push(variant.ident.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if not_founds.len() > 1 {
|
||||
return Err(syn::Error::new_spanned(
|
||||
quote! { #(#not_found_attrs)* },
|
||||
format!("there can only be one {}", NOT_FOUND_ATTR_IDENT),
|
||||
));
|
||||
}
|
||||
|
||||
Ok((not_founds.into_iter().next(), ats))
|
||||
}
|
||||
|
||||
impl Routable {
|
||||
fn build_from_path(&self) -> TokenStream {
|
||||
let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
let ident = &variant.ident;
|
||||
let right = match &variant.fields {
|
||||
Fields::Unit => quote! { Self::#ident },
|
||||
Fields::Named(field) => {
|
||||
let fields = field.named.iter().map(|it| {
|
||||
//named fields have idents
|
||||
it.ident.as_ref().unwrap()
|
||||
});
|
||||
quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } }
|
||||
}
|
||||
Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
};
|
||||
|
||||
let left = self.ats.get(i).unwrap();
|
||||
quote! {
|
||||
#left => ::std::option::Option::Some(#right)
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option<Self> {
|
||||
match path {
|
||||
#(#from_path_matches),*,
|
||||
_ => ::std::option::Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_to_path(&self) -> TokenStream {
|
||||
let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
let ident = &variant.ident;
|
||||
let mut right = self.ats.get(i).unwrap().value();
|
||||
|
||||
match &variant.fields {
|
||||
Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
|
||||
Fields::Named(field) => {
|
||||
let fields = field
|
||||
.named
|
||||
.iter()
|
||||
.map(|it| it.ident.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for field in fields.iter() {
|
||||
// :param -> {param}
|
||||
// so we can pass it to `format!("...", param)`
|
||||
right = right.replace(&format!(":{}", field), &format!("{{{}}}", field))
|
||||
}
|
||||
|
||||
quote! {
|
||||
Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*)
|
||||
}
|
||||
}
|
||||
Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn to_path(&self) -> ::std::string::String {
|
||||
match self {
|
||||
#(#to_path_matches),*,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routable_derive_impl(input: Routable) -> TokenStream {
|
||||
let Routable {
|
||||
ats,
|
||||
not_found_route,
|
||||
ident,
|
||||
..
|
||||
} = &input;
|
||||
|
||||
let from_path = input.build_from_path();
|
||||
let to_path = input.build_to_path();
|
||||
|
||||
let not_found_route = match not_found_route {
|
||||
Some(route) => quote! { ::std::option::Option::Some(Self::#route) },
|
||||
None => quote! { ::std::option::Option::None },
|
||||
};
|
||||
|
||||
let cache_thread_local_ident = Ident::new(
|
||||
&format!("__{}_ROUTER_CURRENT_ROUTE_CACHE", ident),
|
||||
ident.span(),
|
||||
);
|
||||
|
||||
quote! {
|
||||
::std::thread_local! {
|
||||
#[doc(hidden)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None);
|
||||
}
|
||||
|
||||
#[automatically_derived]
|
||||
impl ::dioxus::router::Routable for #ident {
|
||||
#from_path
|
||||
#to_path
|
||||
|
||||
fn routes() -> ::std::vec::Vec<&'static str> {
|
||||
::std::vec![#(#ats),*]
|
||||
}
|
||||
|
||||
fn not_found_route() -> ::std::option::Option<Self> {
|
||||
#not_found_route
|
||||
}
|
||||
|
||||
fn current_route() -> ::std::option::Option<Self> {
|
||||
#cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
|
||||
}
|
||||
|
||||
fn recognize(pathname: &str) -> ::std::option::Option<Self> {
|
||||
::std::thread_local! {
|
||||
static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
|
||||
}
|
||||
let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
|
||||
{
|
||||
let route = ::std::clone::Clone::clone(&route);
|
||||
#cache_thread_local_ident.with(move |val| {
|
||||
*val.borrow_mut() = route;
|
||||
});
|
||||
}
|
||||
route
|
||||
}
|
||||
|
||||
fn cleanup() {
|
||||
#cache_thread_local_ident.with(move |val| {
|
||||
*val.borrow_mut() = ::std::option::Option::None;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,8 @@ impl ToTokens for CallBody {
|
||||
match &self.custom_context {
|
||||
// The `in cx` pattern allows directly rendering
|
||||
Some(ident) => out_tokens.append_all(quote! {
|
||||
#ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory| -> VNode {
|
||||
#ident.render(NodeFactory::annotate_lazy(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
|
||||
#inner
|
||||
}))
|
||||
}),
|
||||
|
||||
Reference in new issue
Block a user