use std::path::PathBuf;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum ExecutionError {
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("Execution not found: {0}")]
NotFound(Uuid),
#[error("Execution timeout after {0}ms")]
Timeout(u64),
#[error("Execution cancelled")]
Cancelled,
#[error("Command failed with exit code {0}")]
CommandFailed(i32),
#[error("Process spawn failed: {0}")]
SpawnFailed(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Output size exceeded maximum of {0} bytes")]
OutputSizeExceeded(usize),
#[error("Concurrency limit reached: {0} executions running")]
ConcurrencyLimitReached(usize),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("Invalid command format: {0}")]
InvalidCommand(String),
#[error("Script file not found: {0}")]
ScriptNotFound(PathBuf),
#[error("Script file not executable: {0}")]
ScriptNotExecutable(PathBuf),
#[error("Timeout exceeds maximum allowed: {0}ms > {1}ms")]
TimeoutTooLarge(u64, u64),
#[error("Invalid working directory: {0}")]
InvalidWorkingDir(PathBuf),
#[error("Working directory does not exist: {0}")]
WorkingDirNotFound(PathBuf),
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Invalid execution plan: {0}")]
InvalidPlan(String),
#[error("Command cannot be empty")]
EmptyCommand,
#[error("Invalid dependency graph: {0}")]
InvalidDependencyGraph(String),
#[error("Program not found in PATH: {0}")]
ProgramNotFound(String),
}
pub type Result<T> = std::result::Result<T, ExecutionError>;
pub type ValidationResult<T> = std::result::Result<T, ValidationError>;
impl ExecutionError {
#[must_use]
pub fn is_retryable(&self) -> bool {
matches!(
self,
ExecutionError::Timeout(_)
| ExecutionError::Io(_)
| ExecutionError::SpawnFailed(_)
| ExecutionError::ConcurrencyLimitReached(_)
)
}
#[must_use]
pub fn is_terminal(&self) -> bool {
!self.is_retryable()
}
#[must_use]
pub fn error_code(&self) -> &str {
match self {
ExecutionError::Validation(_) => "VALIDATION_ERROR",
ExecutionError::NotFound(_) => "NOT_FOUND",
ExecutionError::Timeout(_) => "TIMEOUT",
ExecutionError::Cancelled => "CANCELLED",
ExecutionError::CommandFailed(_) => "COMMAND_FAILED",
ExecutionError::SpawnFailed(_) => "SPAWN_FAILED",
ExecutionError::Io(_) => "IO_ERROR",
ExecutionError::Serialization(_) => "SERIALIZATION_ERROR",
ExecutionError::OutputSizeExceeded(_) => "OUTPUT_SIZE_EXCEEDED",
ExecutionError::ConcurrencyLimitReached(_) => "CONCURRENCY_LIMIT_REACHED",
ExecutionError::InvalidConfig(_) => "INVALID_CONFIG",
ExecutionError::Internal(_) => "INTERNAL_ERROR",
}
}
}
impl ValidationError {
pub fn invalid_command<S: Into<String>>(msg: S) -> Self {
ValidationError::InvalidCommand(msg.into())
}
pub fn missing_field<S: Into<String>>(field: S) -> Self {
ValidationError::MissingField(field.into())
}
pub fn invalid_plan<S: Into<String>>(msg: S) -> Self {
ValidationError::InvalidPlan(msg.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execution_error_display() {
let err = ExecutionError::Timeout(5000);
assert_eq!(err.to_string(), "Execution timeout after 5000ms");
let err = ExecutionError::CommandFailed(1);
assert_eq!(err.to_string(), "Command failed with exit code 1");
let err = ExecutionError::Cancelled;
assert_eq!(err.to_string(), "Execution cancelled");
}
#[test]
fn test_validation_error_display() {
let err = ValidationError::EmptyCommand;
assert_eq!(err.to_string(), "Command cannot be empty");
let err = ValidationError::TimeoutTooLarge(10000, 5000);
assert_eq!(
err.to_string(),
"Timeout exceeds maximum allowed: 10000ms > 5000ms"
);
let err = ValidationError::MissingField("command".to_string());
assert_eq!(err.to_string(), "Missing required field: command");
}
#[test]
fn test_error_from_validation() {
let validation_err = ValidationError::EmptyCommand;
let exec_err: ExecutionError = validation_err.into();
match exec_err {
ExecutionError::Validation(e) => {
assert_eq!(e.to_string(), "Command cannot be empty");
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_error_is_retryable() {
assert!(ExecutionError::Timeout(5000).is_retryable());
assert!(ExecutionError::SpawnFailed("error".to_string()).is_retryable());
assert!(ExecutionError::ConcurrencyLimitReached(100).is_retryable());
assert!(!ExecutionError::Cancelled.is_retryable());
assert!(!ExecutionError::CommandFailed(1).is_retryable());
assert!(!ExecutionError::NotFound(Uuid::new_v4()).is_retryable());
}
#[test]
fn test_error_is_terminal() {
assert!(!ExecutionError::Timeout(5000).is_terminal());
assert!(ExecutionError::Cancelled.is_terminal());
assert!(ExecutionError::CommandFailed(1).is_terminal());
}
#[test]
fn test_error_code() {
assert_eq!(ExecutionError::Timeout(5000).error_code(), "TIMEOUT");
assert_eq!(ExecutionError::Cancelled.error_code(), "CANCELLED");
assert_eq!(
ExecutionError::CommandFailed(1).error_code(),
"COMMAND_FAILED"
);
assert_eq!(
ExecutionError::NotFound(Uuid::new_v4()).error_code(),
"NOT_FOUND"
);
assert_eq!(
ExecutionError::OutputSizeExceeded(1000).error_code(),
"OUTPUT_SIZE_EXCEEDED"
);
}
#[test]
fn test_validation_error_helpers() {
let err = ValidationError::invalid_command("invalid syntax");
assert_eq!(err.to_string(), "Invalid command format: invalid syntax");
let err = ValidationError::missing_field("timeout_ms");
assert_eq!(err.to_string(), "Missing required field: timeout_ms");
let err = ValidationError::invalid_plan("circular dependency");
assert_eq!(
err.to_string(),
"Invalid execution plan: circular dependency"
);
}
#[test]
fn test_result_type_alias() {
fn returns_result() -> Result<i32> {
Ok(42)
}
fn returns_validation_result() -> ValidationResult<String> {
Ok("valid".to_string())
}
assert_eq!(returns_result().unwrap(), 42);
assert_eq!(returns_validation_result().unwrap(), "valid");
}
#[test]
fn test_error_chain() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let exec_err: ExecutionError = io_err.into();
match exec_err {
ExecutionError::Io(e) => {
assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
}
_ => panic!("Expected IO error"),
}
}
}