use thiserror::Error;
use std::time::Duration;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("OpenScript executable not found. Please ensure 'openscript' is installed and in your PATH, or specify the correct path using ScriptOptions::openscript_path()")]
OpenScriptNotFound,
#[error("Command execution failed: {message}")]
CommandFailed {
message: String,
},
#[error("Command timed out after {duration:?}. Consider increasing the timeout or optimizing your script.")]
Timeout {
duration: Duration,
},
#[error("Failed to write script to temporary file: {source}")]
ScriptWriteError {
#[source]
source: std::io::Error,
},
#[error("Failed to read script file '{path}': {source}")]
ScriptReadError {
path: String,
#[source]
source: std::io::Error,
},
#[error("Invalid script path '{path}': {reason}")]
InvalidScriptPath {
path: String,
reason: String,
},
#[error("Working directory '{path}' does not exist or is not accessible: {source}")]
InvalidWorkingDirectory {
path: String,
#[source]
source: std::io::Error,
},
#[error("Environment variable '{key}' contains invalid characters: {reason}")]
InvalidEnvironmentVariable {
key: String,
reason: String,
},
#[error("Failed to spawn process: {source}")]
ProcessSpawnError {
#[source]
source: std::io::Error,
},
#[error("Failed to wait for process completion: {source}")]
ProcessWaitError {
#[source]
source: std::io::Error,
},
#[error("Failed to capture process output: {source}")]
OutputCaptureError {
#[source]
source: std::io::Error,
},
#[error("Permission denied when executing script. Check file permissions and execution rights.")]
PermissionDenied,
#[error("Script execution was interrupted by signal {signal}")]
Interrupted {
signal: i32,
},
#[error("Resource limit exceeded: {resource} ({limit})")]
ResourceLimitExceeded {
resource: String,
limit: String,
},
#[error("I/O operation failed: {context}")]
IoError {
context: String,
#[source]
source: std::io::Error,
},
}
impl Error {
pub fn command_failed<S: Into<String>>(message: S) -> Self {
Self::CommandFailed {
message: message.into(),
}
}
pub fn timeout(duration: Duration) -> Self {
Self::Timeout { duration }
}
pub fn script_write_error(source: std::io::Error) -> Self {
Self::ScriptWriteError { source }
}
pub fn script_read_error<S: Into<String>>(path: S, source: std::io::Error) -> Self {
Self::ScriptReadError {
path: path.into(),
source,
}
}
pub fn invalid_script_path<P: Into<String>, R: Into<String>>(path: P, reason: R) -> Self {
Self::InvalidScriptPath {
path: path.into(),
reason: reason.into(),
}
}
pub fn invalid_working_directory<P: Into<String>>(path: P, source: std::io::Error) -> Self {
Self::InvalidWorkingDirectory {
path: path.into(),
source,
}
}
pub fn invalid_environment_variable<K: Into<String>, R: Into<String>>(key: K, reason: R) -> Self {
Self::InvalidEnvironmentVariable {
key: key.into(),
reason: reason.into(),
}
}
pub fn process_spawn_error(source: std::io::Error) -> Self {
Self::ProcessSpawnError { source }
}
pub fn process_wait_error(source: std::io::Error) -> Self {
Self::ProcessWaitError { source }
}
pub fn output_capture_error(source: std::io::Error) -> Self {
Self::OutputCaptureError { source }
}
pub fn interrupted(signal: i32) -> Self {
Self::Interrupted { signal }
}
pub fn resource_limit_exceeded<R: Into<String>, L: Into<String>>(resource: R, limit: L) -> Self {
Self::ResourceLimitExceeded {
resource: resource.into(),
limit: limit.into(),
}
}
pub fn io_error<C: Into<String>>(context: C, source: std::io::Error) -> Self {
Self::IoError {
context: context.into(),
source,
}
}
pub fn is_retryable(&self) -> bool {
match self {
Error::Timeout { .. } => true,
Error::ProcessSpawnError { source } |
Error::ProcessWaitError { source } |
Error::OutputCaptureError { source } |
Error::IoError { source, .. } => {
matches!(source.kind(),
std::io::ErrorKind::Interrupted |
std::io::ErrorKind::TimedOut |
std::io::ErrorKind::WouldBlock
)
}
Error::ResourceLimitExceeded { .. } => true,
_ => false,
}
}
pub fn user_message(&self) -> String {
match self {
Error::OpenScriptNotFound => {
"OpenScript is not installed or not in your PATH. Please install OpenScript and try again.".to_string()
}
Error::Timeout { duration } => {
format!("Script took too long to execute (>{:?}). Consider optimizing your script or increasing the timeout.", duration)
}
Error::PermissionDenied => {
"Permission denied. Make sure the script file is executable and you have the necessary permissions.".to_string()
}
Error::InvalidWorkingDirectory { path, .. } => {
format!("The directory '{}' doesn't exist or isn't accessible. Please check the path and permissions.", path)
}
Error::ScriptReadError { path, .. } => {
format!("Couldn't read the script file '{}'. Please check if the file exists and is readable.", path)
}
_ => self.to_string(),
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
match err.kind() {
std::io::ErrorKind::NotFound => Error::OpenScriptNotFound,
std::io::ErrorKind::PermissionDenied => Error::PermissionDenied,
std::io::ErrorKind::TimedOut => Error::Timeout {
duration: Duration::from_secs(0) },
_ => Error::IoError {
context: "Unknown I/O operation".to_string(),
source: err,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_error_display() {
let err = Error::timeout(Duration::from_secs(30));
assert!(err.to_string().contains("30s"));
}
#[test]
fn test_error_retryable() {
assert!(Error::timeout(Duration::from_secs(10)).is_retryable());
assert!(!Error::OpenScriptNotFound.is_retryable());
assert!(!Error::PermissionDenied.is_retryable());
}
#[test]
fn test_user_message() {
let err = Error::OpenScriptNotFound;
let message = err.user_message();
assert!(message.contains("install OpenScript"));
}
#[test]
fn test_error_constructors() {
let err = Error::command_failed("test failed");
assert!(matches!(err, Error::CommandFailed { .. }));
let err = Error::invalid_script_path("/invalid/path", "does not exist");
assert!(matches!(err, Error::InvalidScriptPath { .. }));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::OpenScriptNotFound));
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
let err: Error = io_err.into();
assert!(matches!(err, Error::PermissionDenied));
}
}