raz_common/
error.rs

1//! Common error types used across raz crates
2
3use thiserror::Error;
4
5/// Common error type for raz operations
6#[derive(Error, Debug)]
7pub enum CommonError {
8    /// Shell command parsing error
9    #[error("Failed to parse shell command: {0}")]
10    ShellParse(String),
11
12    /// Command execution error
13    #[error("Command execution failed: {0}")]
14    CommandExecution(String),
15
16    /// File system operation error
17    #[error("File system error: {0}")]
18    FileSystem(#[from] std::io::Error),
19
20    /// JSON serialization/deserialization error
21    #[error("JSON error: {0}")]
22    Json(#[from] serde_json::Error),
23
24    /// Generic error with context
25    #[error("{context}: {source}")]
26    WithContext {
27        context: String,
28        #[source]
29        source: Box<dyn std::error::Error + Send + Sync>,
30    },
31
32    /// Other errors
33    #[error(transparent)]
34    Other(#[from] anyhow::Error),
35}
36
37/// Common result type
38pub type Result<T> = std::result::Result<T, CommonError>;
39
40impl CommonError {
41    /// Create an error with additional context
42    pub fn with_context<E>(context: impl Into<String>, error: E) -> Self
43    where
44        E: std::error::Error + Send + Sync + 'static,
45    {
46        Self::WithContext {
47            context: context.into(),
48            source: Box::new(error),
49        }
50    }
51
52    /// Create a shell parse error
53    pub fn shell_parse(msg: impl Into<String>) -> Self {
54        Self::ShellParse(msg.into())
55    }
56
57    /// Create a command execution error
58    pub fn command_execution(msg: impl Into<String>) -> Self {
59        Self::CommandExecution(msg.into())
60    }
61}
62
63/// Extension trait for adding context to Results
64pub trait ErrorContext<T> {
65    /// Add context to an error
66    fn context(self, context: impl Into<String>) -> Result<T>;
67
68    /// Add context with a closure (only called on error)
69    fn with_context<F>(self, f: F) -> Result<T>
70    where
71        F: FnOnce() -> String;
72}
73
74impl<T, E> ErrorContext<T> for std::result::Result<T, E>
75where
76    E: std::error::Error + Send + Sync + 'static,
77{
78    fn context(self, context: impl Into<String>) -> Result<T> {
79        self.map_err(|e| CommonError::with_context(context, e))
80    }
81
82    fn with_context<F>(self, f: F) -> Result<T>
83    where
84        F: FnOnce() -> String,
85    {
86        self.map_err(|e| CommonError::with_context(f(), e))
87    }
88}
89
90/// Extension trait for adding context to Options
91pub trait OptionContext<T> {
92    /// Convert None to an error with context
93    fn context(self, context: impl Into<String>) -> Result<T>;
94}
95
96impl<T> OptionContext<T> for Option<T> {
97    fn context(self, context: impl Into<String>) -> Result<T> {
98        self.ok_or_else(|| CommonError::Other(anyhow::anyhow!(context.into())))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_error_context() {
108        let result: std::result::Result<i32, std::io::Error> = Err(std::io::Error::new(
109            std::io::ErrorKind::NotFound,
110            "file not found",
111        ));
112
113        let with_context = result.context("Failed to read config file");
114        assert!(with_context.is_err());
115
116        let err = with_context.unwrap_err();
117        match err {
118            CommonError::WithContext { context, .. } => {
119                assert_eq!(context, "Failed to read config file");
120            }
121            _ => panic!("Expected WithContext error"),
122        }
123    }
124
125    #[test]
126    fn test_option_context() {
127        let value: Option<i32> = None;
128        let result = value.context("Value not found");
129
130        assert!(result.is_err());
131        let err = result.unwrap_err();
132        assert!(matches!(err, CommonError::Other(_)));
133    }
134}