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 #[error("Platform error: {message}")]
186 Platform {
187 message: String,
188 context: ErrorContext,
189 },
190
191 #[error("Crypto error: {message}")]
193 Crypto {
194 message: String,
195 context: ErrorContext,
196 },
197}
198
199impl Error {
200 pub fn auth<S: Into<String>>(message: S) -> Self {
202 Self::Auth {
203 message: message.into(),
204 context: ErrorContext::default(),
205 }
206 }
207
208 pub fn auth_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
210 Self::Auth {
211 message: message.into(),
212 context,
213 }
214 }
215
216 pub fn database<S: Into<String>>(message: S) -> Self {
218 Self::Database {
219 message: message.into(),
220 context: ErrorContext::default(),
221 }
222 }
223
224 pub fn database_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
226 Self::Database {
227 message: message.into(),
228 context,
229 }
230 }
231
232 pub fn storage<S: Into<String>>(message: S) -> Self {
234 Self::Storage {
235 message: message.into(),
236 context: ErrorContext::default(),
237 }
238 }
239
240 pub fn storage_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
242 Self::Storage {
243 message: message.into(),
244 context,
245 }
246 }
247
248 pub fn realtime<S: Into<String>>(message: S) -> Self {
250 Self::Realtime {
251 message: message.into(),
252 context: ErrorContext::default(),
253 }
254 }
255
256 pub fn realtime_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
258 Self::Realtime {
259 message: message.into(),
260 context,
261 }
262 }
263
264 pub fn functions<S: Into<String>>(message: S) -> Self {
266 Self::Functions {
267 message: message.into(),
268 context: ErrorContext::default(),
269 }
270 }
271
272 pub fn functions_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
274 Self::Functions {
275 message: message.into(),
276 context,
277 }
278 }
279
280 pub fn network<S: Into<String>>(message: S) -> Self {
282 Self::Network {
283 message: message.into(),
284 context: ErrorContext::default(),
285 }
286 }
287
288 pub fn rate_limit<S: Into<String>>(message: S, retry_after: Option<u64>) -> Self {
290 let context = ErrorContext {
291 retry: Some(RetryInfo {
292 attempts: 0,
293 retryable: true,
294 retry_after,
295 }),
296 ..Default::default()
297 };
298
299 Self::RateLimit {
300 message: message.into(),
301 context,
302 }
303 }
304
305 pub fn permission_denied<S: Into<String>>(message: S) -> Self {
307 Self::PermissionDenied {
308 message: message.into(),
309 context: ErrorContext::default(),
310 }
311 }
312
313 pub fn not_found<S: Into<String>>(message: S) -> Self {
315 Self::NotFound {
316 message: message.into(),
317 context: ErrorContext::default(),
318 }
319 }
320
321 pub fn config<S: Into<String>>(message: S) -> Self {
323 Self::Config {
324 message: message.into(),
325 }
326 }
327
328 pub fn invalid_input<S: Into<String>>(message: S) -> Self {
330 Self::InvalidInput {
331 message: message.into(),
332 }
333 }
334
335 pub fn generic<S: Into<String>>(message: S) -> Self {
337 Self::Generic {
338 message: message.into(),
339 }
340 }
341
342 pub fn context(&self) -> Option<&ErrorContext> {
344 match self {
345 Error::Http { context, .. } => Some(context),
346 Error::Auth { context, .. } => Some(context),
347 Error::Database { context, .. } => Some(context),
348 Error::Storage { context, .. } => Some(context),
349 Error::Realtime { context, .. } => Some(context),
350 Error::Network { context, .. } => Some(context),
351 Error::RateLimit { context, .. } => Some(context),
352 Error::PermissionDenied { context, .. } => Some(context),
353 Error::NotFound { context, .. } => Some(context),
354 Error::Functions { context, .. } => Some(context),
355 _ => None,
356 }
357 }
358
359 pub fn is_retryable(&self) -> bool {
361 self.context()
362 .and_then(|ctx| ctx.retry.as_ref())
363 .map(|retry| retry.retryable)
364 .unwrap_or(false)
365 }
366
367 pub fn retry_after(&self) -> Option<u64> {
369 self.context()
370 .and_then(|ctx| ctx.retry.as_ref())
371 .and_then(|retry| retry.retry_after)
372 }
373
374 pub fn status_code(&self) -> Option<u16> {
376 self.context()
377 .and_then(|ctx| ctx.http.as_ref())
378 .and_then(|http| http.status_code)
379 }
380
381 pub fn platform<S: Into<String>>(message: S) -> Self {
383 Self::Platform {
384 message: message.into(),
385 context: ErrorContext::default(),
386 }
387 }
388
389 pub fn platform_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
391 Self::Platform {
392 message: message.into(),
393 context,
394 }
395 }
396
397 pub fn crypto<S: Into<String>>(message: S) -> Self {
399 Self::Crypto {
400 message: message.into(),
401 context: ErrorContext::default(),
402 }
403 }
404
405 pub fn crypto_with_context<S: Into<String>>(message: S, context: ErrorContext) -> Self {
407 Self::Crypto {
408 message: message.into(),
409 context,
410 }
411 }
412}
413
414pub fn detect_platform_context() -> PlatformContext {
416 #[cfg(target_arch = "wasm32")]
417 {
418 PlatformContext::Wasm {
419 user_agent: web_sys::window().and_then(|window| window.navigator().user_agent().ok()),
420 available_apis: detect_available_web_apis(),
421 cors_enabled: true, }
423 }
424
425 #[cfg(not(target_arch = "wasm32"))]
426 {
427 PlatformContext::Native {
428 os_info: Some(format!(
429 "{} {}",
430 std::env::consts::OS,
431 std::env::consts::ARCH
432 )),
433 system_resources: None, }
435 }
436}
437
438#[cfg(target_arch = "wasm32")]
440#[allow(dead_code)]
441fn detect_available_web_apis() -> Vec<String> {
442 let mut apis = Vec::new();
443
444 if let Some(window) = web_sys::window() {
445 if window.local_storage().is_ok() {
447 apis.push("localStorage".to_string());
448 }
449 if window.session_storage().is_ok() {
450 apis.push("sessionStorage".to_string());
451 }
452 apis.push("fetch".to_string()); }
454
455 apis
456}
457
458#[cfg(not(target_arch = "wasm32"))]
459#[allow(dead_code)]
460fn detect_available_web_apis() -> Vec<String> {
461 Vec::new()
462}
463
464impl From<reqwest::Error> for Error {
465 fn from(err: reqwest::Error) -> Self {
466 let mut context = ErrorContext::default();
467
468 if let Some(status) = err.status() {
470 context.http = Some(HttpErrorContext {
471 status_code: Some(status.as_u16()),
472 headers: None,
473 response_body: None,
474 url: err.url().map(|u| u.to_string()),
475 method: None,
476 });
477
478 let retryable = match status.as_u16() {
480 500..=599 | 429 | 408 => true, _ => false,
482 };
483
484 context.retry = Some(RetryInfo {
485 attempts: 0,
486 retryable,
487 retry_after: None,
488 });
489 }
490
491 context.platform = Some(detect_platform_context());
493
494 Error::Http {
495 message: err.to_string(),
496 source: Some(err),
497 context,
498 }
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_error_creation() {
508 let error = Error::auth("test message");
509 assert_eq!(error.to_string(), "Authentication error: test message");
510 }
511
512 #[test]
513 fn test_database_error() {
514 let error = Error::database("query failed");
515 assert_eq!(error.to_string(), "Database error: query failed");
516 }
517
518 #[test]
519 fn test_error_context() {
520 let error = Error::auth("test message");
521 assert!(error.context().is_some());
522 if let Some(context) = error.context() {
523 assert!(context.timestamp <= chrono::Utc::now());
524 }
525 }
526}