1use std::path::PathBuf;
4
5use miette::Diagnostic;
6use thiserror::Error;
7
8#[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#[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#[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#[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#[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
211pub struct IoContext {
213 path: PathBuf,
214 operation: IoOperation,
215}
216
217#[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
360pub 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#[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}