Skip to main content

snip_it/
error.rs

1//! Error types and handling for snp.
2//!
3//! This module defines the [`SnipError`] enum which categorizes all errors
4//! that can occur during snp operations. Errors are grouped by domain:
5//! I/O operations, TOML parsing, clipboard access, command execution, and runtime errors.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use snip_it::error::{SnipError, SnipResult};
11//!
12//! fn read_config() -> SnipResult<String> {
13//!     std::fs::read_to_string("config.toml")
14//!         .map_err(|e| SnipError::io_error("read config", "config.toml", e))
15//! }
16//! ```
17
18use std::fmt;
19use std::io;
20use std::path::PathBuf;
21
22/// All possible errors that can occur in snp.
23///
24/// Errors are categorized by domain to make debugging and handling easier.
25/// Each variant includes context about the operation that failed.
26#[derive(Debug)]
27#[non_exhaustive]
28pub enum SnipError {
29    /// I/O operation failures.
30    ///
31    /// Includes file read/write errors, directory creation failures, etc.
32    Io {
33        operation: String,
34        path: Option<PathBuf>,
35        source: io::Error,
36    },
37
38    /// TOML parsing or serialization errors.
39    ///
40    /// Indicates malformed TOML content or serialization failures.
41    Toml {
42        operation: String,
43        source: Box<dyn std::error::Error + Send + Sync>,
44    },
45
46    /// Clipboard operation failures.
47    ///
48    /// Includes clipboard access errors and content transfer failures.
49    Clipboard { operation: String, message: String },
50
51    /// Command execution failures.
52    ///
53    /// Indicates errors when spawning or running external commands.
54    Command {
55        command: String,
56        args: Vec<String>,
57        source: io::Error,
58    },
59
60    /// Runtime errors during operation.
61    ///
62    /// General-purpose errors for sync failures, validation errors, etc.
63    Runtime {
64        message: String,
65        detail: Option<String>,
66    },
67}
68
69impl fmt::Display for SnipError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            SnipError::Io {
73                operation,
74                path,
75                source,
76            } => {
77                let path_info = match path {
78                    Some(p) => format!(" (path: {})", p.display()),
79                    None => String::new(),
80                };
81                write!(f, "I/O error during {operation}: {source}{path_info}")
82            }
83            SnipError::Toml { operation, source } => {
84                write!(f, "TOML error during {operation}: {source}")
85            }
86            SnipError::Clipboard { operation, message } => {
87                write!(f, "Clipboard error during {operation}: {message}")
88            }
89            SnipError::Command {
90                command,
91                args,
92                source,
93            } => {
94                let args_display: Vec<String> = args
95                    .iter()
96                    .map(|a| {
97                        if a.chars().count() > 40 {
98                            let truncated: String = a.chars().take(37).collect();
99                            format!("{truncated}...")
100                        } else {
101                            a.clone()
102                        }
103                    })
104                    .collect();
105                let args_str = args_display.join(" ");
106                write!(
107                    f,
108                    "Command execution error: '{command}' {args_str} - {source}"
109                )
110            }
111            SnipError::Runtime { message, detail } => {
112                let detail_str = detail
113                    .as_ref()
114                    .map(|d| format!(": {d}"))
115                    .unwrap_or_default();
116                write!(f, "Runtime error: {message}{detail_str}")
117            }
118        }
119    }
120}
121
122impl std::error::Error for SnipError {
123    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124        match self {
125            SnipError::Io { source, .. } => Some(source),
126            SnipError::Toml { source, .. } => Some(&**source),
127            SnipError::Command { source, .. } => Some(source),
128            _ => None,
129        }
130    }
131}
132
133impl From<io::Error> for SnipError {
134    fn from(error: io::Error) -> Self {
135        let operation = match error.kind() {
136            io::ErrorKind::NotFound => "file not found",
137            io::ErrorKind::PermissionDenied => "permission denied",
138            io::ErrorKind::AlreadyExists => "file already exists",
139            io::ErrorKind::InvalidInput => "invalid input",
140            io::ErrorKind::InvalidData => "invalid data",
141            io::ErrorKind::UnexpectedEof => "unexpected end of file",
142            _ => "I/O operation",
143        };
144        SnipError::Io {
145            operation: operation.to_string(),
146            path: None,
147            source: error,
148        }
149    }
150}
151
152// Convenient error constructors
153impl SnipError {
154    /// Create an I/O error with operation context and optional file path.
155    pub fn io_error(operation: &str, path: impl Into<PathBuf>, source: io::Error) -> Self {
156        SnipError::Io {
157            operation: operation.to_string(),
158            path: Some(path.into()),
159            source,
160        }
161    }
162
163    /// Create a TOML parsing or serialization error with operation context.
164    pub fn toml_error(
165        operation: &str,
166        source: impl std::error::Error + Send + Sync + 'static,
167    ) -> Self {
168        SnipError::Toml {
169            operation: operation.to_string(),
170            source: Box::new(source),
171        }
172    }
173
174    /// Create a clipboard error with operation context and message.
175    pub fn clipboard_error(operation: &str, message: impl Into<String>) -> Self {
176        SnipError::Clipboard {
177            operation: operation.to_string(),
178            message: message.into(),
179        }
180    }
181
182    /// Create a command execution error with command name, arguments, and source error.
183    pub fn command_error(command: &str, args: Vec<String>, source: io::Error) -> Self {
184        SnipError::Command {
185            command: command.to_string(),
186            args,
187            source,
188        }
189    }
190
191    /// Create a runtime error with a message and optional detail string.
192    pub fn runtime_error(message: &str, detail: Option<&str>) -> Self {
193        SnipError::Runtime {
194            message: message.to_string(),
195            detail: detail.map(ToString::to_string),
196        }
197    }
198}
199
200/// Convenient Result type
201pub type SnipResult<T> = Result<T, SnipError>;
202
203impl From<crate::encryption::CryptoError> for SnipError {
204    fn from(e: crate::encryption::CryptoError) -> Self {
205        SnipError::Runtime {
206            message: "Encryption operation failed".to_string(),
207            detail: Some(e.to_string()),
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use std::error::Error;
216
217    #[test]
218    fn test_io_error_display() {
219        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
220        let err = SnipError::io_error("read config", "/etc/config.toml", io_err);
221        let msg = err.to_string();
222        assert!(msg.contains("read config"));
223        assert!(msg.contains("/etc/config.toml"));
224    }
225
226    #[test]
227    fn test_toml_error_display() {
228        let toml_err = toml::from_str::<toml::Value>("invalid = [toml").unwrap_err();
229        let err = SnipError::toml_error("parse config", toml_err);
230        let msg = err.to_string();
231        assert!(msg.contains("parse config"));
232    }
233
234    #[test]
235    fn test_clipboard_error_display() {
236        let err = SnipError::clipboard_error("copy to clipboard", "no clipboard available");
237        let msg = err.to_string();
238        assert!(msg.contains("copy to clipboard"));
239        assert!(msg.contains("no clipboard available"));
240    }
241
242    #[test]
243    fn test_command_error_display() {
244        let io_err = io::Error::new(io::ErrorKind::NotFound, "command not found");
245        let err = SnipError::command_error("git", vec!["status".to_string()], io_err);
246        let msg = err.to_string();
247        assert!(msg.contains("git"));
248        assert!(msg.contains("status"));
249    }
250
251    #[test]
252    fn test_runtime_error_display_with_detail() {
253        let err = SnipError::runtime_error("sync failed", Some("server unavailable"));
254        let msg = err.to_string();
255        assert!(msg.contains("sync failed"));
256        assert!(msg.contains("server unavailable"));
257    }
258
259    #[test]
260    fn test_runtime_error_display_without_detail() {
261        let err = SnipError::runtime_error("sync failed", None);
262        let msg = err.to_string();
263        assert!(msg.contains("sync failed"));
264        assert!(!msg.contains("server unavailable"));
265    }
266
267    #[test]
268    fn test_from_io_error() {
269        let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
270        let err: SnipError = io_err.into();
271        let msg = err.to_string();
272        assert!(msg.contains("permission denied"));
273    }
274
275    #[test]
276    fn test_from_io_error_not_found() {
277        let io_err = io::Error::new(io::ErrorKind::NotFound, "no such file");
278        let err: SnipError = io_err.into();
279        let msg = err.to_string();
280        assert!(msg.contains("file not found"));
281    }
282
283    #[test]
284    fn test_error_source_io() {
285        let io_err = io::Error::other("test");
286        let err = SnipError::io_error("op", "path", io_err);
287        assert!(err.source().is_some());
288    }
289
290    #[test]
291    fn test_error_source_toml() {
292        let toml_err = toml::from_str::<toml::Value>("invalid = [toml").unwrap_err();
293        let err = SnipError::toml_error("op", toml_err);
294        assert!(err.source().is_some());
295    }
296
297    #[test]
298    fn test_error_source_runtime() {
299        let err = SnipError::runtime_error("msg", None);
300        assert!(err.source().is_none());
301    }
302}