1use thiserror::Error;
4use std::time::Duration;
5
6pub type Result<T> = std::result::Result<T, Error>;
8
9#[derive(Error, Debug)]
11pub enum Error {
12 #[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 #[error("Command execution failed: {message}")]
18 CommandFailed {
19 message: String,
21 },
22
23 #[error("Command timed out after {duration:?}. Consider increasing the timeout or optimizing your script.")]
25 Timeout {
26 duration: Duration,
28 },
29
30 #[error("Failed to write script to temporary file: {source}")]
32 ScriptWriteError {
33 #[source]
35 source: std::io::Error,
36 },
37
38 #[error("Failed to read script file '{path}': {source}")]
40 ScriptReadError {
41 path: String,
43 #[source]
45 source: std::io::Error,
46 },
47
48 #[error("Invalid script path '{path}': {reason}")]
50 InvalidScriptPath {
51 path: String,
53 reason: String,
55 },
56
57 #[error("Working directory '{path}' does not exist or is not accessible: {source}")]
59 InvalidWorkingDirectory {
60 path: String,
62 #[source]
64 source: std::io::Error,
65 },
66
67 #[error("Environment variable '{key}' contains invalid characters: {reason}")]
69 InvalidEnvironmentVariable {
70 key: String,
72 reason: String,
74 },
75
76 #[error("Failed to spawn process: {source}")]
78 ProcessSpawnError {
79 #[source]
81 source: std::io::Error,
82 },
83
84 #[error("Failed to wait for process completion: {source}")]
86 ProcessWaitError {
87 #[source]
89 source: std::io::Error,
90 },
91
92 #[error("Failed to capture process output: {source}")]
94 OutputCaptureError {
95 #[source]
97 source: std::io::Error,
98 },
99
100 #[error("Permission denied when executing script. Check file permissions and execution rights.")]
102 PermissionDenied,
103
104 #[error("Script execution was interrupted by signal {signal}")]
106 Interrupted {
107 signal: i32,
109 },
110
111 #[error("Resource limit exceeded: {resource} ({limit})")]
113 ResourceLimitExceeded {
114 resource: String,
116 limit: String,
118 },
119
120 #[error("I/O operation failed: {context}")]
122 IoError {
123 context: String,
125 #[source]
127 source: std::io::Error,
128 },
129}
130
131impl Error {
132 pub fn command_failed<S: Into<String>>(message: S) -> Self {
134 Self::CommandFailed {
135 message: message.into(),
136 }
137 }
138
139 pub fn timeout(duration: Duration) -> Self {
141 Self::Timeout { duration }
142 }
143
144 pub fn script_write_error(source: std::io::Error) -> Self {
146 Self::ScriptWriteError { source }
147 }
148
149 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 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 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 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 pub fn process_spawn_error(source: std::io::Error) -> Self {
183 Self::ProcessSpawnError { source }
184 }
185
186 pub fn process_wait_error(source: std::io::Error) -> Self {
188 Self::ProcessWaitError { source }
189 }
190
191 pub fn output_capture_error(source: std::io::Error) -> Self {
193 Self::OutputCaptureError { source }
194 }
195
196 pub fn interrupted(signal: i32) -> Self {
198 Self::Interrupted { signal }
199 }
200
201 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 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 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 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) },
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}