mvutils_proc_macro/
lib.rs

1extern crate proc_macro;
2
3use crate::savable::{enumerator, named, unit, unnamed};
4use proc_macro::{TokenStream};
5use std::str::FromStr;
6use proc_macro2::{Ident, Span};
7use quote::quote;
8use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprClosure, Fields, LitStr, Meta, Path, Token};
9use syn::parse::{ParseBuffer, Parser};
10use syn::punctuated::Punctuated;
11
12mod savable;
13mod savable2;
14
15#[proc_macro_derive(Savable, attributes(unsaved, custom, varint))]
16pub fn derive_savable(input: TokenStream) -> TokenStream {
17    let input = parse_macro_input!(input as DeriveInput);
18
19    let name = input.ident;
20    let generics = input.generics;
21
22    let varint = input.attrs.iter().any(|attr| {
23        if let Meta::Path(ref p) = attr.meta {
24            p.segments.iter().any(|s| s.ident == "varint")
25        } else {
26            false
27        }
28    });
29
30    match &input.data {
31        Data::Struct(s) => match &s.fields {
32            Fields::Named(fields) => named(fields, name, generics),
33            Fields::Unnamed(fields) => unnamed(fields, name, generics),
34            Fields::Unit => unit(name, generics),
35        },
36        Data::Enum(e) => enumerator(e, name, generics, varint),
37        Data::Union(_) => panic!("Deriving Savable for unions is not supported!"),
38    }
39}
40
41#[proc_macro_derive(Savable2, attributes(unsaved, custom, varint))]
42pub fn derive_savable2(input: TokenStream) -> TokenStream {
43    let input = parse_macro_input!(input as DeriveInput);
44
45    let name = input.ident;
46    let generics = input.generics;
47
48    let varint = input.attrs.iter().any(|attr| {
49        if let Meta::Path(ref p) = attr.meta {
50            p.segments.iter().any(|s| s.ident == "varint")
51        } else {
52            false
53        }
54    });
55
56    match &input.data {
57        Data::Struct(s) => match &s.fields {
58            Fields::Named(fields) => savable2::named(fields, name, generics),
59            Fields::Unnamed(fields) => savable2::unnamed(fields, name, generics),
60            Fields::Unit => savable2::unit(name, generics),
61        },
62        Data::Enum(e) => savable2::enumerator(e, name, generics, varint),
63        Data::Union(_) => panic!("Deriving Savable2 for unions is not supported!"),
64    }
65}
66
67#[proc_macro_derive(TryFromString, attributes(exclude, casing, pattern, custom, inner))]
68pub fn try_from_string(input: TokenStream) -> TokenStream {
69    let input = parse_macro_input!(input as DeriveInput);
70    let name = input.ident.clone();
71
72    #[derive(Clone, Copy)]
73    enum Casing {
74        Lower,
75        Upper,
76        Both,
77    }
78
79    // helper: check #[exclude]
80    fn is_excluded(v: &syn::Variant) -> bool {
81        v.attrs.iter().any(|attr| attr.path().is_ident("exclude"))
82    }
83
84    // helper: check #[casing(...)]
85    fn get_casing(v: &syn::Variant) -> Casing {
86        for attr in &v.attrs {
87            if attr.path().is_ident("casing") {
88                if let Ok(list) = attr.meta.require_list() {
89                    if let Ok(path) = list.parse_args::<Path>() {
90                        let ident = path.get_ident().unwrap().to_string();
91                        return match ident.as_str() {
92                            "Lower" => Casing::Lower,
93                            "Upper" => Casing::Upper,
94                            "Both" => Casing::Both,
95                            other => panic!("Invalid casing: {}", other),
96                        };
97                    }
98                }
99            }
100        }
101        Casing::Both
102    }
103
104    fn get_pattern(v: &syn::Variant) -> Option<String> {
105        for attr in &v.attrs {
106            if attr.path().is_ident("pattern") {
107                if let Ok(list) = attr.meta.require_list() {
108                    let l = list.parse_args::<LitStr>().ok()?;
109                    return Some(l.value());
110                }
111            }
112        }
113        None
114    }
115
116    fn get_custom(v: &syn::Variant) -> Option<Vec<LitStr>> {
117        for attr in &v.attrs {
118            if attr.path().is_ident("custom") {
119                if let Ok(list) = attr.meta.require_list() {
120                    let parser = Punctuated::<LitStr, Token![,]>::parse_terminated;
121                    if let Ok(punctuated) = parser.parse2(list.tokens.clone()) {
122                        return Some(
123                            punctuated
124                                .into_iter()
125                                .collect()
126                        );
127                    }
128                }
129            }
130        }
131        None
132    }
133
134    fn get_inner(v: &syn::Variant) -> Option<Expr> {
135        for attr in &v.attrs {
136            if attr.path().is_ident("inner") {
137                if let Ok(list) = attr.meta.require_list() {
138                    return list.parse_args::<Expr>().ok();
139                }
140            }
141        }
142        None
143    }
144
145    match &input.data {
146        Data::Enum(e) => {
147            let mut statics = quote! {};
148
149            let values: Vec<proc_macro2::TokenStream> = e.variants.iter().filter(|v| !is_excluded(v)).flat_map(|v| {
150                let ident = &v.ident;
151                let name_str = ident.to_string();
152                let casing = get_casing(v);
153                let pattern = get_pattern(v);
154                let custom = get_custom(v);
155                let inner = get_inner(v);
156
157                let constructor = if let Some(inner) = inner {
158                    quote! {{
159                        let e = #inner;
160                        Ok(Self::#ident(e(value).ok_or(())?))
161                    }}
162                } else {
163                    if !v.fields.is_empty() {
164                        panic!("Attention! Inner fields must be provided a valid parse closure using the #[inner()] attribute! The closure takes an &String and returns a Option<T>")
165                    }
166                    quote! {
167                        Ok(Self::#ident)
168                    }
169                };
170
171                if let Some(custom) = custom {
172                    vec![quote! {
173                        s if [#(#custom),*].contains(s) => #constructor
174                    }]
175                } else if let Some(pattern) = pattern {
176                    let regex_name_s = format!("{name}_{name_str}_regex");
177                    let regex_name = Ident::new(&regex_name_s, Span::call_site());
178
179                    statics.extend(quote! {
180                        static #regex_name: Lazy<Regex> = Lazy::new(|| Regex::new(#pattern).unwrap());
181                    });
182
183                    vec![quote! {
184                        s if #regex_name.is_match(s) => #constructor
185                    }]
186                } else {
187                    let mut arms = Vec::new();
188                    match casing {
189                        Casing::Lower => {
190                            let lower = name_str.to_lowercase();
191                            arms.push(quote! { #lower => #constructor });
192                        }
193                        Casing::Upper => {
194                            let upper = name_str.to_uppercase();
195                            arms.push(quote! { #upper => #constructor });
196                        }
197                        Casing::Both => {
198                            let lower = name_str.to_lowercase();
199                            let upper = name_str.to_uppercase();
200                            arms.push(quote! { #lower => #constructor });
201                            arms.push(quote! { #upper => #constructor });
202                        }
203                    }
204                    arms
205                }
206            }).collect();
207
208            let expanded = quote! {
209                #statics
210
211                impl core::str::FromStr for #name {
212                    type Err = ();
213
214                    fn from_str(value: &str) -> Result<Self, Self::Err> {
215                        match value {
216                            #(#values,)*
217                            _ => Err(()),
218                        }
219                    }
220                }
221            };
222
223            expanded.into()
224        }
225        _ => panic!("`TryFromString` can only be derived for enums"),
226    }
227}
228
229enum Casing {
230    Lower,
231    Upper,
232    Both,
233}
234
235#[proc_macro_derive(TryFromStringLegacy, attributes(exclude))]
236pub fn try_from_string_legacy(input: TokenStream) -> TokenStream {
237    let mut input = parse_macro_input!(input as DeriveInput);
238    let name = input.ident.clone();
239
240    fn is_excluded(v: &&syn::Variant) -> bool {
241        v.attrs.iter().any(|attr| {
242            if let Meta::Path(ref p) = attr.meta {
243                p.segments.iter().any(|s| s.ident == "exclude")
244            } else {
245                false
246            }
247        })
248    }
249
250    match &input.data {
251        Data::Enum(e) => {
252            let values = e.variants.iter().filter(|v| !is_excluded(v)).map(|v| {
253                let str = v.ident.to_string();
254                let alt = str.chars().next().unwrap().to_lowercase().to_string() + &str.chars().skip(1).map(|c| {
255                    if c.is_uppercase() {
256                        "_".to_string() + &c.to_lowercase().to_string()
257                    } else {
258                        c.to_string()
259                    }
260                }).collect::<String>();
261                format!("\"{}\" => Ok(Self::{}),\n\"{}\" => Ok(Self::{}),", str, str, alt, str)
262            }).map(|s| {
263                proc_macro2::TokenStream::from_str(&s).unwrap()
264            });
265            quote! {
266                impl core::str::FromStr for #name {
267                    type Err = ();
268
269                    fn from_str(value: &str) -> Result<Self, Self::Err> {
270                        match value {
271                            #( #values )*
272                            _ => Err(())
273                        }
274                    }
275                }
276            }.into()
277        },
278        _ => panic!("`try_from_string` is only meant for enums")
279    }
280}