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 | |
16 | extern crate proc_macro; |
17 | |
18 | use proc_macro2::{Literal, Punct, Span, TokenStream, TokenTree}; |
19 | use quote::{quote, quote_spanned}; |
20 | use std::iter::FromIterator; |
21 | |
22 | use syn::Lit::{self}; |
23 | use syn::Meta::{self}; |
24 | use 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))] |
93 | pub 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 | |