Skip to main content

descry_tool_core/
error.rs

1//! Tool error types
2//!
3//! Provides structured error handling with thiserror.
4
5use thiserror::Error;
6
7/// Tool execution error
8///
9/// Uses `#[non_exhaustive]` to allow future expansion without breaking changes.
10/// Supports error chaining via `#[source]`.
11#[non_exhaustive]
12#[derive(Error, Debug)]
13pub enum ToolError {
14    /// Invalid parameters
15    #[error("Invalid parameters: {message}")]
16    InvalidParams {
17        message: String,
18        #[source]
19        source: Option<Box<dyn std::error::Error + Send + Sync>>,
20    },
21
22    /// Tool not found
23    #[error("Tool not found: {0}")]
24    NotFound(String),
25
26    /// Unauthorized access
27    #[error("Unauthorized: {0}")]
28    Unauthorized(String),
29
30    /// Forbidden
31    #[error("Forbidden: {0}")]
32    Forbidden(String),
33
34    /// Timeout
35    #[error("Timeout after {timeout_ms}ms: {message}")]
36    Timeout {
37        timeout_ms: u64,
38        message: String,
39        #[source]
40        source: Option<Box<dyn std::error::Error + Send + Sync>>,
41    },
42
43    /// Internal error
44    #[error("Internal error: {message}")]
45    Internal {
46        message: String,
47        #[source]
48        source: Option<Box<dyn std::error::Error + Send + Sync>>,
49    },
50
51    /// Custom error with code
52    #[error("[{code}] {message}")]
53    Custom {
54        code: String,
55        message: String,
56        #[source]
57        source: Option<Box<dyn std::error::Error + Send + Sync>>,
58    },
59
60    /// JSON serialization/deserialization error
61    #[error("JSON error: {0}")]
62    Json(#[from] serde_json::Error),
63
64    /// IO error
65    #[error("IO error: {0}")]
66    Io(#[from] std::io::Error),
67}
68
69impl ToolError {
70    /// Create invalid parameters error
71    pub fn invalid_params(message: impl Into<String>) -> Self {
72        Self::InvalidParams {
73            message: message.into(),
74            source: None,
75        }
76    }
77
78    /// Create invalid parameters error with source
79    pub fn invalid_params_with_source(
80        message: impl Into<String>,
81        source: impl std::error::Error + Send + Sync + 'static,
82    ) -> Self {
83        Self::InvalidParams {
84            message: message.into(),
85            source: Some(Box::new(source)),
86        }
87    }
88
89    /// Create tool not found error
90    pub fn not_found(name: impl Into<String>) -> Self {
91        Self::NotFound(name.into())
92    }
93
94    /// Create unauthorized error
95    pub fn unauthorized(message: impl Into<String>) -> Self {
96        Self::Unauthorized(message.into())
97    }
98
99    /// Create forbidden error
100    pub fn forbidden(message: impl Into<String>) -> Self {
101        Self::Forbidden(message.into())
102    }
103
104    /// Create timeout error
105    pub fn timeout(timeout_ms: u64, message: impl Into<String>) -> Self {
106        Self::Timeout {
107            timeout_ms,
108            message: message.into(),
109            source: None,
110        }
111    }
112
113    /// Create timeout error with source
114    pub fn timeout_with_source(
115        timeout_ms: u64,
116        message: impl Into<String>,
117        source: impl std::error::Error + Send + Sync + 'static,
118    ) -> Self {
119        Self::Timeout {
120            timeout_ms,
121            message: message.into(),
122            source: Some(Box::new(source)),
123        }
124    }
125
126    /// Create internal error
127    pub fn internal(message: impl Into<String>) -> Self {
128        Self::Internal {
129            message: message.into(),
130            source: None,
131        }
132    }
133
134    /// Create internal error with source
135    pub fn internal_with_source(
136        message: impl Into<String>,
137        source: impl std::error::Error + Send + Sync + 'static,
138    ) -> Self {
139        Self::Internal {
140            message: message.into(),
141            source: Some(Box::new(source)),
142        }
143    }
144
145    /// Create custom error
146    pub fn custom(code: impl Into<String>, message: impl Into<String>) -> Self {
147        Self::Custom {
148            code: code.into(),
149            message: message.into(),
150            source: None,
151        }
152    }
153
154    /// Create custom error with source
155    pub fn custom_with_source(
156        code: impl Into<String>,
157        message: impl Into<String>,
158        source: impl std::error::Error + Send + Sync + 'static,
159    ) -> Self {
160        Self::Custom {
161            code: code.into(),
162            message: message.into(),
163            source: Some(Box::new(source)),
164        }
165    }
166
167    /// Get error code
168    pub fn code(&self) -> &str {
169        match self {
170            Self::InvalidParams { .. } => "INVALID_PARAMS",
171            Self::NotFound(_) => "NOT_FOUND",
172            Self::Unauthorized(_) => "UNAUTHORIZED",
173            Self::Forbidden(_) => "FORBIDDEN",
174            Self::Timeout { .. } => "TIMEOUT",
175            Self::Internal { .. } => "INTERNAL_ERROR",
176            Self::Custom { code, .. } => code,
177            Self::Json(_) => "JSON_ERROR",
178            Self::Io(_) => "IO_ERROR",
179        }
180    }
181
182    /// Check if error has source
183    pub fn has_source(&self) -> bool {
184        match self {
185            Self::InvalidParams { source, .. } => source.is_some(),
186            Self::Timeout { source, .. } => source.is_some(),
187            Self::Internal { source, .. } => source.is_some(),
188            Self::Custom { source, .. } => source.is_some(),
189            _ => false,
190        }
191    }
192}
193
194impl From<String> for ToolError {
195    fn from(message: String) -> Self {
196        Self::internal(message)
197    }
198}
199
200impl From<&str> for ToolError {
201    fn from(message: &str) -> Self {
202        Self::internal(message)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_error_creation() {
212        let err = ToolError::internal("Test error");
213        assert_eq!(err.code(), "INTERNAL_ERROR");
214    }
215
216    #[test]
217    fn test_error_with_source() {
218        let source = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
219        let err = ToolError::internal_with_source("Failed to read file", source);
220        assert!(err.has_source());
221    }
222
223    #[test]
224    fn test_predefined_errors() {
225        let err = ToolError::invalid_params("Parameter error");
226        assert_eq!(err.code(), "INVALID_PARAMS");
227
228        let err = ToolError::not_found("shell");
229        assert_eq!(err.code(), "NOT_FOUND");
230
231        let err = ToolError::unauthorized("Not logged in");
232        assert_eq!(err.code(), "UNAUTHORIZED");
233
234        let err = ToolError::forbidden("No permission");
235        assert_eq!(err.code(), "FORBIDDEN");
236
237        let err = ToolError::timeout(5000, "Operation timed out");
238        assert_eq!(err.code(), "TIMEOUT");
239    }
240
241    #[test]
242    fn test_custom_error() {
243        let err = ToolError::custom("E001", "Custom error");
244        assert_eq!(err.code(), "E001");
245        assert_eq!(err.to_string(), "[E001] Custom error");
246    }
247
248    #[test]
249    fn test_error_from_string() {
250        let err: ToolError = "Error message".into();
251        assert_eq!(err.code(), "INTERNAL_ERROR");
252    }
253
254    #[test]
255    fn test_error_from_json() {
256        let json_err = serde_json::from_str::<i32>("invalid");
257        assert!(json_err.is_err());
258
259        let tool_err: ToolError = json_err.unwrap_err().into();
260        assert_eq!(tool_err.code(), "JSON_ERROR");
261    }
262
263    #[test]
264    fn test_error_from_io() {
265        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
266        let tool_err: ToolError = io_err.into();
267        assert_eq!(tool_err.code(), "IO_ERROR");
268    }
269
270    #[test]
271    fn test_error_display() {
272        let err = ToolError::invalid_params("Parameter error");
273        assert_eq!(format!("{}", err), "Invalid parameters: Parameter error");
274    }
275}