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}