shinyframework_jobs 0.1.2

Shiny Jobs
Documentation
use std::str::FromStr;
use std::time::Duration;
use thiserror::Error;
use crate::job_trigger::JobTrigger;

pub struct IntervalTrigger {
    interval: Duration,
}

impl FromStr for IntervalTrigger {
    type Err = IntervalTriggerFromStrError;

    fn from_str(schedule: &str) -> Result<Self, Self::Err> {
        Ok(Self {
            interval: parse_duration(schedule).map_err(IntervalTriggerFromStrError)?,
        })
    }
}

impl IntervalTrigger {
    pub fn new(duration: Duration) -> Self {
        Self {
            interval: duration
        }
    }

    pub fn interval(&self) -> Duration {
        self.interval
    }
}

#[async_trait::async_trait]
impl JobTrigger for IntervalTrigger {
    async fn next(&self) {
        tracing::debug!("waiting {}ms", self.interval.as_millis());
        tokio::time::sleep(self.interval).await;
    }
}

#[derive(Debug, Error)]
#[error(transparent)]
pub struct IntervalTriggerFromStrError(ParseDurationError);

#[derive(Debug, Error)]
enum ParseDurationError {
    #[error("Unexpected character")]
    UnexpectedCharacter,
    #[error("Invalid order")]
    InvalidOrder,
}

fn parse_duration(duration_str: &str) -> Result<Duration, ParseDurationError> {
    if !duration_str.starts_with("PT") {
        return Err(ParseDurationError::UnexpectedCharacter);
    }

    let mut value = 0;

    let mut hours_parsed = false;
    let mut minutes_parsed = false;
    let mut seconds_parsed = false;
    let mut duration = Duration::ZERO;

    for character in duration_str.trim_start_matches("PT").chars() {
        if let Some(digit) = character.to_digit(10) {
            value = value * 10 + digit;
            continue;
        }

        match character.to_ascii_lowercase() {
            'h' => {
                if hours_parsed || minutes_parsed || seconds_parsed {
                    return Err(ParseDurationError::InvalidOrder);
                }
                duration += value * Duration::from_secs(3600);
                hours_parsed = true;
                value = 0;
            }
            'm' => {
                if minutes_parsed || seconds_parsed {
                    return Err(ParseDurationError::InvalidOrder);
                }
                duration += value * Duration::from_secs(60);
                minutes_parsed = true;
                value = 0;
            }
            's' => {
                if seconds_parsed {
                    return Err(ParseDurationError::InvalidOrder);
                }
                duration += value * Duration::from_secs(1);
                seconds_parsed = true;
                value = 0;
            }
            _ => return Err(ParseDurationError::UnexpectedCharacter)
        }
    }

    Ok(duration)
}

#[cfg(test)]
mod tests {
    use std::time::Duration;
    use super::parse_duration;

    #[test]
    fn test_parser() {
        assert("PT1S", 1);
        assert("PT1M", 60);
        assert("PT1H", 3600);
        assert("PT13S", 13);
        assert("PT13M", 13 * 60);
        assert("PT13H", 13 * 3600);
        assert("PT1039H219M13S", 1039 * 3600 + 219 * 60 + 13);

        assert_error("PT1S1H", "Invalid order");
        assert_error("PT-1H", "Unexpected character");
        assert_error("P2DT1H", "Unexpected character");
    }

    fn assert(string: &str, expected_duration_seconds: u64) {
        let duration = parse_duration(string).unwrap();
        assert_eq!(duration, Duration::from_secs(expected_duration_seconds))
    }

    fn assert_error(string: &str, error_message: &str) {
        let error = parse_duration(string).unwrap_err();
        assert_eq!(error.to_string(), error_message)
    }
}