arg_enum_proc_macro/
lib.rs

1//! # arg_enum_proc_macro
2//!
3//! This crate consists in a procedural macro derive that provides the
4//! same implementations that clap the [`clap::arg_enum`][1] macro provides:
5//! [`std::fmt::Display`], [`std::str::FromStr`] and a `variants()` function.
6//!
7//! By using a procedural macro it allows documenting the enum fields
8//! correctly and avoids the requirement of expanding the macro to use
9//! the structure with [cbindgen](https://crates.io/crates/cbindgen).
10//!
11//! [1]: https://docs.rs/clap/2.32.0/clap/macro.arg_enum.html
12//!
13
14#![recursion_limit = "128"]
15
16extern crate proc_macro;
17
18use proc_macro2::{Literal, Punct, Span, TokenStream, TokenTree};
19use quote::{quote, quote_spanned};
20use std::iter::FromIterator;
21
22use syn::Lit::{self};
23use syn::Meta::{self};
24use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Ident, LitStr};
25
26/// Implement [`std::fmt::Display`], [`std::str::FromStr`] and `variants()`.
27///
28/// The invocation:
29/// ``` no_run
30/// use arg_enum_proc_macro::ArgEnum;
31///
32/// #[derive(ArgEnum)]
33/// enum Foo {
34///     A,
35///     /// Describe B
36///     #[arg_enum(alias = "Bar")]
37///     B,
38///     /// Describe C
39///     /// Multiline
40///     #[arg_enum(name = "Baz")]
41///     C,
42/// }
43/// ```
44///
45/// produces:
46/// ``` no_run
47/// enum Foo {
48///     A,
49///     B,
50///     C
51/// }
52/// impl ::std::str::FromStr for Foo {
53///     type Err = String;
54///
55///     fn from_str(s: &str) -> ::std::result::Result<Self,Self::Err> {
56///         match s {
57///             "A" | _ if s.eq_ignore_ascii_case("A") => Ok(Foo::A),
58///             "B" | _ if s.eq_ignore_ascii_case("B") => Ok(Foo::B),
59///             "Bar" | _ if s.eq_ignore_ascii_case("Bar") => Ok(Foo::B),
60///             "Baz" | _ if s.eq_ignore_ascii_case("Baz") => Ok(Foo::C),
61///             _ => Err({
62///                 let v = vec![ "A", "B", "Bar", "Baz" ];
63///                 format!("valid values: {}", v.join(" ,"))
64///             }),
65///         }
66///     }
67/// }
68/// impl ::std::fmt::Display for Foo {
69///     fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
70///         match *self {
71///             Foo::A => write!(f, "A"),
72///             Foo::B => write!(f, "B"),
73///             Foo::C => write!(f, "C"),
74///         }
75///     }
76/// }
77///
78/// impl Foo {
79///     /// Returns an array of valid values which can be converted into this enum.
80///     #[allow(dead_code)]
81///     pub fn variants() -> [&'static str; 4] {
82///         [ "A", "B", "Bar", "Baz", ]
83///     }
84///     #[allow(dead_code)]
85///     pub fn descriptions() -> [(&'static [&'static str], &'static [&'static str]) ;3] {
86///         [(&["A"], &[]),
87///          (&["B", "Bar"], &[" Describe B"]),
88///          (&["Baz"], &[" Describe C", " Multiline"]),]
89///     }
90/// }
91/// ```
92#[proc_macro_derive(ArgEnum, attributes(arg_enum))]
93pub fn arg_enum(items: proc_macro::TokenStream) -> proc_macro::TokenStream {
94    let input = parse_macro_input!(items as DeriveInput);
95
96    let name = input.ident;
97    let variants = if let Data::Enum(data) = input.data {
98        data.variants
99    } else {
100        panic!("Only enum supported");
101    };
102
103    let all_variants: Vec<(TokenTree, &Ident)> = variants
104        .iter()
105        .flat_map(|item| {
106            let id = &item.ident;
107            if !item.fields.is_empty() {
108                panic!(
109                    "Only enum with unit variants are supported! \n\
110                    Variant {}::{} is not an unit variant",
111                    name,
112                    &id.to_string()
113                );
114            }
115
116            let lit: TokenTree = Literal::string(&id.to_string()).into();
117            let mut all_lits = vec![(lit, id)];
118            item.attrs
119                .iter()
120                .filter(|attr| attr.path().is_ident("arg_enum"))
121                // .flat_map(|attr| {
122                .for_each(|attr| {
123                    attr.parse_nested_meta(|meta| {
124                        if meta.path.is_ident("alias") {
125                            let val = meta.value()?;
126                            let alias: Literal = val.parse()?;
127                            all_lits.push((alias.into(), id));
128                        }
129                        if meta.path.is_ident("name") {
130                            let val = meta.value()?;
131                            let name: Literal = val.parse()?;
132                            all_lits[0] = (name.into(), id);
133                        }
134                        Ok(())
135                    })
136                    .unwrap();
137                });
138            all_lits.into_iter()
139        })
140        .collect();
141
142    let len = all_variants.len();
143
144    let from_str_match = all_variants.iter().flat_map(|(lit, id)| {
145        let pat: TokenStream = quote! {
146            #lit | _ if s.eq_ignore_ascii_case(#lit) => Ok(#name::#id),
147        };
148
149        pat.into_iter()
150    });
151
152    let from_str_match = TokenStream::from_iter(from_str_match);
153
154    let all_descriptions: Vec<(Vec<TokenTree>, Vec<LitStr>)> = variants
155        .iter()
156        .map(|item| {
157            let id = &item.ident;
158            let description = item
159                .attrs
160                .iter()
161                .filter_map(|attr| {
162                    let expr = match &attr.meta {
163                        Meta::NameValue(name_value) if name_value.path.is_ident("doc") => {
164                            Some(name_value.value.to_owned())
165                        }
166                        _ =>
167                        // non #[doc = "..."] attributes are not our concern
168                        // we leave them for rustc to handle
169                        {
170                            None
171                        }
172                    };
173
174                    expr.and_then(|expr| {
175                        if let Expr::Lit(ExprLit {
176                            lit: Lit::Str(s), ..
177                        }) = expr
178                        {
179                            Some(s)
180                        } else {
181                            None
182                        }
183                    })
184                })
185                .collect();
186            let lit: TokenTree = Literal::string(&id.to_string()).into();
187            let mut all_names = vec![lit];
188            item.attrs
189                .iter()
190                .filter(|attr| attr.path().is_ident("arg_enum"))
191                // .flat_map(|attr| {
192                .for_each(|attr| {
193                    attr.parse_nested_meta(|meta| {
194                        if meta.path.is_ident("alias") {
195                            let val = meta.value()?;
196                            let alias: Literal = val.parse()?;
197                            all_names.push(alias.into());
198                        }
199                        if meta.path.is_ident("name") {
200                            let val = meta.value()?;
201                            let name: Literal = val.parse()?;
202                            all_names[0] = name.into();
203                        }
204                        Ok(())
205                    })
206                    .unwrap();
207                });
208
209            (all_names, description)
210        })
211        .collect();
212
213    let display_match = variants.iter().flat_map(|item| {
214        let id = &item.ident;
215        let lit: TokenTree = Literal::string(&id.to_string()).into();
216
217        let pat: TokenStream = quote! {
218            #name::#id => write!(f, #lit),
219        };
220
221        pat.into_iter()
222    });
223
224    let display_match = TokenStream::from_iter(display_match);
225
226    let comma: TokenTree = Punct::new(',', proc_macro2::Spacing::Alone).into();
227    let array_items = all_variants
228        .iter()
229        .flat_map(|(tok, _id)| vec![tok.clone(), comma.clone()].into_iter());
230
231    let array_items = TokenStream::from_iter(array_items);
232
233    let array_descriptions = all_descriptions.iter().map(|(names, descr)| {
234        quote! {
235            (&[ #(#names),* ], &[ #(#descr),* ]),
236        }
237    });
238    let array_descriptions = TokenStream::from_iter(array_descriptions);
239
240    let len_descriptions = all_descriptions.len();
241
242    let ret: TokenStream = quote_spanned! {
243        Span::call_site() =>
244        impl ::std::str::FromStr for #name {
245            type Err = String;
246
247            fn from_str(s: &str) -> ::std::result::Result<Self,Self::Err> {
248                match s {
249                    #from_str_match
250                    _ => {
251                        let values = [ #array_items ];
252
253                        Err(format!("valid values: {}", values.join(" ,")))
254                    }
255                }
256            }
257        }
258        impl ::std::fmt::Display for #name {
259            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
260                match *self {
261                    #display_match
262                }
263            }
264        }
265        impl #name {
266            #[allow(dead_code)]
267            /// Returns an array of valid values which can be converted into this enum.
268            pub fn variants() -> [&'static str; #len] {
269                [ #array_items ]
270            }
271            #[allow(dead_code)]
272            /// Returns an array of touples (variants, description)
273            pub fn descriptions() -> [(&'static [&'static str], &'static [&'static str]); #len_descriptions] {
274                [ #array_descriptions ]
275            }
276        }
277    };
278
279    ret.into()
280}