Skip to main content

alien_platform_api/
lib.rs

1//! Alien Platform API
2//!
3//! Auto-generated from OpenAPI spec with custom error conversion support.
4//!
5//! ## Error Handling
6//!
7//! For SDK API calls, use `SdkResultExt::into_sdk_error()` instead of
8//! `.into_alien_error()` to preserve structured API error information:
9//!
10//! ```ignore
11//! use alien_platform_api::SdkResultExt;
12//!
13//! // ✅ Good: preserves API error code, message, retryable flag
14//! client.some_method().send().await.into_sdk_error().context(...)?
15//!
16//! // ❌ Bad: loses structured error information
17//! client.some_method().send().await.into_alien_error().context(...)?
18//! ```
19//!
20//! For non-SDK errors (serde, std, etc.), continue using `.into_alien_error()`.
21
22include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
23
24use alien_error::{AlienError, GenericError, HumanLayerPresentation};
25
26/// Extension trait for converting SDK API results to `AlienError`.
27///
28/// This properly extracts error information from progenitor's error types,
29/// preserving API error details that would be lost with `.into_alien_error()`.
30///
31/// ## When to use
32///
33/// Use `into_sdk_error()` for SDK API calls:
34/// ```ignore
35/// client.sync_acquire().send().await.into_sdk_error().context(...)?
36/// ```
37///
38/// Continue using `into_alien_error()` for non-SDK errors (serde, std, etc.):
39/// ```ignore
40/// serde_json::to_value(&data).into_alien_error().context(...)?
41/// ```
42///
43/// ## What it preserves
44///
45/// When the API returns an error response, `into_sdk_error()` preserves:
46/// - `code`: The API error code (e.g., "DEPLOYMENT_NOT_FOUND")
47/// - `message`: The error message
48/// - `retryable`: Whether the operation can be retried
49/// - `context`: Additional error context as JSON
50/// - `source`: Nested error chain
51/// - HTTP status code
52pub trait SdkResultExt<T> {
53    /// Convert SDK result to `AlienError` result, preserving API error details.
54    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
63/// Convert a progenitor SDK error to AlienError, preserving all details.
64pub fn convert_sdk_error(err: Error<types::ApiError>) -> AlienError<GenericError> {
65    match err {
66        // API returned a documented error response with ApiError body
67        // This is the main case where we gain value over .into_alien_error()
68        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, // API errors sent to clients are external by nature
79                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        // Network/connection error - typically retryable
89        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        // Request validation failed (client-side, before sending)
110        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        // Failed to read response body
126        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, // Transient network issue
132            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        // Response body couldn't be parsed as expected type
142        // Include raw body in context for debugging
143        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        // WebSocket upgrade error
177        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        // Response with status code not in OpenAPI spec
193        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, // Server errors are typically retryable
208                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        // Custom hook error
219        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
234/// Build a source error chain from a reqwest error
235fn build_reqwest_source(err: &reqwest::Error) -> Option<Box<AlienError<GenericError>>> {
236    // Walk the error chain and build AlienError source chain
237    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    // Build chain from innermost to outermost
252    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
272/// Try to parse a JSON value as a nested AlienError source chain.
273fn 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    // Recursively parse nested source
295    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        // Verify generated types work as expected
318        let code = types::ApiErrorCode::try_from("TEST_ERROR").unwrap();
319        assert_eq!(code.as_str(), "TEST_ERROR");
320    }
321}