1use serde::Deserialize;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
6pub enum ComposioError {
7 #[error("API error: {message} (status: {status})")]
9 ApiError {
10 status: u16,
11 message: String,
12 code: Option<String>,
13 slug: Option<String>,
14 request_id: Option<String>,
15 suggested_fix: Option<String>,
16 errors: Option<Vec<ErrorDetail>>,
17 },
18
19 #[error("Network error: {0}")]
21 NetworkError(#[from] reqwest::Error),
22
23 #[error("Serialization error: {0}")]
25 SerializationError(#[from] serde_json::Error),
26
27 #[error("Invalid input: {0}")]
29 InvalidInput(String),
30
31 #[error("Configuration error: {0}")]
33 ConfigError(String),
34}
35
36#[derive(Debug, Clone, PartialEq, Deserialize)]
38pub struct ErrorDetail {
39 pub field: Option<String>,
41 pub message: String,
43}
44
45#[derive(Debug, Clone, Deserialize)]
47pub struct ErrorResponse {
48 pub message: String,
49 pub code: Option<String>,
50 pub slug: Option<String>,
51 pub status: u16,
52 pub request_id: Option<String>,
53 pub suggested_fix: Option<String>,
54 pub errors: Option<Vec<ErrorDetail>>,
55}
56
57impl ComposioError {
58 pub async fn from_response(response: reqwest::Response) -> Self {
63 let status = response.status().as_u16();
64
65 match response.json::<ErrorResponse>().await {
66 Ok(err_resp) => ComposioError::ApiError {
67 status,
68 message: err_resp.message,
69 code: err_resp.code,
70 slug: err_resp.slug,
71 request_id: err_resp.request_id,
72 suggested_fix: err_resp.suggested_fix,
73 errors: err_resp.errors,
74 },
75 Err(_) => ComposioError::ApiError {
76 status,
77 message: format!("HTTP error {}", status),
78 code: None,
79 slug: None,
80 request_id: None,
81 suggested_fix: None,
82 errors: None,
83 },
84 }
85 }
86
87 pub fn is_retryable(&self) -> bool {
99 match self {
100 ComposioError::ApiError { status, .. } => {
101 matches!(status, 429 | 500 | 502 | 503 | 504)
102 }
103 ComposioError::NetworkError(_) => true,
104 _ => false,
105 }
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_api_error_display() {
115 let error = ComposioError::ApiError {
116 status: 404,
117 message: "Resource not found".to_string(),
118 code: Some("NOT_FOUND".to_string()),
119 slug: Some("resource-not-found".to_string()),
120 request_id: Some("req_123".to_string()),
121 suggested_fix: Some("Check the resource ID".to_string()),
122 errors: None,
123 };
124
125 let display = format!("{}", error);
126 assert!(display.contains("API error"));
127 assert!(display.contains("Resource not found"));
128 assert!(display.contains("404"));
129 }
130
131 #[test]
132 fn test_invalid_input_error() {
133 let error = ComposioError::InvalidInput("Invalid API key".to_string());
134 let display = format!("{}", error);
135 assert!(display.contains("Invalid input"));
136 assert!(display.contains("Invalid API key"));
137 }
138
139 #[test]
140 fn test_config_error() {
141 let error = ComposioError::ConfigError("Invalid base URL".to_string());
142 let display = format!("{}", error);
143 assert!(display.contains("Configuration error"));
144 assert!(display.contains("Invalid base URL"));
145 }
146
147 #[test]
148 fn test_serialization_error_conversion() {
149 let json_error = serde_json::from_str::<serde_json::Value>("invalid json")
150 .unwrap_err();
151 let error: ComposioError = json_error.into();
152
153 match error {
154 ComposioError::SerializationError(_) => (),
155 _ => panic!("Expected SerializationError"),
156 }
157 }
158
159 #[test]
160 fn test_is_retryable_for_rate_limit() {
161 let error = ComposioError::ApiError {
162 status: 429,
163 message: "Rate limited".to_string(),
164 code: None,
165 slug: None,
166 request_id: None,
167 suggested_fix: None,
168 errors: None,
169 };
170
171 assert!(error.is_retryable());
172 }
173
174 #[test]
175 fn test_is_retryable_for_server_errors() {
176 for status in [500, 502, 503, 504] {
177 let error = ComposioError::ApiError {
178 status,
179 message: "Server error".to_string(),
180 code: None,
181 slug: None,
182 request_id: None,
183 suggested_fix: None,
184 errors: None,
185 };
186
187 assert!(
188 error.is_retryable(),
189 "Status {} should be retryable",
190 status
191 );
192 }
193 }
194
195 #[test]
196 fn test_is_not_retryable_for_client_errors() {
197 for status in [400, 401, 403, 404] {
198 let error = ComposioError::ApiError {
199 status,
200 message: "Client error".to_string(),
201 code: None,
202 slug: None,
203 request_id: None,
204 suggested_fix: None,
205 errors: None,
206 };
207
208 assert!(
209 !error.is_retryable(),
210 "Status {} should not be retryable",
211 status
212 );
213 }
214 }
215
216 #[test]
217 fn test_serialization_error_is_not_retryable() {
218 let json_error = serde_json::from_str::<serde_json::Value>("invalid json")
219 .unwrap_err();
220 let error: ComposioError = json_error.into();
221
222 assert!(!error.is_retryable());
223 }
224
225 #[test]
226 fn test_invalid_input_not_retryable() {
227 let error = ComposioError::InvalidInput("Invalid API key".to_string());
228 assert!(!error.is_retryable());
229 }
230
231 #[test]
232 fn test_config_error_not_retryable() {
233 let error = ComposioError::ConfigError("Invalid base URL".to_string());
234 assert!(!error.is_retryable());
235 }
236
237 #[test]
238 fn test_error_detail_deserialization() {
239 let json = r#"{
240 "field": "email",
241 "message": "Invalid email format"
242 }"#;
243
244 let detail: ErrorDetail = serde_json::from_str(json).unwrap();
245 assert_eq!(detail.field, Some("email".to_string()));
246 assert_eq!(detail.message, "Invalid email format");
247 }
248
249 #[test]
250 fn test_error_response_deserialization() {
251 let json = r#"{
252 "message": "Validation failed",
253 "code": "VALIDATION_ERROR",
254 "slug": "validation-failed",
255 "status": 400,
256 "request_id": "req_abc123",
257 "suggested_fix": "Check your input parameters",
258 "errors": [
259 {
260 "field": "user_id",
261 "message": "User ID is required"
262 }
263 ]
264 }"#;
265
266 let response: ErrorResponse = serde_json::from_str(json).unwrap();
267 assert_eq!(response.message, "Validation failed");
268 assert_eq!(response.code, Some("VALIDATION_ERROR".to_string()));
269 assert_eq!(response.status, 400);
270 assert!(response.errors.is_some());
271 assert_eq!(response.errors.as_ref().unwrap().len(), 1);
272 }
273
274 #[test]
275 fn test_error_response_minimal_deserialization() {
276 let json = r#"{
277 "message": "Internal server error",
278 "status": 500
279 }"#;
280
281 let response: ErrorResponse = serde_json::from_str(json).unwrap();
282 assert_eq!(response.message, "Internal server error");
283 assert_eq!(response.status, 500);
284 assert!(response.code.is_none());
285 assert!(response.errors.is_none());
286 }
287}