use super::{
map_write_routine_err, max_runtime_ceiling_secs, migrate_workbenches, normalize_model,
normalize_schedule, now_secs, reject_blank, reject_over_ceiling, reject_zero_secs,
remove_routine_dir, slugify, ttl_ceiling_secs, validate_agent, validate_cron, validate_env,
validate_goal, validate_machines, validate_prompt, validate_repositories, validate_tags,
validate_title, write_routine, AppError, LockRecover, RoutineResponse, RoutineStore,
UpdateRoutineRequest,
};
pub fn svc_update(
store: &RoutineStore,
id: &str,
req: UpdateRoutineRequest,
) -> Result<RoutineResponse, AppError> {
if let Some(ref sched) = req.schedule {
validate_cron(sched)?;
}
if let Some(ref title) = req.title {
reject_blank("title", title)?;
validate_title(title)?;
}
if let Some(ref prompt) = req.prompt {
validate_prompt(prompt)?;
}
if let Some(ref agent) = req.agent {
validate_agent(agent)?;
}
reject_zero_secs("ttl_secs", req.ttl_secs)?;
reject_zero_secs("max_runtime_secs", req.max_runtime_secs)?;
let repositories = match req.repositories {
Some(ref repos) => Some(validate_repositories(repos)?),
None => None,
};
let tags = match req.tags {
Some(ref tags) => Some(validate_tags(tags)?),
None => None,
};
let goal = match req.goal {
Some(ref goal) => Some(validate_goal(Some(goal))?),
None => None,
};
let machines = match req.machines {
Some(ref machines) => Some(validate_machines(machines)?),
None => None,
};
if let Some(ref env) = req.env {
validate_env(env)?;
}
let mut lock = store.lock_recover();
let old_slug = slugify(&lock.get(id).ok_or(AppError::NotFound)?.title);
if let Some(ref new_title) = req.title {
let new_slug = slugify(new_title);
if new_slug != old_slug
&& lock
.values()
.any(|routine| routine.id != id && slugify(&routine.title) == new_slug)
{
return Err(AppError::Conflict(format!(
"a routine with the name \"{new_slug}\" already exists"
)));
}
}
let effective_schedule = if let Some(schedule) = req.schedule.as_deref() {
normalize_schedule(schedule)
} else {
#[allow(
clippy::expect_used,
reason = "id existence was checked via `ok_or(AppError::NotFound)` above, and \
`lock` has been held continuously since — a miss here is a logic bug, \
not a recoverable runtime error"
)]
let routine = lock
.get(id)
.expect("id existence checked above, and the lock has been held continuously since");
routine.schedule.clone()
};
reject_over_ceiling(
"ttl_secs",
req.ttl_secs,
ttl_ceiling_secs(&effective_schedule),
)?;
reject_over_ceiling(
"max_runtime_secs",
req.max_runtime_secs,
max_runtime_ceiling_secs(&effective_schedule),
)?;
#[allow(
clippy::expect_used,
reason = "id existence was checked via `ok_or(AppError::NotFound)` above, and `lock` has \
been held continuously since — a miss here is a logic bug, not a recoverable \
runtime error"
)]
let routine = lock
.get_mut(id)
.expect("id existence checked above, and the lock has been held continuously since");
if let Some(schedule) = req.schedule {
routine.schedule = normalize_schedule(&schedule);
}
if let Some(title) = req.title {
routine.title = title.trim().to_string();
}
if let Some(agent) = req.agent {
routine.agent = agent;
}
if let Some(model) = req.model {
routine.model = normalize_model(Some(model));
}
if let Some(prompt) = req.prompt {
routine.prompt = prompt;
}
if let Some(goal) = goal {
routine.goal = goal;
}
if let Some(repositories) = repositories {
routine.repositories = repositories;
}
if let Some(machines) = machines {
routine.machines = machines;
}
if let Some(enabled) = req.enabled {
routine.enabled = enabled;
}
if let Some(ttl) = req.ttl_secs {
routine.ttl_secs = Some(ttl);
}
if let Some(max_runtime) = req.max_runtime_secs {
routine.max_runtime_secs = Some(max_runtime);
}
if let Some(tags) = tags {
routine.tags = tags;
}
if let Some(env) = req.env {
routine.env = env;
}
routine.updated_at = now_secs();
let routine = routine.clone();
drop(lock);
let new_slug = slugify(&routine.title);
write_routine(&routine).map_err(|err| map_write_routine_err(&err))?;
if new_slug != old_slug {
migrate_workbenches(&old_slug, &new_slug);
remove_routine_dir(&old_slug).map_err(|_| AppError::Internal)?;
}
if let Err(err) = crate::sync::routines::sync_routines_to_crontab(store) {
log::warn!("crontab sync after routine update failed: {err}");
}
Ok(RoutineResponse::from_routine(routine))
}