Skip to main content

ferro_rs/schedule/
builder.rs

1//! Task builder for fluent schedule configuration
2//!
3//! Provides a fluent API for configuring scheduled tasks with closures.
4
5use super::expression::{CronExpression, DayOfWeek};
6use super::task::{BoxedFuture, BoxedTask, ClosureTask, Task, TaskEntry, TaskResult};
7use std::sync::Arc;
8
9/// Builder for configuring scheduled tasks with a fluent API
10///
11/// This builder is returned by `Schedule::call()` and allows you to configure
12/// when and how a closure-based task should run.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// schedule.call(|| async {
18///     println!("Running task!");
19///     Ok(())
20/// })
21/// .daily()
22/// .at("03:00")
23/// .name("daily-task")
24/// .description("Runs every day at 3 AM");
25/// ```
26pub struct TaskBuilder {
27    pub(crate) task: BoxedTask,
28    pub(crate) expression: CronExpression,
29    pub(crate) name: Option<String>,
30    pub(crate) description: Option<String>,
31    pub(crate) without_overlapping: bool,
32    pub(crate) run_in_background: bool,
33}
34
35impl TaskBuilder {
36    /// Create a new task builder with a closure
37    ///
38    /// The closure should return a future that resolves to `Result<(), FrameworkError>`.
39    pub fn new<F>(f: F) -> Self
40    where
41        F: Fn() -> BoxedFuture<'static> + Send + Sync + 'static,
42    {
43        Self {
44            task: Arc::new(ClosureTask { handler: f }),
45            expression: CronExpression::every_minute(),
46            name: None,
47            description: None,
48            without_overlapping: false,
49            run_in_background: false,
50        }
51    }
52
53    /// Create a TaskBuilder from an async closure
54    ///
55    /// This is a convenience method that wraps the async closure properly.
56    pub fn from_async<F, Fut>(f: F) -> Self
57    where
58        F: Fn() -> Fut + Send + Sync + 'static,
59        Fut: std::future::Future<Output = TaskResult> + Send + 'static,
60    {
61        Self::new(move || Box::pin(f()))
62    }
63
64    /// Create a TaskBuilder from a struct implementing the Task trait
65    ///
66    /// This allows using the fluent schedule API with struct-based tasks.
67    ///
68    /// # Example
69    /// ```rust,ignore
70    /// schedule.add(
71    ///     schedule.task(CleanupLogsTask::new())
72    ///         .daily()
73    ///         .at("03:00")
74    ///         .name("cleanup:logs")
75    /// );
76    /// ```
77    pub fn from_task<T: Task + 'static>(task: T) -> Self {
78        Self {
79            task: Arc::new(task),
80            expression: CronExpression::every_minute(),
81            name: None,
82            description: None,
83            without_overlapping: false,
84            run_in_background: false,
85        }
86    }
87
88    // =========================================================================
89    // Schedule Expression Methods
90    // =========================================================================
91
92    /// Set a custom cron expression
93    ///
94    /// # Example
95    /// ```rust,ignore
96    /// .cron("0 */5 * * *") // Every 5 hours at minute 0
97    /// ```
98    ///
99    /// # Panics
100    /// Panics if the cron expression is invalid.
101    pub fn cron(mut self, expression: &str) -> Self {
102        self.expression = CronExpression::parse(expression).expect("Invalid cron expression");
103        self
104    }
105
106    /// Try to set a custom cron expression, returning an error if invalid
107    pub fn try_cron(mut self, expression: &str) -> Result<Self, String> {
108        self.expression = CronExpression::parse(expression)?;
109        Ok(self)
110    }
111
112    /// Run every minute
113    pub fn every_minute(mut self) -> Self {
114        self.expression = CronExpression::every_minute();
115        self
116    }
117
118    /// Run every 2 minutes
119    pub fn every_two_minutes(mut self) -> Self {
120        self.expression = CronExpression::every_n_minutes(2);
121        self
122    }
123
124    /// Run every 5 minutes
125    pub fn every_five_minutes(mut self) -> Self {
126        self.expression = CronExpression::every_n_minutes(5);
127        self
128    }
129
130    /// Run every 10 minutes
131    pub fn every_ten_minutes(mut self) -> Self {
132        self.expression = CronExpression::every_n_minutes(10);
133        self
134    }
135
136    /// Run every 15 minutes
137    pub fn every_fifteen_minutes(mut self) -> Self {
138        self.expression = CronExpression::every_n_minutes(15);
139        self
140    }
141
142    /// Run every 30 minutes
143    pub fn every_thirty_minutes(mut self) -> Self {
144        self.expression = CronExpression::every_n_minutes(30);
145        self
146    }
147
148    /// Run every hour at minute 0
149    pub fn hourly(mut self) -> Self {
150        self.expression = CronExpression::hourly();
151        self
152    }
153
154    /// Run every hour at a specific minute
155    ///
156    /// # Example
157    /// ```rust,ignore
158    /// .hourly_at(30) // Every hour at XX:30
159    /// ```
160    pub fn hourly_at(mut self, minute: u32) -> Self {
161        self.expression = CronExpression::hourly_at(minute);
162        self
163    }
164
165    /// Run every 2 hours
166    pub fn every_two_hours(mut self) -> Self {
167        self.expression = CronExpression::parse("0 */2 * * *").unwrap();
168        self
169    }
170
171    /// Run every 3 hours
172    pub fn every_three_hours(mut self) -> Self {
173        self.expression = CronExpression::parse("0 */3 * * *").unwrap();
174        self
175    }
176
177    /// Run every 4 hours
178    pub fn every_four_hours(mut self) -> Self {
179        self.expression = CronExpression::parse("0 */4 * * *").unwrap();
180        self
181    }
182
183    /// Run every 6 hours
184    pub fn every_six_hours(mut self) -> Self {
185        self.expression = CronExpression::parse("0 */6 * * *").unwrap();
186        self
187    }
188
189    /// Run once daily at midnight
190    pub fn daily(mut self) -> Self {
191        self.expression = CronExpression::daily();
192        self
193    }
194
195    /// Run daily at a specific time
196    ///
197    /// # Example
198    /// ```rust,ignore
199    /// .daily_at("13:00") // Daily at 1:00 PM
200    /// ```
201    pub fn daily_at(mut self, time: &str) -> Self {
202        self.expression = CronExpression::daily_at(time);
203        self
204    }
205
206    /// Run twice daily at specific times
207    ///
208    /// # Example
209    /// ```rust,ignore
210    /// .twice_daily(1, 13) // At 1:00 AM and 1:00 PM
211    /// ```
212    pub fn twice_daily(mut self, first_hour: u32, second_hour: u32) -> Self {
213        self.expression =
214            CronExpression::parse(&format!("0 {first_hour},{second_hour} * * *")).unwrap();
215        self
216    }
217
218    /// Set the time for the current schedule
219    ///
220    /// This can be chained with other methods to set a specific time.
221    ///
222    /// # Example
223    /// ```rust,ignore
224    /// .daily().at("14:30") // Daily at 2:30 PM
225    /// .weekly().at("09:00") // Weekly at 9:00 AM
226    /// ```
227    pub fn at(mut self, time: &str) -> Self {
228        self.expression = self.expression.at(time);
229        self
230    }
231
232    /// Run once weekly on Sunday at midnight
233    pub fn weekly(mut self) -> Self {
234        self.expression = CronExpression::weekly();
235        self
236    }
237
238    /// Run weekly on a specific day at midnight
239    ///
240    /// # Example
241    /// ```rust,ignore
242    /// .weekly_on(DayOfWeek::Monday)
243    /// ```
244    pub fn weekly_on(mut self, day: DayOfWeek) -> Self {
245        self.expression = CronExpression::weekly_on(day);
246        self
247    }
248
249    /// Run on specific days of the week at midnight
250    ///
251    /// # Example
252    /// ```rust,ignore
253    /// .days(&[DayOfWeek::Monday, DayOfWeek::Wednesday, DayOfWeek::Friday])
254    /// ```
255    pub fn days(mut self, days: &[DayOfWeek]) -> Self {
256        self.expression = CronExpression::on_days(days);
257        self
258    }
259
260    /// Run on weekdays (Monday-Friday) at midnight
261    pub fn weekdays(mut self) -> Self {
262        self.expression = CronExpression::weekdays();
263        self
264    }
265
266    /// Run on weekends (Saturday-Sunday) at midnight
267    pub fn weekends(mut self) -> Self {
268        self.expression = CronExpression::weekends();
269        self
270    }
271
272    /// Run on Sundays at midnight
273    pub fn sundays(mut self) -> Self {
274        self.expression = CronExpression::weekly_on(DayOfWeek::Sunday);
275        self
276    }
277
278    /// Run on Mondays at midnight
279    pub fn mondays(mut self) -> Self {
280        self.expression = CronExpression::weekly_on(DayOfWeek::Monday);
281        self
282    }
283
284    /// Run on Tuesdays at midnight
285    pub fn tuesdays(mut self) -> Self {
286        self.expression = CronExpression::weekly_on(DayOfWeek::Tuesday);
287        self
288    }
289
290    /// Run on Wednesdays at midnight
291    pub fn wednesdays(mut self) -> Self {
292        self.expression = CronExpression::weekly_on(DayOfWeek::Wednesday);
293        self
294    }
295
296    /// Run on Thursdays at midnight
297    pub fn thursdays(mut self) -> Self {
298        self.expression = CronExpression::weekly_on(DayOfWeek::Thursday);
299        self
300    }
301
302    /// Run on Fridays at midnight
303    pub fn fridays(mut self) -> Self {
304        self.expression = CronExpression::weekly_on(DayOfWeek::Friday);
305        self
306    }
307
308    /// Run on Saturdays at midnight
309    pub fn saturdays(mut self) -> Self {
310        self.expression = CronExpression::weekly_on(DayOfWeek::Saturday);
311        self
312    }
313
314    /// Run once monthly on the first day at midnight
315    pub fn monthly(mut self) -> Self {
316        self.expression = CronExpression::monthly();
317        self
318    }
319
320    /// Run monthly on a specific day at midnight
321    ///
322    /// # Example
323    /// ```rust,ignore
324    /// .monthly_on(15) // On the 15th of each month
325    /// ```
326    pub fn monthly_on(mut self, day: u32) -> Self {
327        self.expression = CronExpression::monthly_on(day);
328        self
329    }
330
331    /// Run quarterly on the first day of each quarter at midnight
332    pub fn quarterly(mut self) -> Self {
333        self.expression = CronExpression::quarterly();
334        self
335    }
336
337    /// Run yearly on January 1st at midnight
338    pub fn yearly(mut self) -> Self {
339        self.expression = CronExpression::yearly();
340        self
341    }
342
343    // =========================================================================
344    // Configuration Methods
345    // =========================================================================
346
347    /// Set a name for this task
348    ///
349    /// The name is used in logs and when listing scheduled tasks.
350    pub fn name(mut self, name: &str) -> Self {
351        self.name = Some(name.to_string());
352        self
353    }
354
355    /// Set a description for this task
356    ///
357    /// The description is shown when listing scheduled tasks.
358    pub fn description(mut self, desc: &str) -> Self {
359        self.description = Some(desc.to_string());
360        self
361    }
362
363    /// Prevent overlapping task runs
364    ///
365    /// When enabled, the scheduler will skip running this task if
366    /// a previous run is still in progress.
367    pub fn without_overlapping(mut self) -> Self {
368        self.without_overlapping = true;
369        self
370    }
371
372    /// Run task in background (non-blocking)
373    ///
374    /// When enabled, the scheduler won't wait for the task to complete
375    /// before continuing to the next task.
376    pub fn run_in_background(mut self) -> Self {
377        self.run_in_background = true;
378        self
379    }
380
381    /// Build the task entry
382    ///
383    /// This is called internally when adding the task to the schedule.
384    pub(crate) fn build(self, task_index: usize) -> TaskEntry {
385        let name = self
386            .name
387            .unwrap_or_else(|| format!("closure-task-{task_index}"));
388
389        TaskEntry {
390            name,
391            expression: self.expression,
392            task: self.task,
393            description: self.description,
394            without_overlapping: self.without_overlapping,
395            run_in_background: self.run_in_background,
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    fn create_test_builder() -> TaskBuilder {
405        TaskBuilder::from_async(|| async { Ok(()) })
406    }
407
408    #[test]
409    fn test_builder_schedule_methods() {
410        let builder = create_test_builder().every_minute();
411        assert_eq!(builder.expression.expression(), "* * * * *");
412
413        let builder = create_test_builder().hourly();
414        assert_eq!(builder.expression.expression(), "0 * * * *");
415
416        let builder = create_test_builder().daily();
417        assert_eq!(builder.expression.expression(), "0 0 * * *");
418
419        let builder = create_test_builder().weekly();
420        assert_eq!(builder.expression.expression(), "0 0 * * 0");
421    }
422
423    #[test]
424    fn test_builder_daily_at() {
425        let builder = create_test_builder().daily_at("14:30");
426        assert_eq!(builder.expression.expression(), "30 14 * * *");
427    }
428
429    #[test]
430    fn test_builder_at_modifier() {
431        let builder = create_test_builder().daily().at("09:15");
432        assert_eq!(builder.expression.expression(), "15 9 * * *");
433    }
434
435    #[test]
436    fn test_builder_configuration() {
437        let builder = create_test_builder()
438            .name("test-task")
439            .description("A test task")
440            .without_overlapping()
441            .run_in_background();
442
443        assert_eq!(builder.name, Some("test-task".to_string()));
444        assert_eq!(builder.description, Some("A test task".to_string()));
445        assert!(builder.without_overlapping);
446        assert!(builder.run_in_background);
447    }
448
449    #[test]
450    fn test_builder_build() {
451        let builder = create_test_builder()
452            .daily()
453            .name("my-task")
454            .description("My task description");
455
456        let entry = builder.build(0);
457
458        assert_eq!(entry.name, "my-task");
459        assert_eq!(entry.description, Some("My task description".to_string()));
460    }
461
462    #[test]
463    fn test_builder_default_name() {
464        let builder = create_test_builder().daily();
465        let entry = builder.build(5);
466
467        assert_eq!(entry.name, "closure-task-5");
468    }
469}