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    /// An error indicating that a feature is not available for the current plan.
118    #[error("Feature not available: {feature} - {message}")]
119    FeatureNotAvailable {
120        /// The feature that is not available.
121        feature: String,
122        /// A message explaining why the feature is not available.
123        message: String,
124    },
125
126    /// A generic network error.
127    #[error("Network error: {0}")]
128    NetworkError(String),
129
130    /// A validation error for invalid input.
131    #[error("Validation error: {0}")]
132    ValidationError(String),
133}
134
135impl RainyError {
136    /// Checks if the error is considered retryable.
137    ///
138    /// Some errors, like network issues or rate limiting, are transient and can be resolved
139    /// by retrying the request.
140    ///
141    /// # Returns
142    ///
143    /// `true` if the error is retryable, `false` otherwise.
144    pub fn is_retryable(&self) -> bool {
145        match self {
146            RainyError::Authentication { retryable, .. } => *retryable,
147            RainyError::Provider { retryable, .. } => *retryable,
148            RainyError::Network { retryable, .. } => *retryable,
149            RainyError::Api { retryable, .. } => *retryable,
150            RainyError::RateLimit { .. } => true,
151            RainyError::Timeout { .. } => true,
152            _ => false,
153        }
154    }
155
156    /// Returns the recommended delay in seconds before a retry, if applicable.
157    ///
158    /// This is typically used with `RateLimit` errors.
159    ///
160    /// # Returns
161    ///
162    /// An `Option<u64>` containing the retry delay in seconds, or `None` if not applicable.
163    pub fn retry_after(&self) -> Option<u64> {
164        match self {
165            RainyError::RateLimit { retry_after, .. } => *retry_after,
166            _ => None,
167        }
168    }
169
170    /// Returns the machine-readable error code, if available.
171    pub fn code(&self) -> Option<&str> {
172        match self {
173            RainyError::Authentication { code, .. }
174            | RainyError::InvalidRequest { code, .. }
175            | RainyError::Provider { code, .. }
176            | RainyError::RateLimit { code, .. }
177            | RainyError::InsufficientCredits { code, .. }
178            | RainyError::Api { code, .. } => Some(code),
179            _ => None,
180        }
181    }
182
183    /// Returns the unique request ID associated with the error, if available.
184    ///
185    /// This is useful for debugging and support requests.
186    pub fn request_id(&self) -> Option<&str> {
187        match self {
188            RainyError::Api { request_id, .. } => request_id.as_deref(),
189            _ => None,
190        }
191    }
192}
193
194/// The structure of a standard error response from the Rainy API.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ApiErrorResponse {
197    /// The detailed error information.
198    pub error: ApiErrorDetails,
199}
200
201/// Detailed information about an API error.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ApiErrorDetails {
204    /// A machine-readable error code.
205    pub code: String,
206    /// A human-readable error message.
207    pub message: String,
208    /// Additional, structured details about the error.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub details: Option<serde_json::Value>,
211    /// Indicates whether the request that caused this error can be retried.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub retryable: Option<bool>,
214    /// The timestamp of when the error occurred.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub timestamp: Option<String>,
217    /// The unique ID of the request.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub request_id: Option<String>,
220}
221
222/// A convenience type alias for `Result<T, RainyError>`.
223pub type Result<T> = std::result::Result<T, RainyError>;
224
225/// Converts a `reqwest::Error` into a `RainyError`.
226///
227/// This implementation categorizes `reqwest` errors into `Timeout`, `Network`,
228/// or other appropriate `RainyError` variants.
229impl From<reqwest::Error> for RainyError {
230    fn from(err: reqwest::Error) -> Self {
231        if err.is_timeout() {
232            RainyError::Timeout {
233                message: "Request timed out".to_string(),
234                duration_ms: 30000, // Default timeout
235            }
236        } else if err.is_connect() || err.is_request() {
237            RainyError::Network {
238                message: err.to_string(),
239                retryable: true,
240                source_error: Some(err.to_string()),
241            }
242        } else {
243            RainyError::Network {
244                message: err.to_string(),
245                retryable: false,
246                source_error: Some(err.to_string()),
247            }
248        }
249    }
250}
251
252/// Converts a `serde_json::Error` into a `RainyError`.
253///
254/// This is used for errors that occur during the serialization or deserialization of JSON data.
255impl From<serde_json::Error> for RainyError {
256    fn from(err: serde_json::Error) -> Self {
257        RainyError::Serialization {
258            message: err.to_string(),
259            source_error: Some(err.to_string()),
260        }
261    }
262}
263
264/// Converts a `reqwest::header::InvalidHeaderValue` into a `RainyError`.
265///
266/// This is used when an invalid value is provided for an HTTP header.
267impl From<reqwest::header::InvalidHeaderValue> for RainyError {
268    fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
269        RainyError::InvalidRequest {
270            code: "INVALID_HEADER".to_string(),
271            message: format!("Invalid header value: {}", err),
272            details: None,
273        }
274    }
275}