shiny_jobs/job_trigger/
interval_trigger.rs

1use 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}