use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use crate::state::AppState;
pub type TaskHandler =
fn(AppState) -> Pin<Box<dyn Future<Output = crate::AutumnResult<()>> + Send>>;
pub struct TaskInfo {
pub name: String,
pub schedule: Schedule,
pub handler: TaskHandler,
}
#[non_exhaustive]
pub enum Schedule {
FixedDelay(Duration),
Cron {
expression: String,
timezone: Option<String>,
},
}
impl std::fmt::Display for Schedule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FixedDelay(d) => write!(f, "every {}s", d.as_secs()),
Self::Cron { expression, .. } => write!(f, "cron {expression}"),
}
}
}
#[must_use]
pub fn parse_duration(s: &str) -> Option<Duration> {
let mut total_secs = 0u64;
let mut current_num = String::new();
for ch in s.chars() {
if ch.is_ascii_digit() {
current_num.push(ch);
} else if ch.is_ascii_alphabetic() {
let num: u64 = current_num.parse().ok()?;
current_num.clear();
match ch {
's' => total_secs = total_secs.checked_add(num)?,
'm' => total_secs = total_secs.checked_add(num.checked_mul(60)?)?,
'h' => total_secs = total_secs.checked_add(num.checked_mul(3600)?)?,
'd' => total_secs = total_secs.checked_add(num.checked_mul(86400)?)?,
_ => return None,
}
} else if ch == ' ' {
} else {
return None;
}
}
if !current_num.is_empty() {
return None; }
if total_secs == 0 {
return None;
}
Some(Duration::from_secs(total_secs))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_seconds() {
assert_eq!(parse_duration("5s"), Some(Duration::from_secs(5)));
}
#[test]
fn parse_minutes() {
assert_eq!(parse_duration("5m"), Some(Duration::from_secs(300)));
}
#[test]
fn parse_hours() {
assert_eq!(parse_duration("2h"), Some(Duration::from_secs(7200)));
}
#[test]
fn parse_compound() {
assert_eq!(parse_duration("1h 30m"), Some(Duration::from_secs(5400)));
}
#[test]
fn parse_day() {
assert_eq!(parse_duration("1d"), Some(Duration::from_secs(86400)));
}
#[test]
fn invalid_unit() {
assert!(parse_duration("5x").is_none());
}
#[test]
fn trailing_number() {
assert!(parse_duration("5").is_none());
}
#[test]
fn empty() {
assert!(parse_duration("").is_none());
}
#[test]
fn zero_duration() {
assert!(parse_duration("0s").is_none());
assert!(parse_duration("0m").is_none());
}
#[test]
fn invalid_characters() {
assert!(parse_duration("1h_30m").is_none());
assert!(parse_duration("1h-30m").is_none());
}
#[test]
fn multiple_spaces() {
assert_eq!(parse_duration("1h 30m"), Some(Duration::from_secs(5400)));
}
#[test]
fn compound_trailing_number() {
assert!(parse_duration("1h 30").is_none());
}
}
#[cfg(test)]
mod havoc_proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn parse_duration_fuzz_panic(s in "[0-9]{15,30}[smhd]") {
let _ = parse_duration(&s);
}
}
}