use cron_union::{union as union_crons, CronUnion};
use croner::Cron;
use crate::error::AppError;
pub(crate) fn normalize_schedule(expr: &str) -> String {
let trimmed = expr.trim();
if trimmed.starts_with('@') {
return trimmed.to_string();
}
let fields: Vec<&str> = trimmed.split_ascii_whitespace().collect();
match fields.len() {
6 | 7 => fields[1..6].join(" "),
_ => trimmed.to_string(),
}
}
pub(crate) fn validate_cron(expr: &str) -> Result<(), AppError> {
let normalized = normalize_schedule(expr.trim());
normalized
.parse::<Cron>()
.map_err(|err| AppError::BadRequest(format!("invalid cron expression: {err}")))?;
Ok(())
}
pub(crate) fn compiled_union(schedule: &str) -> Option<CronUnion> {
compiled_union_many(std::slice::from_ref(&schedule.to_string()))
}
pub(crate) fn compiled_union_many(schedules: &[String]) -> Option<CronUnion> {
let normalized: Vec<String> = schedules
.iter()
.map(|schedule| schedule.trim())
.filter(|schedule| !matches!(*schedule, "@reboot" | "@midnight"))
.map(normalize_schedule)
.collect();
if normalized.is_empty() {
return None;
}
let refs: Vec<&str> = normalized.iter().map(String::as_str).collect();
union_crons(refs).ok()
}
#[cfg(test)]
#[path = "cron_tests.rs"]
mod cron_tests;