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()
}