enum_conversion_derive/
parse_attributes.rs

1use std::convert::TryFrom;
2
3use quote::ToTokens;
4use syn::parse::Parser;
5use syn::punctuated::Punctuated;
6use syn::Token;
7use syn::__private::TokenStream2;
8use syn::{Attribute, Expr};
9
10const ATTR_TRY_FROM: &str = "DeriveTryFrom";
11
12/// The information for each variant
13/// in the enum.
14#[derive(Debug, Clone, Hash, PartialEq, Eq)]
15pub(crate) struct VariantInfo {
16    /// The type of the variant.
17    pub ty: String,
18    /// Indicates if a `TryFrom` trait should be derived
19    /// for this variant.
20    pub try_from: bool,
21}
22
23impl From<&str> for VariantInfo {
24    fn from(ty: &str) -> Self {
25        VariantInfo {
26            ty: ty.to_string(),
27            try_from: false,
28        }
29    }
30}
31
32/// The input to the `EnumConversion` macro
33/// can configure errors for the
34/// `TryTo`/ `TryFrom` traits. In that case,
35/// custom error types and a closure for converting
36/// to that error type must be given.
37#[derive(Debug, Clone, Hash, PartialEq, Eq)]
38pub enum ErrorConfig {
39    Custom { error_ty: String, map_err: String },
40    Default,
41}
42
43impl ErrorConfig {
44    /// Parse the config into the strings to be
45    /// passed into the templates.
46    pub(crate) fn to_template(&self) -> (String, String) {
47        match self {
48            Self::Default => ("EnumConversionError".to_string(), "".into()),
49            Self::Custom { error_ty, map_err } => {
50                (error_ty.to_string(), format!(".map_err({})", map_err))
51            }
52        }
53    }
54}
55
56/// As we extract args one by one,
57/// we need to know how to add them
58/// to the config.
59enum ErrorConfigParam {
60    ErrorTy(String),
61    MapErr(String),
62}
63
64impl Default for ErrorConfig {
65    fn default() -> Self {
66        Self::Default
67    }
68}
69
70/// Parse attribute macros on the enum and variants.
71///
72/// Once the attribute macros are processed, they
73/// are removed from the AST.
74///
75/// Returns `true` iff the attribute is `[DeriveTryFrom]`.
76pub(crate) fn parse_attrs(attrs: &mut Vec<Attribute>) -> bool {
77    let mut derive_try_from = false;
78    *attrs = attrs
79        .clone()
80        .into_iter()
81        .filter(|attr| {
82            if let Some(prefix) = attr.path.segments.first().map(|seg| seg.ident.to_string()) {
83                if prefix == ATTR_TRY_FROM {
84                    derive_try_from = true;
85                    false
86                } else {
87                    true
88                }
89            } else {
90                true
91            }
92        })
93        .collect();
94    derive_try_from
95}
96
97/// Process the arguments passed into the attribute.
98/// Panics if they are not of the right format or
99/// the wrong number of arguments were passed in.
100fn split_args(args: TokenStream2) -> [Expr; 2] {
101    let err_str = format!(
102        "EnumConversion attribute macros expect either no arguments or exactly two \
103         of the form 'Error: Type' and a closure. Found '{}'",
104        args,
105    );
106    // split the args by commas
107    let parser = Punctuated::<syn::Expr, Token![,]>::parse_terminated;
108    let args = match parser.parse2(args) {
109        Ok(args) => args,
110        Err(_) => panic!("{}", &err_str),
111    };
112    // check that we got exactly two args
113    if let Ok(res) = <[Expr; 2]>::try_from(args.iter().take(2).cloned().collect::<Vec<Expr>>()) {
114        res
115    } else {
116        panic!("{}", &err_str)
117    }
118}
119
120/// If an attribute macro is labelled as specifying a custom
121/// ErrorConfig and it has arguments, this function parses them.
122pub(crate) fn parse_custom_error_config(args: TokenStream2) -> ErrorConfig {
123    if args.is_empty() {
124        return ErrorConfig::Default;
125    }
126    let arg_string = args.to_string();
127    let [arg1, arg2] = split_args(args);
128    let parsed_1 = parse_attr_args(arg1);
129    let parsed_2 = parse_attr_args(arg2);
130    match (parsed_1, parsed_2) {
131        (ErrorConfigParam::ErrorTy(ty), ErrorConfigParam::MapErr(map)) => ErrorConfig::Custom {
132            error_ty: ty,
133            map_err: map,
134        },
135        (ErrorConfigParam::MapErr(map), ErrorConfigParam::ErrorTy(ty)) => ErrorConfig::Custom {
136            error_ty: ty,
137            map_err: map,
138        },
139        _ => panic!(
140            "EnumConversion attribute macros expect either no arguments or exactly two \
141            of the form 'Error: Type' and a closure. Found '{}'",
142            arg_string,
143        ),
144    }
145}
146
147/// Parse the attribute macros of variants and / or enums as
148/// a whole.
149fn parse_attr_args(arg: Expr) -> ErrorConfigParam {
150    match arg {
151        Expr::Type(type_expr) => {
152            if type_expr.expr.to_token_stream().to_string() == "Error" {
153                ErrorConfigParam::ErrorTy(type_expr.ty.to_token_stream().to_string())
154            } else {
155                panic!(
156                    "EnumConversions expected 'Error: Type', found '{}'",
157                    type_expr.to_token_stream(),
158                )
159            }
160        }
161        Expr::Closure(closure) => ErrorConfigParam::MapErr(closure.to_token_stream().to_string()),
162        _ => panic!(
163            "Attribute macros for EnumConversions must either be of the form: \
164                     'Error: Type' or a closure."
165        ),
166    }
167}
168
169#[cfg(test)]
170mod test_attrs {
171    use quote::quote;
172    use syn::{parse_str, DeriveInput};
173
174    use super::*;
175
176    /// Test that if no attribute macros are present,
177    /// the default error config is generated.
178    /// Furthermore, macros unrelated to this crate
179    /// are not stripped.
180    #[test]
181    fn test_no_op() {
182        let mut ast: DeriveInput = parse_str(
183            r#"
184            #[derive(Debug)]
185            enum Enum {
186                #[random]
187                F1(i64),
188                F2(bool),
189            }
190        "#,
191        )
192        .expect("Test failed");
193        let ast_clone = ast.clone();
194        let attrs = parse_attrs(&mut ast.attrs);
195        assert!(!attrs);
196        assert_eq!(ast, ast_clone);
197    }
198
199    /// Test that the top level macros are stripped when they
200    /// are processed.
201    #[test]
202    fn test_strip_macros() {
203        let mut ast: DeriveInput = parse_str(
204            r#"
205            #[EnumConversion]
206            #[DeriveTryFrom]
207            enum Enum {
208                F1(i64),
209                #[DeriveTryFrom]
210                F2(bool),
211            }
212        "#,
213        )
214        .expect("Test failed.");
215        assert!(parse_attrs(&mut ast.attrs));
216        let expected: DeriveInput = parse_str(
217            r#"
218            #[EnumConversion]
219            enum Enum {
220                F1(i64),
221                #[DeriveTryFrom]
222                F2(bool),
223            }
224        "#,
225        )
226        .expect("Test failed");
227        assert_eq!(ast, expected);
228    }
229
230    /// Test that providing no arguments to
231    /// `EnumConversion` returns the default
232    /// error config.
233    #[test]
234    fn test_default_error_config() {
235        let args = quote!();
236        let error_config = parse_custom_error_config(args);
237        assert_eq!(error_config, ErrorConfig::Default);
238    }
239
240    /// Test that arguments to the `EnumConversion` trait
241    /// get parsed correctly into an `ErrorConfig`.
242    #[test]
243    fn test_parse_custom_error_config() {
244        let args = quote!(Error: std::io::Error, |e| Error::new(
245            ErrorKind::Other,
246            e.to_string()
247        ));
248
249        let error_config = parse_custom_error_config(args);
250        let expected = ErrorConfig::Custom {
251            error_ty: "std :: io :: Error".to_string(),
252            map_err: "| e | Error :: new (ErrorKind :: Other , e . to_string ())".to_string(),
253        };
254        assert_eq!(error_config, expected);
255    }
256
257    #[test]
258    #[should_panic(
259        expected = "EnumConversion attribute macros expect either no arguments or exactly two of the form 'Error: Type' and a closure. Found 'Error : std :: io :: Error ,'"
260    )]
261    fn test_wrong_arg_number() {
262        let args = quote!(Error: std::io::Error,);
263
264        _ = parse_custom_error_config(args);
265    }
266
267    #[test]
268    #[should_panic(
269        expected = "Attribute macros for EnumConversions must either be of the form: 'Error: Type' or a closure."
270    )]
271    fn test_non_closure() {
272        let args = quote!(Error: std::io::Error, Vec::new);
273
274        _ = parse_custom_error_config(args);
275    }
276
277    #[test]
278    #[should_panic(
279        expected = "EnumConversions expected 'Error: Type', found 'err : std :: io :: Error'"
280    )]
281    fn test_bad_key() {
282        let args = quote!(err: std::io::Error, |e| Error::new(
283            ErrorKind::Other,
284            e.to_string()
285        ));
286
287        _ = parse_custom_error_config(args);
288    }
289
290    #[test]
291    #[should_panic]
292    fn test_non_error_type() {
293        let args = quote!(
294                Error: || false,
295                |e| Error::new(ErrorKind::Other, e.to_string())
296        );
297
298        _ = parse_custom_error_config(args);
299    }
300}