use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::WorkflowId;
pub type ScheduleId = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Schedule {
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub id: ScheduleId,
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub workflow_id: WorkflowId,
pub name: String,
pub description: Option<String>,
pub cron: String,
pub timezone: String,
pub enabled: bool,
pub input_variables: std::collections::HashMap<String, serde_json::Value>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_run: Option<DateTime<Utc>>,
pub next_run: Option<DateTime<Utc>>,
pub run_count: u64,
pub max_runs: Option<u64>,
pub expires_at: Option<DateTime<Utc>>,
}
impl Schedule {
pub fn new(workflow_id: WorkflowId, name: String, cron: String) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
workflow_id,
name,
description: None,
cron,
timezone: "UTC".to_string(),
enabled: true,
input_variables: std::collections::HashMap::new(),
created_at: now,
updated_at: now,
last_run: None,
next_run: None,
run_count: 0,
max_runs: None,
expires_at: None,
}
}
pub fn should_run(&self) -> bool {
if !self.enabled {
return false;
}
if let Some(expires_at) = self.expires_at {
if Utc::now() > expires_at {
return false;
}
}
if let Some(max_runs) = self.max_runs {
if self.run_count >= max_runs {
return false;
}
}
if let Some(next_run) = self.next_run {
return Utc::now() >= next_run;
}
false
}
pub fn mark_executed(&mut self) {
self.last_run = Some(Utc::now());
self.run_count += 1;
}
pub fn validate(&self) -> Result<(), String> {
let parts: Vec<&str> = self.cron.split_whitespace().collect();
if parts.len() != 5 && parts.len() != 6 {
return Err(format!(
"Invalid cron expression '{}': must have 5 or 6 parts",
self.cron
));
}
if self.timezone.is_empty() {
return Err("Timezone cannot be empty".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ScheduleExecution {
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub id: Uuid,
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub schedule_id: ScheduleId,
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub execution_id: Uuid,
pub triggered_at: DateTime<Utc>,
pub success: bool,
pub error: Option<String>,
pub duration_ms: Option<u64>,
}
impl ScheduleExecution {
pub fn new(schedule_id: ScheduleId, execution_id: Uuid) -> Self {
Self {
id: Uuid::new_v4(),
schedule_id,
execution_id,
triggered_at: Utc::now(),
success: false,
error: None,
duration_ms: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schedule_creation() {
let workflow_id = Uuid::new_v4();
let schedule = Schedule::new(
workflow_id,
"Daily Report".to_string(),
"0 0 * * *".to_string(),
);
assert_eq!(schedule.workflow_id, workflow_id);
assert_eq!(schedule.name, "Daily Report");
assert_eq!(schedule.cron, "0 0 * * *");
assert!(schedule.enabled);
assert_eq!(schedule.run_count, 0);
}
#[test]
fn test_schedule_validation() {
let mut schedule =
Schedule::new(Uuid::new_v4(), "Test".to_string(), "0 0 * * *".to_string());
assert!(schedule.validate().is_ok());
schedule.cron = "0 0 *".to_string();
assert!(schedule.validate().is_err());
schedule.cron = "0 0 0 * * *".to_string();
assert!(schedule.validate().is_ok());
}
#[test]
fn test_should_run() {
let mut schedule =
Schedule::new(Uuid::new_v4(), "Test".to_string(), "0 0 * * *".to_string());
schedule.enabled = false;
assert!(!schedule.should_run());
schedule.enabled = true;
assert!(!schedule.should_run());
schedule.max_runs = Some(5);
schedule.run_count = 5;
schedule.next_run = Some(Utc::now());
assert!(!schedule.should_run());
schedule.max_runs = None;
schedule.run_count = 0;
schedule.expires_at = Some(Utc::now() - chrono::Duration::hours(1));
assert!(!schedule.should_run());
}
#[test]
fn test_mark_executed() {
let mut schedule =
Schedule::new(Uuid::new_v4(), "Test".to_string(), "0 0 * * *".to_string());
assert_eq!(schedule.run_count, 0);
assert!(schedule.last_run.is_none());
schedule.mark_executed();
assert_eq!(schedule.run_count, 1);
assert!(schedule.last_run.is_some());
}
}