rainy_sdk/
error.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4/// The comprehensive error type for all operations within the Rainy SDK.
5///
6/// `RainyError` is an enumeration of all possible errors that can occur,
7/// providing detailed context for each error variant.
8#[derive(Error, Debug, Clone)]
9pub enum RainyError {
10    /// An error related to authentication, such as an invalid or expired API key.
11    #[error("Authentication failed: {message}")]
12    Authentication {
13        /// A machine-readable error code (e.g., `INVALID_API_KEY`).
14        code: String,
15        /// A human-readable error message.
16        message: String,
17        /// Indicates whether the request can be retried.
18        retryable: bool,
19    },
20
21    /// An error due to an invalid request, such as a missing required field.
22    #[error("Invalid request: {message}")]
23    InvalidRequest {
24        /// A machine-readable error code (e.g., `MISSING_REQUIRED_FIELD`).
25        code: String,
26        /// A human-readable error message.
27        message: String,
28        /// Additional details about the error, if available.
29        details: Option<serde_json::Value>,
30    },
31
32    /// An error that originates from an underlying AI provider (e.g., OpenAI, Anthropic).
33    #[error("Provider error ({provider}): {message}")]
34    Provider {
35        /// The error code from the provider.
36        code: String,
37        /// The error message from the provider.
38        message: String,
39        /// The name of the provider that returned the error.
40        provider: String,
41        /// Indicates whether the request can be retried.
42        retryable: bool,
43    },
44
45    /// An error indicating that the rate limit for the API has been exceeded.
46    #[error("Rate limit exceeded: {message}")]
47    RateLimit {
48        /// A machine-readable error code (e.g., `RATE_LIMIT_EXCEEDED`).
49        code: String,
50        /// A human-readable error message.
51        message: String,
52        /// The recommended time to wait before retrying, in seconds.
53        retry_after: Option<u64>,
54        /// Information about the current usage, if available.
55        current_usage: Option<String>,
56    },
57
58    /// An error indicating that the account has insufficient credits to perform the request.
59    #[error("Insufficient credits: {message}")]
60    InsufficientCredits {
61        /// A machine-readable error code (e.g., `INSUFFICIENT_CREDITS`).
62        code: String,
63        /// A human-readable error message.
64        message: String,
65        /// The current credit balance of the account.
66        current_credits: f64,
67        /// The estimated cost of the request.
68        estimated_cost: f64,
69        /// The date when the credits are scheduled to be reset or topped up.
70        reset_date: Option<String>,
71    },
72
73    /// An error related to network connectivity or HTTP-level issues.
74    #[error("Network error: {message}")]
75    Network {
76        /// A message describing the network error.
77        message: String,
78        /// Indicates whether the request can be retried.
79        retryable: bool,
80        /// The underlying error message, if available.
81        source_error: Option<String>,
82    },
83
84    /// A generic API error that doesn't fit into the other categories.
85    #[error("API error [{status_code}]: {message}")]
86    Api {
87        /// A machine-readable error code.
88        code: String,
89        /// A human-readable error message.
90        message: String,
91        /// The HTTP status code of the response.
92        status_code: u16,
93        /// Indicates whether the request can be retried.
94        retryable: bool,
95        /// The unique ID of the request, for debugging purposes.
96        request_id: Option<String>,
97    },
98
99    /// An error indicating that the request timed out.
100    #[error("Request timeout: {message}")]
101    Timeout {
102        /// A message describing the timeout.
103        message: String,
104        /// The timeout duration in milliseconds.
105        duration_ms: u64,
106    },
107
108    /// An error that occurs during serialization or deserialization of data.
109    #[error("Serialization error: {message}")]
110    Serialization {
111        /// A message describing the serialization error.
112        message: String,
113        /// The underlying error message, if available.
114        source_error: Option<String>,
115    },
116}
117
118impl RainyError {
119    /// Checks if the error is considered retryable.
120    ///
121    /// Some errors, like network issues or rate limiting, are transient and can be resolved
122    /// by retrying the request.
123    ///
124    /// # Returns
125    ///
126    /// `true` if the error is retryable, `false` otherwise.
127    pub fn is_retryable(&self) -> bool {
128        match self {
129            RainyError::Authentication { retryable, .. } => *retryable,
130            RainyError::Provider { retryable, .. } => *retryable,
131            RainyError::Network { retryable, .. } => *retryable,
132            RainyError::Api { retryable, .. } => *retryable,
133            RainyError::RateLimit { .. } => true,
134            RainyError::Timeout { .. } => true,
135            _ => false,
136        }
137    }
138
139    /// Returns the recommended delay in seconds before a retry, if applicable.
140    ///
141    /// This is typically used with `RateLimit` errors.
142    ///
143    /// # Returns
144    ///
145    /// An `Option<u64>` containing the retry delay in seconds, or `None` if not applicable.
146    pub fn retry_after(&self) -> Option<u64> {
147        match self {
148            RainyError::RateLimit { retry_after, .. } => *retry_after,
149            _ => None,
150        }
151    }
152
153    /// Returns the machine-readable error code, if available.
154    pub fn code(&self) -> Option<&str> {
155        match self {
156            RainyError::Authentication { code, .. }
157            | RainyError::InvalidRequest { code, .. }
158            | RainyError::Provider { code, .. }
159            | RainyError::RateLimit { code, .. }
160            | RainyError::InsufficientCredits { code, .. }
161            | RainyError::Api { code, .. } => Some(code),
162            _ => None,
163        }
164    }
165
166    /// Returns the unique request ID associated with the error, if available.
167    ///
168    /// This is useful for debugging and support requests.
169    pub fn request_id(&self) -> Option<&str> {
170        match self {
171            RainyError::Api { request_id, .. } => request_id.as_deref(),
172            _ => None,
173        }
174    }
175}
176
177/// The structure of a standard error response from the Rainy API.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ApiErrorResponse {
180    /// The detailed error information.
181    pub error: ApiErrorDetails,
182}
183
184/// Detailed information about an API error.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ApiErrorDetails {
187    /// A machine-readable error code.
188    pub code: String,
189    /// A human-readable error message.
190    pub message: String,
191    /// Additional, structured details about the error.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub details: Option<serde_json::Value>,
194    /// Indicates whether the request that caused this error can be retried.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub retryable: Option<bool>,
197    /// The timestamp of when the error occurred.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub timestamp: Option<String>,
200    /// The unique ID of the request.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub request_id: Option<String>,
203}
204
205/// A convenience type alias for `Result<T, RainyError>`.
206pub type Result<T> = std::result::Result<T, RainyError>;
207
208/// Converts a `reqwest::Error` into a `RainyError`.
209///
210/// This implementation categorizes `reqwest` errors into `Timeout`, `Network`,
211/// or other appropriate `RainyError` variants.
212impl From<reqwest::Error> for RainyError {
213    fn from(err: reqwest::Error) -> Self {
214        if err.is_timeout() {
215            RainyError::Timeout {
216                message: "Request timed out".to_string(),
217                duration_ms: 30000, // Default timeout
218            }
219        } else if err.is_connect() || err.is_request() {
220            RainyError::Network {
221                message: err.to_string(),
222                retryable: true,
223                source_error: Some(err.to_string()),
224            }
225        } else {
226            RainyError::Network {
227                message: err.to_string(),
228                retryable: false,
229                source_error: Some(err.to_string()),
230            }
231        }
232    }
233}
234
235/// Converts a `serde_json::Error` into a `RainyError`.
236///
237/// This is used for errors that occur during the serialization or deserialization of JSON data.
238impl From<serde_json::Error> for RainyError {
239    fn from(err: serde_json::Error) -> Self {
240        RainyError::Serialization {
241            message: err.to_string(),
242            source_error: Some(err.to_string()),
243        }
244    }
245}
246
247/// Converts a `reqwest::header::InvalidHeaderValue` into a `RainyError`.
248///
249/// This is used when an invalid value is provided for an HTTP header.
250impl From<reqwest::header::InvalidHeaderValue> for RainyError {
251    fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
252        RainyError::InvalidRequest {
253            code: "INVALID_HEADER".to_string(),
254            message: format!("Invalid header value: {}", err),
255            details: None,
256        }
257    }
258}