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}