use proc_macro::TokenStream;
use proc_macro2::Span;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident, LitStr};
#[proc_macro_derive(JobArgs, attributes(awa))]
pub fn derive_job_args(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let custom_kind = input.attrs.iter().find_map(|attr| {
if !attr.path().is_ident("awa") {
return None;
}
let mut kind_value = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("kind") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
kind_value = Some(s.value());
}
Ok(())
});
kind_value
});
let kind_str = custom_kind.unwrap_or_else(|| camel_to_snake(&name.to_string()));
let model_path = if let Ok(found) = crate_name("awa") {
let ident = match found {
FoundCrate::Itself => Ident::new("awa", Span::call_site()),
FoundCrate::Name(name) => Ident::new(&name, Span::call_site()),
};
quote!(::#ident::awa_model)
} else if let Ok(found) = crate_name("awa-model") {
match found {
FoundCrate::Itself => quote!(crate),
FoundCrate::Name(name) => {
let ident = Ident::new(&name, Span::call_site());
quote!(::#ident)
}
}
} else {
quote!(::awa_model)
};
let expanded = quote! {
impl #model_path::JobArgs for #name {
fn kind() -> &'static str {
#kind_str
}
}
};
TokenStream::from(expanded)
}
fn camel_to_snake(name: &str) -> String {
let mut result = String::with_capacity(name.len() + 4);
let chars: Vec<char> = name.chars().collect();
for i in 0..chars.len() {
let current = chars[i];
if current.is_uppercase() {
let needs_separator = i > 0
&& ((chars[i - 1].is_lowercase() || chars[i - 1].is_ascii_digit())
|| (chars[i - 1].is_uppercase()
&& i + 1 < chars.len()
&& chars[i + 1].is_lowercase()));
if needs_separator {
result.push('_');
}
result.push(current.to_lowercase().next().unwrap());
} else {
result.push(current);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kind_derivation_golden_cases() {
let cases = vec![
("SendEmail", "send_email"),
("SendConfirmationEmail", "send_confirmation_email"),
("SMTPEmail", "smtp_email"),
("OAuthRefresh", "o_auth_refresh"),
("PDFRenderJob", "pdf_render_job"),
("ProcessV2Import", "process_v2_import"),
("ReconcileQ3Revenue", "reconcile_q3_revenue"),
("HTMLToPDF", "html_to_pdf"),
("IOError", "io_error"),
];
for (input, expected) in cases {
let result = camel_to_snake(input);
assert_eq!(
result, expected,
"camel_to_snake({input:?}): expected {expected:?}, got {result:?}"
);
}
}
#[test]
fn test_simple_cases() {
assert_eq!(camel_to_snake("Job"), "job");
assert_eq!(camel_to_snake("MyJob"), "my_job");
assert_eq!(camel_to_snake("A"), "a");
assert_eq!(camel_to_snake("AB"), "ab");
}
}