mvutils_proc_macro/
lib.rs1extern 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 fn is_excluded(v: &syn::Variant) -> bool {
81 v.attrs.iter().any(|attr| attr.path().is_ident("exclude"))
82 }
83
84 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(®ex_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}