kit_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 =
103            CronExpression::parse(expression).expect("Invalid cron expression");
104        self
105    }
106
107    /// Try to set a custom cron expression, returning an error if invalid
108    pub fn try_cron(mut self, expression: &str) -> Result<Self, String> {
109        self.expression = CronExpression::parse(expression)?;
110        Ok(self)
111    }
112
113    /// Run every minute
114    pub fn every_minute(mut self) -> Self {
115        self.expression = CronExpression::every_minute();
116        self
117    }
118
119    /// Run every 2 minutes
120    pub fn every_two_minutes(mut self) -> Self {
121        self.expression = CronExpression::every_n_minutes(2);
122        self
123    }
124
125    /// Run every 5 minutes
126    pub fn every_five_minutes(mut self) -> Self {
127        self.expression = CronExpression::every_n_minutes(5);
128        self
129    }
130
131    /// Run every 10 minutes
132    pub fn every_ten_minutes(mut self) -> Self {
133        self.expression = CronExpression::every_n_minutes(10);
134        self
135    }
136
137    /// Run every 15 minutes
138    pub fn every_fifteen_minutes(mut self) -> Self {
139        self.expression = CronExpression::every_n_minutes(15);
140        self
141    }
142
143    /// Run every 30 minutes
144    pub fn every_thirty_minutes(mut self) -> Self {
145        self.expression = CronExpression::every_n_minutes(30);
146        self
147    }
148
149    /// Run every hour at minute 0
150    pub fn hourly(mut self) -> Self {
151        self.expression = CronExpression::hourly();
152        self
153    }
154
155    /// Run every hour at a specific minute
156    ///
157    /// # Example
158    /// ```rust,ignore
159    /// .hourly_at(30) // Every hour at XX:30
160    /// ```
161    pub fn hourly_at(mut self, minute: u32) -> Self {
162        self.expression = CronExpression::hourly_at(minute);
163        self
164    }
165
166    /// Run every 2 hours
167    pub fn every_two_hours(mut self) -> Self {
168        self.expression = CronExpression::parse("0 */2 * * *").unwrap();
169        self
170    }
171
172    /// Run every 3 hours
173    pub fn every_three_hours(mut self) -> Self {
174        self.expression = CronExpression::parse("0 */3 * * *").unwrap();
175        self
176    }
177
178    /// Run every 4 hours
179    pub fn every_four_hours(mut self) -> Self {
180        self.expression = CronExpression::parse("0 */4 * * *").unwrap();
181        self
182    }
183
184    /// Run every 6 hours
185    pub fn every_six_hours(mut self) -> Self {
186        self.expression = CronExpression::parse("0 */6 * * *").unwrap();
187        self
188    }
189
190    /// Run once daily at midnight
191    pub fn daily(mut self) -> Self {
192        self.expression = CronExpression::daily();
193        self
194    }
195
196    /// Run daily at a specific time
197    ///
198    /// # Example
199    /// ```rust,ignore
200    /// .daily_at("13:00") // Daily at 1:00 PM
201    /// ```
202    pub fn daily_at(mut self, time: &str) -> Self {
203        self.expression = CronExpression::daily_at(time);
204        self
205    }
206
207    /// Run twice daily at specific times
208    ///
209    /// # Example
210    /// ```rust,ignore
211    /// .twice_daily(1, 13) // At 1:00 AM and 1:00 PM
212    /// ```
213    pub fn twice_daily(mut self, first_hour: u32, second_hour: u32) -> Self {
214        self.expression =
215            CronExpression::parse(&format!("0 {},{} * * *", first_hour, second_hour)).unwrap();
216        self
217    }
218
219    /// Set the time for the current schedule
220    ///
221    /// This can be chained with other methods to set a specific time.
222    ///
223    /// # Example
224    /// ```rust,ignore
225    /// .daily().at("14:30") // Daily at 2:30 PM
226    /// .weekly().at("09:00") // Weekly at 9:00 AM
227    /// ```
228    pub fn at(mut self, time: &str) -> Self {
229        self.expression = self.expression.at(time);
230        self
231    }
232
233    /// Run once weekly on Sunday at midnight
234    pub fn weekly(mut self) -> Self {
235        self.expression = CronExpression::weekly();
236        self
237    }
238
239    /// Run weekly on a specific day at midnight
240    ///
241    /// # Example
242    /// ```rust,ignore
243    /// .weekly_on(DayOfWeek::Monday)
244    /// ```
245    pub fn weekly_on(mut self, day: DayOfWeek) -> Self {
246        self.expression = CronExpression::weekly_on(day);
247        self
248    }
249
250    /// Run on specific days of the week at midnight
251    ///
252    /// # Example
253    /// ```rust,ignore
254    /// .days(&[DayOfWeek::Monday, DayOfWeek::Wednesday, DayOfWeek::Friday])
255    /// ```
256    pub fn days(mut self, days: &[DayOfWeek]) -> Self {
257        self.expression = CronExpression::on_days(days);
258        self
259    }
260
261    /// Run on weekdays (Monday-Friday) at midnight
262    pub fn weekdays(mut self) -> Self {
263        self.expression = CronExpression::weekdays();
264        self
265    }
266
267    /// Run on weekends (Saturday-Sunday) at midnight
268    pub fn weekends(mut self) -> Self {
269        self.expression = CronExpression::weekends();
270        self
271    }
272
273    /// Run on Sundays at midnight
274    pub fn sundays(mut self) -> Self {
275        self.expression = CronExpression::weekly_on(DayOfWeek::Sunday);
276        self
277    }
278
279    /// Run on Mondays at midnight
280    pub fn mondays(mut self) -> Self {
281        self.expression = CronExpression::weekly_on(DayOfWeek::Monday);
282        self
283    }
284
285    /// Run on Tuesdays at midnight
286    pub fn tuesdays(mut self) -> Self {
287        self.expression = CronExpression::weekly_on(DayOfWeek::Tuesday);
288        self
289    }
290
291    /// Run on Wednesdays at midnight
292    pub fn wednesdays(mut self) -> Self {
293        self.expression = CronExpression::weekly_on(DayOfWeek::Wednesday);
294        self
295    }
296
297    /// Run on Thursdays at midnight
298    pub fn thursdays(mut self) -> Self {
299        self.expression = CronExpression::weekly_on(DayOfWeek::Thursday);
300        self
301    }
302
303    /// Run on Fridays at midnight
304    pub fn fridays(mut self) -> Self {
305        self.expression = CronExpression::weekly_on(DayOfWeek::Friday);
306        self
307    }
308
309    /// Run on Saturdays at midnight
310    pub fn saturdays(mut self) -> Self {
311        self.expression = CronExpression::weekly_on(DayOfWeek::Saturday);
312        self
313    }
314
315    /// Run once monthly on the first day at midnight
316    pub fn monthly(mut self) -> Self {
317        self.expression = CronExpression::monthly();
318        self
319    }
320
321    /// Run monthly on a specific day at midnight
322    ///
323    /// # Example
324    /// ```rust,ignore
325    /// .monthly_on(15) // On the 15th of each month
326    /// ```
327    pub fn monthly_on(mut self, day: u32) -> Self {
328        self.expression = CronExpression::monthly_on(day);
329        self
330    }
331
332    /// Run quarterly on the first day of each quarter at midnight
333    pub fn quarterly(mut self) -> Self {
334        self.expression = CronExpression::quarterly();
335        self
336    }
337
338    /// Run yearly on January 1st at midnight
339    pub fn yearly(mut self) -> Self {
340        self.expression = CronExpression::yearly();
341        self
342    }
343
344    // =========================================================================
345    // Configuration Methods
346    // =========================================================================
347
348    /// Set a name for this task
349    ///
350    /// The name is used in logs and when listing scheduled tasks.
351    pub fn name(mut self, name: &str) -> Self {
352        self.name = Some(name.to_string());
353        self
354    }
355
356    /// Set a description for this task
357    ///
358    /// The description is shown when listing scheduled tasks.
359    pub fn description(mut self, desc: &str) -> Self {
360        self.description = Some(desc.to_string());
361        self
362    }
363
364    /// Prevent overlapping task runs
365    ///
366    /// When enabled, the scheduler will skip running this task if
367    /// a previous run is still in progress.
368    pub fn without_overlapping(mut self) -> Self {
369        self.without_overlapping = true;
370        self
371    }
372
373    /// Run task in background (non-blocking)
374    ///
375    /// When enabled, the scheduler won't wait for the task to complete
376    /// before continuing to the next task.
377    pub fn run_in_background(mut self) -> Self {
378        self.run_in_background = true;
379        self
380    }
381
382    /// Build the task entry
383    ///
384    /// This is called internally when adding the task to the schedule.
385    pub(crate) fn build(self, task_index: usize) -> TaskEntry {
386        let name = self
387            .name
388            .unwrap_or_else(|| format!("closure-task-{}", task_index));
389
390        TaskEntry {
391            name,
392            expression: self.expression,
393            task: self.task,
394            description: self.description,
395            without_overlapping: self.without_overlapping,
396            run_in_background: self.run_in_background,
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    fn create_test_builder() -> TaskBuilder {
406        TaskBuilder::from_async(|| async { Ok(()) })
407    }
408
409    #[test]
410    fn test_builder_schedule_methods() {
411        let builder = create_test_builder().every_minute();
412        assert_eq!(builder.expression.expression(), "* * * * *");
413
414        let builder = create_test_builder().hourly();
415        assert_eq!(builder.expression.expression(), "0 * * * *");
416
417        let builder = create_test_builder().daily();
418        assert_eq!(builder.expression.expression(), "0 0 * * *");
419
420        let builder = create_test_builder().weekly();
421        assert_eq!(builder.expression.expression(), "0 0 * * 0");
422    }
423
424    #[test]
425    fn test_builder_daily_at() {
426        let builder = create_test_builder().daily_at("14:30");
427        assert_eq!(builder.expression.expression(), "30 14 * * *");
428    }
429
430    #[test]
431    fn test_builder_at_modifier() {
432        let builder = create_test_builder().daily().at("09:15");
433        assert_eq!(builder.expression.expression(), "15 9 * * *");
434    }
435
436    #[test]
437    fn test_builder_configuration() {
438        let builder = create_test_builder()
439            .name("test-task")
440            .description("A test task")
441            .without_overlapping()
442            .run_in_background();
443
444        assert_eq!(builder.name, Some("test-task".to_string()));
445        assert_eq!(builder.description, Some("A test task".to_string()));
446        assert!(builder.without_overlapping);
447        assert!(builder.run_in_background);
448    }
449
450    #[test]
451    fn test_builder_build() {
452        let builder = create_test_builder()
453            .daily()
454            .name("my-task")
455            .description("My task description");
456
457        let entry = builder.build(0);
458
459        assert_eq!(entry.name, "my-task");
460        assert_eq!(entry.description, Some("My task description".to_string()));
461    }
462
463    #[test]
464    fn test_builder_default_name() {
465        let builder = create_test_builder().daily();
466        let entry = builder.build(5);
467
468        assert_eq!(entry.name, "closure-task-5");
469    }
470}