extern crate proc_macro;
use crate::savable::{enumerator, named, unit, unnamed};
use proc_macro::{TokenStream};
use std::str::FromStr;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprClosure, Fields, LitStr, Meta, Path, Token};
use syn::parse::{ParseBuffer, Parser};
use syn::punctuated::Punctuated;
mod savable;
mod savable2;
#[proc_macro_derive(Savable, attributes(unsaved, custom, varint))]
pub fn derive_savable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let generics = input.generics;
let varint = input.attrs.iter().any(|attr| {
if let Meta::Path(ref p) = attr.meta {
p.segments.iter().any(|s| s.ident == "varint")
} else {
false
}
});
match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(fields) => named(fields, name, generics),
Fields::Unnamed(fields) => unnamed(fields, name, generics),
Fields::Unit => unit(name, generics),
},
Data::Enum(e) => enumerator(e, name, generics, varint),
Data::Union(_) => panic!("Deriving Savable for unions is not supported!"),
}
}
#[proc_macro_derive(Savable2, attributes(unsaved, custom, varint))]
pub fn derive_savable2(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let generics = input.generics;
let varint = input.attrs.iter().any(|attr| {
if let Meta::Path(ref p) = attr.meta {
p.segments.iter().any(|s| s.ident == "varint")
} else {
false
}
});
match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(fields) => savable2::named(fields, name, generics),
Fields::Unnamed(fields) => savable2::unnamed(fields, name, generics),
Fields::Unit => savable2::unit(name, generics),
},
Data::Enum(e) => savable2::enumerator(e, name, generics, varint),
Data::Union(_) => panic!("Deriving Savable2 for unions is not supported!"),
}
}
#[proc_macro_derive(TryFromString, attributes(exclude, casing, pattern, custom, inner))]
pub fn try_from_string(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident.clone();
#[derive(Clone, Copy)]
enum Casing {
Lower,
Upper,
Both,
}
fn is_excluded(v: &syn::Variant) -> bool {
v.attrs.iter().any(|attr| attr.path().is_ident("exclude"))
}
fn get_casing(v: &syn::Variant) -> Casing {
for attr in &v.attrs {
if attr.path().is_ident("casing") {
if let Ok(list) = attr.meta.require_list() {
if let Ok(path) = list.parse_args::<Path>() {
let ident = path.get_ident().unwrap().to_string();
return match ident.as_str() {
"Lower" => Casing::Lower,
"Upper" => Casing::Upper,
"Both" => Casing::Both,
other => panic!("Invalid casing: {}", other),
};
}
}
}
}
Casing::Both
}
fn get_pattern(v: &syn::Variant) -> Option<String> {
for attr in &v.attrs {
if attr.path().is_ident("pattern") {
if let Ok(list) = attr.meta.require_list() {
let l = list.parse_args::<LitStr>().ok()?;
return Some(l.value());
}
}
}
None
}
fn get_custom(v: &syn::Variant) -> Option<Vec<LitStr>> {
for attr in &v.attrs {
if attr.path().is_ident("custom") {
if let Ok(list) = attr.meta.require_list() {
let parser = Punctuated::<LitStr, Token![,]>::parse_terminated;
if let Ok(punctuated) = parser.parse2(list.tokens.clone()) {
return Some(
punctuated
.into_iter()
.collect()
);
}
}
}
}
None
}
fn get_inner(v: &syn::Variant) -> Option<Expr> {
for attr in &v.attrs {
if attr.path().is_ident("inner") {
if let Ok(list) = attr.meta.require_list() {
return list.parse_args::<Expr>().ok();
}
}
}
None
}
match &input.data {
Data::Enum(e) => {
let mut statics = quote! {};
let values: Vec<proc_macro2::TokenStream> = e.variants.iter().filter(|v| !is_excluded(v)).flat_map(|v| {
let ident = &v.ident;
let name_str = ident.to_string();
let casing = get_casing(v);
let pattern = get_pattern(v);
let custom = get_custom(v);
let inner = get_inner(v);
let constructor = if let Some(inner) = inner {
quote! {{
let e = #inner;
Ok(Self::#ident(e(value).ok_or(())?))
}}
} else {
if !v.fields.is_empty() {
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>")
}
quote! {
Ok(Self::#ident)
}
};
if let Some(custom) = custom {
vec![quote! {
s if [#(#custom),*].contains(s) => #constructor
}]
} else if let Some(pattern) = pattern {
let regex_name_s = format!("{name}_{name_str}_regex");
let regex_name = Ident::new(®ex_name_s, Span::call_site());
statics.extend(quote! {
static #regex_name: Lazy<Regex> = Lazy::new(|| Regex::new(#pattern).unwrap());
});
vec![quote! {
s if #regex_name.is_match(s) => #constructor
}]
} else {
let mut arms = Vec::new();
match casing {
Casing::Lower => {
let lower = name_str.to_lowercase();
arms.push(quote! { #lower => #constructor });
}
Casing::Upper => {
let upper = name_str.to_uppercase();
arms.push(quote! { #upper => #constructor });
}
Casing::Both => {
let lower = name_str.to_lowercase();
let upper = name_str.to_uppercase();
arms.push(quote! { #lower => #constructor });
arms.push(quote! { #upper => #constructor });
}
}
arms
}
}).collect();
let expanded = quote! {
#statics
impl core::str::FromStr for #name {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
#(#values,)*
_ => Err(()),
}
}
}
};
expanded.into()
}
_ => panic!("`TryFromString` can only be derived for enums"),
}
}
enum Casing {
Lower,
Upper,
Both,
}
#[proc_macro_derive(TryFromStringLegacy, attributes(exclude))]
pub fn try_from_string_legacy(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
let name = input.ident.clone();
fn is_excluded(v: &&syn::Variant) -> bool {
v.attrs.iter().any(|attr| {
if let Meta::Path(ref p) = attr.meta {
p.segments.iter().any(|s| s.ident == "exclude")
} else {
false
}
})
}
match &input.data {
Data::Enum(e) => {
let values = e.variants.iter().filter(|v| !is_excluded(v)).map(|v| {
let str = v.ident.to_string();
let alt = str.chars().next().unwrap().to_lowercase().to_string() + &str.chars().skip(1).map(|c| {
if c.is_uppercase() {
"_".to_string() + &c.to_lowercase().to_string()
} else {
c.to_string()
}
}).collect::<String>();
format!("\"{}\" => Ok(Self::{}),\n\"{}\" => Ok(Self::{}),", str, str, alt, str)
}).map(|s| {
proc_macro2::TokenStream::from_str(&s).unwrap()
});
quote! {
impl core::str::FromStr for #name {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
#( #values )*
_ => Err(())
}
}
}
}.into()
},
_ => panic!("`try_from_string` is only meant for enums")
}
}