yew_router_macro/
routable_derive.rs1use 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 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!(), };
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 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!(), }
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}