1use proc_macro::TokenStream;
8use quote::quote;
9use syn::{DeriveInput, LitStr, parse_macro_input};
10
11#[proc_macro_derive(Job, attributes(job))]
26pub fn derive_job(input: TokenStream) -> TokenStream {
27 let input = parse_macro_input!(input as DeriveInput);
28 let name = &input.ident;
29
30 let mut job_name: Option<String> = None;
32 let mut job_queue: Option<String> = None;
33 let mut job_max_attempts: Option<u32> = None;
34
35 for attr in &input.attrs {
36 if !attr.path().is_ident("job") {
37 continue;
38 }
39 let _ = attr.parse_nested_meta(|meta| {
40 if meta.path.is_ident("name") {
41 let s: LitStr = meta.value()?.parse()?;
42 job_name = Some(s.value());
43 } else if meta.path.is_ident("queue") {
44 let s: LitStr = meta.value()?.parse()?;
45 job_queue = Some(s.value());
46 } else if meta.path.is_ident("max_attempts") {
47 let s: LitStr = meta.value()?.parse()?;
48 job_max_attempts = Some(s.value().parse().unwrap_or(3));
49 }
50 Ok(())
51 });
52 }
53
54 let name_str = job_name.unwrap_or_else(|| camel_to_snake(&name.to_string()));
55 let queue_str = job_queue.unwrap_or_else(|| "default".into());
56 let max = job_max_attempts.unwrap_or(3);
57
58 let expanded = quote! {
59 #[async_trait::async_trait]
60 impl ravel_support::queue::Job for #name {
61 async fn handle(&self) -> anyhow::Result<()> {
62 self.__ravel_job_handle().await
63 }
64
65 fn name() -> &'static str {
66 #name_str
67 }
68
69 fn queue() -> &'static str {
70 #queue_str
71 }
72
73 fn max_attempts() -> u32 {
74 #max
75 }
76 }
77 };
78
79 TokenStream::from(expanded)
80}
81
82fn camel_to_snake(s: &str) -> String {
84 let mut out = String::with_capacity(s.len() + 4);
85 let chars = s.chars().peekable();
86 for c in chars {
87 if c.is_uppercase() {
88 if !out.is_empty() && !out.ends_with('_') {
89 out.push('_');
90 }
91 out.push(c.to_lowercase().next().unwrap());
92 } else {
93 out.push(c);
94 }
95 }
96 out
97}