Skip to main content

ferro_rs/schedule/
mod.rs

1//! Task Scheduler module for Ferro framework
2//!
3//! Provides a Laravel-like task scheduling system with support for:
4//! - Trait-based tasks (implement `Task`)
5//! - Closure-based tasks (inline definitions)
6//! - Fluent scheduling API (`.daily()`, `.hourly()`, etc.)
7//!
8//! # Quick Start
9//!
10//! ## Using Trait-Based Tasks
11//!
12//! ```rust,ignore
13//! use ferro_rs::{Task, TaskResult};
14//! use async_trait::async_trait;
15//!
16//! pub struct CleanupLogsTask;
17//!
18//! #[async_trait]
19//! impl Task for CleanupLogsTask {
20//!     async fn handle(&self) -> TaskResult {
21//!         // Your task logic here
22//!         Ok(())
23//!     }
24//! }
25//!
26//! // Register in schedule.rs with fluent API
27//! pub fn register(schedule: &mut Schedule) {
28//!     schedule.add(
29//!         schedule.task(CleanupLogsTask)
30//!             .daily()
31//!             .at("03:00")
32//!             .name("cleanup:logs")
33//!     );
34//! }
35//! ```
36//!
37//! ## Using Closure-Based Tasks
38//!
39//! ```rust,ignore
40//! use ferro_rs::Schedule;
41//!
42//! pub fn register(schedule: &mut Schedule) {
43//!     // Simple closure task
44//!     schedule.add(
45//!         schedule.call(|| async {
46//!             println!("Running every minute!");
47//!             Ok(())
48//!         }).every_minute().name("minute-task")
49//!     );
50//!
51//!     // Configured closure task
52//!     schedule.add(
53//!         schedule.call(|| async {
54//!             println!("Daily cleanup!");
55//!             Ok(())
56//!         })
57//!         .daily()
58//!         .at("03:00")
59//!         .name("daily-cleanup")
60//!         .description("Cleans up temporary files")
61//!     );
62//! }
63//! ```
64//!
65//! # Running the Scheduler
66//!
67//! Use the CLI commands to run scheduled tasks:
68//!
69//! ```bash
70//! # Run due tasks once (for cron)
71//! ferro schedule:run
72//!
73//! # Run as daemon (continuous)
74//! ferro schedule:work
75//!
76//! # List all scheduled tasks
77//! ferro schedule:list
78//! ```
79
80pub mod builder;
81pub mod expression;
82pub mod task;
83
84pub use builder::TaskBuilder;
85pub use expression::{CronExpression, DayOfWeek};
86pub use task::{BoxedFuture, BoxedTask, Task, TaskEntry, TaskHandler, TaskResult};
87
88use crate::error::FrameworkError;
89
90/// Schedule - main entry point for scheduling tasks
91///
92/// Provides methods for registering and running scheduled tasks.
93/// Tasks can be registered via trait implementations or closures.
94///
95/// # Example
96///
97/// ```rust,ignore
98/// use ferro_rs::Schedule;
99///
100/// pub fn register(schedule: &mut Schedule) {
101///     // Register a struct implementing Task trait
102///     schedule.add(
103///         schedule.task(MyCleanupTask::new())
104///             .daily()
105///             .at("03:00")
106///             .name("cleanup")
107///     );
108///
109///     // Or use a closure
110///     schedule.add(
111///         schedule.call(|| async {
112///             println!("Hello!");
113///             Ok(())
114///         }).daily().at("03:00").name("greeting")
115///     );
116/// }
117/// ```
118pub struct Schedule {
119    tasks: Vec<TaskEntry>,
120}
121
122impl Schedule {
123    /// Create a new empty schedule
124    pub fn new() -> Self {
125        Self { tasks: Vec::new() }
126    }
127
128    /// Register a trait-based scheduled task
129    ///
130    /// Returns a `TaskBuilder` that allows fluent schedule configuration.
131    ///
132    /// # Example
133    /// ```rust,ignore
134    /// schedule.add(
135    ///     schedule.task(CleanupLogsTask::new())
136    ///         .daily()
137    ///         .at("03:00")
138    ///         .name("cleanup:logs")
139    /// );
140    /// ```
141    pub fn task<T: Task + 'static>(&self, task: T) -> TaskBuilder {
142        TaskBuilder::from_task(task)
143    }
144
145    /// Register a closure-based scheduled task
146    ///
147    /// Returns a `TaskBuilder` that allows you to configure the schedule
148    /// using a fluent API.
149    ///
150    /// # Example
151    ///
152    /// ```rust,ignore
153    /// schedule.call(|| async {
154    ///     // Task logic here
155    ///     Ok(())
156    /// }).daily().at("03:00").name("my-task");
157    /// ```
158    pub fn call<F, Fut>(&mut self, f: F) -> TaskBuilder
159    where
160        F: Fn() -> Fut + Send + Sync + 'static,
161        Fut: std::future::Future<Output = Result<(), FrameworkError>> + Send + 'static,
162    {
163        TaskBuilder::from_async(f)
164    }
165
166    /// Add a configured task builder to the schedule
167    ///
168    /// This method is typically called after configuring a task with `call()`.
169    ///
170    /// # Example
171    ///
172    /// ```rust,ignore
173    /// let builder = schedule.call(|| async { Ok(()) }).daily();
174    /// schedule.add(builder);
175    /// ```
176    pub fn add(&mut self, builder: TaskBuilder) -> &mut Self {
177        let task_index = self.tasks.len();
178        self.tasks.push(builder.build(task_index));
179        self
180    }
181
182    /// Get all registered tasks
183    pub fn tasks(&self) -> &[TaskEntry] {
184        &self.tasks
185    }
186
187    /// Get the number of registered tasks
188    pub fn len(&self) -> usize {
189        self.tasks.len()
190    }
191
192    /// Check if there are no registered tasks
193    pub fn is_empty(&self) -> bool {
194        self.tasks.is_empty()
195    }
196
197    /// Get tasks that are due to run now
198    pub fn due_tasks(&self) -> Vec<&TaskEntry> {
199        self.tasks.iter().filter(|t| t.is_due()).collect()
200    }
201
202    /// Run all due tasks once
203    ///
204    /// Returns a vector of results for each task that was run.
205    pub async fn run_due_tasks(&self) -> Vec<(&str, Result<(), FrameworkError>)> {
206        let due = self.due_tasks();
207        let mut results = Vec::new();
208
209        for task in due {
210            let result = task.run().await;
211            results.push((task.name.as_str(), result));
212        }
213
214        results
215    }
216
217    /// Run all tasks regardless of their schedule
218    ///
219    /// Useful for testing or manual triggering.
220    pub async fn run_all_tasks(&self) -> Vec<(&str, Result<(), FrameworkError>)> {
221        let mut results = Vec::new();
222
223        for task in &self.tasks {
224            let result = task.run().await;
225            results.push((task.name.as_str(), result));
226        }
227
228        results
229    }
230
231    /// Find a task by name
232    pub fn find(&self, name: &str) -> Option<&TaskEntry> {
233        self.tasks.iter().find(|t| t.name == name)
234    }
235
236    /// Run a specific task by name
237    pub async fn run_task(&self, name: &str) -> Option<Result<(), FrameworkError>> {
238        if let Some(task) = self.find(name) {
239            Some(task.run().await)
240        } else {
241            None
242        }
243    }
244}
245
246impl Default for Schedule {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252/// Macro for creating closure-based tasks more ergonomically
253///
254/// # Example
255///
256/// ```rust,ignore
257/// use ferro_rs::{schedule_task, Schedule};
258///
259/// pub fn register(schedule: &mut Schedule) {
260///     schedule.add(
261///         schedule_task!(|| async {
262///             println!("Running!");
263///             Ok(())
264///         })
265///         .daily()
266///         .name("my-task")
267///     );
268/// }
269/// ```
270#[macro_export]
271macro_rules! schedule_task {
272    ($f:expr) => {
273        $crate::schedule::TaskBuilder::from_async($f)
274    };
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use async_trait::async_trait;
281
282    struct TestTask;
283
284    #[async_trait]
285    impl Task for TestTask {
286        async fn handle(&self) -> Result<(), FrameworkError> {
287            Ok(())
288        }
289    }
290
291    #[test]
292    fn test_schedule_new() {
293        let schedule = Schedule::new();
294        assert!(schedule.is_empty());
295        assert_eq!(schedule.len(), 0);
296    }
297
298    #[test]
299    fn test_schedule_add_trait_task() {
300        let mut schedule = Schedule::new();
301        schedule.add(schedule.task(TestTask).every_minute().name("test-1"));
302        schedule.add(schedule.task(TestTask).every_minute().name("test-2"));
303
304        assert_eq!(schedule.len(), 2);
305        assert!(!schedule.is_empty());
306    }
307
308    #[test]
309    fn test_schedule_add_closure_task() {
310        let mut schedule = Schedule::new();
311
312        let builder = schedule
313            .call(|| async { Ok(()) })
314            .daily()
315            .name("closure-task");
316
317        schedule.add(builder);
318
319        assert_eq!(schedule.len(), 1);
320    }
321
322    #[test]
323    fn test_schedule_find_task() {
324        let mut schedule = Schedule::new();
325        schedule.add(schedule.task(TestTask).every_minute().name("find-me"));
326
327        let found = schedule.find("find-me");
328        assert!(found.is_some());
329        assert_eq!(found.unwrap().name, "find-me");
330
331        let not_found = schedule.find("not-exists");
332        assert!(not_found.is_none());
333    }
334
335    #[tokio::test]
336    async fn test_schedule_run_task() {
337        let mut schedule = Schedule::new();
338        schedule.add(schedule.task(TestTask).every_minute().name("run-me"));
339
340        let result = schedule.run_task("run-me").await;
341        assert!(result.is_some());
342        assert!(result.unwrap().is_ok());
343
344        let not_found = schedule.run_task("not-exists").await;
345        assert!(not_found.is_none());
346    }
347
348    #[tokio::test]
349    async fn test_schedule_run_all_tasks() {
350        let mut schedule = Schedule::new();
351        schedule.add(schedule.task(TestTask).every_minute().name("task-1"));
352        schedule.add(schedule.task(TestTask).every_minute().name("task-2"));
353
354        let results = schedule.run_all_tasks().await;
355        assert_eq!(results.len(), 2);
356
357        for (name, result) in results {
358            assert!(result.is_ok(), "Task {name} failed");
359        }
360    }
361}