riglr_web_tools/
error.rs

1//! Error types for riglr-web-tools.
2
3use riglr_macros::IntoToolError;
4use thiserror::Error;
5
6/// Main error type for web tool operations.
7///
8/// The IntoToolError derive macro automatically classifies errors:
9/// - Retriable: Network (includes HTTP), Api (includes request errors), RateLimit
10/// - Permanent: Auth, Parsing (includes JSON), Config, Client, InvalidInput
11#[derive(Error, Debug, IntoToolError)]
12pub enum WebToolError {
13    /// Network error (includes HTTP) - automatically retriable
14    #[error("Network error: {0}")]
15    Network(String),
16
17    /// HTTP request error - automatically retriable (converted to Network)
18    #[error("HTTP error: {0}")]
19    Http(#[from] reqwest::Error),
20
21    /// API error (includes general API issues) - automatically retriable
22    #[error("API error: {0}")]
23    Api(String),
24
25    /// API rate limit exceeded - automatically handled as rate_limited
26    #[error("Rate limit exceeded: {0}")]
27    #[tool_error(rate_limited)]
28    RateLimit(String),
29
30    /// API authentication failed - permanent
31    #[error("Authentication error: {0}")]
32    #[tool_error(permanent)]
33    Auth(String),
34
35    /// Parsing error (includes JSON and response parsing) - permanent
36    #[error("Parsing error: {0}")]
37    #[tool_error(permanent)]
38    Parsing(String),
39
40    /// Serialization error - automatically permanent
41    #[error("Serialization error: {0}")]
42    Serialization(#[from] serde_json::Error),
43
44    /// URL parsing error - permanent
45    #[error("URL error: {0}")]
46    #[tool_error(permanent)]
47    Url(#[from] url::ParseError),
48
49    /// Configuration error - permanent
50    #[error("Configuration error: {0}")]
51    #[tool_error(permanent)]
52    Config(String),
53
54    /// Client creation error - permanent
55    #[error("Client error: {0}")]
56    #[tool_error(permanent)]
57    Client(String),
58
59    /// Invalid input provided - permanent
60    #[error("Invalid input: {0}")]
61    #[tool_error(permanent)]
62    InvalidInput(String),
63
64    /// Core riglr error
65    #[error("Core error: {0}")]
66    #[tool_error(permanent)]
67    Core(#[from] riglr_core::CoreError),
68}
69
70// The From<WebToolError> for ToolError implementation is now automatically
71// generated by the IntoToolError derive macro. The macro intelligently
72// classifies errors based on variant names and explicit #[tool_error] attributes.
73
74/// Result type alias for web tool operations.
75pub type Result<T> = std::result::Result<T, WebToolError>;
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use serde_json;
81    use std::error::Error;
82
83    #[test]
84    fn test_network_error_display() {
85        let error = WebToolError::Network("Connection failed".to_string());
86        assert_eq!(error.to_string(), "Network error: Connection failed");
87    }
88
89    #[test]
90    fn test_network_error_debug() {
91        let error = WebToolError::Network("Connection failed".to_string());
92        let debug_str = format!("{:?}", error);
93        assert!(debug_str.contains("Network"));
94        assert!(debug_str.contains("Connection failed"));
95    }
96
97    #[test]
98    fn test_http_error_conversion() {
99        // Test that we can convert from reqwest::Error to WebToolError::Http
100        // Since creating a reqwest::Error requires network calls or complex setup,
101        // we'll just test that the type conversion compiles and displays correctly
102
103        // Create a URL parsing error to demonstrate error conversion
104        let url_error = reqwest::Url::parse("not a valid url").unwrap_err();
105        let web_error = WebToolError::Url(url_error);
106        assert!(web_error.to_string().contains("URL error:"));
107
108        // Verify that our HTTP error variant exists and formats correctly
109        // We can't easily create a reqwest::Error in unit tests without network I/O
110        // So we'll test the display format with a mock description
111        let mock_http_error = WebToolError::Network("HTTP connection failed".to_string());
112        assert!(mock_http_error
113            .to_string()
114            .contains("Network error: HTTP connection failed"));
115    }
116
117    #[test]
118    fn test_api_error_display() {
119        let error = WebToolError::Api("Invalid API key".to_string());
120        assert_eq!(error.to_string(), "API error: Invalid API key");
121    }
122
123    #[test]
124    fn test_rate_limit_error_display() {
125        let error = WebToolError::RateLimit("Too many requests".to_string());
126        assert_eq!(error.to_string(), "Rate limit exceeded: Too many requests");
127    }
128
129    #[test]
130    fn test_auth_error_display() {
131        let error = WebToolError::Auth("Invalid credentials".to_string());
132        assert_eq!(
133            error.to_string(),
134            "Authentication error: Invalid credentials"
135        );
136    }
137
138    #[test]
139    fn test_parsing_error_display() {
140        let error = WebToolError::Parsing("JSON malformed".to_string());
141        assert_eq!(error.to_string(), "Parsing error: JSON malformed");
142    }
143
144    #[test]
145    fn test_serialization_error_from_serde_json() {
146        // Create a serde_json error by trying to serialize something that can't be serialized
147        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
148        let error = WebToolError::from(json_error);
149
150        assert!(error.to_string().contains("Serialization error:"));
151        assert!(matches!(error, WebToolError::Serialization(_)));
152    }
153
154    #[test]
155    fn test_url_error_from_parse_error() {
156        let parse_error = url::Url::parse("not a valid url").unwrap_err();
157        let error = WebToolError::from(parse_error);
158
159        assert!(error.to_string().contains("URL error:"));
160        assert!(matches!(error, WebToolError::Url(_)));
161    }
162
163    #[test]
164    fn test_config_error_display() {
165        let error = WebToolError::Config("Missing configuration".to_string());
166        assert_eq!(
167            error.to_string(),
168            "Configuration error: Missing configuration"
169        );
170    }
171
172    #[test]
173    fn test_client_error_display() {
174        let error = WebToolError::Client("Failed to create client".to_string());
175        assert_eq!(error.to_string(), "Client error: Failed to create client");
176    }
177
178    #[test]
179    fn test_invalid_input_error_display() {
180        let error = WebToolError::InvalidInput("Empty parameter".to_string());
181        assert_eq!(error.to_string(), "Invalid input: Empty parameter");
182    }
183
184    #[test]
185    fn test_core_error_from_riglr_core() {
186        // Since we can't easily create a riglr_core::CoreError in tests without importing it,
187        // we'll test the variant directly if possible, or skip this test if CoreError is not accessible
188        // For now, we'll test that the variant exists and displays correctly
189        use riglr_core::CoreError;
190
191        // Create a CoreError - we'll use whatever variant is available
192        let core_error = CoreError::Generic("test config error".to_string());
193        let error = WebToolError::from(core_error);
194
195        assert!(error.to_string().contains("Core error:"));
196        assert!(matches!(error, WebToolError::Core(_)));
197    }
198
199    #[test]
200    fn test_error_source_chain() {
201        // Test that errors properly implement the Error trait and maintain source chains
202        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
203        let error = WebToolError::from(json_error);
204
205        // The error should have a source
206        assert!(error.source().is_some());
207    }
208
209    #[test]
210    fn test_error_debug_impl() {
211        // Test that all error variants can be debug printed
212        let errors = vec![
213            WebToolError::Network("test".to_string()),
214            WebToolError::Api("test".to_string()),
215            WebToolError::RateLimit("test".to_string()),
216            WebToolError::Auth("test".to_string()),
217            WebToolError::Parsing("test".to_string()),
218            WebToolError::Config("test".to_string()),
219            WebToolError::Client("test".to_string()),
220            WebToolError::InvalidInput("test".to_string()),
221        ];
222
223        for error in errors {
224            let debug_str = format!("{:?}", error);
225            assert!(!debug_str.is_empty());
226        }
227    }
228
229    #[test]
230    fn test_result_type_alias() {
231        // Test that our Result type alias works correctly
232        let ok_result: Result<i32> = Ok(42);
233        let err_result: Result<i32> = Err(WebToolError::Network("test".to_string()));
234
235        assert!(ok_result.is_ok());
236        assert_eq!(ok_result.unwrap(), 42);
237
238        assert!(err_result.is_err());
239        assert!(matches!(err_result.unwrap_err(), WebToolError::Network(_)));
240    }
241
242    #[test]
243    fn test_error_equality() {
244        // Test that errors can be compared (though they don't implement PartialEq by default,
245        // we can test that they're consistent in their string representation)
246        let error1 = WebToolError::Network("same message".to_string());
247        let error2 = WebToolError::Network("same message".to_string());
248        let error3 = WebToolError::Network("different message".to_string());
249
250        assert_eq!(error1.to_string(), error2.to_string());
251        assert_ne!(error1.to_string(), error3.to_string());
252    }
253
254    #[test]
255    fn test_empty_string_errors() {
256        // Test edge case with empty strings
257        let errors = vec![
258            WebToolError::Network("".to_string()),
259            WebToolError::Api("".to_string()),
260            WebToolError::RateLimit("".to_string()),
261            WebToolError::Auth("".to_string()),
262            WebToolError::Parsing("".to_string()),
263            WebToolError::Config("".to_string()),
264            WebToolError::Client("".to_string()),
265            WebToolError::InvalidInput("".to_string()),
266        ];
267
268        for error in errors {
269            let error_str = error.to_string();
270            assert!(!error_str.is_empty());
271            // Each should still have the prefix even with empty message
272            assert!(error_str.contains("error:"));
273        }
274    }
275
276    #[test]
277    fn test_very_long_error_messages() {
278        // Test edge case with very long error messages
279        let long_message = "a".repeat(1000);
280        let error = WebToolError::Network(long_message.clone());
281        let error_str = error.to_string();
282
283        assert!(error_str.contains(&long_message));
284        assert!(error_str.len() > 1000);
285    }
286
287    #[test]
288    fn test_special_characters_in_error_messages() {
289        // Test edge case with special characters
290        let special_message = "Error with special chars: 你好 🚀 \n\t\"quotes\"";
291        let error = WebToolError::InvalidInput(special_message.to_string());
292        let error_str = error.to_string();
293
294        assert!(error_str.contains(&special_message));
295    }
296
297    #[test]
298    fn test_error_is_send_and_sync() {
299        // Test that our error type implements Send and Sync (important for async code)
300        fn assert_send<T: Send>() {}
301        fn assert_sync<T: Sync>() {}
302
303        assert_send::<WebToolError>();
304        assert_sync::<WebToolError>();
305    }
306}