openrunner_rs/
error.rs

1//! Error types for OpenRunner.
2
3use thiserror::Error;
4use std::time::Duration;
5
6/// Result type alias for this crate.
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Errors that can occur when running OpenScript commands.
10#[derive(Error, Debug)]
11pub enum Error {
12    /// OpenScript executable was not found in PATH or at the specified location.
13    #[error("OpenScript executable not found. Please ensure 'openscript' is installed and in your PATH, or specify the correct path using ScriptOptions::openscript_path()")]
14    OpenScriptNotFound,
15
16    /// A command failed to execute.
17    #[error("Command execution failed: {message}")]
18    CommandFailed {
19        /// The error message describing what went wrong.
20        message: String,
21    },
22
23    /// The command timed out.
24    #[error("Command timed out after {duration:?}. Consider increasing the timeout or optimizing your script.")]
25    Timeout {
26        /// The duration after which the timeout occurred.
27        duration: Duration,
28    },
29
30    /// Failed to write script content to temporary file.
31    #[error("Failed to write script to temporary file: {source}")]
32    ScriptWriteError {
33        /// The underlying I/O error.
34        #[source]
35        source: std::io::Error,
36    },
37
38    /// Failed to read script file.
39    #[error("Failed to read script file '{path}': {source}")]
40    ScriptReadError {
41        /// The path to the script file that couldn't be read.
42        path: String,
43        /// The underlying I/O error.
44        #[source]
45        source: std::io::Error,
46    },
47
48    /// Invalid script path provided.
49    #[error("Invalid script path '{path}': {reason}")]
50    InvalidScriptPath {
51        /// The invalid path that was provided.
52        path: String,
53        /// The reason why the path is invalid.
54        reason: String,
55    },
56
57    /// Working directory does not exist or is not accessible.
58    #[error("Working directory '{path}' does not exist or is not accessible: {source}")]
59    InvalidWorkingDirectory {
60        /// The path to the invalid working directory.
61        path: String,
62        /// The underlying I/O error.
63        #[source]
64        source: std::io::Error,
65    },
66
67    /// Environment variable contains invalid characters.
68    #[error("Environment variable '{key}' contains invalid characters: {reason}")]
69    InvalidEnvironmentVariable {
70        /// The environment variable key.
71        key: String,
72        /// The reason why the variable is invalid.
73        reason: String,
74    },
75
76    /// Process spawning failed.
77    #[error("Failed to spawn process: {source}")]
78    ProcessSpawnError {
79        /// The underlying I/O error.
80        #[source]
81        source: std::io::Error,
82    },
83
84    /// Process wait failed.
85    #[error("Failed to wait for process completion: {source}")]
86    ProcessWaitError {
87        /// The underlying I/O error.
88        #[source]
89        source: std::io::Error,
90    },
91
92    /// Output capture failed.
93    #[error("Failed to capture process output: {source}")]
94    OutputCaptureError {
95        /// The underlying I/O error.
96        #[source]
97        source: std::io::Error,
98    },
99
100    /// Permission denied when executing script.
101    #[error("Permission denied when executing script. Check file permissions and execution rights.")]
102    PermissionDenied,
103
104    /// Script execution was interrupted.
105    #[error("Script execution was interrupted by signal {signal}")]
106    Interrupted {
107        /// The signal that interrupted execution.
108        signal: i32,
109    },
110
111    /// Resource limit exceeded during execution.
112    #[error("Resource limit exceeded: {resource} ({limit})")]
113    ResourceLimitExceeded {
114        /// The type of resource that exceeded limits.
115        resource: String,
116        /// The limit that was exceeded.
117        limit: String,
118    },
119
120    /// Generic I/O error with context.
121    #[error("I/O operation failed: {context}")]
122    IoError {
123        /// Context about what I/O operation failed.
124        context: String,
125        /// The underlying I/O error.
126        #[source]
127        source: std::io::Error,
128    },
129}
130
131impl Error {
132    /// Create a new command failed error.
133    pub fn command_failed<S: Into<String>>(message: S) -> Self {
134        Self::CommandFailed {
135            message: message.into(),
136        }
137    }
138
139    /// Create a new timeout error.
140    pub fn timeout(duration: Duration) -> Self {
141        Self::Timeout { duration }
142    }
143
144    /// Create a new script write error.
145    pub fn script_write_error(source: std::io::Error) -> Self {
146        Self::ScriptWriteError { source }
147    }
148
149    /// Create a new script read error.
150    pub fn script_read_error<S: Into<String>>(path: S, source: std::io::Error) -> Self {
151        Self::ScriptReadError {
152            path: path.into(),
153            source,
154        }
155    }
156
157    /// Create a new invalid script path error.
158    pub fn invalid_script_path<P: Into<String>, R: Into<String>>(path: P, reason: R) -> Self {
159        Self::InvalidScriptPath {
160            path: path.into(),
161            reason: reason.into(),
162        }
163    }
164
165    /// Create a new invalid working directory error.
166    pub fn invalid_working_directory<P: Into<String>>(path: P, source: std::io::Error) -> Self {
167        Self::InvalidWorkingDirectory {
168            path: path.into(),
169            source,
170        }
171    }
172
173    /// Create a new invalid environment variable error.
174    pub fn invalid_environment_variable<K: Into<String>, R: Into<String>>(key: K, reason: R) -> Self {
175        Self::InvalidEnvironmentVariable {
176            key: key.into(),
177            reason: reason.into(),
178        }
179    }
180
181    /// Create a new process spawn error.
182    pub fn process_spawn_error(source: std::io::Error) -> Self {
183        Self::ProcessSpawnError { source }
184    }
185
186    /// Create a new process wait error.
187    pub fn process_wait_error(source: std::io::Error) -> Self {
188        Self::ProcessWaitError { source }
189    }
190
191    /// Create a new output capture error.
192    pub fn output_capture_error(source: std::io::Error) -> Self {
193        Self::OutputCaptureError { source }
194    }
195
196    /// Create a new interrupted error.
197    pub fn interrupted(signal: i32) -> Self {
198        Self::Interrupted { signal }
199    }
200
201    /// Create a new resource limit exceeded error.
202    pub fn resource_limit_exceeded<R: Into<String>, L: Into<String>>(resource: R, limit: L) -> Self {
203        Self::ResourceLimitExceeded {
204            resource: resource.into(),
205            limit: limit.into(),
206        }
207    }
208
209    /// Create a new I/O error with context.
210    pub fn io_error<C: Into<String>>(context: C, source: std::io::Error) -> Self {
211        Self::IoError {
212            context: context.into(),
213            source,
214        }
215    }
216
217    /// Check if this error is retryable.
218    pub fn is_retryable(&self) -> bool {
219        match self {
220            Error::Timeout { .. } => true,
221            Error::ProcessSpawnError { source } | 
222            Error::ProcessWaitError { source } | 
223            Error::OutputCaptureError { source } |
224            Error::IoError { source, .. } => {
225                matches!(source.kind(), 
226                    std::io::ErrorKind::Interrupted | 
227                    std::io::ErrorKind::TimedOut |
228                    std::io::ErrorKind::WouldBlock
229                )
230            }
231            Error::ResourceLimitExceeded { .. } => true,
232            _ => false,
233        }
234    }
235
236    /// Get a user-friendly error message with suggestions.
237    pub fn user_message(&self) -> String {
238        match self {
239            Error::OpenScriptNotFound => {
240                "OpenScript is not installed or not in your PATH. Please install OpenScript and try again.".to_string()
241            }
242            Error::Timeout { duration } => {
243                format!("Script took too long to execute (>{:?}). Consider optimizing your script or increasing the timeout.", duration)
244            }
245            Error::PermissionDenied => {
246                "Permission denied. Make sure the script file is executable and you have the necessary permissions.".to_string()
247            }
248            Error::InvalidWorkingDirectory { path, .. } => {
249                format!("The directory '{}' doesn't exist or isn't accessible. Please check the path and permissions.", path)
250            }
251            Error::ScriptReadError { path, .. } => {
252                format!("Couldn't read the script file '{}'. Please check if the file exists and is readable.", path)
253            }
254            _ => self.to_string(),
255        }
256    }
257}
258
259impl From<std::io::Error> for Error {
260    fn from(err: std::io::Error) -> Self {
261        match err.kind() {
262            std::io::ErrorKind::NotFound => Error::OpenScriptNotFound,
263            std::io::ErrorKind::PermissionDenied => Error::PermissionDenied,
264            std::io::ErrorKind::TimedOut => Error::Timeout { 
265                duration: Duration::from_secs(0) // Default timeout
266            },
267            _ => Error::IoError {
268                context: "Unknown I/O operation".to_string(),
269                source: err,
270            },
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use std::time::Duration;
279
280    #[test]
281    fn test_error_display() {
282        let err = Error::timeout(Duration::from_secs(30));
283        assert!(err.to_string().contains("30s"));
284    }
285
286    #[test]
287    fn test_error_retryable() {
288        assert!(Error::timeout(Duration::from_secs(10)).is_retryable());
289        assert!(!Error::OpenScriptNotFound.is_retryable());
290        assert!(!Error::PermissionDenied.is_retryable());
291    }
292
293    #[test]
294    fn test_user_message() {
295        let err = Error::OpenScriptNotFound;
296        let message = err.user_message();
297        assert!(message.contains("install OpenScript"));
298    }
299
300    #[test]
301    fn test_error_constructors() {
302        let err = Error::command_failed("test failed");
303        assert!(matches!(err, Error::CommandFailed { .. }));
304
305        let err = Error::invalid_script_path("/invalid/path", "does not exist");
306        assert!(matches!(err, Error::InvalidScriptPath { .. }));
307    }
308
309    #[test]
310    fn test_io_error_conversion() {
311        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
312        let err: Error = io_err.into();
313        assert!(matches!(err, Error::OpenScriptNotFound));
314
315        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
316        let err: Error = io_err.into();
317        assert!(matches!(err, Error::PermissionDenied));
318    }
319}