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