1include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
23
24use alien_error::{AlienError, GenericError, HumanLayerPresentation};
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 hint: None,
77 retryable: api_error.retryable,
78 internal: false, http_status_code: Some(status),
80 source: api_error.source.and_then(parse_source_error),
81 human_layer_presentation: HumanLayerPresentation::Normal,
82 error: Some(GenericError {
83 message: api_error.message.to_string(),
84 }),
85 }
86 }
87
88 Error::CommunicationError(reqwest_err) => {
90 let retryable =
91 reqwest_err.is_connect() || reqwest_err.is_timeout() || reqwest_err.is_request();
92
93 AlienError {
94 code: "COMMUNICATION_ERROR".to_string(),
95 message: format!("Communication Error: {}", reqwest_err),
96 context: None,
97 hint: None,
98 retryable,
99 internal: false,
100 http_status_code: reqwest_err.status().map(|s| s.as_u16()),
101 source: build_reqwest_source(&reqwest_err),
102 human_layer_presentation: HumanLayerPresentation::Normal,
103 error: Some(GenericError {
104 message: format!("Communication Error: {}", reqwest_err),
105 }),
106 }
107 }
108
109 Error::InvalidRequest(msg) => AlienError {
111 code: "INVALID_REQUEST".to_string(),
112 message: format!("Invalid Request: {}", msg),
113 context: None,
114 hint: None,
115 retryable: false,
116 internal: false,
117 http_status_code: Some(400),
118 source: None,
119 human_layer_presentation: HumanLayerPresentation::Normal,
120 error: Some(GenericError {
121 message: format!("Invalid Request: {}", msg),
122 }),
123 },
124
125 Error::ResponseBodyError(reqwest_err) => AlienError {
127 code: "RESPONSE_BODY_ERROR".to_string(),
128 message: format!("Error reading response body: {}", reqwest_err),
129 context: None,
130 hint: None,
131 retryable: true, internal: false,
133 http_status_code: reqwest_err.status().map(|s| s.as_u16()),
134 source: build_reqwest_source(&reqwest_err),
135 human_layer_presentation: HumanLayerPresentation::Normal,
136 error: Some(GenericError {
137 message: format!("Error reading response body: {}", reqwest_err),
138 }),
139 },
140
141 Error::InvalidResponsePayload(bytes, json_err) => {
144 let raw_body = String::from_utf8_lossy(&bytes);
145 let truncated = if raw_body.len() > 1000 {
146 format!(
147 "{}...(truncated {} bytes)",
148 &raw_body[..1000],
149 raw_body.len() - 1000
150 )
151 } else {
152 raw_body.to_string()
153 };
154
155 AlienError {
156 code: "INVALID_RESPONSE_PAYLOAD".to_string(),
157 message: format!("Failed to parse response: {}", json_err),
158 context: Some(serde_json::json!({
159 "parseError": json_err.to_string(),
160 "responseBody": truncated,
161 })),
162 hint: None,
163 retryable: false,
164 internal: false,
165 http_status_code: None,
166 source: Some(Box::new(AlienError::new(GenericError {
167 message: json_err.to_string(),
168 }))),
169 human_layer_presentation: HumanLayerPresentation::Normal,
170 error: Some(GenericError {
171 message: format!("Failed to parse response: {}", json_err),
172 }),
173 }
174 }
175
176 Error::InvalidUpgrade(reqwest_err) => AlienError {
178 code: "INVALID_UPGRADE".to_string(),
179 message: format!("Connection upgrade failed: {}", reqwest_err),
180 context: None,
181 hint: None,
182 retryable: false,
183 internal: false,
184 http_status_code: reqwest_err.status().map(|s| s.as_u16()),
185 source: build_reqwest_source(&reqwest_err),
186 human_layer_presentation: HumanLayerPresentation::Normal,
187 error: Some(GenericError {
188 message: format!("Connection upgrade failed: {}", reqwest_err),
189 }),
190 },
191
192 Error::UnexpectedResponse(response) => {
194 let status = response.status().as_u16();
195 AlienError {
196 code: "UNEXPECTED_RESPONSE".to_string(),
197 message: format!(
198 "Unexpected response: {} {}",
199 status,
200 response.status().canonical_reason().unwrap_or("Unknown")
201 ),
202 context: Some(serde_json::json!({
203 "status": status,
204 "url": response.url().to_string(),
205 })),
206 hint: None,
207 retryable: status >= 500, internal: false,
209 http_status_code: Some(status),
210 source: None,
211 human_layer_presentation: HumanLayerPresentation::Normal,
212 error: Some(GenericError {
213 message: format!("Unexpected response status: {}", status),
214 }),
215 }
216 }
217
218 Error::Custom(msg) => AlienError {
220 code: "SDK_HOOK_ERROR".to_string(),
221 message: msg.clone(),
222 context: None,
223 hint: None,
224 retryable: false,
225 internal: false,
226 http_status_code: None,
227 source: None,
228 human_layer_presentation: HumanLayerPresentation::Normal,
229 error: Some(GenericError { message: msg }),
230 },
231 }
232}
233
234fn build_reqwest_source(err: &reqwest::Error) -> Option<Box<AlienError<GenericError>>> {
236 use std::error::Error;
238
239 let mut sources = Vec::new();
240 let mut current: Option<&(dyn Error + 'static)> = err.source();
241
242 while let Some(src) = current {
243 sources.push(src.to_string());
244 current = src.source();
245 }
246
247 if sources.is_empty() {
248 return None;
249 }
250
251 let mut result: Option<Box<AlienError<GenericError>>> = None;
253 for msg in sources.into_iter().rev() {
254 let error = AlienError {
255 code: "GENERIC_ERROR".to_string(),
256 message: msg.clone(),
257 context: None,
258 hint: None,
259 retryable: false,
260 internal: false,
261 http_status_code: None,
262 source: result,
263 human_layer_presentation: HumanLayerPresentation::Normal,
264 error: Some(GenericError { message: msg }),
265 };
266 result = Some(Box::new(error));
267 }
268
269 result
270}
271
272fn parse_source_error(value: serde_json::Value) -> Option<Box<AlienError<GenericError>>> {
274 let obj = value.as_object()?;
275
276 let code = obj
277 .get("code")
278 .and_then(|v| v.as_str())
279 .unwrap_or("NESTED_ERROR")
280 .to_string();
281
282 let message = obj
283 .get("message")
284 .and_then(|v| v.as_str())
285 .unwrap_or("Nested error")
286 .to_string();
287
288 let context = obj.get("context").cloned();
289 let retryable = obj
290 .get("retryable")
291 .and_then(|v| v.as_bool())
292 .unwrap_or(false);
293
294 let nested_source = obj.get("source").cloned().and_then(parse_source_error);
296
297 Some(Box::new(AlienError {
298 code,
299 message: message.clone(),
300 context,
301 hint: None,
302 retryable,
303 internal: false,
304 http_status_code: None,
305 source: nested_source,
306 human_layer_presentation: HumanLayerPresentation::Normal,
307 error: Some(GenericError { message }),
308 }))
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_api_error_code_deref() {
317 let code = types::ApiErrorCode::try_from("TEST_ERROR").unwrap();
319 assert_eq!(code.as_str(), "TEST_ERROR");
320 }
321}