plaster_router_macro/
lib.rs

1extern crate proc_macro;
2#[macro_use]
3extern crate quote;
4
5use proc_macro::TokenStream;
6
7#[proc_macro_derive(Routes, attributes(route))]
8pub fn plaster_router(input: TokenStream) -> TokenStream {
9    match syn::parse2::<syn::Item>(input.into()) {
10        Ok(item) => match item {
11            syn::Item::Enum(item_enum) => parse_enum(item_enum).into(),
12            _ => panic!("plaster_router must be used on an enum"),
13        },
14        Err(e) => {
15            panic!("parse error: {}", e);
16        }
17    }
18}
19
20fn parse_enum(item: syn::ItemEnum) -> proc_macro2::TokenStream {
21    let ident = item.ident;
22    let routes = item.variants.into_iter().map(|variant| {
23        if let Some(path) = parse_route_attr(&variant.attrs) {
24            let mut route = path.as_str();
25            if route.len() != 0 && route.as_bytes()[0] == b'/' {
26                route = &route[1..];
27            }
28
29            let route_literal = syn::LitStr::new(route, proc_macro2::Span::call_site());
30            let variant_ident = variant.ident;
31            let mut params = Vec::new();
32
33            for segment in route.split('/') {
34                if segment.len() > 0 && segment.as_bytes()[0] == b':' {
35                    params.push(segment[1..].to_string());
36                } else if segment.len() > 0 && segment.as_bytes()[0] == b'*' {
37                    params.push(segment[1..].to_string());
38                }
39            }
40
41            if params.len() > 0 {
42                if let syn::Fields::Named(fields) = variant.fields {
43                    // todo: make this optional
44                    // let field_names: Vec<String> = fields
45                    //     .named
46                    //     .iter()
47                    //     .map(|field| field.ident.as_ref().unwrap().to_string())
48                    //     .collect();
49
50                    // if params.len() != field_names.len()
51                    //     || params.difference(&field_names).count() > 0
52                    // {
53                    //     panic!("all params must have a field in the variant");
54                    // }
55
56                    let field_idents: Vec<_> = fields
57                        .named
58                        .into_iter()
59                        .map(|field| field.ident.unwrap())
60                        .collect();
61                    let params_literal: Vec<syn::LitStr> = params
62                        .iter()
63                        .map(|param| syn::LitStr::new(param, proc_macro2::Span::call_site()))
64                        .collect();
65
66                    quote! {
67                        router.add_route(#route_literal, |params| {
68                            #ident::#variant_ident {
69                                #(
70                                    #field_idents: params.find(#params_literal).unwrap().to_string()
71                                ),*
72                            }
73                        });
74                    }
75                } else {
76                    panic!("all variants with params must have named fields");
77                }
78            } else {
79                quote! {
80                    router.add_route(#route_literal, |_| #ident::#variant_ident);
81                }
82            }
83        } else {
84            panic!("all variants of the enum must have a route attribute");
85        }
86    });
87
88    quote! {
89        impl plaster_router::Routes<#ident> for #ident {
90            fn router(callback: plaster::callback::Callback<()>) -> plaster_router::Router<#ident> {
91                let mut router = plaster_router::Router::new(callback);
92                #(#routes)*
93                router
94            }
95        }
96    }
97}
98
99fn parse_route_attr(attrs: &[syn::Attribute]) -> Option<String> {
100    attrs.iter().find_map(|attr| {
101        let meta = attr
102            .parse_meta()
103            .expect("could not parse meta for attribute");
104        match meta {
105            syn::Meta::List(list) => {
106                if list.ident == "route" {
107                    if let Some(route) = list.nested.first() {
108                        if let syn::NestedMeta::Literal(syn::Lit::Str(route)) = route.value() {
109                            Some(route.value())
110                        } else {
111                            panic!("route spec in route attribute must be a string in quotes");
112                        }
113                    } else {
114                        panic!("must specify a route spec in route attribute");
115                    }
116                } else {
117                    None
118                }
119            }
120            _ => None,
121        }
122    })
123}