Skip to main content

ravel_macros/
lib.rs

1//! Procedural macros for the Ravel framework.
2//!
3//! ## Derives
4//!
5//! - `#[derive(Job)]` — implements the `Job` trait for a struct.
6
7use proc_macro::TokenStream;
8use quote::quote;
9use syn::{DeriveInput, LitStr, parse_macro_input};
10
11/// Derive macro that implements the `Job` trait.
12///
13/// Supports the following attributes:
14/// - `#[job(name = "my_job")]` — override the default snake_case job name
15/// - `#[job(queue = "high_priority")]` — set the queue name (default: "default")
16/// - `#[job(max_attempts = 5)]` — set max retry attempts (default: 3)
17///
18/// # Example
19///
20/// ```rust,ignore
21/// #[derive(Serialize, Deserialize, Job)]
22/// #[job(name = "send_welcome", queue = "mail", max_attempts = 5)]
23/// struct SendWelcomeEmail { user_id: u32 }
24/// ```
25#[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    // Parse #[job(...)] attributes
31    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
82/// Convert `CamelCase` or `PascalCase` to `snake_case`.
83fn 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}