use async_trait::async_trait;
use std::fmt::Debug;
use time::{Date, OffsetDateTime, Time, UtcOffset, macros::format_description};
use tokio_util::sync::CancellationToken;
#[async_trait]
pub trait Notifiable: Sync + Send + Debug {
fn get_task(&self) -> Task;
async fn on_time(&self, cancel: CancellationToken) {
cancel.cancel();
}
async fn on_skip(&self, _cancel: CancellationToken) {
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Skip {
Date(Date),
DateRange(Date, Date),
Day(Vec<u8>),
DayRange(usize, usize),
Time(Time),
TimeRange(Time, Time),
None,
}
impl Default for Skip {
fn default() -> Self {
Self::None
}
}
impl std::fmt::Display for Skip {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Skip::Date(date) => write!(f, "date: {date}"),
Skip::DateRange(start, end) => write!(f, "date range: {start} - {end}"),
Skip::Day(day) => write!(f, "day: {day:?}"),
Skip::DayRange(start, end) => write!(f, "day range: {start} - {end}"),
Skip::Time(time) => write!(f, "time: {time}"),
Skip::TimeRange(start, end) => write!(f, "time range: {start} - {end}"),
Skip::None => write!(f, "none"),
}
}
}
impl Skip {
pub fn is_skip(&self, time: OffsetDateTime) -> bool {
match self {
Skip::Date(date) => time.date() == *date,
Skip::DateRange(start, end) => time.date() >= *start && time.date() <= *end,
Skip::Day(day) => day.contains(&(time.weekday().number_from_monday())),
Skip::DayRange(start, end) => {
let weekday = time.weekday().number_from_monday() as usize;
weekday >= *start && weekday <= *end
}
Skip::Time(skip_time) => time.time() == *skip_time,
Skip::TimeRange(start, end) => {
let current_time = time.time();
if start <= end {
current_time >= *start && current_time <= *end
} else {
current_time >= *start || current_time <= *end
}
}
Skip::None => false,
}
}
}
#[derive(Debug, Clone)]
pub enum Task {
Wait(u64, Option<Vec<Skip>>),
Interval(u64, Option<Vec<Skip>>),
At(Time, Option<Vec<Skip>>),
Once(OffsetDateTime, Option<Vec<Skip>>),
}
impl PartialEq for Task {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Task::Wait(a, skip_a), Task::Wait(b, skip_b)) => a == b && skip_a == skip_b,
(Task::Interval(a, skip_a), Task::Interval(b, skip_b)) => a == b && skip_a == skip_b,
(Task::At(a, skip_a), Task::At(b, skip_b)) => a == b && skip_a == skip_b,
(Task::Once(a, skip_a), Task::Once(b, skip_b)) => a == b && skip_a == skip_b,
_ => false,
}
}
}
impl Task {
pub fn get_next_run_time<T: Notifiable + 'static>(
&self,
timezone_minutes: i16,
) -> Option<OffsetDateTime> {
let now = get_now(timezone_minutes).unwrap_or_else(|_| OffsetDateTime::now_utc());
match self.clone() {
Task::Wait(wait, skip) => {
let mut next_time = now + time::Duration::seconds(wait as i64);
if let Some(skip_rules) = skip {
let mut attempts = 0;
const MAX_ATTEMPTS: u32 = 1000;
while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
{
next_time += time::Duration::seconds(wait as i64);
attempts += 1;
}
if attempts >= MAX_ATTEMPTS {
return None;
}
}
Some(next_time)
}
Task::Interval(interval, skip) => {
let mut next_time = now + time::Duration::seconds(interval as i64);
if let Some(skip_rules) = skip {
let mut attempts = 0;
const MAX_ATTEMPTS: u32 = 1000;
while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
{
next_time += time::Duration::seconds(interval as i64);
attempts += 1;
}
if attempts >= MAX_ATTEMPTS {
return None;
}
}
Some(next_time)
}
Task::At(time, skip) => {
let mut next_time = get_next_time(now, time);
if let Some(skip_rules) = skip {
let mut attempts = 0;
const MAX_ATTEMPTS: u32 = 365;
while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
{
next_time += time::Duration::days(1);
attempts += 1;
}
if attempts >= MAX_ATTEMPTS {
return None;
}
}
Some(next_time)
}
Task::Once(once_time, skip) => {
if once_time <= now {
return None;
}
if let Some(skip_rules) = skip {
if skip_rules.iter().any(|s| s.is_skip(once_time)) {
return None;
}
}
Some(once_time)
}
}
}
}
impl Task {
pub fn parse(s: &str) -> Result<Self, String> {
let s = s.trim();
let open_paren = s.find('(').ok_or_else(|| {
format!("Invalid task format: '{s}'. Expected format like 'wait(10)'")
})?;
let close_paren = s
.rfind(')')
.ok_or_else(|| format!("Missing closing parenthesis in: '{s}'"))?;
if close_paren <= open_paren {
return Err(format!("Invalid parentheses in: '{s}'"));
}
let function_name = s[..open_paren].trim();
let args = s[open_paren + 1..close_paren].trim();
let (primary_arg, skip_conditions) = Self::parse_arguments(args)?;
match function_name {
"wait" => {
let seconds = primary_arg.parse::<u64>().map_err(|_| {
format!("Invalid seconds value '{primary_arg}' in wait({primary_arg})")
})?;
Ok(Task::Wait(seconds, skip_conditions))
}
"interval" => {
let seconds = primary_arg.parse::<u64>().map_err(|_| {
format!("Invalid seconds value '{primary_arg}' in interval({primary_arg})")
})?;
Ok(Task::Interval(seconds, skip_conditions))
}
"at" => {
let format = format_description!("[hour]:[minute]");
let time = Time::parse(&primary_arg, &format).map_err(|_| {
format!("Invalid time format '{primary_arg}' in at({primary_arg}). Expected format: HH:MM")
})?;
Ok(Task::At(time, skip_conditions))
}
"once" => {
let format = format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]"
);
let datetime = OffsetDateTime::parse(&primary_arg, &format)
.map_err(|_| format!("Invalid datetime format '{primary_arg}' in once({primary_arg}). Expected format: YYYY-MM-DD HH:MM:SS +HH"))?;
Ok(Task::Once(datetime, skip_conditions))
}
_ => Err(format!(
"Unknown task type '{function_name}'. Supported types: wait, interval, at, once"
)),
}
}
fn parse_arguments(args: &str) -> Result<(String, Option<Vec<Skip>>), String> {
let args = args.trim();
if let Some(comma_pos) = args.find(',') {
let primary_arg = args[..comma_pos].trim().to_string();
let skip_part = args[comma_pos + 1..].trim();
let skip_conditions = Self::parse_skip_conditions(skip_part)?;
Ok((primary_arg, Some(skip_conditions)))
} else {
Ok((args.to_string(), None))
}
}
fn parse_skip_conditions(skip_str: &str) -> Result<Vec<Skip>, String> {
let skip_str = skip_str.trim();
if skip_str.starts_with('[') && skip_str.ends_with(']') {
let list_content = &skip_str[1..skip_str.len() - 1];
Self::parse_skip_list(list_content)
} else {
let skip = Self::parse_single_skip(skip_str)?;
Ok(vec![skip])
}
}
fn parse_skip_list(list_str: &str) -> Result<Vec<Skip>, String> {
let mut skips = Vec::new();
let list_str = list_str.trim();
if list_str.is_empty() {
return Ok(skips);
}
for part in list_str.split(',') {
let part = part.trim();
if !part.is_empty() {
let skip = Self::parse_single_skip(part)?;
skips.push(skip);
}
}
Ok(skips)
}
fn parse_single_skip(skip_str: &str) -> Result<Skip, String> {
let skip_str = skip_str.trim();
let parts: Vec<&str> = skip_str.split_whitespace().collect();
if parts.is_empty() {
return Err("Empty skip condition".to_string());
}
match parts[0] {
"weekday" => {
if parts.len() != 2 {
return Err(format!(
"Invalid weekday format: '{skip_str}'. Expected 'weekday N'"
));
}
let day = parts[1]
.parse::<u8>()
.map_err(|_| format!("Invalid weekday number: '{}'", parts[1]))?;
if !(1..=7).contains(&day) {
return Err(format!("Weekday must be between 1-7, got: {day}"));
}
Ok(Skip::Day(vec![day]))
}
"date" => {
if parts.len() != 2 {
return Err(format!(
"Invalid date format: '{skip_str}'. Expected 'date YYYY-MM-DD'"
));
}
let date_str = parts[1];
let date_parts: Vec<&str> = date_str.split('-').collect();
if date_parts.len() != 3 {
return Err(format!(
"Invalid date format: '{date_str}'. Expected 'YYYY-MM-DD'"
));
}
let year = date_parts[0]
.parse::<i32>()
.map_err(|_| format!("Invalid year: '{}'", date_parts[0]))?;
let month = date_parts[1]
.parse::<u8>()
.map_err(|_| format!("Invalid month: '{}'", date_parts[1]))?;
let day = date_parts[2]
.parse::<u8>()
.map_err(|_| format!("Invalid day: '{}'", date_parts[2]))?;
let month_enum =
time::Month::try_from(month).map_err(|_| format!("Invalid month: {month}"))?;
let date = time::Date::from_calendar_date(year, month_enum, day)
.map_err(|_| format!("Invalid date: {year}-{month}-{day}"))?;
Ok(Skip::Date(date))
}
"time" => {
if parts.len() != 2 {
return Err(format!(
"Invalid time format: '{skip_str}'. Expected 'time HH:MM..HH:MM'"
));
}
let time_range = parts[1];
if let Some(range_pos) = time_range.find("..") {
let start_str = &time_range[..range_pos];
let end_str = &time_range[range_pos + 2..];
let format = format_description!("[hour]:[minute]");
let start_time = Time::parse(start_str, &format)
.map_err(|_| format!("Invalid start time: '{start_str}'"))?;
let end_time = Time::parse(end_str, &format)
.map_err(|_| format!("Invalid end time: '{end_str}'"))?;
Ok(Skip::TimeRange(start_time, end_time))
} else {
let format = format_description!("[hour]:[minute]");
let time = Time::parse(time_range, &format)
.map_err(|_| format!("Invalid time: '{time_range}'"))?;
Ok(Skip::Time(time))
}
}
_ => Err(format!(
"Unknown skip type: '{}'. Supported types: weekday, date, time",
parts[0]
)),
}
}
}
impl From<&str> for Task {
fn from(s: &str) -> Self {
Task::parse(s).unwrap_or_else(|err| {
panic!("Failed to parse task from string '{s}': {err}");
})
}
}
impl From<String> for Task {
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
impl From<&String> for Task {
fn from(s: &String) -> Self {
Self::from(s.as_str())
}
}
#[macro_export]
macro_rules! task {
(wait $seconds:tt) => {
$crate::Task::Wait($seconds, None)
};
(interval $seconds:tt) => {
$crate::Task::Interval($seconds, None)
};
(at $hour:tt : $minute:tt) => {
$crate::Task::At(
time::Time::from_hms($hour, $minute, 0).unwrap(),
None
)
};
(wait $seconds:tt, weekday $day:tt) => {
$crate::Task::Wait($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
};
(wait $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
$crate::Task::Wait($seconds, Some(vec![$crate::Skip::Date(
time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
)]))
};
(wait $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
$crate::Task::Wait($seconds, Some(vec![$crate::Skip::TimeRange(
time::Time::from_hms($start_h, $start_m, 0).unwrap(),
time::Time::from_hms($end_h, $end_m, 0).unwrap()
)]))
};
(interval $seconds:tt, weekday $day:tt) => {
$crate::Task::Interval($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
};
(interval $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
$crate::Task::Interval($seconds, Some(vec![$crate::Skip::Date(
time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
)]))
};
(interval $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
$crate::Task::Interval($seconds, Some(vec![$crate::Skip::TimeRange(
time::Time::from_hms($start_h, $start_m, 0).unwrap(),
time::Time::from_hms($end_h, $end_m, 0).unwrap()
)]))
};
(at $hour:tt : $minute:tt, weekday $day:tt) => {
$crate::Task::At(
time::Time::from_hms($hour, $minute, 0).unwrap(),
Some(vec![$crate::Skip::Day(vec![$day])])
)
};
(at $hour:tt : $minute:tt, date $year:tt - $month:tt - $day:tt) => {
$crate::Task::At(
time::Time::from_hms($hour, $minute, 0).unwrap(),
Some(vec![$crate::Skip::Date(
time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
)])
)
};
(at $hour:tt : $minute:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
$crate::Task::At(
time::Time::from_hms($hour, $minute, 0).unwrap(),
Some(vec![$crate::Skip::TimeRange(
time::Time::from_hms($start_h, $start_m, 0).unwrap(),
time::Time::from_hms($end_h, $end_m, 0).unwrap()
)])
)
};
(wait $seconds:tt, [$($skip:tt)*]) => {
$crate::Task::Wait($seconds, Some($crate::task!(@build_skips $($skip)*)))
};
(interval $seconds:tt, [$($skip:tt)*]) => {
$crate::Task::Interval($seconds, Some($crate::task!(@build_skips $($skip)*)))
};
(at $hour:tt : $minute:tt, [$($skip:tt)*]) => {
$crate::Task::At(
time::Time::from_hms($hour, $minute, 0).unwrap(),
Some($crate::task!(@build_skips $($skip)*))
)
};
(@build_skips) => { vec![] };
(@build_skips weekday $day:tt $(, $($rest:tt)*)?) => {
{
let mut skips = vec![$crate::Skip::Day(vec![$day])];
$(skips.extend($crate::task!(@build_skips $($rest)*));)?
skips
}
};
(@build_skips date $year:tt - $month:tt - $day:tt $(, $($rest:tt)*)?) => {
{
let mut skips = vec![$crate::Skip::Date(
time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
)];
$(skips.extend($crate::task!(@build_skips $($rest)*));)?
skips
}
};
(@build_skips time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt $(, $($rest:tt)*)?) => {
{
let mut skips = vec![$crate::Skip::TimeRange(
time::Time::from_hms($start_h, $start_m, 0).unwrap(),
time::Time::from_hms($end_h, $end_m, 0).unwrap()
)];
$(skips.extend($crate::task!(@build_skips $($rest)*));)?
skips
}
};
}
impl std::fmt::Display for Task {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Task::Wait(wait, skip) => {
let skip = skip
.clone()
.unwrap_or_default()
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(", ");
write!(f, "wait: {wait} {skip}")
}
Task::Interval(interval, skip) => {
let skip = skip
.clone()
.unwrap_or_default()
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(", ");
write!(f, "interval: {interval} {skip}")
}
Task::At(time, skip) => {
let skip = skip
.clone()
.unwrap_or_default()
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(", ");
write!(f, "at: {time} {skip}")
}
Task::Once(time, skip) => {
let skip = skip
.clone()
.unwrap_or_default()
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(", ");
write!(f, "once: {time} {skip}")
}
}
}
}
pub fn get_next_time(now: OffsetDateTime, time: Time) -> OffsetDateTime {
let mut next = now.replace_time(time);
if next < now {
next += time::Duration::days(1);
}
next
}
pub fn get_now(timezone_minutes: i16) -> Result<OffsetDateTime, time::error::ComponentRange> {
let hours = timezone_minutes / 60;
let minutes = timezone_minutes % 60;
let offset = UtcOffset::from_hms(hours as i8, minutes as i8, 0)?;
Ok(OffsetDateTime::now_utc().to_offset(offset))
}