1#![allow(dead_code)]
2use std::path::PathBuf;
3use thiserror::Error;
4
5pub type TwinResult<T> = Result<T, TwinError>;
7
8#[derive(Error, Debug)]
11pub enum TwinError {
12 #[error("Git error: {message}")]
14 Git {
15 message: String,
16 #[source]
17 source: Option<Box<dyn std::error::Error + Send + Sync>>,
18 },
19
20 #[error("Symlink error: {message}")]
22 Symlink {
23 message: String,
24 path: Option<PathBuf>,
25 #[source]
26 source: Option<Box<dyn std::error::Error + Send + Sync>>,
27 },
28
29 #[error("Config error: {message}")]
31 Config {
32 message: String,
33 path: Option<PathBuf>,
34 #[source]
35 source: Option<Box<dyn std::error::Error + Send + Sync>>,
36 },
37
38 #[error("Environment error: {message}")]
40 Environment {
41 message: String,
42 agent_name: Option<String>,
43 },
44
45 #[error("IO error: {message}")]
47 Io {
48 message: String,
49 path: Option<PathBuf>,
50 #[source]
51 source: Option<std::io::Error>,
52 },
53
54 #[error("Lock error: {message}")]
56 Lock {
57 message: String,
58 lock_path: Option<PathBuf>,
59 },
60
61 #[error("Hook execution failed: {message}")]
63 Hook {
64 message: String,
65 hook_type: String,
66 exit_code: Option<i32>,
67 },
68
69 #[error("{resource} already exists: {name}")]
71 AlreadyExists { resource: String, name: String },
72
73 #[error("{resource} not found: {name}")]
75 NotFound { resource: String, name: String },
76
77 #[error("Invalid argument: {message}")]
79 InvalidArgument { message: String },
80
81 #[error("{0}")]
83 Other(String),
84}
85
86impl TwinError {
87 pub fn git(message: impl Into<String>) -> Self {
89 Self::Git {
90 message: message.into(),
91 source: None,
92 }
93 }
94
95 pub fn symlink(message: impl Into<String>, path: Option<PathBuf>) -> Self {
97 Self::Symlink {
98 message: message.into(),
99 path,
100 source: None,
101 }
102 }
103
104 pub fn environment(message: impl Into<String>, agent_name: Option<String>) -> Self {
106 Self::Environment {
107 message: message.into(),
108 agent_name,
109 }
110 }
111
112 pub fn already_exists(resource: impl Into<String>, name: impl Into<String>) -> Self {
114 Self::AlreadyExists {
115 resource: resource.into(),
116 name: name.into(),
117 }
118 }
119
120 pub fn not_found(resource: impl Into<String>, name: impl Into<String>) -> Self {
122 Self::NotFound {
123 resource: resource.into(),
124 name: name.into(),
125 }
126 }
127}
128
129impl From<std::io::Error> for TwinError {
131 fn from(err: std::io::Error) -> Self {
132 Self::Io {
133 message: err.to_string(),
134 path: None,
135 source: Some(err),
136 }
137 }
138}
139
140impl From<git2::Error> for TwinError {
142 fn from(err: git2::Error) -> Self {
143 Self::Git {
144 message: err.to_string(),
145 source: Some(Box::new(err)),
146 }
147 }
148}
149
150impl From<anyhow::Error> for TwinError {
152 fn from(err: anyhow::Error) -> Self {
153 Self::Other(err.to_string())
154 }
155}
156
157impl From<toml::de::Error> for TwinError {
159 fn from(err: toml::de::Error) -> Self {
160 Self::Config {
161 message: format!("Failed to parse TOML: {err}"),
162 path: None,
163 source: Some(Box::new(err)),
164 }
165 }
166}
167
168impl From<toml::ser::Error> for TwinError {
170 fn from(err: toml::ser::Error) -> Self {
171 Self::Config {
172 message: format!("Failed to serialize TOML: {err}"),
173 path: None,
174 source: Some(Box::new(err)),
175 }
176 }
177}
178
179impl From<serde_json::Error> for TwinError {
181 fn from(err: serde_json::Error) -> Self {
182 Self::Config {
183 message: format!("Failed to parse/serialize JSON: {err}"),
184 path: None,
185 source: Some(Box::new(err)),
186 }
187 }
188}
189
190impl TwinError {
191 pub fn config(message: impl Into<String>, path: Option<PathBuf>) -> Self {
193 Self::Config {
194 message: message.into(),
195 path,
196 source: None,
197 }
198 }
199
200 pub fn io(message: impl Into<String>, path: Option<PathBuf>) -> Self {
202 Self::Io {
203 message: message.into(),
204 path,
205 source: None,
206 }
207 }
208
209 pub fn lock(message: impl Into<String>, lock_path: Option<PathBuf>) -> Self {
211 Self::Lock {
212 message: message.into(),
213 lock_path,
214 }
215 }
216
217 pub fn hook(
219 message: impl Into<String>,
220 hook_type: impl Into<String>,
221 exit_code: Option<i32>,
222 ) -> Self {
223 Self::Hook {
224 message: message.into(),
225 hook_type: hook_type.into(),
226 exit_code,
227 }
228 }
229
230 pub fn invalid_argument(message: impl Into<String>) -> Self {
232 Self::InvalidArgument {
233 message: message.into(),
234 }
235 }
236
237 pub fn other(message: impl Into<String>) -> Self {
239 Self::Other(message.into())
240 }
241
242 pub fn is_retryable(&self) -> bool {
244 matches!(self, Self::Lock { .. } | Self::Io { .. })
245 }
246
247 pub fn is_fatal(&self) -> bool {
249 !matches!(self, Self::Hook { .. } | Self::Lock { .. })
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use std::io;
257
258 #[test]
259 fn test_twin_error_git() {
260 let error = TwinError::git("Failed to checkout branch");
261 match &error {
262 TwinError::Git { message, source } => {
263 assert_eq!(message, "Failed to checkout branch");
264 assert!(source.is_none());
265 }
266 _ => panic!("Expected Git error"),
267 }
268
269 let display_str = format!("{error}");
271 assert!(display_str.contains("Git error"));
272 assert!(display_str.contains("Failed to checkout branch"));
273 }
274
275 #[test]
276 fn test_twin_error_symlink() {
277 let path = PathBuf::from("/tmp/test.txt");
278 let error = TwinError::symlink("Failed to create symlink", Some(path.clone()));
279
280 match error {
281 TwinError::Symlink {
282 message,
283 path: p,
284 source,
285 } => {
286 assert_eq!(message, "Failed to create symlink");
287 assert_eq!(p, Some(path));
288 assert!(source.is_none());
289 }
290 _ => panic!("Expected Symlink error"),
291 }
292 }
293
294 #[test]
295 fn test_twin_error_config() {
296 let path = PathBuf::from("config.toml");
297 let error = TwinError::Config {
298 message: "Invalid TOML".to_string(),
299 path: Some(path.clone()),
300 source: None,
301 };
302
303 match &error {
304 TwinError::Config {
305 message,
306 path: p,
307 source,
308 } => {
309 assert_eq!(message, "Invalid TOML");
310 assert_eq!(p, &Some(path));
311 assert!(source.is_none());
312 }
313 _ => panic!("Expected Config error"),
314 }
315
316 let display_str = format!("{error}");
318 assert!(display_str.contains("Config error"));
319 assert!(display_str.contains("Invalid TOML"));
320 }
321
322 #[test]
323 fn test_twin_error_display() {
324 let errors = vec![
325 (TwinError::git("git error"), "Git error: git error"),
326 (
327 TwinError::symlink("symlink error", None),
328 "Symlink error: symlink error",
329 ),
330 (
331 TwinError::Environment {
332 message: "env error".to_string(),
333 agent_name: Some("agent1".to_string()),
334 },
335 "Environment error: env error",
336 ),
337 (
338 TwinError::Hook {
339 message: "hook failed".to_string(),
340 hook_type: "pre_create".to_string(),
341 exit_code: Some(1),
342 },
343 "Hook execution failed: hook failed",
344 ),
345 (
346 TwinError::AlreadyExists {
347 resource: "Environment".to_string(),
348 name: "test".to_string(),
349 },
350 "Environment already exists: test",
351 ),
352 (
353 TwinError::NotFound {
354 resource: "Branch".to_string(),
355 name: "feature".to_string(),
356 },
357 "Branch not found: feature",
358 ),
359 (
360 TwinError::InvalidArgument {
361 message: "invalid arg".to_string(),
362 },
363 "Invalid argument: invalid arg",
364 ),
365 (TwinError::Other("other error".to_string()), "other error"),
366 ];
367
368 for (error, expected) in errors {
369 let display_str = format!("{error}");
370 assert_eq!(display_str, expected);
371 }
372 }
373
374 #[test]
375 fn test_twin_error_from_io() {
376 let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
377 let twin_error = TwinError::from(io_error);
378
379 match twin_error {
380 TwinError::Io {
381 message,
382 path,
383 source,
384 } => {
385 assert!(message.contains("not found") || message.contains("File not found"));
386 assert!(path.is_none());
387 assert!(source.is_some());
388 }
389 _ => panic!("Expected Io error"),
390 }
391 }
392}