This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_router_macro/
routable_derive.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::parse::{Parse, ParseStream};
4use syn::punctuated::Punctuated;
5use syn::spanned::Spanned;
6use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
7
8const AT_ATTR_IDENT: &str = "at";
9const NOT_FOUND_ATTR_IDENT: &str = "not_found";
10
11pub struct Routable {
12    ident: Ident,
13    ats: Vec<LitStr>,
14    variants: Punctuated<Variant, syn::token::Comma>,
15    not_found_route: Option<Ident>,
16}
17
18impl Parse for Routable {
19    fn parse(input: ParseStream) -> syn::Result<Self> {
20        let DeriveInput { ident, data, .. } = input.parse()?;
21
22        let data = match data {
23            Data::Enum(data) => data,
24            Data::Struct(s) => {
25                return Err(syn::Error::new(
26                    s.struct_token.span(),
27                    "expected enum, found struct",
28                ))
29            }
30            Data::Union(u) => {
31                return Err(syn::Error::new(
32                    u.union_token.span(),
33                    "expected enum, found union",
34                ))
35            }
36        };
37
38        let (not_found_route, ats) = parse_variants_attributes(&data.variants)?;
39
40        Ok(Self {
41            ident,
42            variants: data.variants,
43            ats,
44            not_found_route,
45        })
46    }
47}
48
49fn parse_variants_attributes(
50    variants: &Punctuated<Variant, syn::token::Comma>,
51) -> syn::Result<(Option<Ident>, Vec<LitStr>)> {
52    let mut not_founds = vec![];
53    let mut ats: Vec<LitStr> = vec![];
54
55    let mut not_found_attrs = vec![];
56
57    for variant in variants.iter() {
58        if let Fields::Unnamed(ref field) = variant.fields {
59            return Err(syn::Error::new(
60                field.span(),
61                "only named fields are supported",
62            ));
63        }
64
65        let attrs = &variant.attrs;
66        let at_attrs = attrs
67            .iter()
68            .filter(|attr| attr.path().is_ident(AT_ATTR_IDENT))
69            .collect::<Vec<_>>();
70
71        let attr = match at_attrs.len() {
72            1 => *at_attrs.first().unwrap(),
73            0 => {
74                return Err(syn::Error::new(
75                    variant.span(),
76                    format!("{AT_ATTR_IDENT} attribute must be present on every variant"),
77                ))
78            }
79            _ => {
80                return Err(syn::Error::new_spanned(
81                    quote! { #(#at_attrs)* },
82                    format!("only one {AT_ATTR_IDENT} attribute must be present"),
83                ))
84            }
85        };
86
87        let lit = attr.parse_args::<LitStr>()?;
88        let val = lit.value();
89
90        if val.find('#').is_some() {
91            return Err(syn::Error::new_spanned(
92                lit,
93                "You cannot use `#` in your routes. Please consider `HashRouter` instead.",
94            ));
95        }
96
97        if !val.starts_with('/') {
98            return Err(syn::Error::new_spanned(
99                lit,
100                "relative paths are not supported at this moment.",
101            ));
102        }
103
104        ats.push(lit);
105
106        for attr in attrs.iter() {
107            if attr.path().is_ident(NOT_FOUND_ATTR_IDENT) {
108                not_found_attrs.push(attr);
109                not_founds.push(variant.ident.clone())
110            }
111        }
112    }
113
114    if not_founds.len() > 1 {
115        return Err(syn::Error::new_spanned(
116            quote! { #(#not_found_attrs)* },
117            format!("there can only be one {NOT_FOUND_ATTR_IDENT}"),
118        ));
119    }
120
121    Ok((not_founds.into_iter().next(), ats))
122}
123
124impl Routable {
125    fn build_from_path(&self) -> TokenStream {
126        let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
127            let ident = &variant.ident;
128            let right = match &variant.fields {
129                Fields::Unit => quote! { Self::#ident },
130                Fields::Named(field) => {
131                    let fields = field.named.iter().map(|it| {
132                        // named fields have idents
133                        it.ident.as_ref().unwrap()
134                    });
135                    quote! { Self::#ident { #(#fields: {
136                        let param = params.get(stringify!(#fields))?;
137                        let param = &*::yew_router::__macro::decode_for_url(param).ok()?;
138                        let param = param.parse().ok()?;
139                        param
140                    },)* } }
141                }
142                Fields::Unnamed(_) => unreachable!(), // already checked
143            };
144
145            let left = self.ats.get(i).unwrap();
146            quote! {
147                #left => ::std::option::Option::Some(#right)
148            }
149        });
150
151        quote! {
152            fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option<Self> {
153                match path {
154                    #(#from_path_matches),*,
155                    _ => ::std::option::Option::None,
156                }
157            }
158        }
159    }
160
161    fn build_to_path(&self) -> TokenStream {
162        let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
163            let ident = &variant.ident;
164            let mut right = self.ats.get(i).unwrap().value();
165
166            match &variant.fields {
167                Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
168                Fields::Named(field) => {
169                    let fields = field
170                        .named
171                        .iter()
172                        .map(|it| it.ident.as_ref().unwrap())
173                        .collect::<Vec<_>>();
174
175                    for field in fields.iter() {
176                        // :param -> {param}
177                        // *param -> {param}
178                        // so we can pass it to `format!("...", param)`
179                        right = right.replace(&format!(":{field}"), &format!("{{{field}}}"));
180                        right = right.replace(&format!("*{field}"), &format!("{{{field}}}"));
181                    }
182
183                    quote! {
184                        Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = ::yew_router::__macro::encode_for_url(&::std::format!("{}", #fields))),*)
185                    }
186                }
187                Fields::Unnamed(_) => unreachable!(), // already checked
188            }
189        });
190
191        quote! {
192            fn to_path(&self) -> ::std::string::String {
193                match self {
194                    #(#to_path_matches),*,
195                }
196            }
197        }
198    }
199}
200
201pub fn routable_derive_impl(input: Routable) -> TokenStream {
202    let Routable {
203        ats,
204        not_found_route,
205        ident,
206        ..
207    } = &input;
208
209    let from_path = input.build_from_path();
210    let to_path = input.build_to_path();
211
212    let maybe_not_found_route = match not_found_route {
213        Some(route) => quote! { ::std::option::Option::Some(Self::#route) },
214        None => quote! { ::std::option::Option::None },
215    };
216
217    let maybe_default = match not_found_route {
218        Some(route) => {
219            quote! {
220                impl ::std::default::Default for #ident {
221                    fn default() -> Self {
222                        Self::#route
223                    }
224                }
225            }
226        }
227        None => TokenStream::new(),
228    };
229
230    quote! {
231        #[automatically_derived]
232        impl ::yew_router::Routable for #ident {
233            #from_path
234            #to_path
235
236            fn routes() -> ::std::vec::Vec<&'static str> {
237                ::std::vec![#(#ats),*]
238            }
239
240            fn not_found_route() -> ::std::option::Option<Self> {
241                #maybe_not_found_route
242            }
243
244            fn recognize(pathname: &str) -> ::std::option::Option<Self> {
245                ::std::thread_local! {
246                    static ROUTER: ::yew_router::__macro::Router = ::yew_router::__macro::build_router::<#ident>();
247                }
248                ROUTER.with(|router| ::yew_router::__macro::recognize_with_router(router, pathname))
249            }
250        }
251
252        #maybe_default
253    }
254}