use crate::types::{ExecutionResult, ExecutionStatus};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event_type", rename_all = "snake_case")]
pub enum ExecutionEvent {
Started {
execution_id: Uuid,
command: String,
timestamp: DateTime<Utc>,
},
Stdout {
execution_id: Uuid,
line: String,
timestamp: DateTime<Utc>,
},
Stderr {
execution_id: Uuid,
line: String,
timestamp: DateTime<Utc>,
},
Completed {
execution_id: Uuid,
result: ExecutionResult,
timestamp: DateTime<Utc>,
},
Failed {
execution_id: Uuid,
error: String,
timestamp: DateTime<Utc>,
},
Cancelled {
execution_id: Uuid,
timestamp: DateTime<Utc>,
},
Timeout {
execution_id: Uuid,
timeout_ms: u64,
timestamp: DateTime<Utc>,
},
Progress {
plan_id: Uuid,
completed: usize,
total: usize,
current_command: Option<String>,
timestamp: DateTime<Utc>,
},
StatusChanged {
execution_id: Uuid,
old_status: ExecutionStatus,
new_status: ExecutionStatus,
timestamp: DateTime<Utc>,
},
}
impl ExecutionEvent {
#[must_use]
pub fn execution_id(&self) -> Option<Uuid> {
match self {
ExecutionEvent::Started { execution_id, .. }
| ExecutionEvent::Stdout { execution_id, .. }
| ExecutionEvent::Stderr { execution_id, .. }
| ExecutionEvent::Completed { execution_id, .. }
| ExecutionEvent::Failed { execution_id, .. }
| ExecutionEvent::Cancelled { execution_id, .. }
| ExecutionEvent::Timeout { execution_id, .. }
| ExecutionEvent::StatusChanged { execution_id, .. } => Some(*execution_id),
ExecutionEvent::Progress { .. } => None,
}
}
#[must_use]
pub fn plan_id(&self) -> Option<Uuid> {
match self {
ExecutionEvent::Progress { plan_id, .. } => Some(*plan_id),
_ => None,
}
}
#[must_use]
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
ExecutionEvent::Started { timestamp, .. }
| ExecutionEvent::Stdout { timestamp, .. }
| ExecutionEvent::Stderr { timestamp, .. }
| ExecutionEvent::Completed { timestamp, .. }
| ExecutionEvent::Failed { timestamp, .. }
| ExecutionEvent::Cancelled { timestamp, .. }
| ExecutionEvent::Timeout { timestamp, .. }
| ExecutionEvent::Progress { timestamp, .. }
| ExecutionEvent::StatusChanged { timestamp, .. } => *timestamp,
}
}
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(
self,
ExecutionEvent::Completed { .. }
| ExecutionEvent::Failed { .. }
| ExecutionEvent::Cancelled { .. }
| ExecutionEvent::Timeout { .. }
)
}
#[must_use]
pub fn event_type_name(&self) -> &str {
match self {
ExecutionEvent::Started { .. } => "started",
ExecutionEvent::Stdout { .. } => "stdout",
ExecutionEvent::Stderr { .. } => "stderr",
ExecutionEvent::Completed { .. } => "completed",
ExecutionEvent::Failed { .. } => "failed",
ExecutionEvent::Cancelled { .. } => "cancelled",
ExecutionEvent::Timeout { .. } => "timeout",
ExecutionEvent::Progress { .. } => "progress",
ExecutionEvent::StatusChanged { .. } => "status_changed",
}
}
}
#[async_trait]
pub trait EventHandler: Send + Sync {
async fn handle_event(&self, event: ExecutionEvent);
async fn handle_error(&self, error: String) {
eprintln!("Event handler error: {error}");
}
}
pub struct NoopEventHandler;
#[async_trait]
impl EventHandler for NoopEventHandler {
async fn handle_event(&self, _event: ExecutionEvent) {
}
}
pub struct LoggingEventHandler {
pub verbose: bool,
}
impl LoggingEventHandler {
#[must_use]
pub fn new(verbose: bool) -> Self {
Self { verbose }
}
}
#[async_trait]
impl EventHandler for LoggingEventHandler {
async fn handle_event(&self, event: ExecutionEvent) {
if self.verbose {
eprintln!(
"[{}] Event: {} - {:?}",
event.timestamp().format("%Y-%m-%d %H:%M:%S"),
event.event_type_name(),
event
);
} else {
match event {
ExecutionEvent::Started { command, .. } => {
eprintln!("Started: {command}");
}
ExecutionEvent::Completed { execution_id, .. } => {
eprintln!("Completed: {execution_id}");
}
ExecutionEvent::Failed { error, .. } => {
eprintln!("Failed: {error}");
}
_ => {}
}
}
}
}
pub struct MultiEventHandler {
handlers: Vec<Box<dyn EventHandler>>,
}
impl MultiEventHandler {
#[must_use]
pub fn new() -> Self {
Self {
handlers: Vec::new(),
}
}
pub fn add_handler(&mut self, handler: Box<dyn EventHandler>) {
self.handlers.push(handler);
}
}
impl Default for MultiEventHandler {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl EventHandler for MultiEventHandler {
async fn handle_event(&self, event: ExecutionEvent) {
for handler in &self.handlers {
handler.handle_event(event.clone()).await;
}
}
async fn handle_error(&self, error: String) {
for handler in &self.handlers {
handler.handle_error(error.clone()).await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ExecutionResult;
use std::time::Duration;
#[test]
fn test_execution_event_execution_id() {
let id = Uuid::new_v4();
let event = ExecutionEvent::Started {
execution_id: id,
command: "test".to_string(),
timestamp: Utc::now(),
};
assert_eq!(event.execution_id(), Some(id));
let plan_id = Uuid::new_v4();
let event = ExecutionEvent::Progress {
plan_id,
completed: 1,
total: 5,
current_command: None,
timestamp: Utc::now(),
};
assert_eq!(event.execution_id(), None);
assert_eq!(event.plan_id(), Some(plan_id));
}
#[test]
fn test_execution_event_is_terminal() {
let id = Uuid::new_v4();
let event = ExecutionEvent::Started {
execution_id: id,
command: "test".to_string(),
timestamp: Utc::now(),
};
assert!(!event.is_terminal());
let event = ExecutionEvent::Stdout {
execution_id: id,
line: "output".to_string(),
timestamp: Utc::now(),
};
assert!(!event.is_terminal());
let result = ExecutionResult {
id,
status: ExecutionStatus::Completed,
success: true,
exit_code: 0,
stdout: "".to_string(),
stderr: "".to_string(),
duration: Duration::from_secs(1),
started_at: Utc::now(),
completed_at: Some(Utc::now()),
error: None,
stdout_overflow_file: None,
stderr_overflow_file: None,
};
let event = ExecutionEvent::Completed {
execution_id: id,
result,
timestamp: Utc::now(),
};
assert!(event.is_terminal());
let event = ExecutionEvent::Failed {
execution_id: id,
error: "error".to_string(),
timestamp: Utc::now(),
};
assert!(event.is_terminal());
let event = ExecutionEvent::Cancelled {
execution_id: id,
timestamp: Utc::now(),
};
assert!(event.is_terminal());
let event = ExecutionEvent::Timeout {
execution_id: id,
timeout_ms: 5000,
timestamp: Utc::now(),
};
assert!(event.is_terminal());
}
#[test]
fn test_event_type_name() {
let id = Uuid::new_v4();
let event = ExecutionEvent::Started {
execution_id: id,
command: "test".to_string(),
timestamp: Utc::now(),
};
assert_eq!(event.event_type_name(), "started");
let event = ExecutionEvent::Stdout {
execution_id: id,
line: "output".to_string(),
timestamp: Utc::now(),
};
assert_eq!(event.event_type_name(), "stdout");
let event = ExecutionEvent::StatusChanged {
execution_id: id,
old_status: ExecutionStatus::Pending,
new_status: ExecutionStatus::Running,
timestamp: Utc::now(),
};
assert_eq!(event.event_type_name(), "status_changed");
}
#[tokio::test]
async fn test_noop_event_handler() {
let handler = NoopEventHandler;
let event = ExecutionEvent::Started {
execution_id: Uuid::new_v4(),
command: "test".to_string(),
timestamp: Utc::now(),
};
handler.handle_event(event).await;
}
#[tokio::test]
async fn test_logging_event_handler() {
let handler = LoggingEventHandler::new(false);
let event = ExecutionEvent::Started {
execution_id: Uuid::new_v4(),
command: "test".to_string(),
timestamp: Utc::now(),
};
handler.handle_event(event).await;
}
#[tokio::test]
async fn test_multi_event_handler() {
let mut multi = MultiEventHandler::new();
multi.add_handler(Box::new(NoopEventHandler));
multi.add_handler(Box::new(LoggingEventHandler::new(false)));
let event = ExecutionEvent::Started {
execution_id: Uuid::new_v4(),
command: "test".to_string(),
timestamp: Utc::now(),
};
multi.handle_event(event).await;
}
#[test]
fn test_event_serialization() {
let event = ExecutionEvent::Started {
execution_id: Uuid::new_v4(),
command: "echo hello".to_string(),
timestamp: Utc::now(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("started"));
assert!(json.contains("echo hello"));
let deserialized: ExecutionEvent = serde_json::from_str(&json).unwrap();
match deserialized {
ExecutionEvent::Started { command, .. } => {
assert_eq!(command, "echo hello");
}
_ => panic!("Wrong event type"),
}
}
}