diff --git a/crates/yew_router_macro/src/lib.rs b/crates/yew_router_macro/src/lib.rs index 28b7e25..8c1cb73 100644 --- a/crates/yew_router_macro/src/lib.rs +++ b/crates/yew_router_macro/src/lib.rs @@ -77,7 +77,7 @@ mod switch; /// } /// ``` /// Check out the examples directory in the repository to see some more usages of the routing syntax. -#[proc_macro_derive(Switch, attributes(to, rest, end))] +#[proc_macro_derive(Switch, attributes(to, rest, end, state))] pub fn switch(tokens: TokenStream) -> TokenStream { let input: DeriveInput = parse_macro_input!(tokens as DeriveInput); @@ -100,3 +100,8 @@ pub fn rest(_: TokenStream, _: TokenStream) -> TokenStream { pub fn end(_: TokenStream, _: TokenStream) -> TokenStream { TokenStream::new() } + +#[proc_macro_attribute] +pub fn state(_: TokenStream, _: TokenStream) -> TokenStream { + TokenStream::new() +} diff --git a/crates/yew_router_macro/src/switch/attribute.rs b/crates/yew_router_macro/src/switch/attribute.rs index 2afce79..ef7d85a 100644 --- a/crates/yew_router_macro/src/switch/attribute.rs +++ b/crates/yew_router_macro/src/switch/attribute.rs @@ -86,3 +86,25 @@ impl AttrToken { } } } + + +pub fn get_attr_strings(attributes: Vec) -> impl Iterator { + attributes + .into_iter() + .filter_map(|attr: Attribute| attr.parse_meta().ok()) + .filter_map(|meta: Meta| match meta { + Meta::NameValue(mnv) => mnv + .path + .clone() + .get_ident() + .into_iter() + .map(|ident| ident.to_string()) + .next(), + Meta::Path(path) => path + .get_ident() + .into_iter() + .map(|ident| ident.to_string()) + .next(), + Meta::List(_) => None + }) +} \ No newline at end of file diff --git a/crates/yew_router_macro/src/switch/struct_impl/from_route_part.rs b/crates/yew_router_macro/src/switch/struct_impl/from_route_part.rs index 7690e86..0f6f28d 100644 --- a/crates/yew_router_macro/src/switch/struct_impl/from_route_part.rs +++ b/crates/yew_router_macro/src/switch/struct_impl/from_route_part.rs @@ -3,6 +3,7 @@ use crate::switch::SwitchItem; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use syn::{Field, Fields, Type}; +use crate::switch::attribute::get_attr_strings; pub struct FromRoutePart<'a>(pub &'a SwitchItem); @@ -36,55 +37,21 @@ impl<'a> ToTokens for FromRoutePart<'a> { fn build_struct_from_captures(ident: &Ident, fields: &Fields) -> TokenStream { match fields { Fields::Named(named_fields) => { - let (field_declarations, fields): (Vec<_>, Vec<_>) = named_fields + let fields: Vec = named_fields .named .iter() .filter_map(|field: &Field| { let field_ty: &Type = &field.ty; - field.ident.as_ref().map(|i| { - let key = i.to_string(); - (i, key, field_ty) + let is_state = get_attr_strings(field.attrs.clone()).any(|s| s.as_str() == "state"); + field.ident.as_ref().map(|field_name| { + let key = field_name.to_string(); + NamedField{field_name, key, field_ty, is_state} }) }) - .map(|(field_name, key, field_ty): (&Ident, String, &Type)| { - let field_decl = quote! { - let #field_name = { - let (v, s) = match captures.remove(#key) { - ::std::option::Option::Some(value) => { - <#field_ty as ::yew_router::Switch>::from_route_part( - value, - state, - ) - } - ::std::option::Option::None => { - ( - <#field_ty as ::yew_router::Switch>::key_not_available(), - state, - ) - } - }; - match v { - ::std::option::Option::Some(val) => { - state = s; // Set state for the next var. - val - }, - ::std::option::Option::None => return (::std::option::Option::None, s) // Failed - } - }; - }; - - (field_decl, field_name) - }) - .unzip(); - - quote! { - if let ::std::option::Option::Some(mut captures) = matcher - .capture_route_into_map(&route_string) - .ok() - .map(|x| x.1) - { - #(#field_declarations)* + .collect(); + return quote! { + if let ::std::option::Option::Some(mut captures) = matcher.capture_route_into_map(&route_string).ok().map(|x| x.1) { return ( ::std::option::Option::Some( #ident { @@ -93,52 +60,42 @@ fn build_struct_from_captures(ident: &Ident, fields: &Fields) -> TokenStream { ), state ); - } - } + }; + }; } Fields::Unnamed(unnamed_fields) => { - let (field_declarations, fields): (Vec<_>, Vec<_>) = unnamed_fields - .unnamed - .iter() - .enumerate() - .map(|(idx, f)| { - let field_ty = &f.ty; - let field_var_name = Ident::new(&format!("field_{}", idx), Span::call_site()); - let field_decl = quote! { - let #field_var_name = { - let (v, s) = match drain.next() { - ::std::option::Option::Some(value) => { - <#field_ty as ::yew_router::Switch>::from_route_part( - value, - state, - ) - }, - ::std::option::Option::None => { - ( - <#field_ty as ::yew_router::Switch>::key_not_available(), - state, - ) - } - }; - match v { - ::std::option::Option::Some(val) => { - state = s; // Set state for the next var. - val - }, - ::std::option::Option::None => return (::std::option::Option::None, s) // Failed + let fields = unnamed_fields.unnamed.iter().map(|f: &Field| { + let field_ty = &f.ty; + quote! { + { + let (v, s) = match drain.next() { + ::std::option::Option::Some(value) => { + <#field_ty as ::yew_router::Switch>::from_route_part( + value, + state, + ) + }, + ::std::option::Option::None => { + ( + <#field_ty as ::yew_router::Switch>::key_not_available(), + state, + ) } }; - }; - - (field_decl, field_var_name) - }) - .unzip(); + match v { + ::std::option::Option::Some(val) => { + state = s; // Set state for the next var. + val + }, + ::std::option::Option::None => return (::std::option::Option::None, s) // Failed + } + } + } + }); quote! { if let Some(mut captures) = matcher.capture_route_into_vec(&route_string).ok().map(|x| x.1) { let mut drain = captures.drain(..); - #(#field_declarations)* - return ( ::std::option::Option::Some( #ident( @@ -161,3 +118,54 @@ fn build_struct_from_captures(ident: &Ident, fields: &Fields) -> TokenStream { } } } + + +pub struct NamedField<'a> { + field_name: &'a Ident, + key: String, + field_ty: &'a Type, + is_state: bool +} + +impl <'a> ToTokens for NamedField<'a> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let NamedField { + field_name, key, field_ty, is_state + } = self; + // If the named field is marked as state, then just take the state, + // otherwise try to match the key in the captures group + if *is_state { + tokens.extend(quote!{ + #field_name: { + state.take().expect("Can't take twice") + } + }) + } else { + tokens.extend(quote! { + #field_name: { + let (v, s) = match captures.remove(#key) { + ::std::option::Option::Some(value) => { + <#field_ty as ::yew_router::Switch>::from_route_part( + value, + state, + ) + } + ::std::option::Option::None => { + ( + <#field_ty as ::yew_router::Switch>::key_not_available(), + state, + ) + } + }; + match v { + ::std::option::Option::Some(val) => { + state = s; // Set state for the next var. + val + }, + ::std::option::Option::None => return (::std::option::Option::None, s) // Failed + } + } + }) + } + } +} \ No newline at end of file diff --git a/tests/macro_test/src/lib.rs b/tests/macro_test/src/lib.rs index 8df83a9..eb294a0 100644 --- a/tests/macro_test/src/lib.rs +++ b/tests/macro_test/src/lib.rs @@ -453,6 +453,28 @@ mod tests { ) } + + #[test] + fn state_minimal() { + #[derive(Debug, Switch, Clone, PartialEq)] + #[to = "/hello"] + pub struct Test { + #[state] + blob: String + } + let route = Route { + route: "/hello".to_string(), + state: "lorem ipsum".to_string() + }; + let switched = Test::switch(route).expect("should produce item"); + assert_eq!( + switched, + Test { + blob: "lorem ipsum".to_string() + } + ) + } + mod fragment { use super::*;