Skip to main content

twin_cli/core/
error.rs

1#![allow(dead_code)]
2use std::path::PathBuf;
3use thiserror::Error;
4
5/// アプリケーション全体で使用するResult型
6pub type TwinResult<T> = Result<T, TwinError>;
7
8/// Twin アプリケーションのエラー型
9/// thiserrorを使って、エラーメッセージの自動生成とFrom実装を行う
10#[derive(Error, Debug)]
11pub enum TwinError {
12    /// Git操作に関するエラー
13    #[error("Git error: {message}")]
14    Git {
15        message: String,
16        #[source]
17        source: Option<Box<dyn std::error::Error + Send + Sync>>,
18    },
19
20    /// シンボリックリンク操作に関するエラー
21    #[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    /// 設定ファイルに関するエラー
30    #[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    /// 環境管理に関するエラー
39    #[error("Environment error: {message}")]
40    Environment {
41        message: String,
42        agent_name: Option<String>,
43    },
44
45    /// ファイルシステム操作エラー
46    #[error("IO error: {message}")]
47    Io {
48        message: String,
49        path: Option<PathBuf>,
50        #[source]
51        source: Option<std::io::Error>,
52    },
53
54    /// 並行実行制御エラー(ロック取得失敗など)
55    #[error("Lock error: {message}")]
56    Lock {
57        message: String,
58        lock_path: Option<PathBuf>,
59    },
60
61    /// フック実行エラー
62    #[error("Hook execution failed: {message}")]
63    Hook {
64        message: String,
65        hook_type: String,
66        exit_code: Option<i32>,
67    },
68
69    /// 既に存在するエラー
70    #[error("{resource} already exists: {name}")]
71    AlreadyExists { resource: String, name: String },
72
73    /// 見つからないエラー
74    #[error("{resource} not found: {name}")]
75    NotFound { resource: String, name: String },
76
77    /// 無効な引数エラー
78    #[error("Invalid argument: {message}")]
79    InvalidArgument { message: String },
80
81    /// その他のエラー
82    #[error("{0}")]
83    Other(String),
84}
85
86impl TwinError {
87    /// Git関連のエラーを作成
88    pub fn git(message: impl Into<String>) -> Self {
89        Self::Git {
90            message: message.into(),
91            source: None,
92        }
93    }
94
95    /// シンボリックリンク関連のエラーを作成
96    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    /// 環境関連のエラーを作成
105    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    /// 既に存在するエラーを作成
113    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    /// 見つからないエラーを作成
121    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
129/// 標準のIOエラーからの変換
130impl 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
140/// git2ライブラリのエラーからの変換
141impl 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
150/// anyhowエラーからの変換
151impl From<anyhow::Error> for TwinError {
152    fn from(err: anyhow::Error) -> Self {
153        Self::Other(err.to_string())
154    }
155}
156
157/// TOML解析エラーからの変換
158impl 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
168/// TOML シリアライズエラーからの変換
169impl 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
179/// JSON解析エラーからの変換
180impl 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    /// 設定関連のエラーを作成
192    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    /// IO関連のエラーを作成
201    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    /// ロック関連のエラーを作成
210    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    /// フック関連のエラーを作成
218    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    /// 無効な引数エラーを作成
231    pub fn invalid_argument(message: impl Into<String>) -> Self {
232        Self::InvalidArgument {
233            message: message.into(),
234        }
235    }
236
237    /// その他のエラーを作成
238    pub fn other(message: impl Into<String>) -> Self {
239        Self::Other(message.into())
240    }
241
242    /// エラーがリトライ可能かどうかを判定
243    pub fn is_retryable(&self) -> bool {
244        matches!(self, Self::Lock { .. } | Self::Io { .. })
245    }
246
247    /// エラーが致命的かどうかを判定
248    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        // Display実装のテスト
270        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        // Display実装のテスト
317        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}