use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum RainyError {
#[error("Authentication failed: {message}")]
Authentication {
code: String,
message: String,
retryable: bool,
},
#[error("Invalid request: {message}")]
InvalidRequest {
code: String,
message: String,
details: Option<serde_json::Value>,
},
#[error("Provider error ({provider}): {message}")]
Provider {
code: String,
message: String,
provider: String,
retryable: bool,
},
#[error("Rate limit exceeded: {message}")]
RateLimit {
code: String,
message: String,
retry_after: Option<u64>,
current_usage: Option<String>,
},
#[error("Insufficient credits: {message}")]
InsufficientCredits {
code: String,
message: String,
current_credits: f64,
estimated_cost: f64,
reset_date: Option<String>,
},
#[error("Network error: {message}")]
Network {
message: String,
retryable: bool,
source_error: Option<String>,
},
#[error("API error [{status_code}]: {message}")]
Api {
code: String,
message: String,
status_code: u16,
retryable: bool,
request_id: Option<String>,
},
#[error("Request timeout: {message}")]
Timeout {
message: String,
duration_ms: u64,
},
#[error("Serialization error: {message}")]
Serialization {
message: String,
source_error: Option<String>,
},
#[error("Feature not available: {feature} - {message}")]
FeatureNotAvailable {
feature: String,
message: String,
},
#[error("Network error: {0}")]
NetworkError(String),
#[error("Validation error: {0}")]
ValidationError(String),
}
impl RainyError {
pub fn is_retryable(&self) -> bool {
match self {
RainyError::Authentication { retryable, .. } => *retryable,
RainyError::Provider { retryable, .. } => *retryable,
RainyError::Network { retryable, .. } => *retryable,
RainyError::Api { retryable, .. } => *retryable,
RainyError::RateLimit { .. } => true,
RainyError::Timeout { .. } => true,
_ => false,
}
}
pub fn retry_after(&self) -> Option<u64> {
match self {
RainyError::RateLimit { retry_after, .. } => *retry_after,
_ => None,
}
}
pub fn code(&self) -> Option<&str> {
match self {
RainyError::Authentication { code, .. }
| RainyError::InvalidRequest { code, .. }
| RainyError::Provider { code, .. }
| RainyError::RateLimit { code, .. }
| RainyError::InsufficientCredits { code, .. }
| RainyError::Api { code, .. } => Some(code),
_ => None,
}
}
pub fn request_id(&self) -> Option<&str> {
match self {
RainyError::Api { request_id, .. } => request_id.as_deref(),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiErrorResponse {
pub error: ApiErrorDetails,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiErrorDetails {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retryable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
pub type Result<T> = std::result::Result<T, RainyError>;
impl From<reqwest::Error> for RainyError {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
RainyError::Timeout {
message: "Request timed out".to_string(),
duration_ms: 30000, }
} else if err.is_connect() || err.is_request() {
RainyError::Network {
message: err.to_string(),
retryable: true,
source_error: Some(err.to_string()),
}
} else {
RainyError::Network {
message: err.to_string(),
retryable: false,
source_error: Some(err.to_string()),
}
}
}
}
impl From<serde_json::Error> for RainyError {
fn from(err: serde_json::Error) -> Self {
RainyError::Serialization {
message: err.to_string(),
source_error: Some(err.to_string()),
}
}
}
impl From<reqwest::header::InvalidHeaderValue> for RainyError {
fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
RainyError::InvalidRequest {
code: "INVALID_HEADER".to_string(),
message: format!("Invalid header value: {}", err),
details: None,
}
}
}