sycamore_router_macro/
route.rs

1use proc_macro2::TokenStream;
2use quote::{quote, quote_spanned, ToTokens};
3use syn::punctuated::Punctuated;
4use syn::spanned::Spanned;
5use syn::{DeriveInput, Fields, Ident, LitStr, Token, Variant};
6
7use crate::parser::{parse_route, RoutePathAst, SegmentAst};
8
9pub fn route_impl(input: DeriveInput) -> syn::Result<TokenStream> {
10    let mut quoted = TokenStream::new();
11    let mut err_quoted = TokenStream::new();
12    // When the `#[not_found]` handler is found, this will store its name so we can use that as the
13    // `Default` implementation
14    let mut error_handler_name = None;
15
16    match &input.data {
17        syn::Data::Enum(de) => {
18            let ty_name = &input.ident;
19
20            for variant in &de.variants {
21                let variant_id = &variant.ident;
22
23                let mut quote_capture_vars = TokenStream::new();
24                let mut route_path_ast = None;
25
26                let mut is_to_route = false;
27
28                for attr in &variant.attrs {
29                    let attr_name = match attr.path().get_ident() {
30                        Some(ident) => ident.to_string(),
31                        None => continue,
32                    };
33
34                    match attr_name.as_str() {
35                        "to" => {
36                            // region: parse route
37                            let route_litstr: LitStr = attr.parse_args()?;
38                            let route_str = route_litstr.value();
39                            let route = match parse_route(&route_str) {
40                                Ok(route_ast) => route_ast,
41                                Err(err) => {
42                                    return Err(syn::Error::new(route_litstr.span(), err.message));
43                                }
44                            };
45                            // endregion
46                            quote_capture_vars.extend(impl_to(variant, variant_id, &route)?);
47                            route_path_ast = Some(route);
48                            is_to_route = true;
49                        }
50                        "not_found" => {
51                            if error_handler_name.is_some() {
52                                return Err(syn::Error::new(
53                                    attr.span(),
54                                    "cannot have more than one error handler",
55                                ));
56                            }
57                            if !variant.fields.is_empty() {
58                                return Err(syn::Error::new(
59                                    variant.fields.span(),
60                                    "not found route cannot have any fields",
61                                ));
62                            }
63                            err_quoted = quote! {
64                                return Self::#variant_id;
65                            };
66                            error_handler_name = Some(quote!(Self::#variant_id));
67                        }
68                        _ => {}
69                    }
70                }
71                if is_to_route {
72                    let route_path_ast = route_path_ast.unwrap();
73                    quoted.extend(quote! {
74                        let __route = #route_path_ast;
75                        if let Some(__captures) = __route.match_path(__segments) {
76                            // Try to capture variables.
77                            #quote_capture_vars
78                        }
79                    });
80                }
81            }
82
83            if error_handler_name.is_none() {
84                return Err(syn::Error::new(
85                    input.span(),
86                    "not found route not specified",
87                ));
88            }
89
90            Ok(quote! {
91                impl ::sycamore_router::Route for #ty_name {
92                    fn match_route(&self, __segments: &[&str]) -> Self {
93                        #quoted
94                        #err_quoted
95                    }
96                }
97                // We implement `Default` as well here for the `Router`/`RouterBase` distinction (`Router` needs to pass a default `impl Route` to `RouterBase`)
98                impl ::std::default::Default for #ty_name {
99                    fn default() -> Self {
100                        #error_handler_name
101                    }
102                }
103            })
104        }
105        _ => Err(syn::Error::new(
106            input.span(),
107            "Route can only be derived on enums",
108        )),
109    }
110}
111
112/// Implementation for `#[to(_)]` attribute.
113fn impl_to(
114    variant: &Variant,
115    variant_id: &Ident,
116    route: &RoutePathAst,
117) -> Result<TokenStream, syn::Error> {
118    let dyn_segments = route.dyn_segments();
119    let expected_fields_len = dyn_segments.len();
120    if expected_fields_len != variant.fields.len() {
121        return Err(syn::Error::new(
122            variant.fields.span(),
123            format!("mismatch between number of capture fields and variant fields (found {} capture field(s) and {} variant field(s))",
124            expected_fields_len, variant.fields.len()),
125        ));
126    }
127
128    Ok(match &variant.fields {
129        // For named fields, captures must match the field name.
130        Fields::Named(f) => {
131            let mut captures = Vec::new();
132
133            for (i, (field, segment)) in f.named.iter().zip(dyn_segments.iter()).enumerate() {
134                match segment {
135                    SegmentAst::Param(_) => unreachable!("not a dynamic segment"),
136                    SegmentAst::DynParam(param) => {
137                        if param != &field.ident.as_ref().unwrap().to_string() {
138                            return Err(syn::Error::new(
139                                field.ident.span(),
140                                format!(
141                                    "capture field name mismatch (expected `{}`, found `{}`)",
142                                    param,
143                                    field.ident.as_ref().unwrap()
144                                ),
145                            ));
146                        }
147                        let param_id: Ident = syn::parse_str(param)?;
148                        captures.push(quote! {
149                            let #param_id = match ::sycamore_router::TryFromParam::try_from_param(
150                                __captures[#i].as_dyn_param().unwrap()
151                            ) {
152                                ::std::option::Option::Some(__value) => __value,
153                                ::std::option::Option::None => break,
154                            };
155                        })
156                    }
157                    SegmentAst::DynSegments(param) => {
158                        if param != &field.ident.as_ref().unwrap().to_string() {
159                            return Err(syn::Error::new(
160                                field.ident.span(),
161                                format!(
162                                    "capture field name mismatch (expected `{}`, found `{}`)",
163                                    param,
164                                    field.ident.as_ref().unwrap()
165                                ),
166                            ));
167                        }
168                        let param_id: Ident = syn::parse_str(param)?;
169                        captures.push(quote! {
170                            let #param_id = match ::sycamore_router::TryFromSegments::try_from_segments(
171                                __captures[#i].as_dyn_segments().unwrap()
172                            ) {
173                                ::std::option::Option::Some(__value) => __value,
174                                ::std::option::Option::None => break,
175                            };
176                        })
177                    }
178                }
179            }
180            let named: Punctuated<&Option<Ident>, Token![,]> =
181                f.named.iter().map(|x| &x.ident).collect();
182            quote_spanned! {variant.span()=>
183                #[allow(clippy::never_loop)]
184                #[allow(clippy::while_let_loop)]
185                loop {
186                    #(#captures)*
187                    return Self::#variant_id {
188                        #named
189                    };
190                }
191            }
192        }
193        // For unnamed fields, captures must be in right order.
194        Fields::Unnamed(_) => {
195            let mut captures = Vec::new();
196
197            for (i, segment) in dyn_segments.iter().enumerate() {
198                match segment {
199                    SegmentAst::Param(_) => unreachable!("not a dynamic segment"),
200                    SegmentAst::DynParam(_) => captures.push(quote! {{
201                        match ::sycamore_router::TryFromParam::try_from_param(
202                            __captures[#i].as_dyn_param().unwrap()
203                        ) {
204                            ::std::option::Option::Some(__value) => __value,
205                            ::std::option::Option::None => break,
206                        }
207                    }}),
208                    SegmentAst::DynSegments(_) => captures.push(quote! {{
209                        match ::sycamore_router::TryFromSegments::try_from_segments(
210                            __captures[#i].as_dyn_segments().unwrap()
211                        ) {
212                            ::std::option::Option::Some(__value) => __value,
213                            ::std::option::Option::None => break,
214                        }
215                    }}),
216                }
217            }
218            quote! {
219                // Run captures inside a loop in order to allow early break inside the expression.
220                loop {
221                    return Self::#variant_id(#(#captures),*);
222                }
223            }
224        }
225        Fields::Unit => quote! {
226            return Self::#variant_id;
227        },
228    })
229}
230
231impl ToTokens for SegmentAst {
232    fn to_tokens(&self, tokens: &mut TokenStream) {
233        match self {
234            SegmentAst::Param(param) => tokens.extend(quote! {
235                ::sycamore_router::Segment::Param(::std::string::ToString::to_string(#param))
236            }),
237            SegmentAst::DynParam(_) => tokens.extend(quote! {
238                ::sycamore_router::Segment::DynParam
239            }),
240            SegmentAst::DynSegments(_) => tokens.extend(quote! {
241                ::sycamore_router::Segment::DynSegments
242            }),
243        }
244    }
245}
246
247impl ToTokens for RoutePathAst {
248    fn to_tokens(&self, tokens: &mut TokenStream) {
249        let segments = self
250            .segments
251            .iter()
252            .map(|s| s.to_token_stream())
253            .collect::<Vec<_>>();
254
255        tokens.extend(quote! {
256            ::sycamore_router::RoutePath::new(::std::vec![#(#segments),*])
257        });
258    }
259}