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