From 955a71081241dd37d78f724a5e7bca69e7997969 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 5 May 2025 02:52:38 +0300 Subject: [PATCH] error: improve error context in `AppError` With nothing other than proper `From` implementations --- src/main.rs | 189 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 162 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7b5cfdd..6d60168 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,20 +16,125 @@ use yansi::Paint; /// Application errors with thiserror #[derive(Debug, Error)] enum AppError { - #[error("Command failed: {0}")] - CommandFailed(String), + #[error("Command failed: {command} {args:?} - {message}")] + CommandFailed { + command: String, + args: Vec, + message: String, + }, - #[error("Failed to decode command output: {0}")] - CommandOutputError(#[from] std::str::Utf8Error), + #[error("Failed to decode command output from {context}: {source}")] + CommandOutputError { + source: std::str::Utf8Error, + context: String, + }, - #[error("Failed to parse data: {0}")] - ParseError(String), + #[error("Failed to parse data in {context}: {message}")] + ParseError { + message: String, + context: String, + #[source] + source: Option>, + }, - #[error("Regex error: {0}")] - RegexError(#[from] regex::Error), + #[error("Regex error in {context}: {source}")] + RegexError { + source: regex::Error, + context: String, + }, - #[error("IO error: {0}")] - IoError(#[from] std::io::Error), + #[error("IO error in {context}: {source}")] + IoError { + source: std::io::Error, + context: String, + }, + + #[error("Nix store error: {message} for path: {store_path}")] + NixStoreError { message: String, store_path: String }, +} + +// Implement From traits to support the ? operator +impl From for AppError { + fn from(source: std::io::Error) -> Self { + Self::IoError { + source, + context: "unknown context".into(), + } + } +} + +impl From for AppError { + fn from(source: std::str::Utf8Error) -> Self { + Self::CommandOutputError { + source, + context: "command output".into(), + } + } +} + +impl From for AppError { + fn from(source: regex::Error) -> Self { + Self::RegexError { + source, + context: "regex operation".into(), + } + } +} + +impl AppError { + /// Create a command failure error with context + pub fn command_failed>(command: S, args: &[&str], message: S) -> Self { + Self::CommandFailed { + command: command.into(), + args: args.iter().map(|&s| s.to_string()).collect(), + message: message.into(), + } + } + + /// Create a parse error with context + pub fn parse_error, C: Into>( + message: S, + context: C, + source: Option>, + ) -> Self { + Self::ParseError { + message: message.into(), + context: context.into(), + source, + } + } + + /// Create an IO error with context + pub fn io_error>(source: std::io::Error, context: C) -> Self { + Self::IoError { + source, + context: context.into(), + } + } + + /// Create a regex error with context + pub fn regex_error>(source: regex::Error, context: C) -> Self { + Self::RegexError { + source, + context: context.into(), + } + } + + /// Create a command output error with context + pub fn command_output_error>(source: std::str::Utf8Error, context: C) -> Self { + Self::CommandOutputError { + source, + context: context.into(), + } + } + + /// Create a Nix store error + pub fn nix_store_error, P: Into>(message: M, store_path: P) -> Self { + Self::NixStoreError { + message: message.into(), + store_path: store_path.into(), + } + } } // Use type alias for Result with our custom error type @@ -264,9 +369,15 @@ fn get_packages(path: &std::path::Path) -> Result> { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!("nix-store command failed: {}", stderr); - return Err(AppError::CommandFailed(format!( - "nix-store command failed: {stderr}" - ))); + return Err(AppError::CommandFailed { + command: "nix-store".to_string(), + args: vec![ + "--query".to_string(), + "--references".to_string(), + path.join("sw").to_string_lossy().to_string(), + ], + message: stderr.to_string(), + }); } let list = str::from_utf8(&output.stdout)?; @@ -289,9 +400,15 @@ fn get_dependencies(path: &std::path::Path) -> Result> { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!("nix-store command failed: {}", stderr); - return Err(AppError::CommandFailed(format!( - "nix-store command failed: {stderr}" - ))); + return Err(AppError::CommandFailed { + command: "nix-store".to_string(), + args: vec![ + "--query".to_string(), + "--requisites".to_string(), + path.join("sw").to_string_lossy().to_string(), + ], + message: stderr.to_string(), + }); } let list = str::from_utf8(&output.stdout)?; @@ -321,17 +438,21 @@ fn get_version<'a>(pack: impl Into<&'a str>) -> Result<(&'a str, &'a str)> { let version = cap.get(2).map_or("", |m| m.as_str()); if name.is_empty() || version.is_empty() { - return Err(AppError::ParseError(format!( - "Failed to extract name or version from path: {path}" - ))); + return Err(AppError::ParseError { + message: format!("Failed to extract name or version from path: {path}"), + context: "get_version".to_string(), + source: None, + }); } return Ok((name, version)); } - Err(AppError::ParseError(format!( - "Path does not match expected nix store format: {path}" - ))) + Err(AppError::ParseError { + message: format!("Path does not match expected nix store format: {path}"), + context: "get_version".to_string(), + source: None, + }) } fn check_nix_available() -> bool { @@ -361,9 +482,15 @@ fn get_closure_size(path: &std::path::Path) -> Result { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!("nix path-info command failed: {}", stderr); - return Err(AppError::CommandFailed(format!( - "nix path-info command failed: {stderr}" - ))); + return Err(AppError::CommandFailed { + command: "nix".to_string(), + args: vec![ + "path-info".to_string(), + "--closure-size".to_string(), + path.join("sw").to_string_lossy().to_string(), + ], + message: stderr.to_string(), + }); } let stdout = str::from_utf8(&output.stdout)?; @@ -375,13 +502,21 @@ fn get_closure_size(path: &std::path::Path) -> Result { .ok_or_else(|| { let err = "Unexpected output format from nix path-info"; error!("{}", err); - AppError::ParseError(err.into()) + AppError::ParseError { + message: err.into(), + context: "get_closure_size".to_string(), + source: None, + } })? .parse::() .map_err(|e| { let err = format!("Failed to parse size value: {e}"); error!("{}", err); - AppError::ParseError(err) + AppError::ParseError { + message: err, + context: "get_closure_size".to_string(), + source: None, + } })?; // Convert to MiB