freya-router-macro 0.4.3

Macro for Freya Router
Documentation
use proc_macro2::TokenStream as TokenStream2;
use quote::{
    format_ident,
    quote,
};
use syn::{
    Field,
    Ident,
    LitStr,
    Path,
    Type,
    parse::{
        Parse,
        ParseStream,
    },
    parse_quote,
};

use crate::{
    hash::HashFragment,
    layout::{
        Layout,
        LayoutId,
    },
    nest::{
        Nest,
        NestId,
    },
    query::QuerySegment,
    segment::{
        RouteSegment,
        create_error_type,
        parse_route_segments,
    },
};

struct RouteArgs {
    route: LitStr,
    comp_name: Option<Path>,
}

impl Parse for RouteArgs {
    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
        let route = input.parse::<LitStr>()?;

        Ok(RouteArgs {
            route,
            comp_name: {
                let _ = input.parse::<syn::Token![,]>();
                input.parse().ok()
            },
        })
    }
}

struct ChildArgs {
    route: LitStr,
}

impl Parse for ChildArgs {
    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
        let route = input.parse::<LitStr>()?;

        Ok(ChildArgs { route })
    }
}

#[derive(Debug)]
pub(crate) struct Route {
    pub route_name: Ident,
    pub ty: RouteType,
    pub route: String,
    pub segments: Vec<RouteSegment>,
    pub query: Option<QuerySegment>,
    pub hash: Option<HashFragment>,
    pub nests: Vec<NestId>,
    pub layouts: Vec<LayoutId>,
    fields: Vec<(Ident, Type)>,
}

impl Route {
    pub(crate) fn parse(
        nests: Vec<NestId>,
        layouts: Vec<LayoutId>,
        variant: syn::Variant,
    ) -> syn::Result<Self> {
        let route_attr = variant
            .attrs
            .iter()
            .find(|attr| attr.path().is_ident("route"));
        let route;
        let ty;
        let route_name = variant.ident.clone();
        match route_attr {
            Some(attr) => {
                let args = attr.parse_args::<RouteArgs>()?;
                let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
                ty = RouteType::Leaf {
                    component: comp_name,
                };
                route = args.route.value();
            }
            None => {
                if let Some(route_attr) = variant
                    .attrs
                    .iter()
                    .find(|attr| attr.path().is_ident("child"))
                {
                    let args = route_attr.parse_args::<ChildArgs>()?;
                    route = args.route.value();
                    match &variant.fields {
                        syn::Fields::Named(fields) => {
                            // find either a field with #[child] or a field named "child"
                            let child_field = fields.named.iter().find(|f| {
                                f.attrs.iter().any(|attr| attr.path().is_ident("child"))
                                    || *f.ident.as_ref().unwrap() == "child"
                            });
                            match child_field {
                                Some(child) => {
                                    ty = RouteType::Child(Box::new(child.clone()));
                                }
                                None => {
                                    return Err(syn::Error::new_spanned(
                                        variant.clone(),
                                        "Routable variants with a #[child(..)] attribute must have a field named \"child\" or a field with a #[child] attribute",
                                    ));
                                }
                            }
                        }
                        _ => {
                            return Err(syn::Error::new_spanned(
                                variant.clone(),
                                "Routable variants with a #[child(..)] attribute must have named fields",
                            ));
                        }
                    }
                } else {
                    return Err(syn::Error::new_spanned(
                        variant.clone(),
                        "Routable variants must either have a #[route(..)] attribute or a #[child(..)] attribute",
                    ));
                }
            }
        };

        let fields = match &variant.fields {
            syn::Fields::Named(fields) => fields
                .named
                .iter()
                .filter_map(|f| {
                    if let RouteType::Child(child) = &ty
                        && f.ident == child.ident
                    {
                        return None;
                    }
                    Some((f.ident.clone().unwrap(), f.ty.clone()))
                })
                .collect(),
            _ => Vec::new(),
        };

        let (route_segments, query, hash) = {
            parse_route_segments(
                variant.ident.span(),
                fields.iter().map(|f| (&f.0, &f.1)),
                &route,
            )?
        };

        Ok(Self {
            ty,
            route_name,
            segments: route_segments,
            route,
            query,
            hash,
            nests,
            layouts,
            fields,
        })
    }

    pub(crate) fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
        let name = &self.route_name;
        let dynamic_segments = self.dynamic_segments();
        let write_query: Option<TokenStream2> = self.query.as_ref().map(|q| q.write());
        let write_hash = self.hash.as_ref().map(|q| q.write());

        match &self.ty {
            RouteType::Child(field) => {
                let write_nests = self.nests.iter().map(|id| nests[id.0].write());
                let write_segments = self.segments.iter().map(|s| s.write_segment());
                let child = field.ident.as_ref().unwrap();
                quote! {
                    Self::#name { #(#dynamic_segments,)* #child } => {
                        use std::fmt::Display;
                        use std::fmt::Write;
                        let mut route = String::new();
                        {
                            let f = &mut route;
                            #(#write_nests)*
                            #(#write_segments)*
                        }
                        if route.ends_with('/') {
                            route.pop();
                        }
                        f.write_str(&route)?;
                        #child.fmt(f)?;
                    }
                }
            }
            RouteType::Leaf { .. } => {
                let write_nests = self.nests.iter().map(|id| nests[id.0].write());
                let write_segments = self.segments.iter().map(|s| s.write_segment());
                quote! {
                    Self::#name { #(#dynamic_segments,)* } => {
                        #(#write_nests)*
                        #(#write_segments)*
                        #write_query
                        #write_hash
                    }
                }
            }
        }
    }

    pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 {
        let name = &self.route_name;

        let mut tokens = TokenStream2::new();

        // First match all layouts
        for (idx, layout_id) in self.layouts.iter().copied().enumerate() {
            let render_layout = layouts[layout_id.0].routable_match(nests);
            let dynamic_segments = self.dynamic_segments();
            let mut field_name = None;
            if let RouteType::Child(field) = &self.ty {
                field_name = field.ident.as_ref();
            }
            let field_name = field_name.map(|f| quote!(#f,));
            // This is a layout
            tokens.extend(quote! {
                #[allow(unused)]
                (#idx, Self::#name { #(#dynamic_segments,)* #field_name .. }) => {
                    #render_layout
                }
            });
        }

        // Then match the route
        let last_index = self.layouts.len();
        tokens.extend(match &self.ty {
            RouteType::Child(field) => {
                let field_name = field.ident.as_ref().unwrap();
                quote! {
                    #[allow(unused)]
                    (#last_index.., Self::#name { #field_name, .. }) => {
                        freya::prelude::Element::from(freya_router::components::child_router::ChildRouter {
                            route: #field_name,
                            // Try to parse the current route as a parent route, and then match it as a child route
                            parse_route_from_root_route: |__route| if let Ok(__route) = __route.parse() {
                                if let Self::#name { #field_name, .. } = __route {
                                    Some(#field_name)
                                } else {
                                    None
                                }
                            } else {
                                None
                            },
                            // Try to parse the child route and turn it into a parent route
                            format_route_as_root_route: |#field_name| Self::#name { #field_name: #field_name }.to_string(),
                        })
                    }
                }
            }
            RouteType::Leaf { component } => {
                let dynamic_segments = self.dynamic_segments();
                let dynamic_segments_from_route = self.dynamic_segments();
                quote! {
                    #[allow(unused)]
                    (#last_index, Self::#name { #(#dynamic_segments,)* }) => {
                        freya::prelude::Element::from(#component {
                            #(#dynamic_segments_from_route: #dynamic_segments_from_route,)*
                        })
                    }
                }
            }
        });

        tokens
    }

    fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
        self.fields.iter().map(|(name, _)| {
            quote! {#name}
        })
    }

    pub(crate) fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
        let segments = self.fields.iter().map(|(name, _)| {
            let mut from_route = false;

            for id in &self.nests {
                let nest = &nests[id.0];
                if nest.dynamic_segments_names().any(|i| &i == name) {
                    from_route = true
                }
            }
            for segment in &self.segments {
                if segment.name().as_ref() == Some(name) {
                    from_route = true
                }
            }
            if let Some(query) = &self.query
                && query.contains_ident(name)
            {
                from_route = true
            }
            if let Some(hash) = &self.hash
                && hash.contains_ident(name)
            {
                from_route = true
            }

            if from_route {
                quote! {#name}
            } else {
                quote! {#name: Default::default()}
            }
        });
        match &self.ty {
            RouteType::Child(field) => {
                let name = &self.route_name;
                let child_name = field.ident.as_ref().unwrap();

                quote! {
                    #enum_name::#name {
                        #child_name,
                        #(#segments,)*
                    }
                }
            }
            RouteType::Leaf { .. } => {
                let name = &self.route_name;

                quote! {
                    #enum_name::#name {
                        #(#segments,)*
                    }
                }
            }
        }
    }

    pub(crate) fn error_ident(&self) -> Ident {
        format_ident!("{}ParseError", self.route_name)
    }

    pub(crate) fn error_type(&self) -> TokenStream2 {
        let error_name = self.error_ident();
        let child_type = match &self.ty {
            RouteType::Child(field) => Some(&field.ty),
            RouteType::Leaf { .. } => None,
        };

        create_error_type(&self.route, error_name, &self.segments, child_type)
    }

    pub(crate) fn parse_query(&self) -> TokenStream2 {
        match &self.query {
            Some(query) => query.parse(),
            None => quote! {},
        }
    }

    pub(crate) fn parse_hash(&self) -> TokenStream2 {
        match &self.hash {
            Some(hash) => hash.parse(),
            None => quote! {},
        }
    }
}

#[derive(Debug)]
pub(crate) enum RouteType {
    Child(Box<Field>),
    Leaf { component: Path },
}