shiny_jobs/job_trigger/
interval_trigger.rs1use std::str::FromStr;
2use std::time::Duration;
3use thiserror::Error;
4use crate::job_trigger::JobTrigger;
5
6pub struct IntervalTrigger {
7 interval: Duration,
8}
9
10impl FromStr for IntervalTrigger {
11 type Err = IntervalTriggerFromStrError;
12
13 fn from_str(schedule: &str) -> Result<Self, Self::Err> {
14 Ok(Self {
15 interval: parse_duration(schedule).map_err(IntervalTriggerFromStrError)?,
16 })
17 }
18}
19
20impl IntervalTrigger {
21 pub fn new(duration: Duration) -> Self {
22 Self {
23 interval: duration
24 }
25 }
26
27 pub fn interval(&self) -> Duration {
28 self.interval
29 }
30}
31
32#[async_trait::async_trait]
33impl JobTrigger for IntervalTrigger {
34 async fn next(&self) {
35 tracing::debug!("waiting {}ms", self.interval.as_millis());
36 tokio::time::sleep(self.interval).await;
37 }
38}
39
40#[derive(Debug, Error)]
41#[error(transparent)]
42pub struct IntervalTriggerFromStrError(ParseDurationError);
43
44#[derive(Debug, Error)]
45enum ParseDurationError {
46 #[error("Unexpected character")]
47 UnexpectedCharacter,
48 #[error("Invalid order")]
49 InvalidOrder,
50}
51
52fn parse_duration(duration_str: &str) -> Result<Duration, ParseDurationError> {
53 if !duration_str.starts_with("PT") {
54 return Err(ParseDurationError::UnexpectedCharacter);
55 }
56
57 let mut value = 0;
58
59 let mut hours_parsed = false;
60 let mut minutes_parsed = false;
61 let mut seconds_parsed = false;
62 let mut duration = Duration::ZERO;
63
64 for character in duration_str.trim_start_matches("PT").chars() {
65 if let Some(digit) = character.to_digit(10) {
66 value = value * 10 + digit;
67 continue;
68 }
69
70 match character.to_ascii_lowercase() {
71 'h' => {
72 if hours_parsed || minutes_parsed || seconds_parsed {
73 return Err(ParseDurationError::InvalidOrder);
74 }
75 duration += value * Duration::from_secs(3600);
76 hours_parsed = true;
77 value = 0;
78 }
79 'm' => {
80 if minutes_parsed || seconds_parsed {
81 return Err(ParseDurationError::InvalidOrder);
82 }
83 duration += value * Duration::from_secs(60);
84 minutes_parsed = true;
85 value = 0;
86 }
87 's' => {
88 if seconds_parsed {
89 return Err(ParseDurationError::InvalidOrder);
90 }
91 duration += value * Duration::from_secs(1);
92 seconds_parsed = true;
93 value = 0;
94 }
95 _ => return Err(ParseDurationError::UnexpectedCharacter)
96 }
97 }
98
99 Ok(duration)
100}
101
102#[cfg(test)]
103mod tests {
104 use std::time::Duration;
105 use super::parse_duration;
106
107 #[test]
108 fn test_parser() {
109 assert("PT1S", 1);
110 assert("PT1M", 60);
111 assert("PT1H", 3600);
112 assert("PT13S", 13);
113 assert("PT13M", 13 * 60);
114 assert("PT13H", 13 * 3600);
115 assert("PT1039H219M13S", 1039 * 3600 + 219 * 60 + 13);
116
117 assert_error("PT1S1H", "Invalid order");
118 assert_error("PT-1H", "Unexpected character");
119 assert_error("P2DT1H", "Unexpected character");
120 }
121
122 fn assert(string: &str, expected_duration_seconds: u64) {
123 let duration = parse_duration(string).unwrap();
124 assert_eq!(duration, Duration::from_secs(expected_duration_seconds))
125 }
126
127 fn assert_error(string: &str, error_message: &str) {
128 let error = parse_duration(string).unwrap_err();
129 assert_eq!(error.to_string(), error_message)
130 }
131}