From 4b353733fff3af06e2a14538ae63bd31200912c2 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 12 Feb 2026 22:01:23 +0300 Subject: [PATCH] error: add MultiError for batch error aggregation Signed-off-by: NotAShelf Change-Id: I468289d7a5c3956fc410b6af1a8d070d6a6a6964 --- src/error.rs | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/src/error.rs b/src/error.rs index 4c1eca5..3a97ffd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,76 @@ 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 @@ -95,6 +165,21 @@ pub enum PakkerError { #[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 { @@ -108,3 +193,96 @@ impl From for PakkerError { 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); + } +}