use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use strum::{Display, EnumString, IntoStaticStr};
use uuid::Uuid;
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString, IntoStaticStr,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ScheduleSource {
Handler,
Api,
}
impl ScheduleSource {
pub fn as_str(&self) -> &'static str {
self.into()
}
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Schedule {
pub id: Uuid,
pub workflow_name: String,
pub cron_expression: String,
pub inputs: Value,
pub source: ScheduleSource,
pub disabled_at: Option<DateTime<Utc>>,
pub last_triggered_at: Option<DateTime<Utc>>,
pub next_trigger_at: Option<DateTime<Utc>>,
pub created_by_user_id: Uuid,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Schedule {
pub fn is_active(&self) -> bool {
self.disabled_at.is_none()
}
}
#[derive(Debug, Clone)]
pub struct NewSchedule {
pub workflow_name: String,
pub cron_expression: String,
pub inputs: Value,
pub source: ScheduleSource,
pub created_by_user_id: Uuid,
pub next_trigger_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default)]
pub struct ScheduleUpdate {
pub cron_expression: Option<String>,
pub inputs: Option<Value>,
pub disabled_at: Option<Option<DateTime<Utc>>>,
pub next_trigger_at: Option<Option<DateTime<Utc>>>,
pub last_triggered_at: Option<Option<DateTime<Utc>>>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn schedule_serde_roundtrip() {
let schedule = Schedule {
id: Uuid::now_v7(),
workflow_name: "deploy".to_string(),
cron_expression: "0 0 * * * *".to_string(),
inputs: json!({"env": "prod"}),
source: ScheduleSource::Api,
disabled_at: None,
last_triggered_at: None,
next_trigger_at: Some(Utc::now()),
created_by_user_id: Uuid::now_v7(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json_str = serde_json::to_string(&schedule).expect("serialize");
let back: Schedule = serde_json::from_str(&json_str).expect("deserialize");
assert_eq!(schedule.id, back.id);
assert_eq!(schedule.workflow_name, back.workflow_name);
assert!(back.is_active());
}
#[test]
fn disabled_schedule_is_not_active() {
let schedule = Schedule {
id: Uuid::now_v7(),
workflow_name: "deploy".to_string(),
cron_expression: "0 0 * * * *".to_string(),
inputs: json!({}),
source: ScheduleSource::Api,
disabled_at: Some(Utc::now()),
last_triggered_at: None,
next_trigger_at: None,
created_by_user_id: Uuid::now_v7(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert!(!schedule.is_active());
}
#[test]
fn schedule_source_roundtrip() {
assert_eq!(ScheduleSource::Handler.as_str(), "handler");
assert_eq!(ScheduleSource::Api.as_str(), "api");
let parsed: ScheduleSource = "handler".parse().unwrap();
assert_eq!(parsed, ScheduleSource::Handler);
let parsed: ScheduleSource = "api".parse().unwrap();
assert_eq!(parsed, ScheduleSource::Api);
assert!("unknown".parse::<ScheduleSource>().is_err());
}
#[test]
fn schedule_update_defaults_to_none() {
let update = ScheduleUpdate::default();
assert!(update.cron_expression.is_none());
assert!(update.inputs.is_none());
assert!(update.disabled_at.is_none());
assert!(update.next_trigger_at.is_none());
assert!(update.last_triggered_at.is_none());
}
}