use std::collections::BTreeSet; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use rust_i18n::t; use crate::action::{ActionInput, ActionResult, display_action_result}; use crate::db::model::package::{ ActionMetadata, Task, TaskCondition, TaskEntry, TaskSeverity, TaskTrigger, }; use crate::rpc_continuations::Guid; use crate::service::cli::ContainerCliContext; use crate::service::effects::prelude::*; use crate::util::serde::HandlerExtSerde; use crate::{ActionId, PackageId, ReplayId}; pub fn action_api() -> ParentHandler { ParentHandler::new() .subcommand("export", from_fn_async(export_action).no_cli()) .subcommand( "clear", from_fn_async(clear_actions) .no_display() .with_call_remote::(), ) .subcommand( "get-input", from_fn_async(get_action_input) .with_display_serializable() .with_call_remote::(), ) .subcommand( "run", from_fn_async(run_action) .with_display_serializable() .with_custom_display_fn(|args, res| display_action_result(args.params, res)) .with_call_remote::(), ) .subcommand("create-task", from_fn_async(create_task).no_cli()) .subcommand( "clear-tasks", from_fn_async(clear_tasks) .no_display() .with_call_remote::(), ) } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct ExportActionParams { id: ActionId, metadata: ActionMetadata, } pub async fn export_action( context: EffectContext, ExportActionParams { id, metadata }: ExportActionParams, ) -> Result<(), Error> { let context = context.deref()?; let package_id = context.seed.id.clone(); context .seed .ctx .db .mutate(|db| { let model = db .as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) .or_not_found(&package_id)? .as_actions_mut(); let mut value = model.de()?; value.insert(id, metadata); model.ser(&value) }) .await .result?; Ok(()) } #[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct ClearActionsParams { #[arg(long, help = "help.arg.except-actions")] pub except: Vec, } async fn clear_actions( context: EffectContext, ClearActionsParams { except }: ClearActionsParams, ) -> Result<(), Error> { let except: BTreeSet<_> = except.into_iter().collect(); let context = context.deref()?; let package_id = context.seed.id.clone(); context .seed .ctx .db .mutate(|db| { db.as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) .or_not_found(&package_id)? .as_actions_mut() .mutate(|a| Ok(a.retain(|e, _| except.contains(e)))) }) .await .result?; Ok(()) } #[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct GetActionInputParams { #[serde(default)] #[ts(skip)] #[arg(skip)] procedure_id: Guid, #[ts(optional)] #[arg(help = "help.arg.package-id")] package_id: Option, #[arg(help = "help.arg.action-id")] action_id: ActionId, } async fn get_action_input( context: EffectContext, GetActionInputParams { procedure_id, package_id, action_id, }: GetActionInputParams, ) -> Result, Error> { let context = context.deref()?; if let Some(package_id) = package_id { context .seed .ctx .services .get(&package_id) .await .as_ref() .or_not_found(&package_id)? .get_action_input(procedure_id, action_id) .await } else { context.get_action_input(procedure_id, action_id).await } } #[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct RunActionParams { #[serde(default)] #[ts(skip)] #[arg(skip)] procedure_id: Guid, #[ts(optional)] #[arg(help = "help.arg.package-id")] package_id: Option, #[arg(help = "help.arg.action-id")] action_id: ActionId, #[ts(type = "any")] #[arg(help = "help.arg.action-input")] input: Value, } async fn run_action( context: EffectContext, RunActionParams { procedure_id, package_id, action_id, input, }: RunActionParams, ) -> Result, Error> { let context = context.deref()?; let package_id = package_id.as_ref().unwrap_or(&context.seed.id); if package_id != &context.seed.id { return Err(Error::new( eyre!( "{}", t!("service.effects.action.calling-actions-on-other-packages-unsupported") ), ErrorKind::InvalidRequest, )); context .seed .ctx .services .get(&package_id) .await .as_ref() .or_not_found(&package_id)? .run_action(procedure_id, action_id, input) .await } else { context.run_action(procedure_id, action_id, input).await } } #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct CreateTaskParams { #[serde(default)] #[ts(skip)] procedure_id: Guid, replay_id: ReplayId, #[serde(flatten)] task: Task, } async fn create_task( context: EffectContext, CreateTaskParams { procedure_id, replay_id, task, }: CreateTaskParams, ) -> Result<(), Error> { let context = context.deref()?; let src_id = &context.seed.id; let active = match &task.when { Some(TaskTrigger { once, condition }) => match condition { TaskCondition::InputNotMatches => { let Some(input) = task.input.as_ref() else { return Err(Error::new( eyre!( "{}", t!("service.effects.action.input-not-matches-requires-input") ), ErrorKind::InvalidRequest, )); }; if let Some(service) = context .seed .ctx .services .get(&task.package_id) .await .as_ref() { let Some(prev) = service .get_action_input(procedure_id.clone(), task.action_id.clone()) .await? else { return Err(Error::new( eyre!( "{}", t!( "service.effects.action.action-has-no-input", action_id = task.action_id, package_id = task.package_id ) ), ErrorKind::InvalidRequest, )); }; if input.matches(prev.value.as_ref()) { if *once { return Ok(()); } else { false } } else { true } } else { true // update when service is installed } } }, None => true, }; context .seed .ctx .db .mutate(|db| { let pde = db .as_public_mut() .as_package_data_mut() .as_idx_mut(src_id) .or_not_found(src_id)?; if active && task.severity == TaskSeverity::Critical { pde.as_status_info_mut().stop()?; } pde.as_tasks_mut() .insert(&replay_id, &TaskEntry { active, task }) }) .await .result?; Ok(()) } #[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] #[ts(type = "{ only: string[] } | { except: string[] }")] #[ts(export)] pub struct ClearTasksParams { #[arg(long, conflicts_with = "except", help = "help.arg.only-tasks")] pub only: Option>, #[arg(long, conflicts_with = "only", help = "help.arg.except-tasks")] pub except: Option>, } async fn clear_tasks( context: EffectContext, ClearTasksParams { only, except }: ClearTasksParams, ) -> Result<(), Error> { let context = context.deref()?; let package_id = context.seed.id.clone(); let only = only.map(|only| only.into_iter().collect::>()); let except = except.map(|except| except.into_iter().collect::>()); context .seed .ctx .db .mutate(|db| { db.as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) .or_not_found(&package_id)? .as_tasks_mut() .mutate(|a| { Ok(a.retain(|e, _| { only.as_ref().map_or(true, |only| !only.contains(e)) && except.as_ref().map_or(true, |except| except.contains(e)) })) }) }) .await .result?; Ok(()) }