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}