Skip to main content

leptos_next_metadata/
error.rs

1//! Unified error handling for leptos-next-metadata
2//!
3//! Provides consistent error types and handling across native and WASM environments
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Unified error type for leptos-next-metadata
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct MetadataError {
11    /// Error kind
12    pub kind: ErrorKind,
13    /// Error message
14    pub message: String,
15    /// Error context
16    pub context: Option<String>,
17    /// Error source (if applicable)
18    pub source: Option<String>,
19    /// Error timestamp
20    pub timestamp: Option<String>,
21    /// Additional error metadata
22    pub metadata: std::collections::HashMap<String, String>,
23}
24
25/// Error kinds for different types of failures
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub enum ErrorKind {
28    /// Validation errors
29    Validation,
30    /// Network/HTTP errors
31    Network,
32    /// File system errors
33    FileSystem,
34    /// Serialization/deserialization errors
35    Serialization,
36    /// Configuration errors
37    Configuration,
38    /// Security errors
39    Security,
40    /// Performance errors
41    Performance,
42    /// Browser/DOM errors (WASM only)
43    Browser,
44    /// Storage errors
45    Storage,
46    /// Image processing errors
47    ImageProcessing,
48    /// Template rendering errors
49    Template,
50    /// Unknown/unexpected errors
51    Unknown,
52}
53
54/// Error severity levels
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub enum ErrorSeverity {
57    /// Low severity - non-critical issues
58    Low,
59    /// Medium severity - may affect functionality
60    Medium,
61    /// High severity - significant functionality impact
62    High,
63    /// Critical severity - application breaking
64    Critical,
65}
66
67/// Error context information
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ErrorContext {
70    /// Component or module where error occurred
71    pub component: Option<String>,
72    /// Operation being performed
73    pub operation: Option<String>,
74    /// User agent or environment info
75    pub environment: Option<String>,
76    /// Request ID or session ID
77    pub request_id: Option<String>,
78    /// Stack trace (if available)
79    pub stack_trace: Option<String>,
80}
81
82/// Error reporting configuration
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ErrorReportingConfig {
85    /// Whether to enable error reporting
86    pub enabled: bool,
87    /// Maximum number of errors to report per session
88    pub max_errors_per_session: usize,
89    /// Error reporting endpoint
90    pub endpoint: Option<String>,
91    /// Whether to include stack traces
92    pub include_stack_traces: bool,
93    /// Whether to include user context
94    pub include_user_context: bool,
95    /// Error sampling rate (0.0 to 1.0)
96    pub sampling_rate: f64,
97}
98
99impl MetadataError {
100    /// Create a new error
101    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
102        Self {
103            kind,
104            message: message.into(),
105            context: None,
106            source: None,
107            timestamp: Some(Self::current_timestamp()),
108            metadata: std::collections::HashMap::new(),
109        }
110    }
111
112    /// Create a new error with context
113    pub fn with_context(mut self, context: impl Into<String>) -> Self {
114        self.context = Some(context.into());
115        self
116    }
117
118    /// Create a new error with source
119    pub fn with_source(mut self, source: impl Into<String>) -> Self {
120        self.source = Some(source.into());
121        self
122    }
123
124    /// Add metadata to the error
125    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
126        self.metadata.insert(key.into(), value.into());
127        self
128    }
129
130    /// Get error severity based on kind
131    pub fn severity(&self) -> ErrorSeverity {
132        match self.kind {
133            ErrorKind::Validation => ErrorSeverity::Medium,
134            ErrorKind::Network => ErrorSeverity::High,
135            ErrorKind::FileSystem => ErrorSeverity::High,
136            ErrorKind::Serialization => ErrorSeverity::Medium,
137            ErrorKind::Configuration => ErrorSeverity::High,
138            ErrorKind::Security => ErrorSeverity::Critical,
139            ErrorKind::Performance => ErrorSeverity::Low,
140            ErrorKind::Browser => ErrorSeverity::Medium,
141            ErrorKind::Storage => ErrorSeverity::Medium,
142            ErrorKind::ImageProcessing => ErrorSeverity::Medium,
143            ErrorKind::Template => ErrorSeverity::Medium,
144            ErrorKind::Unknown => ErrorSeverity::High,
145        }
146    }
147
148    /// Check if error is recoverable
149    pub fn is_recoverable(&self) -> bool {
150        matches!(
151            self.kind,
152            ErrorKind::Validation
153                | ErrorKind::Performance
154                | ErrorKind::Browser
155                | ErrorKind::Storage
156        )
157    }
158
159    /// Get user-friendly error message
160    pub fn user_message(&self) -> String {
161        match self.kind {
162            ErrorKind::Validation => "Please check your input and try again.".to_string(),
163            ErrorKind::Network => {
164                "Network connection issue. Please check your internet connection.".to_string()
165            }
166            ErrorKind::FileSystem => "File system error. Please try again.".to_string(),
167            ErrorKind::Serialization => "Data processing error. Please try again.".to_string(),
168            ErrorKind::Configuration => "Configuration error. Please contact support.".to_string(),
169            ErrorKind::Security => "Security error detected. Please refresh the page.".to_string(),
170            ErrorKind::Performance => {
171                "Performance issue detected. The page may be slow.".to_string()
172            }
173            ErrorKind::Browser => {
174                "Browser compatibility issue. Please try a different browser.".to_string()
175            }
176            ErrorKind::Storage => "Storage error. Your data may not be saved.".to_string(),
177            ErrorKind::ImageProcessing => {
178                "Image processing error. Please try a different image.".to_string()
179            }
180            ErrorKind::Template => "Template rendering error. Please try again.".to_string(),
181            ErrorKind::Unknown => "An unexpected error occurred. Please try again.".to_string(),
182        }
183    }
184
185    /// Convert to JSON string
186    pub fn to_json(&self) -> Result<String, serde_json::Error> {
187        serde_json::to_string_pretty(self)
188    }
189
190    /// Create from JSON string
191    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
192        serde_json::from_str(json)
193    }
194
195    /// Get current timestamp
196    fn current_timestamp() -> String {
197        #[cfg(target_arch = "wasm32")]
198        {
199            // Use JavaScript Date for WASM
200            if let Some(_window) = web_sys::window() {
201                let date = js_sys::Date::new_0();
202                date.to_iso_string()
203                    .as_string()
204                    .unwrap_or_else(|| "unknown".to_string())
205            } else {
206                "unknown".to_string()
207            }
208        }
209        #[cfg(not(target_arch = "wasm32"))]
210        {
211            // Use std::time for native
212            use std::time::{SystemTime, UNIX_EPOCH};
213            SystemTime::now()
214                .duration_since(UNIX_EPOCH)
215                .map(|d| d.as_secs().to_string())
216                .unwrap_or_else(|_| "unknown".to_string())
217        }
218    }
219}
220
221impl fmt::Display for MetadataError {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(f, "[{:?}] {}", self.kind, self.message)?;
224        if let Some(context) = &self.context {
225            write!(f, " (Context: {})", context)?;
226        }
227        if let Some(source) = &self.source {
228            write!(f, " (Source: {})", source)?;
229        }
230        Ok(())
231    }
232}
233
234impl std::error::Error for MetadataError {}
235
236/// Result type alias for MetadataError
237pub type MetadataResult<T> = Result<T, MetadataError>;
238
239/// Error handler trait for different environments
240pub trait ErrorHandler {
241    /// Handle an error
242    fn handle_error(&self, error: &MetadataError) -> Result<(), MetadataError>;
243
244    /// Report an error
245    fn report_error(&self, error: &MetadataError) -> Result<(), MetadataError>;
246
247    /// Log an error
248    fn log_error(&self, error: &MetadataError) -> Result<(), MetadataError>;
249}
250
251/// Console error handler for WASM
252#[cfg(target_arch = "wasm32")]
253pub struct ConsoleErrorHandler {
254    config: ErrorReportingConfig,
255}
256
257#[cfg(target_arch = "wasm32")]
258impl ConsoleErrorHandler {
259    pub fn new(config: ErrorReportingConfig) -> Self {
260        Self { config }
261    }
262}
263
264#[cfg(target_arch = "wasm32")]
265impl ErrorHandler for ConsoleErrorHandler {
266    fn handle_error(&self, error: &MetadataError) -> Result<(), MetadataError> {
267        // Log to console
268        web_sys::console::error_1(&format!("[ERROR] {}", error).into());
269
270        // Report if enabled
271        if self.config.enabled {
272            self.report_error(error)?;
273        }
274
275        Ok(())
276    }
277
278    fn report_error(&self, error: &MetadataError) -> Result<(), MetadataError> {
279        // In a real implementation, this would send to an error reporting service
280        web_sys::console::warn_1(&format!("[REPORT] {}", error).into());
281        Ok(())
282    }
283
284    fn log_error(&self, error: &MetadataError) -> Result<(), MetadataError> {
285        web_sys::console::log_1(&format!("[LOG] {}", error).into());
286        Ok(())
287    }
288}
289
290/// File error handler for native
291#[cfg(not(target_arch = "wasm32"))]
292pub struct FileErrorHandler {
293    config: ErrorReportingConfig,
294    log_file: Option<std::path::PathBuf>,
295}
296
297#[cfg(not(target_arch = "wasm32"))]
298impl FileErrorHandler {
299    pub fn new(config: ErrorReportingConfig, log_file: Option<std::path::PathBuf>) -> Self {
300        Self { config, log_file }
301    }
302}
303
304#[cfg(not(target_arch = "wasm32"))]
305impl ErrorHandler for FileErrorHandler {
306    fn handle_error(&self, error: &MetadataError) -> Result<(), MetadataError> {
307        // Log to stderr
308        eprintln!("[ERROR] {}", error);
309
310        // Log to file if configured
311        if let Some(log_file) = &self.log_file {
312            if let Ok(mut file) = std::fs::OpenOptions::new()
313                .create(true)
314                .append(true)
315                .open(log_file)
316            {
317                use std::io::Write;
318                let _ = writeln!(
319                    file,
320                    "[{}] [ERROR] {}",
321                    chrono::Utc::now().to_rfc3339(),
322                    error
323                );
324            }
325        }
326
327        // Report if enabled
328        if self.config.enabled {
329            self.report_error(error)?;
330        }
331
332        Ok(())
333    }
334
335    fn report_error(&self, error: &MetadataError) -> Result<(), MetadataError> {
336        // In a real implementation, this would send to an error reporting service
337        eprintln!("[REPORT] {}", error);
338        Ok(())
339    }
340
341    fn log_error(&self, error: &MetadataError) -> Result<(), MetadataError> {
342        println!("[LOG] {}", error);
343        Ok(())
344    }
345}
346
347/// Error context builder
348pub struct ErrorContextBuilder {
349    context: ErrorContext,
350}
351
352impl ErrorContextBuilder {
353    pub fn new() -> Self {
354        Self {
355            context: ErrorContext {
356                component: None,
357                operation: None,
358                environment: None,
359                request_id: None,
360                stack_trace: None,
361            },
362        }
363    }
364
365    pub fn component(mut self, component: impl Into<String>) -> Self {
366        self.context.component = Some(component.into());
367        self
368    }
369
370    pub fn operation(mut self, operation: impl Into<String>) -> Self {
371        self.context.operation = Some(operation.into());
372        self
373    }
374
375    pub fn environment(mut self, environment: impl Into<String>) -> Self {
376        self.context.environment = Some(environment.into());
377        self
378    }
379
380    pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
381        self.context.request_id = Some(request_id.into());
382        self
383    }
384
385    pub fn stack_trace(mut self, stack_trace: impl Into<String>) -> Self {
386        self.context.stack_trace = Some(stack_trace.into());
387        self
388    }
389
390    pub fn build(self) -> ErrorContext {
391        self.context
392    }
393}
394
395/// Error utilities
396pub struct ErrorUtils;
397
398impl ErrorUtils {
399    /// Create a validation error
400    pub fn validation_error(message: impl Into<String>) -> MetadataError {
401        MetadataError::new(ErrorKind::Validation, message)
402    }
403
404    /// Create a network error
405    pub fn network_error(message: impl Into<String>) -> MetadataError {
406        MetadataError::new(ErrorKind::Network, message)
407    }
408
409    /// Create a security error
410    pub fn security_error(message: impl Into<String>) -> MetadataError {
411        MetadataError::new(ErrorKind::Security, message)
412    }
413
414    /// Create a browser error (WASM only)
415    #[cfg(target_arch = "wasm32")]
416    pub fn browser_error(message: impl Into<String>) -> MetadataError {
417        MetadataError::new(ErrorKind::Browser, message)
418    }
419
420    /// Create a storage error
421    pub fn storage_error(message: impl Into<String>) -> MetadataError {
422        MetadataError::new(ErrorKind::Storage, message)
423    }
424
425    /// Create an image processing error
426    pub fn image_processing_error(message: impl Into<String>) -> MetadataError {
427        MetadataError::new(ErrorKind::ImageProcessing, message)
428    }
429
430    /// Wrap a standard error
431    pub fn wrap_error(
432        error: impl std::error::Error + Send + Sync + 'static,
433        kind: ErrorKind,
434    ) -> MetadataError {
435        MetadataError::new(kind, error.to_string())
436    }
437
438    /// Get error statistics
439    pub fn get_error_stats(errors: &[MetadataError]) -> ErrorStats {
440        let mut stats = ErrorStats::default();
441
442        for error in errors {
443            stats.total_errors += 1;
444
445            match error.kind {
446                ErrorKind::Validation => stats.validation_errors += 1,
447                ErrorKind::Network => stats.network_errors += 1,
448                ErrorKind::Security => stats.security_errors += 1,
449                ErrorKind::Browser => stats.browser_errors += 1,
450                ErrorKind::Storage => stats.storage_errors += 1,
451                ErrorKind::ImageProcessing => stats.image_processing_errors += 1,
452                _ => stats.other_errors += 1,
453            }
454
455            match error.severity() {
456                ErrorSeverity::Low => stats.low_severity += 1,
457                ErrorSeverity::Medium => stats.medium_severity += 1,
458                ErrorSeverity::High => stats.high_severity += 1,
459                ErrorSeverity::Critical => stats.critical_severity += 1,
460            }
461        }
462
463        stats
464    }
465}
466
467/// Error statistics
468#[derive(Debug, Default, Clone, Serialize, Deserialize)]
469pub struct ErrorStats {
470    pub total_errors: usize,
471    pub validation_errors: usize,
472    pub network_errors: usize,
473    pub security_errors: usize,
474    pub browser_errors: usize,
475    pub storage_errors: usize,
476    pub image_processing_errors: usize,
477    pub other_errors: usize,
478    pub low_severity: usize,
479    pub medium_severity: usize,
480    pub high_severity: usize,
481    pub critical_severity: usize,
482}
483
484impl ErrorStats {
485    /// Get error rate by kind
486    pub fn error_rate_by_kind(&self) -> std::collections::HashMap<String, f64> {
487        let mut rates = std::collections::HashMap::new();
488        if self.total_errors > 0 {
489            rates.insert(
490                "validation".to_string(),
491                self.validation_errors as f64 / self.total_errors as f64,
492            );
493            rates.insert(
494                "network".to_string(),
495                self.network_errors as f64 / self.total_errors as f64,
496            );
497            rates.insert(
498                "security".to_string(),
499                self.security_errors as f64 / self.total_errors as f64,
500            );
501            rates.insert(
502                "browser".to_string(),
503                self.browser_errors as f64 / self.total_errors as f64,
504            );
505            rates.insert(
506                "storage".to_string(),
507                self.storage_errors as f64 / self.total_errors as f64,
508            );
509            rates.insert(
510                "image_processing".to_string(),
511                self.image_processing_errors as f64 / self.total_errors as f64,
512            );
513            rates.insert(
514                "other".to_string(),
515                self.other_errors as f64 / self.total_errors as f64,
516            );
517        }
518        rates
519    }
520
521    /// Get severity distribution
522    pub fn severity_distribution(&self) -> std::collections::HashMap<String, f64> {
523        let mut distribution = std::collections::HashMap::new();
524        if self.total_errors > 0 {
525            distribution.insert(
526                "low".to_string(),
527                self.low_severity as f64 / self.total_errors as f64,
528            );
529            distribution.insert(
530                "medium".to_string(),
531                self.medium_severity as f64 / self.total_errors as f64,
532            );
533            distribution.insert(
534                "high".to_string(),
535                self.high_severity as f64 / self.total_errors as f64,
536            );
537            distribution.insert(
538                "critical".to_string(),
539                self.critical_severity as f64 / self.total_errors as f64,
540            );
541        }
542        distribution
543    }
544}