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}
281