description_macro/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::ToTokens;
4use syn::{parse_macro_input, Attribute, Data, DataEnum, DeriveInput, Error, Ident, LitStr, Result};
5
6#[proc_macro_derive(Description, attributes(description))]
7pub fn derive_description(input: TokenStream) -> TokenStream {
8    let input = parse_macro_input!(input as DeriveInput);
9
10    match try_expand(&input) {
11        Ok(expanded) => expanded,
12        Err(error) => error.to_compile_error().into(),
13    }
14}
15
16fn try_expand(input: &DeriveInput) -> Result<TokenStream> {
17    match &input.data {
18        Data::Enum(e) => Ok(impl_enum(&input.ident, e)?),
19        _ => Err(Error::new_spanned(input, "Description cannot be implemented on structs or unions"))
20    }
21}
22
23fn impl_enum(ident: &Ident, input: &DataEnum) -> Result<TokenStream> {
24    let mut vec = Vec::with_capacity(input.variants.len());
25    for variant in &input.variants {
26        let attr = variant.attrs.iter()
27        .find(|x| x.path().is_ident("description"))
28        .ok_or(syn::Error::new_spanned(variant, "Missing 'description' attribute"))?;
29
30        let result: Result<LitStr> = attr.parse_args();
31
32        let var = parse_args(result, attr)?;
33        let ident = &variant.ident;
34        vec.push(quote::quote! { Self::#ident => #var, });
35    }
36
37    Ok(quote::quote! {
38        impl ::description::Description for #ident {
39            fn description(&self) -> &'static str {
40                match self {
41                    #(#vec)*
42                }
43            }
44        }
45    }.into())
46}
47
48fn parse_args(result: Result<LitStr>, attr: &Attribute) -> Result<proc_macro2::TokenStream> {
49    
50    let format = if let Ok(v) = result.clone() {
51        v.value().contains('{')
52        // use format logic
53    } else {true};
54
55    if format {
56        #[cfg(feature = "format")]
57        {
58            let args: proc_macro2::TokenStream = attr.parse_args()?;
59            Ok(quote::quote! {
60                ::const_format::formatcp!(#args)
61            })
62        }
63
64        #[cfg(not(feature = "format"))]
65        {
66            // check if the first argument is a string literal containing curly brackets
67            let args: proc_macro2::TokenStream = attr.parse_args()?;
68            let str = args.into_iter()
69            .next().map(|s| s.to_string().contains('{'));
70
71            match str {
72                Some(true) => Err(Error::new_spanned(attr, "You need the 'format' feature to use format arguments")),
73                _ => Err(result.err().unwrap())
74            }
75        }
76        // TODO: give a better error if the user is trying to use format without the proper feature flag
77    } else {
78        let segment = result.unwrap();
79        Ok(segment.to_token_stream())
80    }
81}
82
83#[proc_macro_derive(OptionalDescription, attributes(description))]
84pub fn derive_optional_description(input: TokenStream) -> TokenStream {
85    let input = parse_macro_input!(input as DeriveInput);
86    
87    match try_expand_optional(&input) {
88        Ok(expanded) => expanded,
89        Err(error) => error.to_compile_error().into(),
90    }
91}
92
93fn try_expand_optional(input: &DeriveInput) -> Result<TokenStream> {
94    match &input.data {
95        Data::Enum(e) => Ok(impl_enum_optional(&input.ident, e)?),
96        _ => Err(Error::new_spanned(input, "Description cannot be implemented on structs or unions"))
97    }
98}
99
100fn impl_enum_optional(ident: &Ident, input: &DataEnum) -> Result<TokenStream> {
101    let mut vec = Vec::with_capacity(input.variants.len());
102
103    for variant in &input.variants {
104        let attr = variant.attrs.iter()
105        .find(|x| x.path().is_ident("description"));
106
107        let segment: Option<Result<LitStr>> = attr.map(|x| x.parse_args());
108
109        let ident = &variant.ident;
110        match segment {
111            Some(result) => {
112                let var = parse_args(result, attr.unwrap())?;
113                let ident = &variant.ident;
114                vec.push(quote::quote! { Self::#ident => Some(#var), });
115            },
116            None => {
117                vec.push(quote::quote! {
118                    Self::#ident => None,
119                });
120            }
121        }
122    }
123
124    Ok(quote::quote! {
125        impl ::description::OptionalDescription for #ident {
126            fn description(&self) -> Option<&'static str> {
127                match self {
128                    #(#vec)*
129                }
130            }
131        }
132    }.into())
133}