Skip to main content

soar_utils/
error.rs

1//! Error types for soar-utils.
2
3use std::path::PathBuf;
4
5use miette::Diagnostic;
6use thiserror::Error;
7
8/// Error type for byte parsing operations.
9#[derive(Error, Diagnostic, Debug)]
10pub enum BytesError {
11    #[error("Failed to parse '{input}' as bytes: {reason}")]
12    #[diagnostic(
13        code(soar_utils::bytes::parse),
14        help("Use a valid byte format like '1KB', '2MB', or '3GB'")
15    )]
16    ParseFailed { input: String, reason: String },
17}
18
19/// Error type for hash operations.
20#[derive(Error, Diagnostic, Debug)]
21pub enum HashError {
22    #[error("Failed to read file '{path}'")]
23    #[diagnostic(
24        code(soar_utils::hash::read),
25        help("Check if the file exists and you have read permissions")
26    )]
27    ReadFailed {
28        path: PathBuf,
29        #[source]
30        source: std::io::Error,
31    },
32}
33
34/// Errors that can occur when working with locks.
35#[derive(Debug, Diagnostic, Error)]
36pub enum LockError {
37    #[error(transparent)]
38    #[diagnostic(
39        code(soar_utils::lock::io),
40        help("Check if you have write permissions to the lock directory")
41    )]
42    Io(#[from] std::io::Error),
43
44    #[error("Failed to acquire lock for '{0}'")]
45    #[diagnostic(
46        code(soar_utils::lock::acquire_failed),
47        help("Check if the lock directory exists and you have write permissions")
48    )]
49    AcquireFailed(String),
50}
51
52/// Error type for path operations.
53#[derive(Error, Diagnostic, Debug)]
54pub enum PathError {
55    #[error("Failed to get current directory")]
56    #[diagnostic(
57        code(soar_utils::path::cwd),
58        help("Check if the current directory still exists")
59    )]
60    FailedToGetCurrentDir {
61        #[source]
62        source: std::io::Error,
63    },
64
65    #[error("Path is empty")]
66    #[diagnostic(code(soar_utils::path::empty), help("Provide a non-empty path"))]
67    Empty,
68
69    #[error("Environment variable '{var}' not set in '{input}'")]
70    #[diagnostic(
71        code(soar_utils::path::env_var),
72        help("Set the environment variable or use a different path")
73    )]
74    MissingEnvVar { var: String, input: String },
75
76    #[error("Unclosed variable expression starting at '{input}'")]
77    #[diagnostic(
78        code(soar_utils::path::unclosed_var),
79        help("Close the variable expression with '}}'")
80    )]
81    UnclosedVariable { input: String },
82}
83
84/// Error type for filesystem operations.
85#[derive(Error, Diagnostic, Debug)]
86pub enum FileSystemError {
87    #[error("Failed to read file '{path}'")]
88    #[diagnostic(
89        code(soar_utils::fs::read_file),
90        help("Check if the file exists and you have read permissions")
91    )]
92    ReadFile {
93        path: PathBuf,
94        #[source]
95        source: std::io::Error,
96    },
97
98    #[error("Failed to write file '{path}'")]
99    #[diagnostic(
100        code(soar_utils::fs::write_file),
101        help("Check if you have write permissions to the directory")
102    )]
103    WriteFile {
104        path: PathBuf,
105        #[source]
106        source: std::io::Error,
107    },
108
109    #[error("Failed to create file '{path}'")]
110    #[diagnostic(
111        code(soar_utils::fs::create_file),
112        help("Check if the directory exists and you have write permissions")
113    )]
114    CreateFile {
115        path: PathBuf,
116        #[source]
117        source: std::io::Error,
118    },
119
120    #[error("Failed to remove file '{path}'")]
121    #[diagnostic(
122        code(soar_utils::fs::remove_file),
123        help("Check if you have write permissions to the file")
124    )]
125    RemoveFile {
126        path: PathBuf,
127        #[source]
128        source: std::io::Error,
129    },
130
131    #[error("Failed to read directory '{path}'")]
132    #[diagnostic(
133        code(soar_utils::fs::read_dir),
134        help("Check if the directory exists and you have read permissions")
135    )]
136    ReadDirectory {
137        path: PathBuf,
138        #[source]
139        source: std::io::Error,
140    },
141
142    #[error("Failed to create directory '{path}'")]
143    #[diagnostic(
144        code(soar_utils::fs::create_dir),
145        help("Check if the parent directory exists and you have write permissions")
146    )]
147    CreateDirectory {
148        path: PathBuf,
149        #[source]
150        source: std::io::Error,
151    },
152
153    #[error("Failed to remove directory '{path}'")]
154    #[diagnostic(
155        code(soar_utils::fs::remove_dir),
156        help("Check if the directory is empty and you have write permissions")
157    )]
158    RemoveDirectory {
159        path: PathBuf,
160        #[source]
161        source: std::io::Error,
162    },
163
164    #[error("Failed to create symlink from '{from}' to '{target}'")]
165    #[diagnostic(
166        code(soar_utils::fs::create_symlink),
167        help("Check if you have write permissions and the target doesn't already exist")
168    )]
169    CreateSymlink {
170        from: PathBuf,
171        target: PathBuf,
172        #[source]
173        source: std::io::Error,
174    },
175
176    #[error("Failed to remove symlink '{path}'")]
177    #[diagnostic(
178        code(soar_utils::fs::remove_symlink),
179        help("Check if you have write permissions")
180    )]
181    RemoveSymlink {
182        path: PathBuf,
183        #[source]
184        source: std::io::Error,
185    },
186
187    #[error("Failed to read symlink '{path}'")]
188    #[diagnostic(
189        code(soar_utils::fs::read_symlink),
190        help("Check if the symlink exists")
191    )]
192    ReadSymlink {
193        path: PathBuf,
194        #[source]
195        source: std::io::Error,
196    },
197
198    #[error("Path '{path}' not found")]
199    #[diagnostic(code(soar_utils::fs::not_found), help("Check if the path exists"))]
200    NotFound { path: PathBuf },
201
202    #[error("'{path}' is not a directory")]
203    #[diagnostic(code(soar_utils::fs::not_a_dir), help("Provide a path to a directory"))]
204    NotADirectory { path: PathBuf },
205
206    #[diagnostic(code(soar_utils::fs::not_a_file), help("Provide a path to a file"))]
207    #[error("'{path}' is not a file")]
208    NotAFile { path: PathBuf },
209}
210
211/// Context for filesystem operations.
212pub struct IoContext {
213    path: PathBuf,
214    operation: IoOperation,
215}
216
217/// Type of filesystem operation.
218#[derive(Debug, Clone)]
219pub enum IoOperation {
220    ReadFile,
221    WriteFile,
222    CreateFile,
223    RemoveFile,
224    CreateDirectory,
225    RemoveDirectory,
226    ReadDirectory,
227    CreateSymlink { target: PathBuf },
228    RemoveSymlink,
229    ReadSymlink,
230}
231
232impl IoContext {
233    pub fn new(path: PathBuf, operation: IoOperation) -> Self {
234        Self {
235            path,
236            operation,
237        }
238    }
239
240    pub fn read_file<P: Into<PathBuf>>(path: P) -> Self {
241        Self::new(path.into(), IoOperation::ReadFile)
242    }
243
244    pub fn write_file<P: Into<PathBuf>>(path: P) -> Self {
245        Self::new(path.into(), IoOperation::WriteFile)
246    }
247
248    pub fn create_file<P: Into<PathBuf>>(path: P) -> Self {
249        Self::new(path.into(), IoOperation::CreateFile)
250    }
251
252    pub fn remove_file<P: Into<PathBuf>>(path: P) -> Self {
253        Self::new(path.into(), IoOperation::RemoveFile)
254    }
255
256    pub fn read_directory<P: Into<PathBuf>>(path: P) -> Self {
257        Self::new(path.into(), IoOperation::ReadDirectory)
258    }
259
260    pub fn create_directory<P: Into<PathBuf>>(path: P) -> Self {
261        Self::new(path.into(), IoOperation::CreateDirectory)
262    }
263
264    pub fn remove_directory<P: Into<PathBuf>>(path: P) -> Self {
265        Self::new(path.into(), IoOperation::RemoveDirectory)
266    }
267
268    pub fn read_symlink<P: Into<PathBuf>>(path: P) -> Self {
269        Self::new(path.into(), IoOperation::ReadSymlink)
270    }
271
272    pub fn create_symlink<P: Into<PathBuf>, T: Into<PathBuf>>(from: P, target: T) -> Self {
273        Self::new(
274            from.into(),
275            IoOperation::CreateSymlink {
276                target: target.into(),
277            },
278        )
279    }
280
281    pub fn remove_symlink<P: Into<PathBuf>>(path: P) -> Self {
282        Self::new(path.into(), IoOperation::RemoveSymlink)
283    }
284
285    pub fn operation(&self) -> &IoOperation {
286        &self.operation
287    }
288}
289
290impl From<(IoContext, std::io::Error)> for FileSystemError {
291    fn from((ctx, source): (IoContext, std::io::Error)) -> Self {
292        match ctx.operation {
293            IoOperation::ReadFile => {
294                FileSystemError::ReadFile {
295                    path: ctx.path,
296                    source,
297                }
298            }
299            IoOperation::WriteFile => {
300                FileSystemError::WriteFile {
301                    path: ctx.path,
302                    source,
303                }
304            }
305            IoOperation::CreateFile => {
306                FileSystemError::CreateFile {
307                    path: ctx.path,
308                    source,
309                }
310            }
311            IoOperation::RemoveFile => {
312                FileSystemError::RemoveFile {
313                    path: ctx.path,
314                    source,
315                }
316            }
317            IoOperation::CreateDirectory => {
318                FileSystemError::CreateDirectory {
319                    path: ctx.path,
320                    source,
321                }
322            }
323            IoOperation::RemoveDirectory => {
324                FileSystemError::RemoveDirectory {
325                    path: ctx.path,
326                    source,
327                }
328            }
329            IoOperation::ReadDirectory => {
330                FileSystemError::ReadDirectory {
331                    path: ctx.path,
332                    source,
333                }
334            }
335            IoOperation::CreateSymlink {
336                target,
337            } => {
338                FileSystemError::CreateSymlink {
339                    from: ctx.path,
340                    target,
341                    source,
342                }
343            }
344            IoOperation::RemoveSymlink => {
345                FileSystemError::RemoveSymlink {
346                    path: ctx.path,
347                    source,
348                }
349            }
350            IoOperation::ReadSymlink => {
351                FileSystemError::ReadSymlink {
352                    path: ctx.path,
353                    source,
354                }
355            }
356        }
357    }
358}
359
360/// Extension trait for adding path context to IO results.
361pub trait IoResultExt<T> {
362    fn with_path<P: Into<PathBuf>>(self, path: P, operation: IoOperation) -> FileSystemResult<T>;
363}
364
365impl<T> IoResultExt<T> for std::io::Result<T> {
366    fn with_path<P: Into<PathBuf>>(self, path: P, operation: IoOperation) -> FileSystemResult<T> {
367        self.map_err(|e| {
368            let ctx = IoContext::new(path.into(), operation);
369            (ctx, e).into()
370        })
371    }
372}
373
374/// Combined error type for all utils errors.
375#[derive(Error, Diagnostic, Debug)]
376pub enum UtilsError {
377    #[error(transparent)]
378    #[diagnostic(transparent)]
379    Bytes(#[from] BytesError),
380
381    #[error(transparent)]
382    #[diagnostic(transparent)]
383    Lock(#[from] LockError),
384
385    #[error(transparent)]
386    #[diagnostic(transparent)]
387    Path(#[from] PathError),
388
389    #[error(transparent)]
390    #[diagnostic(transparent)]
391    FileSystem(#[from] FileSystemError),
392
393    #[error(transparent)]
394    #[diagnostic(transparent)]
395    Hash(#[from] HashError),
396}
397
398pub type BytesResult<T> = std::result::Result<T, BytesError>;
399pub type FileSystemResult<T> = std::result::Result<T, FileSystemError>;
400pub type HashResult<T> = std::result::Result<T, HashError>;
401pub type LockResult<T> = std::result::Result<T, LockError>;
402pub type PathResult<T> = std::result::Result<T, PathError>;
403pub type UtilsResult<T> = std::result::Result<T, UtilsError>;
404
405#[cfg(test)]
406mod tests {
407    use std::io;
408
409    use super::*;
410
411    #[test]
412    fn test_bytes_error_display() {
413        let error = BytesError::ParseFailed {
414            input: "test".to_string(),
415            reason: "invalid".to_string(),
416        };
417        assert_eq!(
418            error.to_string(),
419            "Failed to parse 'test' as bytes: invalid"
420        );
421    }
422
423    #[test]
424    fn test_hash_error_display_and_source() {
425        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
426        let error = HashError::ReadFailed {
427            path: PathBuf::from("/test"),
428            source: io_error,
429        };
430        assert_eq!(error.to_string(), "Failed to read file '/test'");
431    }
432
433    #[test]
434    fn test_path_error_display() {
435        let empty_error = PathError::Empty;
436        assert_eq!(empty_error.to_string(), "Path is empty");
437
438        let missing_env_var_error = PathError::MissingEnvVar {
439            var: "VAR".to_string(),
440            input: "$VAR".to_string(),
441        };
442        assert_eq!(
443            missing_env_var_error.to_string(),
444            "Environment variable 'VAR' not set in '$VAR'"
445        );
446
447        let unclosed_variable_error = PathError::UnclosedVariable {
448            input: "${VAR".to_string(),
449        };
450        assert_eq!(
451            unclosed_variable_error.to_string(),
452            "Unclosed variable expression starting at '${VAR'"
453        );
454    }
455
456    #[test]
457    fn test_file_system_error_display() {
458        let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "permission denied");
459        let file_error = FileSystemError::ReadFile {
460            path: PathBuf::from("/file"),
461            source: io_error,
462        };
463        assert_eq!(file_error.to_string(), "Failed to read file '/file'");
464
465        let not_a_dir_error = FileSystemError::NotADirectory {
466            path: PathBuf::from("/path"),
467        };
468        assert_eq!(not_a_dir_error.to_string(), "'/path' is not a directory");
469    }
470}