#![allow(clippy::non_canonical_partial_ord_impl)]
use chrono::prelude::*;
use getset::Getters;
#[derive(Debug, PartialEq, Eq, Clone, Getters, Default)]
#[getset(get = "pub")]
pub struct Crontab {
pub timer: CrontabTimer,
pub task_identifier: String,
pub options: CrontabOptions,
pub payload: Option<serde_json::Value>,
}
#[derive(Debug, PartialEq, Eq, Default, Clone)]
pub enum CrontabValue {
Number(u32),
Range(u32, u32),
Step(u32),
#[default]
Any,
}
#[derive(Debug, PartialEq, Eq, Clone, Getters)]
#[getset(get = "pub")]
pub struct CrontabTimer {
pub minutes: Vec<CrontabValue>,
pub hours: Vec<CrontabValue>,
pub days: Vec<CrontabValue>,
pub months: Vec<CrontabValue>,
pub dows: Vec<CrontabValue>,
}
#[derive(Debug, PartialEq, Eq, Getters, Clone)]
#[getset(get = "pub")]
pub struct CrontabFill {
pub w: u32,
pub d: u32,
pub h: u32,
pub m: u32,
pub s: u32,
}
#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)]
pub enum JobKeyMode {
#[serde(rename = "replace")]
Replace,
#[serde(rename = "preserve_run_at")]
PreserveRunAt,
}
#[derive(Debug, PartialEq, Eq, Default, Getters, Clone)]
#[getset(get = "pub")]
pub struct CrontabOptions {
pub id: Option<String>,
pub fill: Option<CrontabFill>,
pub max: Option<u16>,
pub queue: Option<String>,
pub priority: Option<i16>,
pub job_key: Option<String>,
pub job_key_mode: Option<JobKeyMode>,
}
const SECOND: u32 = 1;
const MINUTE: u32 = 60 * SECOND;
const HOUR: u32 = 60 * MINUTE;
const DAY: u32 = 24 * HOUR;
const WEEK: u32 = 7 * DAY;
impl CrontabFill {
pub fn new(w: u32, d: u32, h: u32, m: u32, s: u32) -> Self {
Self { w, d, h, m, s }
}
pub fn to_secs(&self) -> u32 {
self.w * WEEK + self.d * DAY + self.h * HOUR + self.m * MINUTE + self.s * SECOND
}
}
impl PartialOrd for CrontabFill {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.to_secs().partial_cmp(&other.to_secs())
}
}
impl Ord for CrontabFill {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.to_secs().cmp(&other.to_secs())
}
}
impl Default for CrontabTimer {
fn default() -> Self {
Self {
minutes: vec![CrontabValue::default()],
hours: vec![CrontabValue::default()],
days: vec![CrontabValue::default()],
months: vec![CrontabValue::default()],
dows: vec![CrontabValue::default()],
}
}
}
impl CrontabTimer {
pub fn should_run_at(&self, at: &NaiveDateTime) -> bool {
self.minutes().iter().any(|v| v.match_value(at.minute(), 0))
&& self.hours().iter().any(|v| v.match_value(at.hour(), 0))
&& self.days().iter().any(|v| v.match_value(at.day(), 1))
&& self.months().iter().any(|v| v.match_value(at.month(), 1))
&& self
.dows()
.iter()
.any(|v| v.match_value(at.weekday().number_from_monday(), 1))
}
}
impl Crontab {
pub fn should_run_at(&self, at: &NaiveDateTime) -> bool {
self.timer().should_run_at(at)
}
pub fn identifier(&self) -> &str {
self.options
.id
.as_deref()
.unwrap_or(self.task_identifier.as_str())
}
}
impl CrontabValue {
pub(crate) fn match_value(&self, value: u32, step_offset: u32) -> bool {
match self {
CrontabValue::Number(n) => &value == n,
CrontabValue::Range(low, high) => &value >= low && &value <= high,
CrontabValue::Step(n) => (value % n) == step_offset,
CrontabValue::Any => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
#[test]
pub fn crontab_timer_should_run_at() -> Result<()> {
let crontab_timer = CrontabTimer {
minutes: vec![CrontabValue::Number(30)],
hours: vec![CrontabValue::Range(8, 10)],
days: vec![CrontabValue::Step(4)],
..Default::default()
};
assert!(crontab_timer.should_run_at(&"2012-12-17T08:30:12".parse()?));
assert!(crontab_timer.should_run_at(&"2015-02-05T09:30:00".parse()?));
assert!(crontab_timer.should_run_at(&"1998-10-13T10:30:59".parse()?));
assert!(!crontab_timer.should_run_at(&"2012-12-17T11:30:59".parse()?));
assert!(!crontab_timer.should_run_at(&"2015-02-05T09:31:00".parse()?));
assert!(!crontab_timer.should_run_at(&"2012-12-13T08:29:12".parse()?));
assert!(!crontab_timer.should_run_at(&"1998-10-04T10:30:59".parse()?));
Ok(())
}
#[test]
pub fn crontab_fill_ordering() {
let biggest_fill = CrontabFill::new(1, 30, 0, 0, 0);
let bigger_fill = CrontabFill::new(0, 32, 0, 0, 0);
let lower_fill = CrontabFill::new(0, 0, 432, 0, 0);
let lowest_fill = CrontabFill::new(2, 0, 0, 0, 0);
let mut fills = vec![&lower_fill, &bigger_fill, &biggest_fill, &lowest_fill];
fills.sort();
assert_eq!(
vec![&lowest_fill, &lower_fill, &bigger_fill, &biggest_fill],
fills
);
}
#[test]
pub fn crontab_fill_to_secs() {
let fill = CrontabFill::new(0, 0, 0, 0, 0);
assert_eq!(0, fill.to_secs());
let fill = CrontabFill::new(0, 0, 0, 0, 1);
assert_eq!(1, fill.to_secs());
let fill = CrontabFill::new(1, 30, 28, 350, 2);
assert_eq!(3318602, fill.to_secs());
}
#[test]
pub fn crontab_value_match_value() {
assert!(CrontabValue::Number(30).match_value(30, 0));
assert!(CrontabValue::Range(8, 10).match_value(8, 0));
assert!(!CrontabValue::Range(8, 10).match_value(7, 0));
assert!(CrontabValue::Step(4).match_value(5, 1));
assert!(CrontabValue::Step(9).match_value(9, 0));
assert!(!CrontabValue::Step(9).match_value(9, 1));
assert!(CrontabValue::Any.match_value(16, 0));
}
#[test]
pub fn crontab_should_run_at() -> Result<()> {
let crontab = Crontab {
timer: CrontabTimer {
minutes: vec![CrontabValue::Number(30)],
hours: vec![CrontabValue::Range(8, 10)],
days: vec![CrontabValue::Step(4)],
..Default::default()
},
task_identifier: "test".to_string(),
..Default::default()
};
assert!(crontab.should_run_at(&"2012-12-17T08:30:12".parse()?));
assert!(crontab.should_run_at(&"2015-02-05T09:30:00".parse()?));
assert!(crontab.should_run_at(&"1998-10-13T10:30:59".parse()?));
assert!(!crontab.should_run_at(&"2012-12-17T11:30:59".parse()?));
assert!(!crontab.should_run_at(&"2015-02-05T09:31:00".parse()?));
assert!(!crontab.should_run_at(&"2012-12-13T08:29:12".parse()?));
assert!(!crontab.should_run_at(&"1998-10-04T10:30:59".parse()?));
Ok(())
}
}