1use std::collections::HashMap;
4use thiserror::Error;
5
6#[allow(clippy::result_large_err)]
8pub type Result<T> = std::result::Result<T, Error>;
9
10#[derive(Debug, Clone)]
12pub enum PlatformContext {
13 Native {
15 os_info: Option<String>,
17 system_resources: Option<String>,
19 },
20 Wasm {
22 user_agent: Option<String>,
24 available_apis: Vec<String>,
26 cors_enabled: bool,
28 },
29}
30
31#[derive(Debug, Clone)]
33pub struct HttpErrorContext {
34 pub status_code: Option<u16>,
36 pub headers: Option<HashMap<String, String>>,
38 pub response_body: Option<String>,
40 pub url: Option<String>,
42 pub method: Option<String>,
44}
45
46#[derive(Debug, Clone)]
48pub struct RetryInfo {
49 pub attempts: u32,
51 pub retryable: bool,
53 pub retry_after: Option<u64>,
55}
56
57#[derive(Debug, Clone)]
59pub struct ErrorContext {
60 pub platform: Option<PlatformContext>,
62 pub http: Option<HttpErrorContext>,
64 pub retry: Option<RetryInfo>,
66 pub metadata: HashMap<String, String>,
68 pub timestamp: chrono::DateTime<chrono::Utc>,
70}
71
72impl Default for ErrorContext {
73 fn default() -> Self {
74 Self {
75 platform: None,
76 http: None,
77 retry: None,
78 metadata: HashMap::new(),
79 timestamp: chrono::Utc::now(),
80 }
81 }
82}
83
84#[derive(Error, Debug)]
86pub enum Error {
87 #[error("HTTP request failed: {message}")]
89 Http {
90 message: String,
91 #[source]
92 source: Option<reqwest::Error>,
93 context: ErrorContext,
94 },
95
96 #[error("JSON error: {0}")]
98 Json(#[from] serde_json::Error),
99
100 #[error("URL parse error: {0}")]
102 UrlParse(#[from] url::ParseError),
103
104 #[cfg(feature = "auth")]
106 #[error("JWT error: {0}")]
107 Jwt(#[from] jsonwebtoken::errors::Error),
108
109 #[error("Authentication error: {message}")]
111 Auth {
112 message: String,
113 context: ErrorContext,
114 },
115
116 #[error("Database error: {message}")]
118 Database {
119 message: String,
120 context: ErrorContext,
121 },
122
123 #[error("Storage error: {message}")]
125 Storage {
126 message: String,
127 context: ErrorContext,
128 },
129
130 #[error("Realtime error: {message}")]
132 Realtime {
133 message: String,
134 context: ErrorContext,
135 },
136
137 #[error("Configuration error: {message}")]
139 Config { message: String },
140
141 #[error("Invalid input: {message}")]
143 InvalidInput { message: String },
144
145 #[error("Network error: {message}")]
147 Network {
148 message: String,
149 context: ErrorContext,
150 },
151
152 #[error("Rate limit exceeded: {message}")]
154 RateLimit {
155 message: String,
156 context: ErrorContext,
157 },
158
159 #[error("Permission denied: {message}")]
161 PermissionDenied {
162 message: String,
163 context: ErrorContext,
164 },
165
166 #[error("Not found: {message}")]
168 NotFound {
169 message: String,
170 context: ErrorContext,
171 },
172
173 #[error("{message}")]
175 Generic { message: String },
176
177 #[error("Functions error: {message}")]
179 Functions {
180 message: String,
181 context: ErrorContext,
182 },
183}
184
185impl Error {
186 pub fn auth<S: Into<String>>(message: S) -> Self {
188 Self::Auth {
189 message: message.into(),
190 context: ErrorContext::default(),
191 }
192 }
193
194 pub fn auth_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
196 Self::Auth {
197 message: message.into(),
198 context,
199 }
200 }
201
202 pub fn database<S: Into<String>>(message: S) -> Self {
204 Self::Database {
205 message: message.into(),
206 context: ErrorContext::default(),
207 }
208 }
209
210 pub fn database_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
212 Self::Database {
213 message: message.into(),
214 context,
215 }
216 }
217
218 pub fn storage<S: Into<String>>(message: S) -> Self {
220 Self::Storage {
221 message: message.into(),
222 context: ErrorContext::default(),
223 }
224 }
225
226 pub fn storage_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
228 Self::Storage {
229 message: message.into(),
230 context,
231 }
232 }
233
234 pub fn realtime<S: Into<String>>(message: S) -> Self {
236 Self::Realtime {
237 message: message.into(),
238 context: ErrorContext::default(),
239 }
240 }
241
242 pub fn realtime_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
244 Self::Realtime {
245 message: message.into(),
246 context,
247 }
248 }
249
250 pub fn functions<S: Into<String>>(message: S) -> Self {
252 Self::Functions {
253 message: message.into(),
254 context: ErrorContext::default(),
255 }
256 }
257
258 pub fn functions_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
260 Self::Functions {
261 message: message.into(),
262 context,
263 }
264 }
265
266 pub fn network<S: Into<String>>(message: S) -> Self {
268 Self::Network {
269 message: message.into(),
270 context: ErrorContext::default(),
271 }
272 }
273
274 pub fn rate_limit<S: Into<String>>(message: S, retry_after: Option<u64>) -> Self {
276 let context = ErrorContext {
277 retry: Some(RetryInfo {
278 attempts: 0,
279 retryable: true,
280 retry_after,
281 }),
282 ..Default::default()
283 };
284
285 Self::RateLimit {
286 message: message.into(),
287 context,
288 }
289 }
290
291 pub fn permission_denied<S: Into<String>>(message: S) -> Self {
293 Self::PermissionDenied {
294 message: message.into(),
295 context: ErrorContext::default(),
296 }
297 }
298
299 pub fn not_found<S: Into<String>>(message: S) -> Self {
301 Self::NotFound {
302 message: message.into(),
303 context: ErrorContext::default(),
304 }
305 }
306
307 pub fn config<S: Into<String>>(message: S) -> Self {
309 Self::Config {
310 message: message.into(),
311 }
312 }
313
314 pub fn invalid_input<S: Into<String>>(message: S) -> Self {
316 Self::InvalidInput {
317 message: message.into(),
318 }
319 }
320
321 pub fn generic<S: Into<String>>(message: S) -> Self {
323 Self::Generic {
324 message: message.into(),
325 }
326 }
327
328 pub fn context(&self) -> Option<&ErrorContext> {
330 match self {
331 Error::Http { context, .. } => Some(context),
332 Error::Auth { context, .. } => Some(context),
333 Error::Database { context, .. } => Some(context),
334 Error::Storage { context, .. } => Some(context),
335 Error::Realtime { context, .. } => Some(context),
336 Error::Network { context, .. } => Some(context),
337 Error::RateLimit { context, .. } => Some(context),
338 Error::PermissionDenied { context, .. } => Some(context),
339 Error::NotFound { context, .. } => Some(context),
340 Error::Functions { context, .. } => Some(context),
341 _ => None,
342 }
343 }
344
345 pub fn is_retryable(&self) -> bool {
347 self.context()
348 .and_then(|ctx| ctx.retry.as_ref())
349 .map(|retry| retry.retryable)
350 .unwrap_or(false)
351 }
352
353 pub fn retry_after(&self) -> Option<u64> {
355 self.context()
356 .and_then(|ctx| ctx.retry.as_ref())
357 .and_then(|retry| retry.retry_after)
358 }
359
360 pub fn status_code(&self) -> Option<u16> {
362 self.context()
363 .and_then(|ctx| ctx.http.as_ref())
364 .and_then(|http| http.status_code)
365 }
366}
367
368fn detect_platform_context() -> PlatformContext {
370 #[cfg(target_arch = "wasm32")]
371 {
372 PlatformContext::Wasm {
373 user_agent: web_sys::window().and_then(|window| window.navigator().user_agent().ok()),
374 available_apis: detect_available_web_apis(),
375 cors_enabled: true, }
377 }
378
379 #[cfg(not(target_arch = "wasm32"))]
380 {
381 PlatformContext::Native {
382 os_info: Some(format!(
383 "{} {}",
384 std::env::consts::OS,
385 std::env::consts::ARCH
386 )),
387 system_resources: None, }
389 }
390}
391
392#[cfg(target_arch = "wasm32")]
394#[allow(dead_code)]
395fn detect_available_web_apis() -> Vec<String> {
396 let mut apis = Vec::new();
397
398 if let Some(window) = web_sys::window() {
399 if window.local_storage().is_ok() {
401 apis.push("localStorage".to_string());
402 }
403 if window.session_storage().is_ok() {
404 apis.push("sessionStorage".to_string());
405 }
406 apis.push("fetch".to_string()); }
408
409 apis
410}
411
412#[cfg(not(target_arch = "wasm32"))]
413#[allow(dead_code)]
414fn detect_available_web_apis() -> Vec<String> {
415 Vec::new()
416}
417
418impl From<reqwest::Error> for Error {
419 fn from(err: reqwest::Error) -> Self {
420 let mut context = ErrorContext::default();
421
422 if let Some(status) = err.status() {
424 context.http = Some(HttpErrorContext {
425 status_code: Some(status.as_u16()),
426 headers: None,
427 response_body: None,
428 url: err.url().map(|u| u.to_string()),
429 method: None,
430 });
431
432 let retryable = match status.as_u16() {
434 500..=599 | 429 | 408 => true, _ => false,
436 };
437
438 context.retry = Some(RetryInfo {
439 attempts: 0,
440 retryable,
441 retry_after: None,
442 });
443 }
444
445 context.platform = Some(detect_platform_context());
447
448 Error::Http {
449 message: err.to_string(),
450 source: Some(err),
451 context,
452 }
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_error_creation() {
462 let error = Error::auth("test message");
463 assert_eq!(error.to_string(), "Authentication error: test message");
464 }
465
466 #[test]
467 fn test_database_error() {
468 let error = Error::database("query failed");
469 assert_eq!(error.to_string(), "Database error: query failed");
470 }
471
472 #[test]
473 fn test_error_context() {
474 let error = Error::auth("test message");
475 assert!(error.context().is_some());
476 if let Some(context) = error.context() {
477 assert!(context.timestamp <= chrono::Utc::now());
478 }
479 }
480}