1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ErrorCode {
14 PathNotFound,
16 InvalidPath,
18 ScanError,
20 AnalysisError,
22 InvalidJson,
24 UnknownMode,
26 InvalidSettings,
28 IoError,
30 InternalError,
32 NotImplemented,
34 GitNotAvailable,
36 NotGitRepository,
38 GitOperationFailed,
40 ConfigNotFound,
42 ConfigInvalid,
44}
45
46impl fmt::Display for ErrorCode {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 ErrorCode::PathNotFound => write!(f, "path_not_found"),
50 ErrorCode::InvalidPath => write!(f, "invalid_path"),
51 ErrorCode::ScanError => write!(f, "scan_error"),
52 ErrorCode::AnalysisError => write!(f, "analysis_error"),
53 ErrorCode::InvalidJson => write!(f, "invalid_json"),
54 ErrorCode::UnknownMode => write!(f, "unknown_mode"),
55 ErrorCode::InvalidSettings => write!(f, "invalid_settings"),
56 ErrorCode::IoError => write!(f, "io_error"),
57 ErrorCode::InternalError => write!(f, "internal_error"),
58 ErrorCode::NotImplemented => write!(f, "not_implemented"),
59 ErrorCode::GitNotAvailable => write!(f, "git_not_available"),
60 ErrorCode::NotGitRepository => write!(f, "not_git_repository"),
61 ErrorCode::GitOperationFailed => write!(f, "git_operation_failed"),
62 ErrorCode::ConfigNotFound => write!(f, "config_not_found"),
63 ErrorCode::ConfigInvalid => write!(f, "config_invalid"),
64 }
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TokmdError {
71 pub code: ErrorCode,
73 pub message: String,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub details: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub suggestions: Option<Vec<String>>,
81}
82
83impl TokmdError {
84 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
86 Self {
87 code,
88 message: message.into(),
89 details: None,
90 suggestions: None,
91 }
92 }
93
94 pub fn with_details(
96 code: ErrorCode,
97 message: impl Into<String>,
98 details: impl Into<String>,
99 ) -> Self {
100 Self {
101 code,
102 message: message.into(),
103 details: Some(details.into()),
104 suggestions: None,
105 }
106 }
107
108 pub fn with_suggestions(
110 code: ErrorCode,
111 message: impl Into<String>,
112 suggestions: Vec<String>,
113 ) -> Self {
114 Self {
115 code,
116 message: message.into(),
117 details: None,
118 suggestions: Some(suggestions),
119 }
120 }
121
122 pub fn with_details_and_suggestions(
124 code: ErrorCode,
125 message: impl Into<String>,
126 details: impl Into<String>,
127 suggestions: Vec<String>,
128 ) -> Self {
129 Self {
130 code,
131 message: message.into(),
132 details: Some(details.into()),
133 suggestions: Some(suggestions),
134 }
135 }
136
137 pub fn git_not_available() -> Self {
139 Self::with_suggestions(
140 ErrorCode::GitNotAvailable,
141 "git is not available on PATH".to_string(),
142 vec![
143 "Install git from https://git-scm.com/downloads".to_string(),
144 "Ensure git is in your system PATH".to_string(),
145 "Verify installation by running: git --version".to_string(),
146 ],
147 )
148 }
149
150 pub fn not_git_repository(path: &str) -> Self {
152 Self::with_details_and_suggestions(
153 ErrorCode::NotGitRepository,
154 format!("Not inside a git repository: {}", path),
155 "The current directory is not a git repository".to_string(),
156 vec![
157 "Initialize a git repository: git init".to_string(),
158 "Navigate to a git repository directory".to_string(),
159 "Use --no-git flag to disable git features".to_string(),
160 ],
161 )
162 }
163
164 pub fn git_operation_failed(operation: &str, reason: &str) -> Self {
166 Self::with_details(
167 ErrorCode::GitOperationFailed,
168 format!("Git operation failed: {}", operation),
169 format!("Reason: {}", reason),
170 )
171 }
172
173 pub fn config_not_found(path: &str) -> Self {
175 Self::with_suggestions(
176 ErrorCode::ConfigNotFound,
177 format!("Configuration file not found: {}", path),
178 vec![
179 "Create a tokmd.toml configuration file".to_string(),
180 "Run 'tokmd init' to generate a template".to_string(),
181 "Use default settings by omitting --config flag".to_string(),
182 ],
183 )
184 }
185
186 pub fn config_invalid(path: &str, reason: &str) -> Self {
188 Self::with_details_and_suggestions(
189 ErrorCode::ConfigInvalid,
190 format!("Invalid configuration file: {}", path),
191 format!("Reason: {}", reason),
192 vec![
193 "Check the configuration file syntax".to_string(),
194 "Refer to documentation for valid options".to_string(),
195 "Run 'tokmd init' to generate a valid template".to_string(),
196 ],
197 )
198 }
199
200 pub fn path_not_found_with_suggestions(path: &str) -> Self {
202 Self::with_details_and_suggestions(
203 ErrorCode::PathNotFound,
204 format!("Path not found: {}", path),
205 "The specified path does not exist or is not accessible".to_string(),
206 vec![
207 "Check the path spelling".to_string(),
208 "Verify the path exists: ls -la".to_string(),
209 "Ensure you have read permissions".to_string(),
210 ],
211 )
212 }
213
214 pub fn path_not_found(path: &str) -> Self {
216 Self::new(ErrorCode::PathNotFound, format!("Path not found: {}", path))
217 }
218
219 pub fn invalid_json(err: impl fmt::Display) -> Self {
221 Self::new(ErrorCode::InvalidJson, format!("Invalid JSON: {}", err))
222 }
223
224 pub fn unknown_mode(mode: &str) -> Self {
226 Self::new(ErrorCode::UnknownMode, format!("Unknown mode: {}", mode))
227 }
228
229 pub fn scan_error(err: impl fmt::Display) -> Self {
231 Self::new(ErrorCode::ScanError, format!("Scan failed: {}", err))
232 }
233
234 pub fn analysis_error(err: impl fmt::Display) -> Self {
236 Self::new(
237 ErrorCode::AnalysisError,
238 format!("Analysis failed: {}", err),
239 )
240 }
241
242 pub fn io_error(err: impl fmt::Display) -> Self {
244 Self::new(ErrorCode::IoError, format!("I/O error: {}", err))
245 }
246
247 pub fn internal(err: impl fmt::Display) -> Self {
249 Self::new(ErrorCode::InternalError, format!("Internal error: {}", err))
250 }
251
252 pub fn not_implemented(feature: impl Into<String>) -> Self {
254 Self::new(ErrorCode::NotImplemented, feature)
255 }
256
257 pub fn invalid_field(field: &str, expected: &str) -> Self {
259 Self::new(
260 ErrorCode::InvalidSettings,
261 format!("Invalid value for '{}': expected {}", field, expected),
262 )
263 }
264
265 pub fn to_json(&self) -> String {
267 serde_json::to_string(self).unwrap_or_else(|_| {
268 format!(r#"{{"code":"{}","message":"{}"}}"#, self.code, self.message)
269 })
270 }
271}
272
273impl fmt::Display for TokmdError {
274 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275 if let Some(details) = &self.details {
276 write!(f, "[{}] {}: {}", self.code, self.message, details)
277 } else {
278 write!(f, "[{}] {}", self.code, self.message)
279 }
280 }
281}
282
283impl std::error::Error for TokmdError {}
284
285impl From<anyhow::Error> for TokmdError {
286 fn from(err: anyhow::Error) -> Self {
287 Self::internal(err)
288 }
289}
290
291impl From<serde_json::Error> for TokmdError {
292 fn from(err: serde_json::Error) -> Self {
293 Self::invalid_json(err)
294 }
295}
296
297impl From<std::io::Error> for TokmdError {
298 fn from(err: std::io::Error) -> Self {
299 Self::io_error(err)
300 }
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct ErrorDetails {
306 pub code: String,
308 pub message: String,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub details: Option<String>,
313}
314
315impl From<&TokmdError> for ErrorDetails {
316 fn from(err: &TokmdError) -> Self {
317 Self {
318 code: err.code.to_string(),
319 message: err.message.clone(),
320 details: err.details.clone(),
321 }
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct ResponseEnvelope {
331 pub ok: bool,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub data: Option<Value>,
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub error: Option<ErrorDetails>,
339}
340
341impl ResponseEnvelope {
342 pub fn success(data: Value) -> Self {
344 Self {
345 ok: true,
346 data: Some(data),
347 error: None,
348 }
349 }
350
351 pub fn error(err: &TokmdError) -> Self {
353 Self {
354 ok: false,
355 data: None,
356 error: Some(ErrorDetails::from(err)),
357 }
358 }
359
360 pub fn to_json(&self) -> String {
362 serde_json::to_string(self).unwrap_or_else(|_| {
363 if self.ok {
364 r#"{"ok":true,"data":null}"#.to_string()
365 } else {
366 let (code, message) = self
367 .error
368 .as_ref()
369 .map(|e| (e.code.as_str(), e.message.as_str()))
370 .unwrap_or(("internal_error", "serialization failed"));
371 format!(
372 r#"{{"ok":false,"error":{{"code":"{}","message":"{}"}}}}"#,
373 code, message
374 )
375 }
376 })
377 }
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct ErrorResponse {
385 pub error: bool,
387 pub code: String,
389 pub message: String,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub details: Option<String>,
394}
395
396impl From<TokmdError> for ErrorResponse {
397 fn from(err: TokmdError) -> Self {
398 Self {
399 error: true,
400 code: err.code.to_string(),
401 message: err.message,
402 details: err.details,
403 }
404 }
405}
406
407impl ErrorResponse {
408 pub fn to_json(&self) -> String {
410 serde_json::to_string(self).unwrap_or_else(|_| {
411 format!(
412 r#"{{"error":true,"code":"{}","message":"{}"}}"#,
413 self.code, self.message
414 )
415 })
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn error_codes_serialize_to_snake_case() {
425 let err = TokmdError::path_not_found("/some/path");
426 let json = err.to_json();
427 assert!(json.contains("\"code\":\"path_not_found\""));
428 }
429
430 #[test]
431 fn error_response_has_error_true() {
432 let err = TokmdError::unknown_mode("foo");
433 let resp: ErrorResponse = err.into();
434 assert!(resp.error);
435 assert_eq!(resp.code, "unknown_mode");
436 }
437
438 #[test]
439 fn error_display_includes_code() {
440 let err = TokmdError::new(ErrorCode::ScanError, "test message");
441 let display = err.to_string();
442 assert!(display.contains("[scan_error]"));
443 assert!(display.contains("test message"));
444 }
445
446 #[test]
447 fn invalid_field_error() {
448 let err = TokmdError::invalid_field("children", "'collapse' or 'separate'");
449 assert_eq!(err.code, ErrorCode::InvalidSettings);
450 assert!(err.message.contains("children"));
451 assert!(err.message.contains("'collapse' or 'separate'"));
452 }
453
454 #[test]
455 fn response_envelope_success() {
456 let data = serde_json::json!({"rows": []});
457 let envelope = ResponseEnvelope::success(data.clone());
458 assert!(envelope.ok);
459 assert!(envelope.error.is_none());
460 assert_eq!(envelope.data, Some(data));
461 }
462
463 #[test]
464 fn error_with_suggestions() {
465 let err = TokmdError::git_not_available();
466 assert_eq!(err.code, ErrorCode::GitNotAvailable);
467 assert!(err.suggestions.is_some());
468 let suggestions = err.suggestions.expect("should have suggestions");
469 assert!(!suggestions.is_empty());
470 }
471
472 #[test]
473 fn error_with_details_and_suggestions() {
474 let err = TokmdError::not_git_repository("/some/path");
475 assert_eq!(err.code, ErrorCode::NotGitRepository);
476 assert!(err.details.is_some());
477 assert!(err.suggestions.is_some());
478 }
479}