use crate::error::{QmlError, Result};
use chrono::{DateTime, Utc};
use cron::Schedule;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RecurringJob {
pub id: String,
pub cron: String,
pub method: String,
pub payload: JsonValue,
pub queue: String,
pub next_run_at: DateTime<Utc>,
pub last_run_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub enabled: bool,
}
impl RecurringJob {
pub fn new(
id: impl Into<String>,
cron: impl Into<String>,
method: impl Into<String>,
payload: JsonValue,
queue: impl Into<String>,
) -> Result<Self> {
let cron = cron.into();
let next_run_at = next_after(&cron, Utc::now())?;
let now = Utc::now();
Ok(Self {
id: id.into(),
cron,
method: method.into(),
payload,
queue: queue.into(),
next_run_at,
last_run_at: None,
created_at: now,
updated_at: now,
enabled: true,
})
}
pub fn advance(&mut self, from: DateTime<Utc>) -> Result<()> {
self.next_run_at = next_after(&self.cron, from)?;
self.updated_at = Utc::now();
Ok(())
}
pub fn schedule(&self) -> Result<Schedule> {
parse_schedule(&self.cron)
}
}
fn parse_schedule(expr: &str) -> Result<Schedule> {
Schedule::from_str(expr).map_err(|e| QmlError::InvalidJobData {
message: format!("Invalid cron expression `{}`: {}", expr, e),
})
}
fn next_after(expr: &str, from: DateTime<Utc>) -> Result<DateTime<Utc>> {
let schedule = parse_schedule(expr)?;
schedule
.after(&from)
.next()
.ok_or_else(|| QmlError::InvalidJobData {
message: format!("Cron expression `{}` has no future occurrences", expr),
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn new_parses_cron_and_computes_next_run() {
let r = RecurringJob::new(
"every-second",
"* * * * * *",
"tick",
json!(null),
"default",
)
.unwrap();
assert!(r.next_run_at >= Utc::now());
assert!(r.enabled);
assert!(r.last_run_at.is_none());
}
#[test]
fn new_rejects_invalid_cron() {
let err = RecurringJob::new("bad", "not a cron", "x", json!(null), "default").unwrap_err();
assert!(matches!(err, QmlError::InvalidJobData { .. }));
}
#[test]
fn advance_moves_next_run_forward() {
let mut r = RecurringJob::new("m", "0 * * * * *", "tick", json!(null), "default").unwrap();
let before = r.next_run_at;
r.advance(before).unwrap();
assert!(r.next_run_at > before);
}
}