use chrono::{DateTime, Datelike, Local, Timelike};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskType {
Agent,
Skill,
Command,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskStatus {
Pending,
Running,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CronTask {
pub id: Arc<str>,
pub expression: Arc<str>,
pub task_type: TaskType,
pub target: Arc<str>,
pub description: Arc<str>,
pub status: TaskStatus,
pub retry_count: u32,
pub max_retries: u32,
pub timeout_secs: u64,
pub created_at: u64,
pub last_run: Option<u64>,
pub next_run: Option<u64>,
pub last_error: Option<Arc<str>>,
}
impl CronTask {
pub fn new(
expression: Arc<str>,
task_type: TaskType,
target: Arc<str>,
description: Arc<str>,
) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
id: Arc::from(uuid::Uuid::new_v4().to_string()),
expression,
task_type,
target,
description,
status: TaskStatus::Pending,
retry_count: 0,
max_retries: 3,
timeout_secs: 30,
created_at: now,
last_run: None,
next_run: None,
last_error: None,
}
}
}
pub struct CronExpression {
pub minutes: Vec<u8>,
pub hours: Vec<u8>,
pub day_of_month: Vec<u8>,
pub month: Vec<u8>,
pub day_of_week: Vec<u8>,
}
impl CronExpression {
pub fn parse(expr: &str) -> Result<Self, String> {
let parts: Vec<&str> = expr.split_whitespace().collect();
if parts.len() != 5 {
return Err(format!(
"Invalid cron expression: expected 5 parts, got {}",
parts.len()
));
}
Ok(Self {
minutes: Self::parse_field(parts[0], 0, 59)?,
hours: Self::parse_field(parts[1], 0, 23)?,
day_of_month: Self::parse_field(parts[2], 1, 31)?,
month: Self::parse_field(parts[3], 1, 12)?,
day_of_week: Self::parse_field(parts[4], 0, 7)?,
})
}
fn parse_field(field: &str, min: u8, max: u8) -> Result<Vec<u8>, String> {
let mut values = Vec::new();
if field == "*" {
for i in min..=max {
values.push(i);
}
return Ok(values);
}
if field.starts_with("*/") {
let step: u8 = field[2..]
.parse()
.map_err(|_| format!("Invalid step value: {}", field))?;
if step == 0 {
return Err("Step cannot be zero".to_string());
}
let mut i = min;
while i <= max {
values.push(i);
i += step;
}
return Ok(values);
}
if field.contains('-') {
let parts: Vec<&str> = field.split('-').collect();
if parts.len() != 2 {
return Err(format!("Invalid range: {}", field));
}
let start: u8 = parts[0]
.parse()
.map_err(|_| format!("Invalid range start: {}", parts[0]))?;
let end: u8 = parts[1]
.parse()
.map_err(|_| format!("Invalid range end: {}", parts[1]))?;
if start > end || start < min || end > max {
return Err(format!("Invalid range: {}-{}", start, end));
}
for i in start..=end {
values.push(i);
}
return Ok(values);
}
for part in field.split(',') {
let value: u8 = part
.parse()
.map_err(|_| format!("Invalid value: {}", part))?;
if value < min || value > max {
return Err(format!(
"Value out of range: {} (expected {}-{})",
value, min, max
));
}
values.push(value);
}
values.sort();
values.dedup();
Ok(values)
}
pub fn matches(&self, datetime: &chrono::DateTime<chrono::Local>) -> bool {
self.minutes.contains(&(datetime.minute() as u8))
&& self.hours.contains(&(datetime.hour() as u8))
&& self.day_of_month.contains(&(datetime.day() as u8))
&& self.month.contains(&(datetime.month() as u8))
&& self
.day_of_week
.contains(&(datetime.weekday().num_days_from_sunday() as u8))
}
}