openrunner-rs 1.0.1

A Rust library for running OpenScript
Documentation
//! Error types for OpenRunner.

use thiserror::Error;
use std::time::Duration;

/// Result type alias for this crate.
pub type Result<T> = std::result::Result<T, Error>;

/// Errors that can occur when running OpenScript commands.
#[derive(Error, Debug)]
pub enum Error {
    /// OpenScript executable was not found in PATH or at the specified location.
    #[error("OpenScript executable not found. Please ensure 'openscript' is installed and in your PATH, or specify the correct path using ScriptOptions::openscript_path()")]
    OpenScriptNotFound,

    /// A command failed to execute.
    #[error("Command execution failed: {message}")]
    CommandFailed {
        /// The error message describing what went wrong.
        message: String,
    },

    /// The command timed out.
    #[error("Command timed out after {duration:?}. Consider increasing the timeout or optimizing your script.")]
    Timeout {
        /// The duration after which the timeout occurred.
        duration: Duration,
    },

    /// Failed to write script content to temporary file.
    #[error("Failed to write script to temporary file: {source}")]
    ScriptWriteError {
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },

    /// Failed to read script file.
    #[error("Failed to read script file '{path}': {source}")]
    ScriptReadError {
        /// The path to the script file that couldn't be read.
        path: String,
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },

    /// Invalid script path provided.
    #[error("Invalid script path '{path}': {reason}")]
    InvalidScriptPath {
        /// The invalid path that was provided.
        path: String,
        /// The reason why the path is invalid.
        reason: String,
    },

    /// Working directory does not exist or is not accessible.
    #[error("Working directory '{path}' does not exist or is not accessible: {source}")]
    InvalidWorkingDirectory {
        /// The path to the invalid working directory.
        path: String,
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },

    /// Environment variable contains invalid characters.
    #[error("Environment variable '{key}' contains invalid characters: {reason}")]
    InvalidEnvironmentVariable {
        /// The environment variable key.
        key: String,
        /// The reason why the variable is invalid.
        reason: String,
    },

    /// Process spawning failed.
    #[error("Failed to spawn process: {source}")]
    ProcessSpawnError {
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },

    /// Process wait failed.
    #[error("Failed to wait for process completion: {source}")]
    ProcessWaitError {
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },

    /// Output capture failed.
    #[error("Failed to capture process output: {source}")]
    OutputCaptureError {
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },

    /// Permission denied when executing script.
    #[error("Permission denied when executing script. Check file permissions and execution rights.")]
    PermissionDenied,

    /// Script execution was interrupted.
    #[error("Script execution was interrupted by signal {signal}")]
    Interrupted {
        /// The signal that interrupted execution.
        signal: i32,
    },

    /// Resource limit exceeded during execution.
    #[error("Resource limit exceeded: {resource} ({limit})")]
    ResourceLimitExceeded {
        /// The type of resource that exceeded limits.
        resource: String,
        /// The limit that was exceeded.
        limit: String,
    },

    /// Generic I/O error with context.
    #[error("I/O operation failed: {context}")]
    IoError {
        /// Context about what I/O operation failed.
        context: String,
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },
}

impl Error {
    /// Create a new command failed error.
    pub fn command_failed<S: Into<String>>(message: S) -> Self {
        Self::CommandFailed {
            message: message.into(),
        }
    }

    /// Create a new timeout error.
    pub fn timeout(duration: Duration) -> Self {
        Self::Timeout { duration }
    }

    /// Create a new script write error.
    pub fn script_write_error(source: std::io::Error) -> Self {
        Self::ScriptWriteError { source }
    }

    /// Create a new script read error.
    pub fn script_read_error<S: Into<String>>(path: S, source: std::io::Error) -> Self {
        Self::ScriptReadError {
            path: path.into(),
            source,
        }
    }

    /// Create a new invalid script path error.
    pub fn invalid_script_path<P: Into<String>, R: Into<String>>(path: P, reason: R) -> Self {
        Self::InvalidScriptPath {
            path: path.into(),
            reason: reason.into(),
        }
    }

    /// Create a new invalid working directory error.
    pub fn invalid_working_directory<P: Into<String>>(path: P, source: std::io::Error) -> Self {
        Self::InvalidWorkingDirectory {
            path: path.into(),
            source,
        }
    }

    /// Create a new invalid environment variable error.
    pub fn invalid_environment_variable<K: Into<String>, R: Into<String>>(key: K, reason: R) -> Self {
        Self::InvalidEnvironmentVariable {
            key: key.into(),
            reason: reason.into(),
        }
    }

    /// Create a new process spawn error.
    pub fn process_spawn_error(source: std::io::Error) -> Self {
        Self::ProcessSpawnError { source }
    }

    /// Create a new process wait error.
    pub fn process_wait_error(source: std::io::Error) -> Self {
        Self::ProcessWaitError { source }
    }

    /// Create a new output capture error.
    pub fn output_capture_error(source: std::io::Error) -> Self {
        Self::OutputCaptureError { source }
    }

    /// Create a new interrupted error.
    pub fn interrupted(signal: i32) -> Self {
        Self::Interrupted { signal }
    }

    /// Create a new resource limit exceeded error.
    pub fn resource_limit_exceeded<R: Into<String>, L: Into<String>>(resource: R, limit: L) -> Self {
        Self::ResourceLimitExceeded {
            resource: resource.into(),
            limit: limit.into(),
        }
    }

    /// Create a new I/O error with context.
    pub fn io_error<C: Into<String>>(context: C, source: std::io::Error) -> Self {
        Self::IoError {
            context: context.into(),
            source,
        }
    }

    /// Check if this error is retryable.
    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,
        }
    }

    /// Get a user-friendly error message with suggestions.
    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) // Default timeout
            },
            _ => 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));
    }
}