Skip to main content

lightcone_sdk/api/
error.rs

1//! API error types for the Lightcone REST API client.
2
3use std::fmt;
4use thiserror::Error;
5
6/// API-specific error type for the Lightcone REST API client.
7#[derive(Debug, Error)]
8pub enum ApiError {
9    /// HTTP/network error from reqwest
10    #[error("HTTP error: {0}")]
11    Http(#[from] reqwest::Error),
12
13    /// Resource not found (404)
14    #[error("Not found: {0}")]
15    NotFound(ErrorResponse),
16
17    /// Invalid request parameters (400)
18    #[error("Bad request: {0}")]
19    BadRequest(ErrorResponse),
20
21    /// Permission denied, signature mismatch (403)
22    #[error("Permission denied: {0}")]
23    Forbidden(ErrorResponse),
24
25    /// Rate limited (429)
26    #[error("Rate limited: {0}")]
27    RateLimited(ErrorResponse),
28
29    /// Authentication required (401)
30    #[error("Unauthorized: {0}")]
31    Unauthorized(ErrorResponse),
32
33    /// Resource already exists (409)
34    #[error("Conflict: {0}")]
35    Conflict(ErrorResponse),
36
37    /// Server-side error (500)
38    #[error("Server error: {0}")]
39    ServerError(ErrorResponse),
40
41    /// JSON deserialization error
42    #[error("Deserialization error: {0}")]
43    Deserialize(String),
44
45    /// Invalid parameter provided
46    #[error("Invalid parameter: {0}")]
47    InvalidParameter(String),
48
49    /// Unexpected HTTP status code
50    #[error("Unexpected status {0}: {1}")]
51    UnexpectedStatus(u16, ErrorResponse),
52}
53
54impl ApiError {
55    /// Get the server error response if this error came from an HTTP response.
56    pub fn error_response(&self) -> Option<&ErrorResponse> {
57        match self {
58            ApiError::NotFound(resp) => Some(resp),
59            ApiError::BadRequest(resp) => Some(resp),
60            ApiError::Forbidden(resp) => Some(resp),
61            ApiError::RateLimited(resp) => Some(resp),
62            ApiError::Unauthorized(resp) => Some(resp),
63            ApiError::Conflict(resp) => Some(resp),
64            ApiError::ServerError(resp) => Some(resp),
65            ApiError::UnexpectedStatus(_, resp) => Some(resp),
66            _ => None,
67        }
68    }
69
70    /// Get the HTTP status code for this error, if applicable.
71    pub fn status_code(&self) -> Option<u16> {
72        match self {
73            ApiError::NotFound(_) => Some(404),
74            ApiError::BadRequest(_) => Some(400),
75            ApiError::Forbidden(_) => Some(403),
76            ApiError::RateLimited(_) => Some(429),
77            ApiError::Unauthorized(_) => Some(401),
78            ApiError::Conflict(_) => Some(409),
79            ApiError::ServerError(_) => Some(500),
80            ApiError::UnexpectedStatus(code, _) => Some(*code),
81            _ => None,
82        }
83    }
84}
85
86/// Result type alias for API operations.
87pub type ApiResult<T> = Result<T, ApiError>;
88
89/// Error response format from the API.
90#[derive(Debug, Clone, serde::Deserialize)]
91pub struct ErrorResponse {
92    /// Error status (usually "error")
93    #[serde(default)]
94    pub status: Option<String>,
95    /// Human-readable error message
96    #[serde(alias = "error")]
97    pub message: Option<String>,
98    /// Additional error details
99    #[serde(default)]
100    pub details: Option<String>,
101}
102
103impl ErrorResponse {
104    /// Create an `ErrorResponse` from a plain text string.
105    ///
106    /// Useful for non-JSON error bodies or synthetic error messages.
107    pub fn from_text(text: String) -> Self {
108        Self {
109            status: None,
110            message: Some(text),
111            details: None,
112        }
113    }
114
115    /// Get the error message, preferring `message` over `details`.
116    #[deprecated(note = "Use Display formatting instead")]
117    pub fn get_message(&self) -> String {
118        self.message
119            .clone()
120            .or_else(|| self.details.clone())
121            .unwrap_or_else(|| "Unknown error".to_string())
122    }
123}
124
125impl fmt::Display for ErrorResponse {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match (&self.message, &self.details) {
128            (Some(msg), Some(details)) => write!(f, "{}: {}", msg, details),
129            (Some(msg), None) => write!(f, "{}", msg),
130            (None, Some(details)) => write!(f, "{}", details),
131            (None, None) => write!(f, "Unknown error"),
132        }
133    }
134}