ricecoder_tools/
error.rs

1//! Error types for ricecoder-tools
2//!
3//! Provides structured error handling with context and suggestions for all tool operations.
4
5use std::fmt;
6
7/// Structured error type for tool operations
8///
9/// Includes error code, message, optional details, and suggestions for corrective action.
10#[derive(Debug, Clone)]
11pub struct ToolError {
12    /// Machine-readable error code (e.g., "TIMEOUT", "INVALID_URL")
13    pub code: String,
14    /// Human-readable error message
15    pub message: String,
16    /// Additional context about the error
17    pub details: Option<String>,
18    /// Suggested corrective action
19    pub suggestion: Option<String>,
20}
21
22impl ToolError {
23    /// Create a new ToolError
24    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
25        Self {
26            code: code.into(),
27            message: message.into(),
28            details: None,
29            suggestion: None,
30        }
31    }
32
33    /// Add details to the error
34    pub fn with_details(mut self, details: impl Into<String>) -> Self {
35        self.details = Some(details.into());
36        self
37    }
38
39    /// Add a suggestion to the error
40    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
41        self.suggestion = Some(suggestion.into());
42        self
43    }
44}
45
46impl fmt::Display for ToolError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "[{}] {}", self.code, self.message)?;
49        if let Some(details) = &self.details {
50            write!(f, " ({})", details)?;
51        }
52        if let Some(suggestion) = &self.suggestion {
53            write!(f, " - Suggestion: {}", suggestion)?;
54        }
55        Ok(())
56    }
57}
58
59impl std::error::Error for ToolError {}
60
61/// Conversion from reqwest errors
62impl From<reqwest::Error> for ToolError {
63    fn from(err: reqwest::Error) -> Self {
64        let (code, message) = if err.is_timeout() {
65            ("TIMEOUT", "Request timeout")
66        } else if err.is_connect() {
67            ("NETWORK_ERROR", "Connection failed")
68        } else if err.is_request() {
69            ("REQUEST_ERROR", "Invalid request")
70        } else if err.is_status() {
71            ("HTTP_ERROR", "HTTP error response")
72        } else {
73            ("NETWORK_ERROR", "Network error")
74        };
75
76        ToolError::new(code, message)
77            .with_details(err.to_string())
78            .with_suggestion("Check your network connection and try again")
79    }
80}
81
82/// Conversion from IO errors
83impl From<std::io::Error> for ToolError {
84    fn from(err: std::io::Error) -> Self {
85        let (code, message) = match err.kind() {
86            std::io::ErrorKind::NotFound => ("FILE_NOT_FOUND", "File not found"),
87            std::io::ErrorKind::PermissionDenied => ("PERMISSION_DENIED", "Permission denied"),
88            std::io::ErrorKind::InvalidInput => ("INVALID_INPUT", "Invalid input"),
89            std::io::ErrorKind::TimedOut => ("TIMEOUT", "Operation timeout"),
90            _ => ("IO_ERROR", "IO error"),
91        };
92
93        ToolError::new(code, message)
94            .with_details(err.to_string())
95    }
96}
97
98/// Conversion from serde_json errors
99impl From<serde_json::Error> for ToolError {
100    fn from(err: serde_json::Error) -> Self {
101        ToolError::new("JSON_ERROR", "JSON parsing error")
102            .with_details(err.to_string())
103            .with_suggestion("Check the JSON format and try again")
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_tool_error_creation() {
113        let err = ToolError::new("TEST_ERROR", "Test message");
114        assert_eq!(err.code, "TEST_ERROR");
115        assert_eq!(err.message, "Test message");
116        assert!(err.details.is_none());
117        assert!(err.suggestion.is_none());
118    }
119
120    #[test]
121    fn test_tool_error_with_details() {
122        let err = ToolError::new("TEST_ERROR", "Test message")
123            .with_details("Additional context");
124        assert_eq!(err.details, Some("Additional context".to_string()));
125    }
126
127    #[test]
128    fn test_tool_error_with_suggestion() {
129        let err = ToolError::new("TEST_ERROR", "Test message")
130            .with_suggestion("Try this instead");
131        assert_eq!(err.suggestion, Some("Try this instead".to_string()));
132    }
133
134    #[test]
135    fn test_tool_error_display() {
136        let err = ToolError::new("TEST_ERROR", "Test message")
137            .with_details("Details")
138            .with_suggestion("Suggestion");
139        let display = format!("{}", err);
140        assert!(display.contains("TEST_ERROR"));
141        assert!(display.contains("Test message"));
142        assert!(display.contains("Details"));
143        assert!(display.contains("Suggestion"));
144    }
145}