dioxus_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::{Nest, NestId};
8use proc_macro::TokenStream;
9use quote::{__private::Span, format_ident, quote, ToTokens};
10use redirect::Redirect;
11use route::{Route, RouteType};
12use segment::RouteSegment;
13use syn::{parse::ParseStream, parse_macro_input, Ident, Token, Type};
14
15use proc_macro2::TokenStream as TokenStream2;
16
17use crate::{layout::LayoutId, route_tree::ParseRouteTree};
18
19mod hash;
20mod layout;
21mod nest;
22mod query;
23mod redirect;
24mod route;
25mod route_tree;
26mod segment;
27
28/// Derives the Routable trait for an enum of routes
29///
30/// Each variant must:
31/// 1. Be struct-like with {}'s
32/// 2. Contain all of the dynamic parameters of the current and nested routes
33/// 3. Have a `#[route("route")]` attribute
34///
35/// Route Segments:
36/// 1. Static Segments: "/static"
37/// 2. Dynamic Segments: "/:dynamic" (where dynamic has a type that is FromStr in all child Variants)
38/// 3. Catch all Segments: "/:..segments" (where segments has a type that is FromSegments in all child Variants)
39/// 4. Query Segments: "/?:..query" (where query has a type that is FromQuery in all child Variants) or "/?:query&:other_query" (where query and other_query has a type that is FromQueryArgument in all child Variants)
40///
41/// Routes are matched:
42/// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route")
43/// 2. By the order they are defined in the enum
44///
45/// All features:
46/// ```rust
47/// use dioxus::prelude::*;
48///
49/// #[rustfmt::skip]
50/// #[derive(Clone, Debug, PartialEq, Routable)]
51/// enum Route {
52///     // Define routes with the route macro. If the name of the component is not the same as the variant, you can specify it as the second parameter
53///     #[route("/", IndexComponent)]
54///     Index {},
55///     // Nests with parameters have types taken from child routes
56///     // Everything inside the nest has the added parameter `user_id: usize`
57///     #[nest("/user/:user_id")]
58///         // All children of layouts will be rendered inside the Outlet in the layout component
59///         // Creates a Layout UserFrame that has the parameter `user_id: usize`
60///         #[layout(UserFrame)]
61///             // If there is a component with the name Route1, you do not need to pass in the component name
62///             #[route("/:dynamic?:query")]
63///             Route1 {
64///                 // The type is taken from the first instance of the dynamic parameter
65///                 user_id: usize,
66///                 dynamic: usize,
67///                 query: String,
68///             },
69///             #[route("/hello_world")]
70///             // You can opt out of the layout by using the `!` prefix
71///             #[layout(!UserFrame)]
72///             Route2 { user_id: usize },
73///         // End layouts with #[end_layout]
74///         #[end_layout]
75///     // End nests with #[end_nest]
76///     #[end_nest]
77///     // Redirects take a path and a function that takes the parameters from the path and returns a new route
78///     #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
79///     #[route("/:dynamic")]
80///     Route3 { dynamic: String },
81/// }
82/// # #[component]
83/// # fn Route1(user_id: usize, dynamic: usize, query: String) -> Element { VNode::empty() }
84/// # #[component]
85/// # fn Route2(user_id: usize) -> Element { VNode::empty() }
86/// # #[component]
87/// # fn Route3(dynamic: String) -> Element { VNode::empty() }
88/// # #[component]
89/// # fn UserFrame(user_id: usize) -> Element { VNode::empty() }
90/// # #[component]
91/// # fn IndexComponent() -> Element { VNode::empty() }
92/// ```
93///
94/// # `#[route("path", component)]`
95///
96/// The `#[route]` attribute is used to define a route. It takes up to 2 parameters:
97/// - `path`: The path to the enum variant (relative to the parent nest)
98/// - (optional) `component`: The component to render when the route is matched. If not specified, the name of the variant is used
99///
100/// Routes are the most basic attribute. They allow you to define a route and the component to render when the route is matched. The component must take all dynamic parameters of the route and all parent nests.
101/// The next variant will be tied to the component. If you link to that variant, the component will be rendered.
102///
103/// ```rust
104/// use dioxus::prelude::*;
105///
106/// #[derive(Clone, Debug, PartialEq, Routable)]
107/// enum Route {
108///     // Define routes that renders the IndexComponent
109///     // The Index component will be rendered when the route is matched (e.g. when the user navigates to /)
110///     #[route("/", Index)]
111///     Index {},
112/// }
113/// # #[component]
114/// # fn Index() -> Element { VNode::empty() }
115/// ```
116///
117/// # `#[redirect("path", function)]`
118///
119/// The `#[redirect]` attribute is used to define a redirect. It takes 2 parameters:
120/// - `path`: The path to the enum variant (relative to the parent nest)
121/// - `function`: A function that takes the parameters from the path and returns a new route
122///
123/// ```rust
124/// use dioxus::prelude::*;
125///
126/// #[derive(Clone, Debug, PartialEq, Routable)]
127/// enum Route {
128///     // Redirects the /:id route to the Index route
129///     #[redirect("/:id", |id: usize| Route::Index {})]
130///     #[route("/", Index)]
131///     Index {},
132/// }
133/// # #[component]
134/// # fn Index() -> Element { VNode::empty() }
135/// ```
136///
137/// Redirects allow you to redirect a route to another route. The function must take all dynamic parameters of the route and all parent nests.
138///
139/// # `#[nest("path")]`
140///
141/// The `#[nest]` attribute is used to define a nest. It takes 1 parameter:
142/// - `path`: The path to the nest (relative to the parent nest)
143///
144/// Nests effect all nests, routes and redirects defined until the next `#[end_nest]` attribute. All children of nests are relative to the nest route and must include all dynamic parameters of the nest.
145///
146/// ```rust
147/// use dioxus::prelude::*;
148///
149/// #[derive(Clone, Debug, PartialEq, Routable)]
150/// enum Route {
151///     // Nests all child routes in the /blog route
152///     #[nest("/blog")]
153///         // This is at /blog/:id
154///         #[redirect("/:id", |id: usize| Route::Index {})]
155///         // This is at /blog
156///         #[route("/", Index)]
157///         Index {},
158/// }
159/// # #[component]
160/// # fn Index() -> Element { VNode::empty() }
161/// ```
162///
163/// # `#[end_nest]`
164///
165/// The `#[end_nest]` attribute is used to end a nest. It takes no parameters.
166///
167/// ```rust
168/// use dioxus::prelude::*;
169///
170/// #[derive(Clone, Debug, PartialEq, Routable)]
171/// enum Route {
172///     #[nest("/blog")]
173///         // This is at /blog/:id
174///         #[redirect("/:id", |id: usize| Route::Index {})]
175///         // This is at /blog
176///         #[route("/", Index)]
177///         Index {},
178///     // Ends the nest
179///     #[end_nest]
180///     // This is at /
181///     #[route("/")]
182///     Home {},
183/// }
184/// # #[component]
185/// # fn Index() -> Element { VNode::empty() }
186/// # #[component]
187/// # fn Home() -> Element { VNode::empty() }
188/// ```
189///
190/// # `#[layout(component)]`
191///
192/// The `#[layout]` attribute is used to define a layout. It takes 1 parameter:
193/// - `component`: The component to render when the route is matched. If not specified, the name of the variant is used
194///
195/// The layout component allows you to wrap all children of the layout in a component. The child routes are rendered in the Outlet of the layout component. The layout component must take all dynamic parameters of the nests it is nested in.
196///
197/// ```rust
198/// use dioxus::prelude::*;
199///
200/// #[derive(Clone, Debug, PartialEq, Routable)]
201/// enum Route {
202///     #[layout(BlogFrame)]
203///         #[redirect("/:id", |id: usize| Route::Index {})]
204///         // Index will be rendered in the Outlet of the BlogFrame component
205///         #[route("/", Index)]
206///         Index {},
207/// }
208/// # #[component]
209/// # fn Index() -> Element { VNode::empty() }
210/// # #[component]
211/// # fn BlogFrame() -> Element { VNode::empty() }
212/// ```
213///
214/// # `#[end_layout]`
215///
216/// The `#[end_layout]` attribute is used to end a layout. It takes no parameters.
217///
218/// ```rust
219/// use dioxus::prelude::*;
220///
221/// #[derive(Clone, Debug, PartialEq, Routable)]
222/// enum Route {
223///     #[layout(BlogFrame)]
224///         #[redirect("/:id", |id: usize| Route::Index {})]
225///         // Index will be rendered in the Outlet of the BlogFrame component
226///         #[route("/", Index)]
227///         Index {},
228///     // Ends the layout
229///     #[end_layout]
230///     // This will be rendered standalone
231///     #[route("/")]
232///     Home {},
233/// }
234/// # #[component]
235/// # fn Index() -> Element { VNode::empty() }
236/// # #[component]
237/// # fn BlogFrame() -> Element { VNode::empty() }
238/// # #[component]
239/// # fn Home() -> Element { VNode::empty() }
240/// ```
241#[doc(alias = "route")]
242#[proc_macro_derive(
243    Routable,
244    attributes(route, nest, end_nest, layout, end_layout, redirect, child)
245)]
246pub fn routable(input: TokenStream) -> TokenStream {
247    let routes_enum = parse_macro_input!(input as syn::ItemEnum);
248
249    let route_enum = match RouteEnum::parse(routes_enum) {
250        Ok(route_enum) => route_enum,
251        Err(err) => return err.to_compile_error().into(),
252    };
253
254    let error_type = route_enum.error_type();
255    let parse_impl = route_enum.parse_impl();
256    let display_impl = route_enum.impl_display();
257    let routable_impl = route_enum.routable_impl();
258
259    (quote! {
260        const _: () = {
261            #error_type
262
263            #display_impl
264
265            #routable_impl
266
267            #parse_impl
268        };
269    })
270    .into()
271}
272
273struct RouteEnum {
274    name: Ident,
275    endpoints: Vec<RouteEndpoint>,
276    nests: Vec<Nest>,
277    layouts: Vec<Layout>,
278    site_map: Vec<SiteMapSegment>,
279}
280
281impl RouteEnum {
282    fn parse(data: syn::ItemEnum) -> syn::Result<Self> {
283        let name = &data.ident;
284
285        let mut site_map = Vec::new();
286        let mut site_map_stack: Vec<Vec<SiteMapSegment>> = Vec::new();
287
288        let mut endpoints = Vec::new();
289
290        let mut layouts: Vec<Layout> = Vec::new();
291        let mut layout_stack = Vec::new();
292
293        let mut nests = Vec::new();
294        let mut nest_stack = Vec::new();
295
296        for variant in &data.variants {
297            let mut excluded = Vec::new();
298            // Apply the any nesting attributes in order
299            for attr in &variant.attrs {
300                if attr.path().is_ident("nest") {
301                    let mut children_routes = Vec::new();
302                    {
303                        // add all of the variants of the enum to the children_routes until we hit an end_nest
304                        let mut level = 0;
305                        'o: for variant in &data.variants {
306                            children_routes.push(variant.fields.clone());
307                            for attr in &variant.attrs {
308                                if attr.path().is_ident("nest") {
309                                    level += 1;
310                                } else if attr.path().is_ident("end_nest") {
311                                    level -= 1;
312                                    if level < 0 {
313                                        break 'o;
314                                    }
315                                }
316                            }
317                        }
318                    }
319
320                    let nest_index = nests.len();
321
322                    let parser = |input: ParseStream| {
323                        Nest::parse(
324                            input,
325                            children_routes
326                                .iter()
327                                .filter_map(|f: &syn::Fields| match f {
328                                    syn::Fields::Named(fields) => Some(fields.clone()),
329                                    _ => None,
330                                })
331                                .collect(),
332                            nest_index,
333                        )
334                    };
335                    let nest = attr.parse_args_with(parser)?;
336
337                    // add the current segment to the site map stack
338                    let segments: Vec<_> = nest
339                        .segments
340                        .iter()
341                        .map(|seg| {
342                            let segment_type = seg.into();
343                            SiteMapSegment {
344                                segment_type,
345                                children: Vec::new(),
346                            }
347                        })
348                        .collect();
349                    if !segments.is_empty() {
350                        site_map_stack.push(segments);
351                    }
352
353                    nests.push(nest);
354                    nest_stack.push(NestId(nest_index));
355                } else if attr.path().is_ident("end_nest") {
356                    nest_stack.pop();
357                    // pop the current nest segment off the stack and add it to the parent or the site map
358                    if let Some(segment) = site_map_stack.pop() {
359                        let children = site_map_stack
360                            .last_mut()
361                            .map(|seg| &mut seg.last_mut().unwrap().children)
362                            .unwrap_or(&mut site_map);
363
364                        // Turn the list of segments in the segments stack into a tree
365                        let mut iter = segment.into_iter().rev();
366                        let mut current = iter.next().unwrap();
367                        for mut segment in iter {
368                            segment.children.push(current);
369                            current = segment;
370                        }
371
372                        children.push(current);
373                    }
374                } else if attr.path().is_ident("layout") {
375                    let parser = |input: ParseStream| {
376                        let bang: Option<Token![!]> = input.parse().ok();
377                        let exclude = bang.is_some();
378                        Ok((exclude, Layout::parse(input, nest_stack.clone())?))
379                    };
380                    let (exclude, layout): (bool, Layout) = attr.parse_args_with(parser)?;
381
382                    if exclude {
383                        let Some(layout_index) = layouts.iter().position(|l| l.comp == layout.comp)
384                        else {
385                            return Err(syn::Error::new(
386                                Span::call_site(),
387                                "Attempted to exclude a layout that does not exist",
388                            ));
389                        };
390                        excluded.push(LayoutId(layout_index));
391                    } else {
392                        let layout_index = layouts.len();
393                        layouts.push(layout);
394                        layout_stack.push(LayoutId(layout_index));
395                    }
396                } else if attr.path().is_ident("end_layout") {
397                    layout_stack.pop();
398                } else if attr.path().is_ident("redirect") {
399                    let parser = |input: ParseStream| {
400                        Redirect::parse(input, nest_stack.clone(), endpoints.len())
401                    };
402                    let redirect = attr.parse_args_with(parser)?;
403                    endpoints.push(RouteEndpoint::Redirect(redirect));
404                }
405            }
406
407            let active_nests = nest_stack.clone();
408            let mut active_layouts = layout_stack.clone();
409            active_layouts.retain(|&id| !excluded.contains(&id));
410
411            let route = Route::parse(active_nests, active_layouts, variant.clone())?;
412
413            // add the route to the site map
414            let mut segment = SiteMapSegment::new(&route.segments);
415            if let RouteType::Child(child) = &route.ty {
416                let new_segment = SiteMapSegment {
417                    segment_type: SegmentType::Child(child.ty.clone()),
418                    children: Vec::new(),
419                };
420                match &mut segment {
421                    Some(segment) => {
422                        fn set_last_child_to(
423                            segment: &mut SiteMapSegment,
424                            new_segment: SiteMapSegment,
425                        ) {
426                            if let Some(last) = segment.children.last_mut() {
427                                set_last_child_to(last, new_segment);
428                            } else {
429                                segment.children = vec![new_segment];
430                            }
431                        }
432                        set_last_child_to(segment, new_segment);
433                    }
434                    None => {
435                        segment = Some(new_segment);
436                    }
437                }
438            }
439
440            if let Some(segment) = segment {
441                let parent = site_map_stack.last_mut();
442                let children = match parent {
443                    Some(parent) => &mut parent.last_mut().unwrap().children,
444                    None => &mut site_map,
445                };
446                children.push(segment);
447            }
448
449            endpoints.push(RouteEndpoint::Route(route));
450        }
451
452        // pop any remaining site map segments
453        while let Some(segment) = site_map_stack.pop() {
454            let children = site_map_stack
455                .last_mut()
456                .map(|seg| &mut seg.last_mut().unwrap().children)
457                .unwrap_or(&mut site_map);
458
459            // Turn the list of segments in the segments stack into a tree
460            let mut iter = segment.into_iter().rev();
461            let mut current = iter.next().unwrap();
462            for mut segment in iter {
463                segment.children.push(current);
464                current = segment;
465            }
466
467            children.push(current);
468        }
469
470        let myself = Self {
471            name: name.clone(),
472            endpoints,
473            nests,
474            layouts,
475            site_map,
476        };
477
478        // If we're on the web, only the URL history is preserved between navigation. We need to warn the user that the segment is not present in the URL.
479        if cfg!(feature = "web") {
480            for variant in &data.variants {
481                for field in &variant.fields {
482                    if !myself.field_present_in_url(field.ident.as_ref().unwrap()) {
483                        return Err(syn::Error::new_spanned(
484                            field.ident.as_ref().unwrap(),
485                            format!("The `{}` field must be present in the url for the web history. You can include the field in the url by using the `#[route(\"/:{}\")]` attribute on the enum variant.", field.ident.as_ref().unwrap(), field.ident.as_ref().unwrap()),
486                        ));
487                    }
488                }
489            }
490        }
491
492        Ok(myself)
493    }
494
495    fn field_present_in_url(&self, field: &Ident) -> bool {
496        let mut from_route = false;
497
498        for nest in &self.nests {
499            if nest.dynamic_segments_names().any(|i| &i == field) {
500                from_route = true
501            }
502        }
503        for route in &self.endpoints {
504            match route {
505                RouteEndpoint::Route(route) => match &route.ty {
506                    RouteType::Child(child) => {
507                        if let Some(child) = child.ident.as_ref() {
508                            if child == "child" {
509                                from_route = true
510                            }
511                        }
512                    }
513                    RouteType::Leaf { .. } => {
514                        for segment in &route.segments {
515                            if segment.name().as_ref() == Some(field) {
516                                from_route = true
517                            }
518                        }
519                        if let Some(query) = &route.query {
520                            if query.contains_ident(field) {
521                                from_route = true
522                            }
523                        }
524                        if let Some(hash) = &route.hash {
525                            if hash.contains_ident(field) {
526                                from_route = true
527                            }
528                        }
529                    }
530                },
531                RouteEndpoint::Redirect(redirect) => {
532                    for segment in &redirect.segments {
533                        if segment.name().as_ref() == Some(field) {
534                            from_route = true
535                        }
536                    }
537                    if let Some(query) = &redirect.query {
538                        if query.contains_ident(field) {
539                            from_route = true
540                        }
541                    }
542                    if let Some(hash) = &redirect.hash {
543                        if hash.contains_ident(field) {
544                            from_route = true
545                        }
546                    }
547                }
548            }
549        }
550
551        from_route
552    }
553
554    fn impl_display(&self) -> TokenStream2 {
555        let mut display_match = Vec::new();
556
557        for route in &self.endpoints {
558            if let RouteEndpoint::Route(route) = route {
559                display_match.push(route.display_match(&self.nests));
560            }
561        }
562
563        let name = &self.name;
564
565        quote! {
566            impl std::fmt::Display for #name {
567                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568                    #[allow(unused)]
569                    match self {
570                        #(#display_match)*
571                    }
572                    Ok(())
573                }
574            }
575        }
576    }
577
578    fn parse_impl(&self) -> TokenStream2 {
579        let tree = ParseRouteTree::new(&self.endpoints, &self.nests);
580        let name = &self.name;
581
582        let error_name = format_ident!("{}MatchError", self.name);
583        let tokens = tree.roots.iter().map(|&id| {
584            let route = tree.get(id).unwrap();
585            route.to_tokens(&self.nests, &tree, self.name.clone(), error_name.clone())
586        });
587
588        quote! {
589            impl<'a> core::convert::TryFrom<&'a str> for #name {
590                type Error = <Self as std::str::FromStr>::Err;
591
592                fn try_from(s: &'a str) -> ::std::result::Result<Self, Self::Error> {
593                    s.parse()
594                }
595            }
596
597            impl std::str::FromStr for #name {
598                type Err = dioxus_router::routable::RouteParseError<#error_name>;
599
600                fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
601                    let route = s;
602                    let (route, hash) = route.split_once('#').unwrap_or((route, ""));
603                    let (route, query) = route.split_once('?').unwrap_or((route, ""));
604                    // Remove any trailing slashes. We parse /route/ and /route in the same way
605                    // Note: we don't use trim because it includes more code
606                    let route = route.strip_suffix('/').unwrap_or(route);
607                    let query = dioxus_router::exports::urlencoding::decode(query).unwrap_or(query.into());
608                    let hash = dioxus_router::exports::urlencoding::decode(hash).unwrap_or(hash.into());
609                    let mut segments = route.split('/').map(|s| dioxus_router::exports::urlencoding::decode(s).unwrap_or(s.into()));
610                    // skip the first empty segment
611                    if s.starts_with('/') {
612                        let _ = segments.next();
613                    } else {
614                        // if this route does not start with a slash, it is not a valid route
615                        return Err(dioxus_router::routable::RouteParseError {
616                            attempted_routes: Vec::new(),
617                        });
618                    }
619                    let mut errors = Vec::new();
620
621                    #(#tokens)*
622
623                    Err(dioxus_router::routable::RouteParseError {
624                        attempted_routes: errors,
625                    })
626                }
627            }
628        }
629    }
630
631    fn error_name(&self) -> Ident {
632        Ident::new(&(self.name.to_string() + "MatchError"), Span::call_site())
633    }
634
635    fn error_type(&self) -> TokenStream2 {
636        let match_error_name = self.error_name();
637
638        let mut type_defs = Vec::new();
639        let mut error_variants = Vec::new();
640        let mut display_match = Vec::new();
641
642        for endpoint in &self.endpoints {
643            match endpoint {
644                RouteEndpoint::Route(route) => {
645                    let route_name = &route.route_name;
646
647                    let error_name = route.error_ident();
648                    let route_str = &route.route;
649                    let comment = format!(
650                        " An error that can occur when trying to parse the route [`{}::{}`] ('{}').",
651                        self.name,
652                        route_name,
653                        route_str
654                    );
655
656                    error_variants.push(quote! {
657                        #[doc = #comment]
658                        #route_name(#error_name)
659                    });
660                    display_match.push(quote! { Self::#route_name(err) => write!(f, "Route '{}' ('{}') did not match:\n{}", stringify!(#route_name), #route_str, err)? });
661                    type_defs.push(route.error_type());
662                }
663                RouteEndpoint::Redirect(redirect) => {
664                    let error_variant = redirect.error_variant();
665                    let error_name = redirect.error_ident();
666                    let route_str = &redirect.route;
667                    let comment = format!(
668                        " An error that can occur when trying to parse the redirect '{}'.",
669                        route_str.value()
670                    );
671
672                    error_variants.push(quote! {
673                        #[doc = #comment]
674                        #error_variant(#error_name)
675                    });
676                    display_match.push(quote! { Self::#error_variant(err) => write!(f, "Redirect '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
677                    type_defs.push(redirect.error_type());
678                }
679            }
680        }
681
682        for nest in &self.nests {
683            let error_variant = nest.error_variant();
684            let error_name = nest.error_ident();
685            let route_str = &nest.route;
686            let comment = format!(
687                " An error that can occur when trying to parse the nested segment {} ('{}').",
688                error_name, route_str
689            );
690
691            error_variants.push(quote! {
692                #[doc = #comment]
693                #error_variant(#error_name)
694            });
695            display_match.push(quote! { Self::#error_variant(err) => write!(f, "Nest '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
696            type_defs.push(nest.error_type());
697        }
698
699        let comment = format!(
700            " An error that can occur when trying to parse the route enum [`{}`].",
701            self.name
702        );
703
704        quote! {
705            #(#type_defs)*
706
707            #[doc = #comment]
708            #[allow(non_camel_case_types)]
709            #[allow(clippy::derive_partial_eq_without_eq)]
710            pub enum #match_error_name {
711                #(#error_variants),*
712            }
713
714            impl std::fmt::Debug for #match_error_name {
715                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
716                    write!(f, "{}({})", stringify!(#match_error_name), self)
717                }
718            }
719
720            impl std::fmt::Display for #match_error_name {
721                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
722                    match self {
723                        #(#display_match),*
724                    }
725                    Ok(())
726                }
727            }
728        }
729    }
730
731    fn routable_impl(&self) -> TokenStream2 {
732        let name = &self.name;
733        let site_map = &self.site_map;
734
735        let mut matches = Vec::new();
736
737        // Collect all routes matches
738        for route in &self.endpoints {
739            if let RouteEndpoint::Route(route) = route {
740                matches.push(route.routable_match(&self.layouts, &self.nests));
741            }
742        }
743
744        quote! {
745            impl dioxus_router::routable::Routable for #name where Self: Clone {
746                const SITE_MAP: &'static [dioxus_router::routable::SiteMapSegment] = &[
747                    #(#site_map,)*
748                ];
749
750                fn render(&self, level: usize) -> dioxus_core::Element {
751                    let myself = self.clone();
752                    match (level, myself) {
753                        #(#matches)*
754                        _ => VNode::empty()
755                    }
756                }
757            }
758        }
759    }
760}
761
762enum RouteEndpoint {
763    Route(Route),
764    Redirect(Redirect),
765}
766
767struct SiteMapSegment {
768    pub segment_type: SegmentType,
769    pub children: Vec<SiteMapSegment>,
770}
771
772impl SiteMapSegment {
773    fn new(segments: &[RouteSegment]) -> Option<Self> {
774        let mut current = None;
775        // walk backwards through the new segments, adding children as we go
776        for segment in segments.iter().rev() {
777            let segment_type = segment.into();
778            let mut segment = SiteMapSegment {
779                segment_type,
780                children: Vec::new(),
781            };
782            // if we have a current segment, add it as a child
783            if let Some(current) = current.take() {
784                segment.children.push(current)
785            }
786            current = Some(segment);
787        }
788        current
789    }
790}
791
792impl ToTokens for SiteMapSegment {
793    fn to_tokens(&self, tokens: &mut TokenStream2) {
794        let segment_type = &self.segment_type;
795        let children = if let SegmentType::Child(ty) = &self.segment_type {
796            quote! { #ty::SITE_MAP }
797        } else {
798            let children = self
799                .children
800                .iter()
801                .map(|child| child.to_token_stream())
802                .collect::<Vec<_>>();
803            quote! {
804                &[
805                    #(#children,)*
806                ]
807            }
808        };
809
810        tokens.extend(quote! {
811            dioxus_router::routable::SiteMapSegment {
812                segment_type: #segment_type,
813                children: #children,
814            }
815        });
816    }
817}
818
819enum SegmentType {
820    Static(String),
821    Dynamic(String),
822    CatchAll(String),
823    Child(Type),
824}
825
826impl ToTokens for SegmentType {
827    fn to_tokens(&self, tokens: &mut TokenStream2) {
828        match self {
829            SegmentType::Static(s) => {
830                tokens.extend(quote! { dioxus_router::routable::SegmentType::Static(#s) })
831            }
832            SegmentType::Dynamic(s) => {
833                tokens.extend(quote! { dioxus_router::routable::SegmentType::Dynamic(#s) })
834            }
835            SegmentType::CatchAll(s) => {
836                tokens.extend(quote! { dioxus_router::routable::SegmentType::CatchAll(#s) })
837            }
838            SegmentType::Child(_) => {
839                tokens.extend(quote! { dioxus_router::routable::SegmentType::Child })
840            }
841        }
842    }
843}
844
845impl<'a> From<&'a RouteSegment> for SegmentType {
846    fn from(value: &'a RouteSegment) -> Self {
847        match value {
848            RouteSegment::Static(s) => SegmentType::Static(s.to_string()),
849            RouteSegment::Dynamic(s, _) => SegmentType::Dynamic(s.to_string()),
850            RouteSegment::CatchAll(s, _) => SegmentType::CatchAll(s.to_string()),
851        }
852    }
853}