use std::sync::{LazyLock, Mutex};
#[derive(Debug, Clone)]
pub struct StoredCronInfo {
pub fn_name: String,
pub expression: String,
pub file_path: Option<String>,
}
pub static CRON_STORAGE: LazyLock<Mutex<Vec<StoredCronInfo>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
pub fn validate_cron_fn(item_fn: &syn::ItemFn) -> Result<(), syn::Error> {
if !matches!(item_fn.vis, syn::Visibility::Public(_)) {
return Err(syn::Error::new_spanned(
item_fn.sig.fn_token,
"#[cron] attribute: function must be public. Add `pub` before `fn`.",
));
}
if item_fn.sig.asyncness.is_none() {
return Err(syn::Error::new_spanned(
item_fn.sig.fn_token,
"#[cron] attribute: function must be async. Add `async` before `fn`.",
));
}
if !item_fn.sig.inputs.is_empty() {
return Err(syn::Error::new_spanned(
&item_fn.sig.inputs,
"#[cron] attribute: cron functions must take no parameters.",
));
}
Ok(())
}
pub fn process_cron_attribute(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> syn::Result<proc_macro2::TokenStream> {
let expression: syn::LitStr = syn::parse2(attr).map_err(|_| syn::Error::new(proc_macro2::Span::call_site(), "#[cron] attribute: expected a cron expression string. Example: #[cron(\"0 */5 * * * *\")]"))?;
let item_fn: syn::ItemFn = syn::parse2(item.clone()).map_err(|e| syn::Error::new(e.span(), "#[cron] attribute: can only be applied to functions, not other items. Move or remove the attribute."))?;
validate_cron_fn(&item_fn)?;
let stored = StoredCronInfo {
fn_name: item_fn.sig.ident.to_string(),
expression: expression.value(),
file_path: proc_macro2::Span::call_site()
.local_file()
.map(|p| p.display().to_string()),
};
CRON_STORAGE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(stored);
Ok(item)
}
#[cfg(test)]
mod tests {
use quote::quote;
use super::*;
#[test]
fn test_validate_cron_fn_not_public() {
let item: syn::ItemFn = syn::parse_quote! {
async fn private_job() {
println!("job");
}
};
let result = validate_cron_fn(&item);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("function must be public")
);
}
#[test]
fn test_validate_cron_fn_not_async() {
let item: syn::ItemFn = syn::parse_quote! {
pub fn sync_job() {
println!("job");
}
};
let result = validate_cron_fn(&item);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("function must be async")
);
}
#[test]
fn test_validate_cron_fn_has_params() {
let item: syn::ItemFn = syn::parse_quote! {
pub async fn job_with_params(x: i32) {
println!("{}", x);
}
};
let result = validate_cron_fn(&item);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("must take no parameters")
);
}
#[test]
fn test_validate_cron_fn_valid() {
let item: syn::ItemFn = syn::parse_quote! {
pub async fn valid_job() {
println!("job");
}
};
let result = validate_cron_fn(&item);
assert!(result.is_ok());
}
#[test]
fn test_process_cron_attribute_valid() {
let attr = quote!("0 */5 * * * *");
let item = quote!(
pub async fn my_job() {
println!("running");
}
);
let result = process_cron_attribute(attr, item.clone());
assert!(result.is_ok());
assert_eq!(result.unwrap().to_string(), item.to_string());
}
#[test]
fn test_process_cron_attribute_invalid_expression() {
let attr = quote!(123);
let item = quote!(
pub async fn my_job() {
println!("running");
}
);
let result = process_cron_attribute(attr, item);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("expected a cron expression string"));
}
#[test]
fn test_process_cron_attribute_not_function() {
let attr = quote!("0 * * * * *");
let item = quote!(
struct NotAFunction;
);
let result = process_cron_attribute(attr, item);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("can only be applied to functions"));
}
#[test]
fn test_process_cron_attribute_not_public() {
let attr = quote!("0 * * * * *");
let item = quote!(
async fn private_job() {
println!("job");
}
);
let result = process_cron_attribute(attr, item);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("function must be public"));
}
#[test]
fn test_process_cron_attribute_not_async() {
let attr = quote!("0 * * * * *");
let item = quote!(
pub fn sync_job() {
println!("job");
}
);
let result = process_cron_attribute(attr, item);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("function must be async"));
}
#[test]
fn test_process_cron_attribute_with_params() {
let attr = quote!("0 * * * * *");
let item = quote!(
pub async fn job(x: i32) {
println!("{}", x);
}
);
let result = process_cron_attribute(attr, item);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("must take no parameters"));
}
}