1use crate::error::{Result, WorkflowError};
4use crate::workflow::{Workflow, WorkflowId};
5use chrono::{DateTime, Utc};
6use cron::Schedule;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::{debug, info};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum Trigger {
19 Cron {
21 expression: String,
23 #[serde(default)]
25 timezone: String,
26 },
27
28 WatchFolder {
30 path: PathBuf,
32 #[serde(default)]
34 pattern: String,
35 #[serde(default)]
37 recursive: bool,
38 },
39
40 Manual,
42
43 Time {
45 at: DateTime<Utc>,
47 },
48
49 Event {
51 event_type: String,
53 #[serde(default)]
55 conditions: HashMap<String, String>,
56 },
57
58 Interval {
60 seconds: u64,
62 },
63}
64
65#[derive(Debug, Clone)]
67pub struct ScheduledWorkflow {
68 pub workflow: Workflow,
70 pub trigger: Trigger,
72 pub enabled: bool,
74 pub last_execution: Option<DateTime<Utc>>,
76 pub next_execution: Option<DateTime<Utc>>,
78}
79
80impl ScheduledWorkflow {
81 #[must_use]
83 pub fn new(workflow: Workflow, trigger: Trigger) -> Self {
84 let next_execution = Self::calculate_next_execution(&trigger, None);
85 Self {
86 workflow,
87 trigger,
88 enabled: true,
89 last_execution: None,
90 next_execution,
91 }
92 }
93
94 fn calculate_next_execution(
96 trigger: &Trigger,
97 after: Option<DateTime<Utc>>,
98 ) -> Option<DateTime<Utc>> {
99 let now = after.unwrap_or_else(Utc::now);
100
101 match trigger {
102 Trigger::Cron { expression, .. } => {
103 if let Ok(schedule) = Schedule::from_str(expression) {
104 schedule.after(&now).next()
105 } else {
106 None
107 }
108 }
109 Trigger::Time { at } => {
110 if *at > now {
111 Some(*at)
112 } else {
113 None
114 }
115 }
116 Trigger::Interval { seconds } => {
117 Some(now + chrono::Duration::seconds(i64::try_from(*seconds).unwrap_or(60)))
118 }
119 Trigger::Manual | Trigger::WatchFolder { .. } | Trigger::Event { .. } => None,
120 }
121 }
122
123 pub fn update_next_execution(&mut self) {
125 self.last_execution = Some(Utc::now());
126 self.next_execution = Self::calculate_next_execution(&self.trigger, self.last_execution);
127 }
128
129 #[must_use]
131 pub fn should_execute(&self) -> bool {
132 if !self.enabled {
133 return false;
134 }
135
136 match self.next_execution {
137 Some(next) => Utc::now() >= next,
138 None => false,
139 }
140 }
141}
142
143pub struct WorkflowScheduler {
145 schedules: Arc<RwLock<HashMap<WorkflowId, ScheduledWorkflow>>>,
147 running: Arc<RwLock<bool>>,
149}
150
151impl WorkflowScheduler {
152 #[must_use]
154 pub fn new() -> Self {
155 Self {
156 schedules: Arc::new(RwLock::new(HashMap::new())),
157 running: Arc::new(RwLock::new(false)),
158 }
159 }
160
161 pub async fn add_schedule(&self, workflow: Workflow, trigger: Trigger) -> Result<WorkflowId> {
163 let workflow_id = workflow.id;
164 let scheduled = ScheduledWorkflow::new(workflow, trigger);
165
166 let mut schedules = self.schedules.write().await;
167 schedules.insert(workflow_id, scheduled);
168
169 info!("Added scheduled workflow: {}", workflow_id);
170 Ok(workflow_id)
171 }
172
173 pub async fn remove_schedule(&self, workflow_id: WorkflowId) -> Result<()> {
175 let mut schedules = self.schedules.write().await;
176 schedules.remove(&workflow_id);
177 info!("Removed scheduled workflow: {}", workflow_id);
178 Ok(())
179 }
180
181 pub async fn set_schedule_enabled(&self, workflow_id: WorkflowId, enabled: bool) -> Result<()> {
183 let mut schedules = self.schedules.write().await;
184 if let Some(schedule) = schedules.get_mut(&workflow_id) {
185 schedule.enabled = enabled;
186 debug!("Schedule {} enabled: {}", workflow_id, enabled);
187 Ok(())
188 } else {
189 Err(WorkflowError::WorkflowNotFound(workflow_id.to_string()))
190 }
191 }
192
193 pub async fn list_schedules(&self) -> Vec<(WorkflowId, ScheduledWorkflow)> {
195 let schedules = self.schedules.read().await;
196 schedules
197 .iter()
198 .map(|(id, sched)| (*id, sched.clone()))
199 .collect()
200 }
201
202 pub async fn start(&self) -> Result<()> {
204 let mut running = self.running.write().await;
205 if *running {
206 return Err(WorkflowError::AlreadyRunning("Scheduler".to_string()));
207 }
208 *running = true;
209 drop(running);
210
211 info!("Scheduler started");
212 Ok(())
213 }
214
215 pub async fn stop(&self) -> Result<()> {
217 let mut running = self.running.write().await;
218 if !*running {
219 return Err(WorkflowError::NotRunning("Scheduler".to_string()));
220 }
221 *running = false;
222
223 info!("Scheduler stopped");
224 Ok(())
225 }
226
227 pub async fn check_schedules(&self) -> Vec<Workflow> {
229 let running = self.running.read().await;
230 if !*running {
231 return Vec::new();
232 }
233 drop(running);
234
235 let mut schedules = self.schedules.write().await;
236 let mut ready_workflows = Vec::new();
237
238 for (_, schedule) in schedules.iter_mut() {
239 if schedule.should_execute() {
240 ready_workflows.push(schedule.workflow.clone());
241 schedule.update_next_execution();
242 debug!(
243 "Workflow {} ready for execution. Next: {:?}",
244 schedule.workflow.id, schedule.next_execution
245 );
246 }
247 }
248
249 ready_workflows
250 }
251
252 pub async fn get_next_execution(&self, workflow_id: WorkflowId) -> Option<DateTime<Utc>> {
254 let schedules = self.schedules.read().await;
255 schedules.get(&workflow_id).and_then(|s| s.next_execution)
256 }
257}
258
259impl Default for WorkflowScheduler {
260 fn default() -> Self {
261 Self::new()
262 }
263}
264
265pub struct FileWatcher {
267 watches: Arc<RwLock<HashMap<PathBuf, Vec<WorkflowId>>>>,
269}
270
271impl FileWatcher {
272 #[must_use]
274 pub fn new() -> Self {
275 Self {
276 watches: Arc::new(RwLock::new(HashMap::new())),
277 }
278 }
279
280 pub async fn add_watch(&self, path: PathBuf, workflow_id: WorkflowId) -> Result<()> {
282 let mut watches = self.watches.write().await;
283 watches.entry(path.clone()).or_default().push(workflow_id);
284 info!("Added file watch: {:?} -> {}", path, workflow_id);
285 Ok(())
286 }
287
288 pub async fn remove_watch(&self, path: &PathBuf, workflow_id: WorkflowId) -> Result<()> {
290 let mut watches = self.watches.write().await;
291 if let Some(workflows) = watches.get_mut(path) {
292 workflows.retain(|id| *id != workflow_id);
293 if workflows.is_empty() {
294 watches.remove(path);
295 }
296 }
297 info!("Removed file watch: {:?} -> {}", path, workflow_id);
298 Ok(())
299 }
300
301 pub async fn get_workflows_for_path(&self, path: &PathBuf) -> Vec<WorkflowId> {
303 let watches = self.watches.read().await;
304 watches.get(path).cloned().unwrap_or_default()
305 }
306}
307
308impl Default for FileWatcher {
309 fn default() -> Self {
310 Self::new()
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_cron_trigger_parsing() {
320 let trigger = Trigger::Cron {
321 expression: "0 0 * * * *".to_string(), timezone: "UTC".to_string(),
323 };
324
325 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
326 assert!(next.is_some());
327 }
328
329 #[test]
330 fn test_interval_trigger() {
331 let trigger = Trigger::Interval { seconds: 60 };
332 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
333 assert!(next.is_some());
334 }
335
336 #[test]
337 fn test_time_trigger_future() {
338 let future_time = Utc::now() + chrono::Duration::hours(1);
339 let trigger = Trigger::Time { at: future_time };
340 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
341 assert_eq!(next, Some(future_time));
342 }
343
344 #[test]
345 fn test_time_trigger_past() {
346 let past_time = Utc::now() - chrono::Duration::hours(1);
347 let trigger = Trigger::Time { at: past_time };
348 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
349 assert!(next.is_none());
350 }
351
352 #[test]
353 fn test_scheduled_workflow_creation() {
354 let workflow = Workflow::new("test");
355 let trigger = Trigger::Manual;
356 let scheduled = ScheduledWorkflow::new(workflow, trigger);
357
358 assert!(scheduled.enabled);
359 assert!(scheduled.last_execution.is_none());
360 }
361
362 #[tokio::test]
363 async fn test_scheduler_creation() {
364 let scheduler = WorkflowScheduler::new();
365 assert!(!*scheduler.running.read().await);
366 }
367
368 #[tokio::test]
369 async fn test_add_schedule() {
370 let scheduler = WorkflowScheduler::new();
371 let workflow = Workflow::new("test-workflow");
372 let trigger = Trigger::Manual;
373
374 let workflow_id = scheduler
375 .add_schedule(workflow, trigger)
376 .await
377 .expect("should succeed in test");
378 let schedules = scheduler.list_schedules().await;
379
380 assert_eq!(schedules.len(), 1);
381 assert_eq!(schedules[0].0, workflow_id);
382 }
383
384 #[tokio::test]
385 async fn test_remove_schedule() {
386 let scheduler = WorkflowScheduler::new();
387 let workflow = Workflow::new("test-workflow");
388 let trigger = Trigger::Manual;
389
390 let workflow_id = scheduler
391 .add_schedule(workflow, trigger)
392 .await
393 .expect("should succeed in test");
394 scheduler
395 .remove_schedule(workflow_id)
396 .await
397 .expect("should succeed in test");
398
399 let schedules = scheduler.list_schedules().await;
400 assert_eq!(schedules.len(), 0);
401 }
402
403 #[tokio::test]
404 async fn test_enable_disable_schedule() {
405 let scheduler = WorkflowScheduler::new();
406 let workflow = Workflow::new("test-workflow");
407 let trigger = Trigger::Manual;
408
409 let workflow_id = scheduler
410 .add_schedule(workflow, trigger)
411 .await
412 .expect("should succeed in test");
413
414 scheduler
415 .set_schedule_enabled(workflow_id, false)
416 .await
417 .expect("should succeed in test");
418 let schedules = scheduler.list_schedules().await;
419 assert!(!schedules[0].1.enabled);
420
421 scheduler
422 .set_schedule_enabled(workflow_id, true)
423 .await
424 .expect("should succeed in test");
425 let schedules = scheduler.list_schedules().await;
426 assert!(schedules[0].1.enabled);
427 }
428
429 #[tokio::test]
430 async fn test_scheduler_start_stop() {
431 let scheduler = WorkflowScheduler::new();
432
433 scheduler.start().await.expect("should succeed in test");
434 assert!(*scheduler.running.read().await);
435
436 scheduler.stop().await.expect("should succeed in test");
437 assert!(!*scheduler.running.read().await);
438 }
439
440 #[tokio::test]
441 async fn test_file_watcher() {
442 let watcher = FileWatcher::new();
443 let path = PathBuf::from("/tmp/test");
444 let workflow_id = WorkflowId::new();
445
446 watcher
447 .add_watch(path.clone(), workflow_id)
448 .await
449 .expect("should succeed in test");
450 let workflows = watcher.get_workflows_for_path(&path).await;
451 assert_eq!(workflows.len(), 1);
452 assert_eq!(workflows[0], workflow_id);
453
454 watcher
455 .remove_watch(&path, workflow_id)
456 .await
457 .expect("should succeed in test");
458 let workflows = watcher.get_workflows_for_path(&path).await;
459 assert_eq!(workflows.len(), 0);
460 }
461}