1use thiserror::Error;
6
7#[non_exhaustive]
12#[derive(Error, Debug)]
13pub enum ToolError {
14 #[error("Invalid parameters: {message}")]
16 InvalidParams {
17 message: String,
18 #[source]
19 source: Option<Box<dyn std::error::Error + Send + Sync>>,
20 },
21
22 #[error("Tool not found: {0}")]
24 NotFound(String),
25
26 #[error("Unauthorized: {0}")]
28 Unauthorized(String),
29
30 #[error("Forbidden: {0}")]
32 Forbidden(String),
33
34 #[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 #[error("Internal error: {message}")]
45 Internal {
46 message: String,
47 #[source]
48 source: Option<Box<dyn std::error::Error + Send + Sync>>,
49 },
50
51 #[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 #[error("JSON error: {0}")]
62 Json(#[from] serde_json::Error),
63
64 #[error("IO error: {0}")]
66 Io(#[from] std::io::Error),
67}
68
69impl ToolError {
70 pub fn invalid_params(message: impl Into<String>) -> Self {
72 Self::InvalidParams {
73 message: message.into(),
74 source: None,
75 }
76 }
77
78 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 pub fn not_found(name: impl Into<String>) -> Self {
91 Self::NotFound(name.into())
92 }
93
94 pub fn unauthorized(message: impl Into<String>) -> Self {
96 Self::Unauthorized(message.into())
97 }
98
99 pub fn forbidden(message: impl Into<String>) -> Self {
101 Self::Forbidden(message.into())
102 }
103
104 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 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 pub fn internal(message: impl Into<String>) -> Self {
128 Self::Internal {
129 message: message.into(),
130 source: None,
131 }
132 }
133
134 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 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 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 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 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}