use std::collections::HashMap;
use thiserror::Error;
#[allow(clippy::result_large_err)]
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
pub enum PlatformContext {
Native {
os_info: Option<String>,
system_resources: Option<String>,
},
Wasm {
user_agent: Option<String>,
available_apis: Vec<String>,
cors_enabled: bool,
},
}
#[derive(Debug, Clone)]
pub struct HttpErrorContext {
pub status_code: Option<u16>,
pub headers: Option<HashMap<String, String>>,
pub response_body: Option<String>,
pub url: Option<String>,
pub method: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RetryInfo {
pub attempts: u32,
pub retryable: bool,
pub retry_after: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub platform: Option<PlatformContext>,
pub http: Option<HttpErrorContext>,
pub retry: Option<RetryInfo>,
pub metadata: HashMap<String, String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl Default for ErrorContext {
fn default() -> Self {
Self {
platform: None,
http: None,
retry: None,
metadata: HashMap::new(),
timestamp: chrono::Utc::now(),
}
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("HTTP request failed: {message}")]
Http {
message: String,
#[source]
source: Option<reqwest::Error>,
context: ErrorContext,
},
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("URL parse error: {0}")]
UrlParse(#[from] url::ParseError),
#[cfg(feature = "auth")]
#[error("JWT error: {0}")]
Jwt(#[from] jsonwebtoken::errors::Error),
#[error("Authentication error: {message}")]
Auth {
message: String,
context: ErrorContext,
},
#[error("Database error: {message}")]
Database {
message: String,
context: ErrorContext,
},
#[error("Storage error: {message}")]
Storage {
message: String,
context: ErrorContext,
},
#[error("Realtime error: {message}")]
Realtime {
message: String,
context: ErrorContext,
},
#[error("Configuration error: {message}")]
Config { message: String },
#[error("Invalid input: {message}")]
InvalidInput { message: String },
#[error("Network error: {message}")]
Network {
message: String,
context: ErrorContext,
},
#[error("Rate limit exceeded: {message}")]
RateLimit {
message: String,
context: ErrorContext,
},
#[error("Permission denied: {message}")]
PermissionDenied {
message: String,
context: ErrorContext,
},
#[error("Not found: {message}")]
NotFound {
message: String,
context: ErrorContext,
},
#[error("{message}")]
Generic { message: String },
#[error("Functions error: {message}")]
Functions {
message: String,
context: ErrorContext,
},
#[error("Platform error: {message}")]
Platform {
message: String,
context: ErrorContext,
},
#[error("Crypto error: {message}")]
Crypto {
message: String,
context: ErrorContext,
},
}
impl Error {
pub fn auth<S: Into<String>>(message: S) -> Self {
Self::Auth {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn auth_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
Self::Auth {
message: message.into(),
context,
}
}
pub fn database<S: Into<String>>(message: S) -> Self {
Self::Database {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn database_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
Self::Database {
message: message.into(),
context,
}
}
pub fn storage<S: Into<String>>(message: S) -> Self {
Self::Storage {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn storage_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
Self::Storage {
message: message.into(),
context,
}
}
pub fn realtime<S: Into<String>>(message: S) -> Self {
Self::Realtime {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn realtime_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
Self::Realtime {
message: message.into(),
context,
}
}
pub fn functions<S: Into<String>>(message: S) -> Self {
Self::Functions {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn functions_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
Self::Functions {
message: message.into(),
context,
}
}
pub fn network<S: Into<String>>(message: S) -> Self {
Self::Network {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn rate_limit<S: Into<String>>(message: S, retry_after: Option<u64>) -> Self {
let context = ErrorContext {
retry: Some(RetryInfo {
attempts: 0,
retryable: true,
retry_after,
}),
..Default::default()
};
Self::RateLimit {
message: message.into(),
context,
}
}
pub fn permission_denied<S: Into<String>>(message: S) -> Self {
Self::PermissionDenied {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn not_found<S: Into<String>>(message: S) -> Self {
Self::NotFound {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn config<S: Into<String>>(message: S) -> Self {
Self::Config {
message: message.into(),
}
}
pub fn invalid_input<S: Into<String>>(message: S) -> Self {
Self::InvalidInput {
message: message.into(),
}
}
pub fn generic<S: Into<String>>(message: S) -> Self {
Self::Generic {
message: message.into(),
}
}
pub fn context(&self) -> Option<&ErrorContext> {
match self {
Error::Http { context, .. } => Some(context),
Error::Auth { context, .. } => Some(context),
Error::Database { context, .. } => Some(context),
Error::Storage { context, .. } => Some(context),
Error::Realtime { context, .. } => Some(context),
Error::Network { context, .. } => Some(context),
Error::RateLimit { context, .. } => Some(context),
Error::PermissionDenied { context, .. } => Some(context),
Error::NotFound { context, .. } => Some(context),
Error::Functions { context, .. } => Some(context),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
self.context()
.and_then(|ctx| ctx.retry.as_ref())
.map(|retry| retry.retryable)
.unwrap_or(false)
}
pub fn retry_after(&self) -> Option<u64> {
self.context()
.and_then(|ctx| ctx.retry.as_ref())
.and_then(|retry| retry.retry_after)
}
pub fn status_code(&self) -> Option<u16> {
self.context()
.and_then(|ctx| ctx.http.as_ref())
.and_then(|http| http.status_code)
}
pub fn platform<S: Into<String>>(message: S) -> Self {
Self::Platform {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn platform_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
Self::Platform {
message: message.into(),
context,
}
}
pub fn crypto<S: Into<String>>(message: S) -> Self {
Self::Crypto {
message: message.into(),
context: ErrorContext::default(),
}
}
pub fn crypto_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
Self::Crypto {
message: message.into(),
context,
}
}
}
pub fn detect_platform_context() -> PlatformContext {
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
{
PlatformContext::Wasm {
user_agent: web_sys::window().and_then(|window| window.navigator().user_agent().ok()),
available_apis: detect_available_web_apis(),
cors_enabled: true, }
}
#[cfg(all(target_arch = "wasm32", not(feature = "wasm")))]
{
PlatformContext::Wasm {
user_agent: None,
available_apis: Vec::new(),
cors_enabled: true,
}
}
#[cfg(not(target_arch = "wasm32"))]
{
PlatformContext::Native {
os_info: Some(format!(
"{} {}",
std::env::consts::OS,
std::env::consts::ARCH
)),
system_resources: None, }
}
}
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[allow(dead_code)]
fn detect_available_web_apis() -> Vec<String> {
let mut apis = Vec::new();
if let Some(window) = web_sys::window() {
if window.local_storage().is_ok() {
apis.push("localStorage".to_string());
}
if window.session_storage().is_ok() {
apis.push("sessionStorage".to_string());
}
apis.push("fetch".to_string()); }
apis
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
fn detect_available_web_apis() -> Vec<String> {
Vec::new()
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
let mut context = ErrorContext::default();
if let Some(status) = err.status() {
context.http = Some(HttpErrorContext {
status_code: Some(status.as_u16()),
headers: None,
response_body: None,
url: err.url().map(|u| u.to_string()),
method: None,
});
let retryable = match status.as_u16() {
500..=599 | 429 | 408 => true, _ => false,
};
context.retry = Some(RetryInfo {
attempts: 0,
retryable,
retry_after: None,
});
}
context.platform = Some(detect_platform_context());
Error::Http {
message: err.to_string(),
source: Some(err),
context,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let error = Error::auth("test message");
assert_eq!(error.to_string(), "Authentication error: test message");
}
#[test]
fn test_database_error() {
let error = Error::database("query failed");
assert_eq!(error.to_string(), "Database error: query failed");
}
#[test]
fn test_error_context() {
let error = Error::auth("test message");
assert!(error.context().is_some());
if let Some(context) = error.context() {
assert!(context.timestamp <= chrono::Utc::now());
}
}
}