use thiserror::Error; pub type Result = std::result::Result; /// Severity level for errors #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ErrorSeverity { /// Fatal error - operation cannot continue #[default] Error, /// Warning - operation can continue but may have issues Warning, /// Info - informational message Info, } /// Container for multiple errors that occurred during an operation #[derive(Debug)] pub struct MultiError { errors: Vec, } impl MultiError { pub const fn new() -> Self { Self { errors: Vec::new() } } pub fn push(&mut self, error: PakkerError) { self.errors.push(error); } pub fn extend(&mut self, errors: impl IntoIterator) { self.errors.extend(errors); } pub const fn is_empty(&self) -> bool { self.errors.is_empty() } pub const fn len(&self) -> usize { self.errors.len() } pub fn into_result(self, success_value: T) -> Result { if self.is_empty() { Ok(success_value) } else { Err(PakkerError::Multiple(self.errors)) } } pub fn errors(&self) -> &[PakkerError] { &self.errors } pub fn into_errors(self) -> Vec { self.errors } } impl Default for MultiError { fn default() -> Self { Self::new() } } impl FromIterator for MultiError { fn from_iter>(iter: I) -> Self { Self { errors: iter.into_iter().collect(), } } } #[derive(Error, Debug)] pub enum PakkerError { // Network errors #[error("Network request failed: {0}")] NetworkError(#[from] reqwest::Error), #[error("Platform API error: {0}")] PlatformApiError(String), // Validation errors #[error("Invalid lock file: {0}")] InvalidLockFile(String), #[error("Invalid config file: {0}")] InvalidConfigFile(String), #[error("Project not found: {0}")] ProjectNotFound(String), #[error("File selection error: {0}")] FileSelectionError(String), #[error("File not found: {0}")] FileNotFound(String), // Conflict errors #[error("Circular dependency detected: {0}")] CircularDependency(String), // File I/O errors #[error("IO error: {0}")] IoError(#[from] std::io::Error), #[error("Serialization error: {0}")] SerializationError(#[from] serde_json::Error), #[error("Hash mismatch for file {file}: expected {expected}, got {actual}")] HashMismatch { file: String, expected: String, actual: String, }, #[error("Download failed: {0}")] DownloadFailed(String), // Export errors #[error("Export failed: {0}")] ExportFailed(String), #[error("Invalid export profile: {0}")] InvalidExportProfile(String), // General errors #[error("Configuration error: {0}")] ConfigError(String), #[error("Internal error: {0}")] InternalError(String), #[error("Already exists: {0}")] AlreadyExists(String), #[error("Invalid input: {0}")] InvalidInput(String), #[error("Invalid project: {0}")] InvalidProject(String), #[error("Invalid import file: {0}")] InvalidImportFile(String), #[error("Zip error: {0}")] ZipError(#[from] zip::result::ZipError), // Git and Fork errors #[error("Git error: {0}")] GitError(String), #[error("Remote not found: {0}")] RemoteNotFound(String), #[error("Fork error: {0}")] Fork(String), #[error("Invalid hash: {0}")] InvalidHash(String), #[error("Invalid response: {0}")] InvalidResponse(String), #[error("IPC error: {0}")] IpcError(String), #[error("{}", format_multiple_errors(.0))] Multiple(Vec), } fn format_multiple_errors(errors: &[PakkerError]) -> String { if errors.len() == 1 { return errors[0].to_string(); } let mut msg = format!("{} errors occurred:\n", errors.len()); for (idx, error) in errors.iter().enumerate() { msg.push_str(&format!(" {}. {}\n", idx + 1, error)); } msg } impl From for PakkerError { fn from(err: git2::Error) -> Self { Self::GitError(err.to_string()) } } impl From for PakkerError { fn from(err: crate::ipc::IpcError) -> Self { Self::IpcError(err.to_string()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_multi_error_empty() { let multi = MultiError::new(); assert!(multi.is_empty()); assert_eq!(multi.len(), 0); } #[test] fn test_multi_error_push() { let mut multi = MultiError::new(); multi.push(PakkerError::ProjectNotFound("mod1".to_string())); multi.push(PakkerError::ProjectNotFound("mod2".to_string())); assert!(!multi.is_empty()); assert_eq!(multi.len(), 2); } #[test] fn test_multi_error_into_result_empty() { let multi = MultiError::new(); let result: Result = multi.into_result(42); assert!(result.is_ok()); assert_eq!(result.unwrap(), 42); } #[test] fn test_multi_error_into_result_with_errors() { let mut multi = MultiError::new(); multi.push(PakkerError::ProjectNotFound("mod1".to_string())); let result: Result = multi.into_result(42); assert!(result.is_err()); } #[test] fn test_multi_error_from_iterator() { let errors = vec![ PakkerError::ProjectNotFound("mod1".to_string()), PakkerError::ProjectNotFound("mod2".to_string()), ]; let multi: MultiError = errors.into_iter().collect(); assert_eq!(multi.len(), 2); } #[test] fn test_multi_error_extend() { let mut multi = MultiError::new(); multi.push(PakkerError::ProjectNotFound("mod1".to_string())); let more_errors = vec![ PakkerError::ProjectNotFound("mod2".to_string()), PakkerError::ProjectNotFound("mod3".to_string()), ]; multi.extend(more_errors); assert_eq!(multi.len(), 3); } #[test] fn test_multiple_errors_formatting() { let errors = vec![ PakkerError::ProjectNotFound("mod1".to_string()), PakkerError::ProjectNotFound("mod2".to_string()), ]; let error = PakkerError::Multiple(errors); let msg = error.to_string(); assert!(msg.contains("2 errors occurred")); assert!(msg.contains("mod1")); assert!(msg.contains("mod2")); } #[test] fn test_single_multiple_error_formatting() { let errors = vec![PakkerError::ProjectNotFound("mod1".to_string())]; let error = PakkerError::Multiple(errors); let msg = error.to_string(); // Single error should just display the error itself assert!(msg.contains("mod1")); assert!(!msg.contains("errors occurred")); } #[test] fn test_error_severity_default() { assert_eq!(ErrorSeverity::default(), ErrorSeverity::Error); } }