use quote::ToTokens;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{Expr, ExprPath, MetaNameValue, Token};
pub(crate) static RENAME_RULES: &[(&str, convert_case::Case<'static>)] = &[
("lowercase", convert_case::Case::Lower),
("UPPERCASE", convert_case::Case::Upper),
("PascalCase", convert_case::Case::Pascal),
("camelCase", convert_case::Case::Camel),
("snake_case", convert_case::Case::Snake),
("SCREAMING_SNAKE_CASE", convert_case::Case::UpperSnake),
("kebab-case", convert_case::Case::Kebab),
("SCREAMING-KEBAB-CASE", convert_case::Case::UpperKebab),
];
pub fn has_attribute(needle: &str, attributes: &[syn::Attribute]) -> bool {
attributes.iter().any(|attr| {
attr.path()
.segments
.iter()
.any(|segment| segment.ident == needle)
})
}
fn check_expression_is_path(expr: Expr) -> Option<ExprPath> {
match expr {
Expr::Path(expr_path) if !expr_path.path.segments.is_empty() => Some(expr_path),
_ => None,
}
}
fn check_token(token: proc_macro2::TokenTree, arg: &str) -> Option<String> {
let proc_macro2::TokenTree::Group(group) = token else {
return None;
};
if group.delimiter() != proc_macro2::Delimiter::Parenthesis {
return None;
}
match Parser::parse2(
Punctuated::<MetaNameValue, Token![,]>::parse_terminated,
group.stream(),
) {
Ok(name_value_pairs) => {
name_value_pairs
.into_iter()
.find(|nvp| nvp.path.is_ident(arg))
.map(|nvp| nvp.value.to_token_stream().to_string())
.map(|value| value[1..value.len() - 1].to_owned())
}
Err(_) => {
Parser::parse2(
Punctuated::<Expr, Token![,]>::parse_terminated,
group.stream(),
)
.map_or(None, |comma_seperated_values| {
comma_seperated_values
.into_iter()
.map_while(check_expression_is_path)
.any(|expr_path| {
if let Some(last) = expr_path.path.segments.last() {
last.ident.to_string().eq(arg)
} else {
false
}
})
.then_some(arg.to_owned())
})
}
}
}
pub fn get_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) -> Option<String> {
get_attribute(needle, attributes).and_then(|attr| {
attr.meta
.to_token_stream()
.into_iter()
.filter_map(|token| check_token(token, arg))
.next()
})
}
pub fn has_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) -> bool {
get_attribute_arg(needle, arg, attributes).is_some()
}
fn check_doc_tokens(tt: proc_macro2::TokenTree) -> Option<String> {
let proc_macro2::TokenTree::Literal(comment) = tt else {
return None;
};
let c = comment.to_string();
Some(c[1..c.len() - 1].trim().to_owned())
}
fn check_doc_attribute(attr: &syn::Attribute) -> Vec<String> {
let syn::Meta::NameValue(ref nv) = attr.meta else {
return Default::default();
};
nv.value
.to_token_stream()
.into_iter()
.filter_map(check_doc_tokens)
.collect::<Vec<String>>()
}
pub fn get_comments(mut attributes: Vec<syn::Attribute>) -> Vec<String> {
attributes.retain(|x| x.path().segments.iter().any(|seg| seg.ident == "doc"));
attributes
.iter()
.flat_map(check_doc_attribute)
.collect::<Vec<String>>()
}
pub fn build_indentation(indentation_amount: i8) -> String {
(0..indentation_amount).map(|_| '\u{0020}').collect()
}
pub fn extract_struct_generics(s: syn::Generics) -> Vec<syn::Ident> {
s.params
.into_iter()
.filter_map(|gp| {
if let syn::GenericParam::Type(ty) = gp {
Some(ty.ident)
} else {
None
}
})
.collect()
}
pub fn format_generics(generics: &[syn::Ident]) -> String {
if generics.is_empty() {
return String::new();
}
let generics = generics
.iter()
.map(|g| g.to_string())
.collect::<Vec<_>>()
.join(", ");
format!("<{}>", generics)
}
pub fn type_contains_ident(ty: &syn::Type, ident: &syn::Ident) -> bool {
match ty {
syn::Type::Path(ty_path) => ty_path.path.segments.iter().any(|segment| {
(match segment.arguments {
syn::PathArguments::AngleBracketed(ref angle_bracketed) => {
angle_bracketed.args.iter().any(|arg| {
if let syn::GenericArgument::Type(ref ty) = arg {
type_contains_ident(ty, ident)
} else {
false
}
})
}
_ => false,
}) || &segment.ident == ident
}),
syn::Type::Slice(ty_slice) => type_contains_ident(&ty_slice.elem, ident),
syn::Type::Array(ty_array) => type_contains_ident(&ty_array.elem, ident),
syn::Type::Reference(ty_ref) => type_contains_ident(&ty_ref.elem, ident),
_ => false,
}
}
pub fn get_attribute<'a>(
needle: &'a str,
attributes: &'a [syn::Attribute],
) -> Option<&'a syn::Attribute> {
attributes
.iter()
.rev()
.find(|attr| {
attr.meta
.path()
.segments
.iter()
.any(|segment| segment.ident == needle)
})
}
pub(crate) fn parse_serde_case(
val: impl Into<Option<String>>,
) -> Option<convert_case::Case<'static>> {
val.into().and_then(|x| {
RENAME_RULES
.iter()
.find(|(name, _)| name == &x)
.map(|(_, rule)| *rule)
})
}
pub fn parse_number_autoradix(input: String) -> Option<i32> {
let normalized = input.to_lowercase().replace("_", "");
let trimmed = normalized.trim();
if let Some(hex) = trimmed.strip_prefix("0x") {
i32::from_str_radix(&hex, 16).ok()
} else if let Some(oct) = trimmed.strip_prefix("0o") {
i32::from_str_radix(&oct, 8).ok()
} else if let Some(bin) = trimmed.strip_prefix("0b") {
i32::from_str_radix(&bin, 2).ok()
} else {
trimmed.parse::<i32>().ok()
}
}
#[test]
fn test_parse_number() {
assert_eq!(parse_number_autoradix("2_3".into()), Some(23));
assert_eq!(parse_number_autoradix("0xf".into()), Some(15));
assert_eq!(parse_number_autoradix("0b11".into()), Some(3));
assert_eq!(parse_number_autoradix("0o1_1".into()), Some(0o1_1));
}