1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
extern crate proc_macro;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;

#[proc_macro_derive(Routes, attributes(route))]
pub fn plaster_router(input: TokenStream) -> TokenStream {
    match syn::parse2::<syn::Item>(input.into()) {
        Ok(item) => match item {
            syn::Item::Enum(item_enum) => parse_enum(item_enum).into(),
            _ => panic!("plaster_router must be used on an enum"),
        },
        Err(e) => {
            panic!("parse error: {}", e);
        }
    }
}

fn parse_enum(item: syn::ItemEnum) -> proc_macro2::TokenStream {
    let ident = item.ident;
    let routes = item.variants.into_iter().map(|variant| {
        if let Some(path) = parse_route_attr(&variant.attrs) {
            let mut route = path.as_str();
            if route.len() != 0 && route.as_bytes()[0] == b'/' {
                route = &route[1..];
            }

            let route_literal = syn::LitStr::new(route, proc_macro2::Span::call_site());
            let variant_ident = variant.ident;
            let mut params = Vec::new();

            for segment in route.split('/') {
                if segment.len() > 0 && segment.as_bytes()[0] == b':' {
                    params.push(segment[1..].to_string());
                } else if segment.len() > 0 && segment.as_bytes()[0] == b'*' {
                    params.push(segment[1..].to_string());
                }
            }

            if params.len() > 0 {
                if let syn::Fields::Named(fields) = variant.fields {
                    // todo: make this optional
                    // let field_names: Vec<String> = fields
                    //     .named
                    //     .iter()
                    //     .map(|field| field.ident.as_ref().unwrap().to_string())
                    //     .collect();

                    // if params.len() != field_names.len()
                    //     || params.difference(&field_names).count() > 0
                    // {
                    //     panic!("all params must have a field in the variant");
                    // }

                    let field_idents: Vec<_> = fields
                        .named
                        .into_iter()
                        .map(|field| field.ident.unwrap())
                        .collect();
                    let params_literal: Vec<syn::LitStr> = params
                        .iter()
                        .map(|param| syn::LitStr::new(param, proc_macro2::Span::call_site()))
                        .collect();

                    quote! {
                        router.add_route(#route_literal, |params| {
                            #ident::#variant_ident {
                                #(
                                    #field_idents: params.find(#params_literal).unwrap().to_string()
                                ),*
                            }
                        });
                    }
                } else {
                    panic!("all variants with params must have named fields");
                }
            } else {
                quote! {
                    router.add_route(#route_literal, |_| #ident::#variant_ident);
                }
            }
        } else {
            panic!("all variants of the enum must have a route attribute");
        }
    });

    quote! {
        impl plaster_router::Routes<#ident> for #ident {
            fn router(callback: plaster::callback::Callback<()>) -> plaster_router::Router<#ident> {
                let mut router = plaster_router::Router::new(callback);
                #(#routes)*
                router
            }
        }
    }
}

fn parse_route_attr(attrs: &[syn::Attribute]) -> Option<String> {
    attrs.iter().find_map(|attr| {
        let meta = attr
            .parse_meta()
            .expect("could not parse meta for attribute");
        match meta {
            syn::Meta::List(list) => {
                if list.ident == "route" {
                    if let Some(route) = list.nested.first() {
                        if let syn::NestedMeta::Literal(syn::Lit::Str(route)) = route.value() {
                            Some(route.value())
                        } else {
                            panic!("route spec in route attribute must be a string in quotes");
                        }
                    } else {
                        panic!("must specify a route spec in route attribute");
                    }
                } else {
                    None
                }
            }
            _ => None,
        }
    })
}