map_enum/
lib.rs

1//! This module contains the procedural macro implementation
2//! for the `StringEnum` attribute.
3
4pub(crate) mod util;
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{parse_macro_input, ItemEnum};
8
9/// This procedural macro generates JS 'string-like' enums,
10/// which support from and to string conversions on all
11/// members.
12///
13/// ```
14/// use map_enum::*;
15/// use std::str::FromStr;
16///
17/// #[derive(Debug)]
18/// #[StringEnum]
19/// pub enum Method {
20///     Get = "Hi",
21///     Post,
22/// }
23///
24/// assert_eq!(Method::Get.to_string(), "Hi".to_string());
25/// assert_eq!(Method::Post.to_string(), "Post".to_string());
26/// assert_eq!(Method::from_str("Hi").unwrap(), Method::Get);
27/// assert_eq!(Method::from_str("Post").unwrap(), Method::Post);
28/// assert!(Method::from_str("other").is_err());
29/// ```
30#[proc_macro_attribute]
31#[allow(non_snake_case)]
32pub fn StringEnum(_attr: TokenStream, input: TokenStream) -> TokenStream {
33    // TODO implement potential usage for attr
34    // #[StringEnum = KebabCase]
35
36    // TODO implement flags for case sensitivity
37    // #[StringEnum(no_case)]
38
39    // https://docs.rs/syn/latest/src/syn/item.rs.html#2072-2093
40    // https://docs.rs/syn/latest/src/syn/derive.rs.html#184-198
41    // https://docs.rs/syn/latest/src/syn/data.rs.html#256-295
42
43    let mut item_enum = parse_macro_input!(input as ItemEnum);
44
45    // A list of variants. We expect the length of the enum
46    // members to be in ascending order, therefore for parsing,
47    // the list has to be reversed to descending order.
48    let mut variants = util::preprocess_string_enum(&mut item_enum);
49    variants.reverse();
50
51    // The name of the enum. This is used to reference the enum
52    // in the generated code.
53    let name = item_enum.ident.clone();
54
55    // A vector of match arm tokens, which are used to match
56    // the input as a string prefix to the corresponding enum
57    // variant.
58    //
59    // Example:
60    // ```rs
61    // _ if input.starts_with("Get") => Some((&input[3..], Self::Get)),
62    // ```
63    let starts_with_arms = variants
64        .iter()
65        .map(|(key, value)| {
66            let len = value.len();
67            quote![_ if input.starts_with(#value) => Ok((&input[#len..], Self::#key)),]
68        })
69        .collect::<Vec<_>>();
70
71    // A vector of match arm tokens, which are used to match
72    // the input as a string to the corresponding enum variant.
73    //
74    // Example:
75    // ```rs
76    // "Get" => Ok(Self::Get),
77    // ```
78    let from_str_arms = variants
79        .iter()
80        .map(|(key, value)| quote![#value => Ok(Self::#key),])
81        .collect::<Vec<_>>();
82
83    // A vector of match arm tokens, which are used to match
84    // the enum variant to the corresponding string.
85    //
86    // Example:
87    // ```rs
88    // Self::Get => "Get",
89    // ```
90    let to_str_arms = variants
91        .iter()
92        .map(|(key, value)| quote![Self::#key => #value,])
93        .collect::<Vec<_>>();
94
95    // Find the longest variant in the enum. This is used to
96    // generated constant values for the `MAX_VARIANT_LEN`
97    let (max_variant, max_variant_len) = {
98        let max_variant = variants
99            .iter()
100            .map(|(_, value)| value)
101            .fold(
102                String::new(),
103                |a, b| if a.len() > b.len() { a } else { b.clone() },
104            );
105        let len = max_variant.len();
106        (max_variant, len)
107    };
108
109    // A document string for the generated MAX_VARIANT_LEN constant.
110    let max_variant_comment = format!(
111        "The string size of the longest enum member string.\n\n```rs\n\"{}\" // len = {} = {:#X}\n```\n\n",
112        max_variant, max_variant_len, max_variant_len
113    );
114
115    quote! {
116        #[non_exhaustive]
117        #[derive(Eq, PartialEq)]
118        #item_enum
119
120        impl #name {
121            #[doc = #max_variant_comment]
122            /// The length of longest the string slice. This
123            /// can be used in a pattern matcher to guarantee
124            /// a mismatch with current enum string.
125            pub const MAX_VARIANT_LEN: usize = #max_variant_len;
126
127            /// Returns a nom-style combinator for parsing the
128            /// enum from a string input.
129            pub fn combinator() -> impl Fn(&str) -> Result<(&str, Self), ()> {
130                move |input: &str| {
131                    match true {
132                        #(#starts_with_arms)*
133                        _ => Err(()),
134                    }
135                }
136            }
137
138            /// Returns the length of the string representation.
139            pub fn str_len(&self) -> usize {
140                self.to_string().len()
141            }
142        }
143
144        // impl<T: AsRef<&str>> std::cmp::PartialEq<T> for #name {
145        //     fn eq(&self, other: &T) -> bool {
146        //         use std::str::FromStr;
147        //         match Self::from_str(other.as_ref()) {
148        //             Ok(variant) => self == &variant,
149        //             Err(_) => false
150        //         }
151        //     }
152        // }
153
154        impl std::str::FromStr for #name {
155            type Err = ();
156
157            fn from_str(method: &str) -> Result<Self, Self::Err> {
158                match method {
159                    #(#from_str_arms)*
160                    _ => Err(())
161                }
162            }
163        }
164
165        // ToString implemented: https://stackoverflow.com/a/27770058/16002144
166        impl std::fmt::Display for #name {
167            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
168                write!(f, "{}", match self {
169                    #(#to_str_arms)*
170                    _ => unreachable!()
171                })
172            }
173        }
174
175        impl std::convert::Into<String> for #name {
176            fn into(self) -> String {
177                use std::string::ToString;
178                self.to_string()
179            }
180        }
181    }
182    .into()
183}