freya_router_macro/
lib.rs

1#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
2#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
3
4extern crate proc_macro;
5
6use layout::Layout;
7use nest::{
8    Nest,
9    NestId,
10};
11use proc_macro::TokenStream;
12use proc_macro2::TokenStream as TokenStream2;
13use quote::{
14    __private::Span,
15    ToTokens,
16    format_ident,
17    quote,
18};
19use redirect::Redirect;
20use route::{
21    Route,
22    RouteType,
23};
24use segment::RouteSegment;
25use syn::{
26    Ident,
27    Token,
28    Type,
29    parse::ParseStream,
30    parse_macro_input,
31};
32
33use crate::{
34    layout::LayoutId,
35    route_tree::ParseRouteTree,
36};
37
38mod hash;
39mod layout;
40mod nest;
41mod query;
42mod redirect;
43mod route;
44mod route_tree;
45mod segment;
46
47#[doc(alias = "route")]
48#[proc_macro_derive(
49    Routable,
50    attributes(route, nest, end_nest, layout, end_layout, redirect, child)
51)]
52pub fn routable(input: TokenStream) -> TokenStream {
53    let routes_enum = parse_macro_input!(input as syn::ItemEnum);
54
55    let route_enum = match RouteEnum::parse(routes_enum) {
56        Ok(route_enum) => route_enum,
57        Err(err) => return err.to_compile_error().into(),
58    };
59
60    let error_type = route_enum.error_type();
61    let parse_impl = route_enum.parse_impl();
62    let display_impl = route_enum.impl_display();
63    let routable_impl = route_enum.routable_impl();
64
65    (quote! {
66        const _: () = {
67            #error_type
68
69            #display_impl
70
71            #routable_impl
72
73            #parse_impl
74        };
75    })
76    .into()
77}
78
79struct RouteEnum {
80    name: Ident,
81    endpoints: Vec<RouteEndpoint>,
82    nests: Vec<Nest>,
83    layouts: Vec<Layout>,
84    site_map: Vec<SiteMapSegment>,
85}
86
87impl RouteEnum {
88    fn parse(data: syn::ItemEnum) -> syn::Result<Self> {
89        let name = &data.ident;
90
91        let mut site_map = Vec::new();
92        let mut site_map_stack: Vec<Vec<SiteMapSegment>> = Vec::new();
93
94        let mut endpoints = Vec::new();
95
96        let mut layouts: Vec<Layout> = Vec::new();
97        let mut layout_stack = Vec::new();
98
99        let mut nests = Vec::new();
100        let mut nest_stack = Vec::new();
101
102        for variant in &data.variants {
103            let mut excluded = Vec::new();
104            // Apply the any nesting attributes in order
105            for attr in &variant.attrs {
106                if attr.path().is_ident("nest") {
107                    let mut children_routes = Vec::new();
108                    {
109                        // add all of the variants of the enum to the children_routes until we hit an end_nest
110                        let mut level = 0;
111                        'o: for variant in &data.variants {
112                            children_routes.push(variant.fields.clone());
113                            for attr in &variant.attrs {
114                                if attr.path().is_ident("nest") {
115                                    level += 1;
116                                } else if attr.path().is_ident("end_nest") {
117                                    level -= 1;
118                                    if level < 0 {
119                                        break 'o;
120                                    }
121                                }
122                            }
123                        }
124                    }
125
126                    let nest_index = nests.len();
127
128                    let parser = |input: ParseStream| {
129                        Nest::parse(
130                            input,
131                            children_routes
132                                .iter()
133                                .filter_map(|f: &syn::Fields| match f {
134                                    syn::Fields::Named(fields) => Some(fields.clone()),
135                                    _ => None,
136                                })
137                                .collect(),
138                            nest_index,
139                        )
140                    };
141                    let nest = attr.parse_args_with(parser)?;
142
143                    // add the current segment to the site map stack
144                    let segments: Vec<_> = nest
145                        .segments
146                        .iter()
147                        .map(|seg| {
148                            let segment_type = seg.into();
149                            SiteMapSegment {
150                                segment_type,
151                                children: Vec::new(),
152                            }
153                        })
154                        .collect();
155                    if !segments.is_empty() {
156                        site_map_stack.push(segments);
157                    }
158
159                    nests.push(nest);
160                    nest_stack.push(NestId(nest_index));
161                } else if attr.path().is_ident("end_nest") {
162                    nest_stack.pop();
163                    // pop the current nest segment off the stack and add it to the parent or the site map
164                    if let Some(segment) = site_map_stack.pop() {
165                        let children = site_map_stack
166                            .last_mut()
167                            .map(|seg| &mut seg.last_mut().unwrap().children)
168                            .unwrap_or(&mut site_map);
169
170                        // Turn the list of segments in the segments stack into a tree
171                        let mut iter = segment.into_iter().rev();
172                        let mut current = iter.next().unwrap();
173                        for mut segment in iter {
174                            segment.children.push(current);
175                            current = segment;
176                        }
177
178                        children.push(current);
179                    }
180                } else if attr.path().is_ident("layout") {
181                    let parser = |input: ParseStream| {
182                        let bang: Option<Token![!]> = input.parse().ok();
183                        let exclude = bang.is_some();
184                        Ok((exclude, Layout::parse(input, nest_stack.clone())?))
185                    };
186                    let (exclude, layout): (bool, Layout) = attr.parse_args_with(parser)?;
187
188                    if exclude {
189                        let Some(layout_index) = layouts.iter().position(|l| l.comp == layout.comp)
190                        else {
191                            return Err(syn::Error::new(
192                                Span::call_site(),
193                                "Attempted to exclude a layout that does not exist",
194                            ));
195                        };
196                        excluded.push(LayoutId(layout_index));
197                    } else {
198                        let layout_index = layouts.len();
199                        layouts.push(layout);
200                        layout_stack.push(LayoutId(layout_index));
201                    }
202                } else if attr.path().is_ident("end_layout") {
203                    layout_stack.pop();
204                } else if attr.path().is_ident("redirect") {
205                    let parser = |input: ParseStream| {
206                        Redirect::parse(input, nest_stack.clone(), endpoints.len())
207                    };
208                    let redirect = attr.parse_args_with(parser)?;
209                    endpoints.push(RouteEndpoint::Redirect(redirect));
210                }
211            }
212
213            let active_nests = nest_stack.clone();
214            let mut active_layouts = layout_stack.clone();
215            active_layouts.retain(|&id| !excluded.contains(&id));
216
217            let route = Route::parse(active_nests, active_layouts, variant.clone())?;
218
219            // add the route to the site map
220            let mut segment = SiteMapSegment::new(&route.segments);
221            if let RouteType::Child(child) = &route.ty {
222                let new_segment = SiteMapSegment {
223                    segment_type: SegmentType::Child(child.ty.clone()),
224                    children: Vec::new(),
225                };
226                match &mut segment {
227                    Some(segment) => {
228                        fn set_last_child_to(
229                            segment: &mut SiteMapSegment,
230                            new_segment: SiteMapSegment,
231                        ) {
232                            if let Some(last) = segment.children.last_mut() {
233                                set_last_child_to(last, new_segment);
234                            } else {
235                                segment.children = vec![new_segment];
236                            }
237                        }
238                        set_last_child_to(segment, new_segment);
239                    }
240                    None => {
241                        segment = Some(new_segment);
242                    }
243                }
244            }
245
246            if let Some(segment) = segment {
247                let parent = site_map_stack.last_mut();
248                let children = match parent {
249                    Some(parent) => &mut parent.last_mut().unwrap().children,
250                    None => &mut site_map,
251                };
252                children.push(segment);
253            }
254
255            endpoints.push(RouteEndpoint::Route(route));
256        }
257
258        // pop any remaining site map segments
259        while let Some(segment) = site_map_stack.pop() {
260            let children = site_map_stack
261                .last_mut()
262                .map(|seg| &mut seg.last_mut().unwrap().children)
263                .unwrap_or(&mut site_map);
264
265            // Turn the list of segments in the segments stack into a tree
266            let mut iter = segment.into_iter().rev();
267            let mut current = iter.next().unwrap();
268            for mut segment in iter {
269                segment.children.push(current);
270                current = segment;
271            }
272
273            children.push(current);
274        }
275
276        let myself = Self {
277            name: name.clone(),
278            endpoints,
279            nests,
280            layouts,
281            site_map,
282        };
283
284        Ok(myself)
285    }
286
287    fn impl_display(&self) -> TokenStream2 {
288        let mut display_match = Vec::new();
289
290        for route in &self.endpoints {
291            if let RouteEndpoint::Route(route) = route {
292                display_match.push(route.display_match(&self.nests));
293            }
294        }
295
296        let name = &self.name;
297
298        quote! {
299            impl std::fmt::Display for #name {
300                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301                    #[allow(unused)]
302                    match self {
303                        #(#display_match)*
304                    }
305                    Ok(())
306                }
307            }
308        }
309    }
310
311    fn parse_impl(&self) -> TokenStream2 {
312        let tree = ParseRouteTree::new(&self.endpoints, &self.nests);
313        let name = &self.name;
314
315        let error_name = format_ident!("{}MatchError", self.name);
316        let tokens = tree.roots.iter().map(|&id| {
317            let route = tree.get(id).unwrap();
318            route.to_tokens(&self.nests, &tree, self.name.clone(), error_name.clone())
319        });
320
321        quote! {
322            impl<'a> core::convert::TryFrom<&'a str> for #name {
323                type Error = <Self as std::str::FromStr>::Err;
324
325                fn try_from(s: &'a str) -> ::std::result::Result<Self, Self::Error> {
326                    s.parse()
327                }
328            }
329
330            impl std::str::FromStr for #name {
331                type Err = freya_router::routable::RouteParseError<#error_name>;
332
333                fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
334                    let route = s;
335                    let (route, hash) = route.split_once('#').unwrap_or((route, ""));
336                    let (route, query) = route.split_once('?').unwrap_or((route, ""));
337                    // Remove any trailing slashes. We parse /route/ and /route in the same way
338                    // Note: we don't use trim because it includes more code
339                    let route = route.strip_suffix('/').unwrap_or(route);
340                    let query = freya_router::exports::urlencoding::decode(query).unwrap_or(query.into());
341                    let hash = freya_router::exports::urlencoding::decode(hash).unwrap_or(hash.into());
342                    let mut segments = route.split('/').map(|s| freya_router::exports::urlencoding::decode(s).unwrap_or(s.into()));
343                    // skip the first empty segment
344                    if s.starts_with('/') {
345                        let _ = segments.next();
346                    } else {
347                        // if this route does not start with a slash, it is not a valid route
348                        return Err(freya_router::routable::RouteParseError {
349                            attempted_routes: Vec::new(),
350                        });
351                    }
352                    let mut errors = Vec::new();
353
354                    #(#tokens)*
355
356                    Err(freya_router::routable::RouteParseError {
357                        attempted_routes: errors,
358                    })
359                }
360            }
361        }
362    }
363
364    fn error_name(&self) -> Ident {
365        Ident::new(&(self.name.to_string() + "MatchError"), Span::call_site())
366    }
367
368    fn error_type(&self) -> TokenStream2 {
369        let match_error_name = self.error_name();
370
371        let mut type_defs = Vec::new();
372        let mut error_variants = Vec::new();
373        let mut display_match = Vec::new();
374
375        for endpoint in &self.endpoints {
376            match endpoint {
377                RouteEndpoint::Route(route) => {
378                    let route_name = &route.route_name;
379
380                    let error_name = route.error_ident();
381                    let route_str = &route.route;
382                    let comment = format!(
383                        " An error that can occur when trying to parse the route [`{}::{}`] ('{}').",
384                        self.name, route_name, route_str
385                    );
386
387                    error_variants.push(quote! {
388                        #[doc = #comment]
389                        #route_name(#error_name)
390                    });
391                    display_match.push(quote! { Self::#route_name(err) => write!(f, "Route '{}' ('{}') did not match:\n{}", stringify!(#route_name), #route_str, err)? });
392                    type_defs.push(route.error_type());
393                }
394                RouteEndpoint::Redirect(redirect) => {
395                    let error_variant = redirect.error_variant();
396                    let error_name = redirect.error_ident();
397                    let route_str = &redirect.route;
398                    let comment = format!(
399                        " An error that can occur when trying to parse the redirect '{}'.",
400                        route_str.value()
401                    );
402
403                    error_variants.push(quote! {
404                        #[doc = #comment]
405                        #error_variant(#error_name)
406                    });
407                    display_match.push(quote! { Self::#error_variant(err) => write!(f, "Redirect '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
408                    type_defs.push(redirect.error_type());
409                }
410            }
411        }
412
413        for nest in &self.nests {
414            let error_variant = nest.error_variant();
415            let error_name = nest.error_ident();
416            let route_str = &nest.route;
417            let comment = format!(
418                " An error that can occur when trying to parse the nested segment {error_name} ('{route_str}').",
419            );
420
421            error_variants.push(quote! {
422                #[doc = #comment]
423                #error_variant(#error_name)
424            });
425            display_match.push(quote! { Self::#error_variant(err) => write!(f, "Nest '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
426            type_defs.push(nest.error_type());
427        }
428
429        let comment = format!(
430            " An error that can occur when trying to parse the route enum [`{}`].",
431            self.name
432        );
433
434        quote! {
435            #(#type_defs)*
436
437            #[doc = #comment]
438            #[allow(non_camel_case_types)]
439            #[allow(clippy::derive_partial_eq_without_eq)]
440            pub enum #match_error_name {
441                #(#error_variants),*
442            }
443
444            impl std::fmt::Debug for #match_error_name {
445                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446                    write!(f, "{}({})", stringify!(#match_error_name), self)
447                }
448            }
449
450            impl std::fmt::Display for #match_error_name {
451                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452                    match self {
453                        #(#display_match),*
454                    }
455                    Ok(())
456                }
457            }
458        }
459    }
460
461    fn routable_impl(&self) -> TokenStream2 {
462        let name = &self.name;
463        let site_map = &self.site_map;
464
465        let mut matches = Vec::new();
466
467        // Collect all routes matches
468        for route in &self.endpoints {
469            if let RouteEndpoint::Route(route) = route {
470                matches.push(route.routable_match(&self.layouts, &self.nests));
471            }
472        }
473
474        quote! {
475            impl freya_router::routable::Routable for #name where Self: Clone {
476                const SITE_MAP: &'static [freya_router::routable::SiteMapSegment] = &[
477                    #(#site_map,)*
478                ];
479
480                fn render(&self, level: usize) -> freya::prelude::Element {
481                    let myself = self.clone();
482                    match (level, myself) {
483                        #(#matches)*
484                        _ => unreachable!()
485                    }
486                }
487            }
488        }
489    }
490}
491
492enum RouteEndpoint {
493    Route(Route),
494    Redirect(Redirect),
495}
496
497struct SiteMapSegment {
498    pub segment_type: SegmentType,
499    pub children: Vec<SiteMapSegment>,
500}
501
502impl SiteMapSegment {
503    fn new(segments: &[RouteSegment]) -> Option<Self> {
504        let mut current = None;
505        // walk backwards through the new segments, adding children as we go
506        for segment in segments.iter().rev() {
507            let segment_type = segment.into();
508            let mut segment = SiteMapSegment {
509                segment_type,
510                children: Vec::new(),
511            };
512            // if we have a current segment, add it as a child
513            if let Some(current) = current.take() {
514                segment.children.push(current)
515            }
516            current = Some(segment);
517        }
518        current
519    }
520}
521
522impl ToTokens for SiteMapSegment {
523    fn to_tokens(&self, tokens: &mut TokenStream2) {
524        let segment_type = &self.segment_type;
525        let children = if let SegmentType::Child(ty) = &self.segment_type {
526            quote! { #ty::SITE_MAP }
527        } else {
528            let children = self
529                .children
530                .iter()
531                .map(|child| child.to_token_stream())
532                .collect::<Vec<_>>();
533            quote! {
534                &[
535                    #(#children,)*
536                ]
537            }
538        };
539
540        tokens.extend(quote! {
541            freya_router::routable::SiteMapSegment {
542                segment_type: #segment_type,
543                children: #children,
544            }
545        });
546    }
547}
548
549enum SegmentType {
550    Static(String),
551    Dynamic(String),
552    CatchAll(String),
553    Child(Type),
554}
555
556impl ToTokens for SegmentType {
557    fn to_tokens(&self, tokens: &mut TokenStream2) {
558        match self {
559            SegmentType::Static(s) => {
560                tokens.extend(quote! { freya_router::routable::SegmentType::Static(#s) })
561            }
562            SegmentType::Dynamic(s) => {
563                tokens.extend(quote! { freya_router::routable::SegmentType::Dynamic(#s) })
564            }
565            SegmentType::CatchAll(s) => {
566                tokens.extend(quote! { freya_router::routable::SegmentType::CatchAll(#s) })
567            }
568            SegmentType::Child(_) => {
569                tokens.extend(quote! { freya_router::routable::SegmentType::Child })
570            }
571        }
572    }
573}
574
575impl<'a> From<&'a RouteSegment> for SegmentType {
576    fn from(value: &'a RouteSegment) -> Self {
577        match value {
578            RouteSegment::Static(s) => SegmentType::Static(s.to_string()),
579            RouteSegment::Dynamic(s, _) => SegmentType::Dynamic(s.to_string()),
580            RouteSegment::CatchAll(s, _) => SegmentType::CatchAll(s.to_string()),
581        }
582    }
583}