use crate::types::{Priority, Timestamp, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ActionType {
SendMessage(String),
StoreData(String),
Publish(String),
Query(String),
RemoteCall(String),
UpdateState(String),
Alert(String),
Wait,
NoOp,
Custom(String),
}
impl ActionType {
pub fn send_message(target: &str) -> Self {
ActionType::SendMessage(target.to_string())
}
pub fn store(key: &str) -> Self {
ActionType::StoreData(key.to_string())
}
pub fn publish(topic: &str) -> Self {
ActionType::Publish(topic.to_string())
}
pub fn alert(message: &str) -> Self {
ActionType::Alert(message.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
pub id: String,
pub action_type: ActionType,
pub params: HashMap<String, Value>,
pub priority: Priority,
pub created_at: Timestamp,
pub deadline: Option<Timestamp>,
}
impl Action {
pub fn new(action_type: ActionType) -> Self {
let id = format!("action_{}", Timestamp::now().0);
Self {
id,
action_type,
params: HashMap::new(),
priority: Priority::Normal,
created_at: Timestamp::now(),
deadline: None,
}
}
pub fn noop() -> Self {
Self::new(ActionType::NoOp)
}
pub fn wait() -> Self {
Self::new(ActionType::Wait)
}
pub fn send_message(target: &str, content: impl Into<Value>) -> Self {
Self::new(ActionType::send_message(target)).with_param("content", content)
}
pub fn store(key: &str, value: impl Into<Value>) -> Self {
Self::new(ActionType::store(key)).with_param("value", value)
}
pub fn alert(message: &str) -> Self {
Self::new(ActionType::alert(message))
}
pub fn with_param(mut self, key: &str, value: impl Into<Value>) -> Self {
self.params.insert(key.to_string(), value.into());
self
}
pub fn with_priority(mut self, priority: Priority) -> Self {
self.priority = priority;
self
}
pub fn is_noop(&self) -> bool {
matches!(self.action_type, ActionType::NoOp)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionResult {
pub action_id: String,
pub success: bool,
pub value: Option<Value>,
pub error: Option<String>,
pub executed_at: Timestamp,
pub duration_us: u64,
}
impl ActionResult {
pub fn success(action_id: &str) -> Self {
Self {
action_id: action_id.to_string(),
success: true,
value: None,
error: None,
executed_at: Timestamp::now(),
duration_us: 0,
}
}
pub fn success_with_value(action_id: &str, value: impl Into<Value>) -> Self {
Self {
action_id: action_id.to_string(),
success: true,
value: Some(value.into()),
error: None,
executed_at: Timestamp::now(),
duration_us: 0,
}
}
pub fn failure(action_id: &str, error: &str) -> Self {
Self {
action_id: action_id.to_string(),
success: false,
value: None,
error: Some(error.to_string()),
executed_at: Timestamp::now(),
duration_us: 0,
}
}
pub fn with_duration(mut self, duration_us: u64) -> Self {
self.duration_us = duration_us;
self
}
}
pub trait ActionExecutor {
fn execute(&mut self, action: &Action) -> ActionResult;
fn supports(&self, action_type: &ActionType) -> bool;
}
pub struct LoggingExecutor;
impl ActionExecutor for LoggingExecutor {
fn execute(&mut self, action: &Action) -> ActionResult {
log::info!("Executing action: {:?}", action.action_type);
ActionResult::success(&action.id)
}
fn supports(&self, _action_type: &ActionType) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_type_send_message() {
let at = ActionType::send_message("target");
assert!(matches!(at, ActionType::SendMessage(s) if s == "target"));
}
#[test]
fn test_action_type_store() {
let at = ActionType::store("key");
assert!(matches!(at, ActionType::StoreData(s) if s == "key"));
}
#[test]
fn test_action_type_publish() {
let at = ActionType::publish("topic");
assert!(matches!(at, ActionType::Publish(s) if s == "topic"));
}
#[test]
fn test_action_type_alert() {
let at = ActionType::alert("message");
assert!(matches!(at, ActionType::Alert(s) if s == "message"));
}
#[test]
fn test_action_type_all_variants() {
let types = [
ActionType::SendMessage("t".to_string()),
ActionType::StoreData("k".to_string()),
ActionType::Publish("p".to_string()),
ActionType::Query("q".to_string()),
ActionType::RemoteCall("r".to_string()),
ActionType::UpdateState("u".to_string()),
ActionType::Alert("a".to_string()),
ActionType::Wait,
ActionType::NoOp,
ActionType::Custom("c".to_string()),
];
for t in types {
let cloned = t.clone();
assert_eq!(t, cloned);
}
}
#[test]
fn test_action_type_serialize() {
let at = ActionType::Alert("test".to_string());
let json = serde_json::to_string(&at).unwrap();
let parsed: ActionType = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, at);
}
#[test]
fn test_action_type_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ActionType::NoOp);
set.insert(ActionType::Wait);
set.insert(ActionType::NoOp); assert_eq!(set.len(), 2);
}
#[test]
fn test_action_creation() {
let action = Action::send_message("peer_123", "hello");
assert!(matches!(action.action_type, ActionType::SendMessage(_)));
assert!(action.params.contains_key("content"));
}
#[test]
fn test_action_new() {
let action = Action::new(ActionType::NoOp);
assert!(action.id.starts_with("action_"));
assert!(matches!(action.action_type, ActionType::NoOp));
assert!(action.params.is_empty());
assert_eq!(action.priority, Priority::Normal);
assert!(action.deadline.is_none());
}
#[test]
fn test_noop_action() {
let action = Action::noop();
assert!(action.is_noop());
}
#[test]
fn test_wait_action() {
let action = Action::wait();
assert!(matches!(action.action_type, ActionType::Wait));
assert!(!action.is_noop());
}
#[test]
fn test_action_send_message() {
let action = Action::send_message("target", "content");
assert!(matches!(action.action_type, ActionType::SendMessage(_)));
assert!(action.params.contains_key("content"));
}
#[test]
fn test_action_store() {
let action = Action::store("key", 42.0);
assert!(matches!(action.action_type, ActionType::StoreData(_)));
assert!(action.params.contains_key("value"));
}
#[test]
fn test_action_alert() {
let action = Action::alert("alert message");
assert!(matches!(action.action_type, ActionType::Alert(_)));
}
#[test]
fn test_action_with_param() {
let action = Action::new(ActionType::Custom("custom".to_string()))
.with_param("key1", "value1")
.with_param("key2", 42i64);
assert_eq!(action.params.len(), 2);
assert!(action.params.contains_key("key1"));
assert!(action.params.contains_key("key2"));
}
#[test]
fn test_action_with_priority() {
let action = Action::alert("test").with_priority(Priority::Critical);
assert_eq!(action.priority, Priority::Critical);
}
#[test]
fn test_action_with_priority_chain() {
let action = Action::noop()
.with_priority(Priority::Low)
.with_priority(Priority::High);
assert_eq!(action.priority, Priority::High);
}
#[test]
fn test_action_is_noop() {
assert!(Action::noop().is_noop());
assert!(!Action::wait().is_noop());
assert!(!Action::alert("test").is_noop());
}
#[test]
fn test_action_serialize() {
let action = Action::store("key", 123i64).with_priority(Priority::High);
let json = serde_json::to_string(&action).unwrap();
let parsed: Action = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.priority, Priority::High);
}
#[test]
fn test_action_result() {
let result = ActionResult::success("action_1");
assert!(result.success);
assert!(result.error.is_none());
}
#[test]
fn test_action_result_success() {
let result = ActionResult::success("action_123");
assert!(result.success);
assert_eq!(result.action_id, "action_123");
assert!(result.value.is_none());
assert!(result.error.is_none());
assert_eq!(result.duration_us, 0);
}
#[test]
fn test_action_result_success_with_value() {
let result = ActionResult::success_with_value("action_123", 42i64);
assert!(result.success);
assert!(result.value.is_some());
assert_eq!(result.value.as_ref().unwrap().as_i64(), Some(42));
}
#[test]
fn test_action_result_failure() {
let result = ActionResult::failure("action_123", "Error occurred");
assert!(!result.success);
assert!(result.error.is_some());
assert_eq!(result.error.as_ref().unwrap(), "Error occurred");
assert!(result.value.is_none());
}
#[test]
fn test_action_result_with_duration() {
let result = ActionResult::success("action_123").with_duration(1500);
assert_eq!(result.duration_us, 1500);
}
#[test]
fn test_action_result_serialize() {
let result = ActionResult::success_with_value("action_123", "value");
let json = serde_json::to_string(&result).unwrap();
let parsed: ActionResult = serde_json::from_str(&json).unwrap();
assert!(parsed.success);
assert_eq!(parsed.action_id, "action_123");
}
#[test]
fn test_logging_executor() {
let mut executor = LoggingExecutor;
let action = Action::store("key", "value");
let result = executor.execute(&action);
assert!(result.success);
assert_eq!(result.action_id, action.id);
}
#[test]
fn test_logging_executor_supports() {
let executor = LoggingExecutor;
assert!(executor.supports(&ActionType::NoOp));
assert!(executor.supports(&ActionType::Wait));
assert!(executor.supports(&ActionType::Alert("test".to_string())));
assert!(executor.supports(&ActionType::Custom("any".to_string())));
}
#[test]
fn test_custom_executor() {
struct TestExecutor {
call_count: usize,
}
impl ActionExecutor for TestExecutor {
fn execute(&mut self, action: &Action) -> ActionResult {
self.call_count += 1;
ActionResult::success(&action.id)
}
fn supports(&self, action_type: &ActionType) -> bool {
matches!(action_type, ActionType::StoreData(_))
}
}
let mut executor = TestExecutor { call_count: 0 };
let action = Action::store("key", "value");
executor.execute(&action);
executor.execute(&action);
assert_eq!(executor.call_count, 2);
assert!(executor.supports(&ActionType::StoreData("x".to_string())));
assert!(!executor.supports(&ActionType::NoOp));
}
}