1include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
23
24use alien_error::{AlienError, GenericError};
25
26pub trait SdkResultExt<T> {
53 fn into_sdk_error(self) -> Result<T, AlienError<GenericError>>;
55}
56
57impl<T> SdkResultExt<ResponseValue<T>> for Result<ResponseValue<T>, Error<types::ApiError>> {
58 fn into_sdk_error(self) -> Result<ResponseValue<T>, AlienError<GenericError>> {
59 self.map_err(convert_sdk_error)
60 }
61}
62
63pub fn convert_sdk_error(err: Error<types::ApiError>) -> AlienError<GenericError> {
65 match err {
66 Error::ErrorResponse(response) => {
69 let status = response.status().as_u16();
70 let api_error = response.into_inner();
71
72 AlienError {
73 code: api_error.code.to_string(),
74 message: api_error.message.to_string(),
75 context: api_error.context,
76 retryable: api_error.retryable,
77 internal: false, http_status_code: Some(status),
79 source: api_error.source.and_then(parse_source_error),
80 error: Some(GenericError {
81 message: api_error.message.to_string(),
82 }),
83 }
84 }
85
86 Error::CommunicationError(reqwest_err) => {
88 let retryable =
89 reqwest_err.is_connect() || reqwest_err.is_timeout() || reqwest_err.is_request();
90
91 AlienError {
92 code: "COMMUNICATION_ERROR".to_string(),
93 message: format!("Communication Error: {}", reqwest_err),
94 context: None,
95 retryable,
96 internal: false,
97 http_status_code: reqwest_err.status().map(|s| s.as_u16()),
98 source: build_reqwest_source(&reqwest_err),
99 error: Some(GenericError {
100 message: format!("Communication Error: {}", reqwest_err),
101 }),
102 }
103 }
104
105 Error::InvalidRequest(msg) => AlienError {
107 code: "INVALID_REQUEST".to_string(),
108 message: format!("Invalid Request: {}", msg),
109 context: None,
110 retryable: false,
111 internal: false,
112 http_status_code: Some(400),
113 source: None,
114 error: Some(GenericError {
115 message: format!("Invalid Request: {}", msg),
116 }),
117 },
118
119 Error::ResponseBodyError(reqwest_err) => AlienError {
121 code: "RESPONSE_BODY_ERROR".to_string(),
122 message: format!("Error reading response body: {}", reqwest_err),
123 context: None,
124 retryable: true, internal: false,
126 http_status_code: reqwest_err.status().map(|s| s.as_u16()),
127 source: build_reqwest_source(&reqwest_err),
128 error: Some(GenericError {
129 message: format!("Error reading response body: {}", reqwest_err),
130 }),
131 },
132
133 Error::InvalidResponsePayload(bytes, json_err) => {
136 let raw_body = String::from_utf8_lossy(&bytes);
137 let truncated = if raw_body.len() > 1000 {
138 format!(
139 "{}...(truncated {} bytes)",
140 &raw_body[..1000],
141 raw_body.len() - 1000
142 )
143 } else {
144 raw_body.to_string()
145 };
146
147 AlienError {
148 code: "INVALID_RESPONSE_PAYLOAD".to_string(),
149 message: format!("Failed to parse response: {}", json_err),
150 context: Some(serde_json::json!({
151 "parseError": json_err.to_string(),
152 "responseBody": truncated,
153 })),
154 retryable: false,
155 internal: false,
156 http_status_code: None,
157 source: Some(Box::new(AlienError::new(GenericError {
158 message: json_err.to_string(),
159 }))),
160 error: Some(GenericError {
161 message: format!("Failed to parse response: {}", json_err),
162 }),
163 }
164 }
165
166 Error::InvalidUpgrade(reqwest_err) => AlienError {
168 code: "INVALID_UPGRADE".to_string(),
169 message: format!("Connection upgrade failed: {}", reqwest_err),
170 context: None,
171 retryable: false,
172 internal: false,
173 http_status_code: reqwest_err.status().map(|s| s.as_u16()),
174 source: build_reqwest_source(&reqwest_err),
175 error: Some(GenericError {
176 message: format!("Connection upgrade failed: {}", reqwest_err),
177 }),
178 },
179
180 Error::UnexpectedResponse(response) => {
182 let status = response.status().as_u16();
183 AlienError {
184 code: "UNEXPECTED_RESPONSE".to_string(),
185 message: format!(
186 "Unexpected response: {} {}",
187 status,
188 response.status().canonical_reason().unwrap_or("Unknown")
189 ),
190 context: Some(serde_json::json!({
191 "status": status,
192 "url": response.url().to_string(),
193 })),
194 retryable: status >= 500, internal: false,
196 http_status_code: Some(status),
197 source: None,
198 error: Some(GenericError {
199 message: format!("Unexpected response status: {}", status),
200 }),
201 }
202 }
203
204 Error::Custom(msg) => AlienError {
206 code: "SDK_HOOK_ERROR".to_string(),
207 message: msg.clone(),
208 context: None,
209 retryable: false,
210 internal: false,
211 http_status_code: None,
212 source: None,
213 error: Some(GenericError { message: msg }),
214 },
215 }
216}
217
218fn build_reqwest_source(err: &reqwest::Error) -> Option<Box<AlienError<GenericError>>> {
220 use std::error::Error;
222
223 let mut sources = Vec::new();
224 let mut current: Option<&(dyn Error + 'static)> = err.source();
225
226 while let Some(src) = current {
227 sources.push(src.to_string());
228 current = src.source();
229 }
230
231 if sources.is_empty() {
232 return None;
233 }
234
235 let mut result: Option<Box<AlienError<GenericError>>> = None;
237 for msg in sources.into_iter().rev() {
238 let error = AlienError {
239 code: "GENERIC_ERROR".to_string(),
240 message: msg.clone(),
241 context: None,
242 retryable: false,
243 internal: false,
244 http_status_code: None,
245 source: result,
246 error: Some(GenericError { message: msg }),
247 };
248 result = Some(Box::new(error));
249 }
250
251 result
252}
253
254fn parse_source_error(value: serde_json::Value) -> Option<Box<AlienError<GenericError>>> {
256 let obj = value.as_object()?;
257
258 let code = obj
259 .get("code")
260 .and_then(|v| v.as_str())
261 .unwrap_or("NESTED_ERROR")
262 .to_string();
263
264 let message = obj
265 .get("message")
266 .and_then(|v| v.as_str())
267 .unwrap_or("Nested error")
268 .to_string();
269
270 let context = obj.get("context").cloned();
271 let retryable = obj
272 .get("retryable")
273 .and_then(|v| v.as_bool())
274 .unwrap_or(false);
275
276 let nested_source = obj.get("source").cloned().and_then(parse_source_error);
278
279 Some(Box::new(AlienError {
280 code,
281 message: message.clone(),
282 context,
283 retryable,
284 internal: false,
285 http_status_code: None,
286 source: nested_source,
287 error: Some(GenericError { message }),
288 }))
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_api_error_code_deref() {
297 let code = types::ApiErrorCode::try_from("TEST_ERROR").unwrap();
299 assert_eq!(code.as_str(), "TEST_ERROR");
300 }
301}