use croner::Cron;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use super::command::slugify;
use crate::paths::{agent_toml_path, routine_toml_path};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)]
pub struct Repository {
pub repository: String,
#[serde(default)]
pub branch: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, JsonSchema, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RoutineSort {
#[default]
Created,
Updated,
Title,
Repository,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, JsonSchema, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SortOrder {
#[default]
Asc,
Desc,
}
#[derive(Debug, Clone, Default, Deserialize, JsonSchema, utoipa::IntoParams)]
#[serde(default)]
#[into_params(parameter_in = Query)]
pub struct RoutineListQuery {
pub repository: Option<String>,
pub sort: RoutineSort,
pub order: SortOrder,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)]
pub struct Routine {
pub id: String,
pub schedule: String,
pub title: String,
pub agent: String,
pub prompt: String,
#[serde(default)]
pub repositories: Vec<Repository>,
pub enabled: bool,
pub source: String,
pub created_at: u64,
pub updated_at: u64,
pub last_triggered_at: Option<u64>,
#[serde(default)]
pub ttl_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, JsonSchema, utoipa::ToSchema)]
pub struct RoutineResponse {
#[serde(flatten)]
pub routine: Routine,
pub agent_registered: bool,
pub file_path: String,
pub schedule_description: Option<String>,
pub timezone: Option<String>,
}
pub fn local_timezone() -> Option<String> {
iana_time_zone::get_timezone().ok()
}
fn describe_schedule(schedule: &str, timezone: Option<&str>) -> Option<String> {
schedule.parse::<Cron>().ok().map(|cron| {
let desc = cron.describe();
match timezone {
Some(tz) => format!("{desc} ({tz})"),
None => desc,
}
})
}
impl RoutineResponse {
pub fn from_routine(routine: Routine) -> Self {
let agent_registered = agent_toml_path(&routine.agent).exists();
let file_path = routine_toml_path(&slugify(&routine.title))
.to_string_lossy()
.into_owned();
let timezone = local_timezone();
let schedule_description = describe_schedule(&routine.schedule, timezone.as_deref());
Self {
routine,
agent_registered,
file_path,
schedule_description,
timezone,
}
}
}
#[derive(Debug, Clone, Serialize, JsonSchema, utoipa::ToSchema)]
pub struct CleanupResponse {
pub removed: usize,
}
pub type RoutineStore = Arc<Mutex<HashMap<String, Routine>>>;
#[cfg(test)]
pub fn new_store() -> RoutineStore {
Arc::new(Mutex::new(HashMap::new()))
}
pub(crate) fn bool_true() -> bool {
true
}
#[derive(Deserialize, JsonSchema, utoipa::ToSchema)]
pub struct CreateRoutineRequest {
pub schedule: String,
pub title: String,
pub agent: String,
pub prompt: String,
#[serde(default)]
pub repositories: Vec<Repository>,
#[serde(default = "bool_true")]
pub enabled: bool,
#[serde(default)]
pub ttl_secs: Option<u64>,
}
#[derive(Deserialize, JsonSchema, utoipa::ToSchema)]
pub struct UpdateRoutineRequest {
pub schedule: Option<String>,
pub title: Option<String>,
pub agent: Option<String>,
pub prompt: Option<String>,
pub repositories: Option<Vec<Repository>>,
pub enabled: Option<bool>,
pub ttl_secs: Option<u64>,
}
#[cfg(test)]
#[path = "model_tests.rs"]
mod model_tests;