1use std::fmt;
19use std::io;
20use std::path::PathBuf;
21
22#[derive(Debug)]
27#[non_exhaustive]
28pub enum SnipError {
29 Io {
33 operation: String,
34 path: Option<PathBuf>,
35 source: io::Error,
36 },
37
38 Toml {
42 operation: String,
43 source: Box<dyn std::error::Error + Send + Sync>,
44 },
45
46 Clipboard { operation: String, message: String },
50
51 Command {
55 command: String,
56 args: Vec<String>,
57 source: io::Error,
58 },
59
60 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
152impl SnipError {
154 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 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 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 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 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
200pub 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}