use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Lit, Meta};
use heck::{ToSnakeCase, ToPascalCase, ToKebabCase};
pub fn get_attribute_value(attrs: &[Attribute], name: &str, key: &str) -> Option<String> {
for attr in attrs {
if attr.path().is_ident(name) {
let mut result = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident(key) {
if let Ok(lit_str) = meta.value()?.parse::<syn::LitStr>() {
result = Some(lit_str.value());
}
}
Ok(())
});
if result.is_some() {
return result;
}
}
}
None
}
pub fn get_attribute_int(attrs: &[Attribute], name: &str, key: &str) -> Option<u64> {
for attr in attrs {
if attr.path().is_ident(name) {
let mut result = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident(key) {
if let Ok(lit_int) = meta.value()?.parse::<syn::LitInt>() {
result = lit_int.base10_parse().ok();
}
}
Ok(())
});
if result.is_some() {
return result;
}
}
}
None
}
pub fn has_attribute_flag(attrs: &[Attribute], name: &str, flag: &str) -> bool {
for attr in attrs {
if attr.path().is_ident(name) {
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident(flag) {
found = true;
}
Ok(())
});
if found {
return true;
}
}
}
false
}
pub fn generate_standard_fields() -> TokenStream {
quote! {
pub id: uuid::Uuid,
pub product: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
}
pub fn generate_standard_derives() -> TokenStream {
quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
}
}
pub fn generate_cache_key_fn(struct_name: &str) -> TokenStream {
let cache_prefix = struct_name.to_snake_case();
quote! {
impl #struct_name {
pub fn cache_key(&self) -> String {
format!("{}:{}:{}", self.product, #cache_prefix, self.id)
}
pub fn cache_key_for(product: &str, id: uuid::Uuid) -> String {
format!("{}:{}:{}", product, #cache_prefix, id)
}
pub fn cache_pattern(product: &str) -> String {
format!("{}:{}:*", product, #cache_prefix)
}
}
}
}
pub fn generate_new_constructor(struct_name: &str, fields: &[String]) -> TokenStream {
let struct_ident = syn::Ident::new(struct_name, proc_macro2::Span::call_site());
let field_params: Vec<TokenStream> = fields.iter().map(|field| {
let field_ident = syn::Ident::new(field, proc_macro2::Span::call_site());
quote! { #field_ident: String }
}).collect();
let field_assigns: Vec<TokenStream> = fields.iter().map(|field| {
let field_ident = syn::Ident::new(field, proc_macro2::Span::call_site());
quote! { #field_ident }
}).collect();
quote! {
impl #struct_ident {
pub fn new(
product: String,
#(#field_params,)*
) -> Self {
let now = chrono::Utc::now();
Self {
id: uuid::Uuid::new_v4(),
product,
created_at: now,
updated_at: now,
#(#field_assigns,)*
}
}
pub fn touch(&mut self) {
self.updated_at = chrono::Utc::now();
}
}
}
}
pub fn generate_validation() -> TokenStream {
quote! {
pub trait Validate {
type Error;
fn validate(&self) -> Result<(), Self::Error>;
}
}
}
pub fn generate_table_constant(struct_name: &str, table_name: Option<String>) -> TokenStream {
let struct_ident = syn::Ident::new(struct_name, proc_macro2::Span::call_site());
let table = table_name.unwrap_or_else(|| struct_name.to_snake_case() + "s");
quote! {
impl #struct_ident {
pub const TABLE_NAME: &'static str = #table;
}
}
}
pub fn rust_to_graphql_type(type_str: &str) -> String {
match type_str {
"rust_decimal::Decimal" | "Decimal" => "f64".to_string(),
"serde_json::Value" | "Value" => "JSON".to_string(),
"chrono::DateTime<chrono::Utc>" | "DateTime<Utc>" => "DateTime".to_string(),
"uuid::Uuid" | "Uuid" => "UUID".to_string(),
other => other.to_string(),
}
}
pub fn generate_graphql_conversion(field_name: &str, rust_type: &str, is_option: bool) -> (TokenStream, TokenStream) {
let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
let (to_graphql, from_graphql) = match rust_type {
"rust_decimal::Decimal" | "Decimal" => {
if is_option {
(
quote! { { use num_traits::ToPrimitive; self.#field_ident.map(|d| d.to_f64().unwrap_or(0.0)) } },
quote! { input.#field_ident.map(rust_decimal::Decimal::from_f64_retain).flatten() }
)
} else {
(
quote! { { use num_traits::ToPrimitive; self.#field_ident.to_f64().unwrap_or(0.0) } },
quote! { rust_decimal::Decimal::from_f64_retain(input.#field_ident).unwrap_or_default() }
)
}
}
"serde_json::Value" | "Value" => {
(
quote! { self.#field_ident.clone() },
quote! { input.#field_ident.clone() }
)
}
_ => {
(
quote! { self.#field_ident.clone() },
quote! { input.#field_ident.clone() }
)
}
};
(to_graphql, from_graphql)
}
pub fn generate_cpf_validation() -> TokenStream {
quote! {
pub fn validate_cpf(cpf: &str) -> bool {
let digits: String = cpf.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return false;
}
if digits.chars().all(|c| c == digits.chars().next().unwrap()) {
return false;
}
let digits: Vec<u32> = digits.chars().map(|c| c.to_digit(10).unwrap()).collect();
let sum1: u32 = digits[0..9].iter().enumerate()
.map(|(i, &d)| d * (10 - i as u32))
.sum();
let check1 = match sum1 % 11 {
0 | 1 => 0,
n => 11 - n,
};
if check1 != digits[9] {
return false;
}
let sum2: u32 = digits[0..10].iter().enumerate()
.map(|(i, &d)| d * (11 - i as u32))
.sum();
let check2 = match sum2 % 11 {
0 | 1 => 0,
n => 11 - n,
};
check2 == digits[10]
}
pub fn format_cpf(cpf: &str) -> String {
let digits: String = cpf.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 11 {
format!("{}.{}.{}-{}",
&digits[0..3], &digits[3..6],
&digits[6..9], &digits[9..11])
} else {
cpf.to_string()
}
}
}
}
pub fn generate_cep_validation() -> TokenStream {
quote! {
pub fn validate_cep(cep: &str) -> bool {
let digits: String = cep.chars().filter(|c| c.is_ascii_digit()).collect();
digits.len() == 8
}
pub fn format_cep(cep: &str) -> String {
let digits: String = cep.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 8 {
format!("{}-{}", &digits[0..5], &digits[5..8])
} else {
cep.to_string()
}
}
}
}