use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Debug;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ActionStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
TimedOut,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ActionPriority {
Low = 1,
Normal = 2,
High = 3,
Critical = 4,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionContext {
pub initiator: String,
pub correlation_id: Option<Uuid>,
pub environment: String,
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionResult {
pub success: bool,
pub duration_ms: u64,
pub data: Option<Value>,
pub error: Option<String>,
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
pub id: Uuid,
pub name: String,
pub description: String,
pub source: String,
pub target_service: String,
pub arguments: HashMap<String, Value>,
pub priority: ActionPriority,
pub status: ActionStatus,
pub context: ActionContext,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_execution: Option<DateTime<Utc>>,
pub execution_count: u32,
pub last_result: Option<ActionResult>,
pub timeout_seconds: Option<u32>,
pub retryable: bool,
pub max_retries: u32,
pub retry_count: u32,
}
impl Action {
pub fn new(name: String, description: String, source: String, target_service: String) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name,
description,
source,
target_service,
arguments: HashMap::new(),
priority: ActionPriority::Normal,
status: ActionStatus::Pending,
context: ActionContext {
initiator: "system".to_string(),
correlation_id: None,
environment: "default".to_string(),
metadata: HashMap::new(),
},
created_at: now,
updated_at: now,
last_execution: None,
execution_count: 0,
last_result: None,
timeout_seconds: None,
retryable: true,
max_retries: 3,
retry_count: 0,
}
}
pub fn new_with_context(
name: String,
description: String,
source: String,
target_service: String,
context: ActionContext,
) -> Self {
let mut action = Self::new(name, description, source, target_service);
action.context = context;
action
}
pub fn with_priority(mut self, priority: ActionPriority) -> Self {
self.priority = priority;
self.updated_at = Utc::now();
self
}
pub fn with_timeout(mut self, timeout_seconds: u32) -> Self {
self.timeout_seconds = Some(timeout_seconds);
self.updated_at = Utc::now();
self
}
pub fn with_retry_config(mut self, retryable: bool, max_retries: u32) -> Self {
self.retryable = retryable;
self.max_retries = max_retries;
self.updated_at = Utc::now();
self
}
pub fn add_argument<T: Serialize>(&mut self, key: String, value: T) -> Result<(), ActionError> {
let json_value = serde_json::to_value(value)
.map_err(|e| ActionError::SerializationError(e.to_string()))?;
self.arguments.insert(key, json_value);
self.updated_at = Utc::now();
Ok(())
}
pub fn get_argument(&self, key: &str) -> Option<&Value> {
self.arguments.get(key)
}
pub fn set_arguments(&mut self, arguments: HashMap<String, Value>) {
self.arguments = arguments;
self.updated_at = Utc::now();
}
pub fn set_status(&mut self, status: ActionStatus) {
self.status = status;
self.updated_at = Utc::now();
}
pub fn start_execution(&mut self) {
self.status = ActionStatus::Running;
self.last_execution = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn complete_execution(&mut self, result: ActionResult) {
self.status = if result.success {
ActionStatus::Completed
} else {
ActionStatus::Failed
};
self.last_result = Some(result);
self.execution_count += 1;
self.retry_count = 0; self.updated_at = Utc::now();
}
pub fn fail_execution(&mut self, error: String, duration_ms: u64) -> bool {
let result = ActionResult {
success: false,
duration_ms,
data: None,
error: Some(error),
metadata: HashMap::new(),
};
self.last_result = Some(result);
self.execution_count += 1;
self.retry_count += 1;
self.updated_at = Utc::now();
let can_retry = self.retryable && self.retry_count <= self.max_retries;
if can_retry {
self.status = ActionStatus::Pending; } else {
self.status = ActionStatus::Failed;
}
can_retry
}
pub fn cancel(&mut self) {
self.status = ActionStatus::Cancelled;
self.updated_at = Utc::now();
}
pub fn timeout(&mut self) {
self.status = ActionStatus::TimedOut;
self.updated_at = Utc::now();
}
pub fn can_execute(&self) -> bool {
matches!(self.status, ActionStatus::Pending)
}
pub fn is_terminal(&self) -> bool {
matches!(
self.status,
ActionStatus::Completed
| ActionStatus::Failed
| ActionStatus::Cancelled
| ActionStatus::TimedOut
)
}
pub fn success_rate(&self) -> f64 {
if self.execution_count == 0 {
return 0.0;
}
let successful_executions = if let Some(result) = &self.last_result {
if result.success { 1.0 } else { 0.0 }
} else {
0.0
};
(successful_executions / self.execution_count as f64) * 100.0
}
pub fn average_duration_ms(&self) -> Option<u64> {
self.last_result.as_ref().map(|r| r.duration_ms)
}
pub fn reset(&mut self) {
self.status = ActionStatus::Pending;
self.retry_count = 0;
self.updated_at = Utc::now();
}
pub fn clone_for_new_execution(&self) -> Self {
let mut cloned = self.clone();
cloned.id = Uuid::new_v4();
cloned.status = ActionStatus::Pending;
cloned.last_execution = None;
cloned.execution_count = 0;
cloned.last_result = None;
cloned.retry_count = 0;
cloned.created_at = Utc::now();
cloned.updated_at = Utc::now();
cloned
}
}
impl Default for Action {
fn default() -> Self {
Self::new(
"Default Action".to_string(),
"A default action".to_string(),
"system".to_string(),
"default_service".to_string(),
)
}
}
impl PartialEq for Action {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Action {}
#[derive(Debug, Error)]
pub enum ActionError {
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("Execution error: {0}")]
ExecutionError(String),
#[error("Timeout error: execution exceeded {0} seconds")]
TimeoutError(u32),
#[error("Action is not in a valid state for this operation: {0:?}")]
InvalidState(ActionStatus),
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_action_creation() {
let action = Action::new(
"Test Action".to_string(),
"A test action".to_string(),
"test_source".to_string(),
"test_service".to_string(),
);
assert_eq!(action.name, "Test Action");
assert_eq!(action.status, ActionStatus::Pending);
assert_eq!(action.execution_count, 0);
assert!(action.can_execute());
assert!(!action.is_terminal());
}
#[test]
fn test_action_execution_lifecycle() {
let mut action = Action::new(
"Test Action".to_string(),
"A test action".to_string(),
"test_source".to_string(),
"test_service".to_string(),
);
action.start_execution();
assert_eq!(action.status, ActionStatus::Running);
assert!(action.last_execution.is_some());
let result = ActionResult {
success: true,
duration_ms: 1000,
data: Some(json!({"result": "success"})),
error: None,
metadata: HashMap::new(),
};
action.complete_execution(result);
assert_eq!(action.status, ActionStatus::Completed);
assert_eq!(action.execution_count, 1);
assert_eq!(action.retry_count, 0);
assert!(action.is_terminal());
}
#[test]
fn test_action_retry_logic() {
let mut action = Action::new(
"Test Action".to_string(),
"A test action".to_string(),
"test_source".to_string(),
"test_service".to_string(),
)
.with_retry_config(true, 2);
let can_retry = action.fail_execution("Test error".to_string(), 500);
assert!(can_retry);
assert_eq!(action.status, ActionStatus::Pending);
assert_eq!(action.retry_count, 1);
let can_retry = action.fail_execution("Test error".to_string(), 500);
assert!(can_retry);
assert_eq!(action.status, ActionStatus::Pending);
assert_eq!(action.retry_count, 2);
let can_retry = action.fail_execution("Test error".to_string(), 500);
assert!(!can_retry);
assert_eq!(action.status, ActionStatus::Failed);
assert_eq!(action.retry_count, 3);
}
#[test]
fn test_action_arguments() {
let mut action = Action::default();
action
.add_argument("string_arg".to_string(), "test_value")
.unwrap();
action.add_argument("number_arg".to_string(), 42).unwrap();
action
.add_argument("object_arg".to_string(), json!({"key": "value"}))
.unwrap();
assert_eq!(
action.get_argument("string_arg"),
Some(&json!("test_value"))
);
assert_eq!(action.get_argument("number_arg"), Some(&json!(42)));
assert_eq!(
action.get_argument("object_arg"),
Some(&json!({"key": "value"}))
);
assert_eq!(action.get_argument("missing_arg"), None);
}
#[test]
fn test_action_priority() {
let action = Action::default()
.with_priority(ActionPriority::Critical)
.with_timeout(30);
assert_eq!(action.priority, ActionPriority::Critical);
assert_eq!(action.timeout_seconds, Some(30));
}
#[test]
fn test_action_context() {
let context = ActionContext {
initiator: "user123".to_string(),
correlation_id: Some(Uuid::new_v4()),
environment: "production".to_string(),
metadata: HashMap::new(),
};
let action = Action::new_with_context(
"Contextual Action".to_string(),
"An action with context".to_string(),
"test_source".to_string(),
"test_service".to_string(),
context.clone(),
);
assert_eq!(action.context.initiator, "user123");
assert_eq!(action.context.environment, "production");
assert!(action.context.correlation_id.is_some());
}
#[test]
fn test_action_clone_for_new_execution() {
let original = Action {
execution_count: 5,
retry_count: 2,
status: ActionStatus::Failed,
..Default::default()
};
let cloned = original.clone_for_new_execution();
assert_ne!(original.id, cloned.id);
assert_eq!(cloned.status, ActionStatus::Pending);
assert_eq!(cloned.execution_count, 0);
assert_eq!(cloned.retry_count, 0);
assert!(cloned.last_result.is_none());
}
#[test]
fn test_action_state_management() {
let mut action = Action::default();
action.cancel();
assert_eq!(action.status, ActionStatus::Cancelled);
assert!(action.is_terminal());
action.reset();
assert_eq!(action.status, ActionStatus::Pending);
assert!(!action.is_terminal());
action.timeout();
assert_eq!(action.status, ActionStatus::TimedOut);
assert!(action.is_terminal());
}
}