use super::*;
use crate::core::file_error::FileOperationError;
const TEMPLATE_ERROR_KEYWORDS: &[&str] = &["template", "variable", "filter"];
const NETWORK_ERROR_KEYWORDS: &[&str] = &["network", "connection", "timeout"];
const GIT_ERROR_KEYWORDS: &[&str] = &["git command", "git operation", "git clone", "git fetch"];
const PERMISSION_ERROR_KEYWORDS: &[&str] = &["permission", "denied", "access"];
#[must_use]
pub fn user_friendly_error(error: anyhow::Error) -> ErrorContext {
if let Some(ccmp_error) = error.downcast_ref::<AgpmError>() {
return create_error_context(ccmp_error);
}
let mut current_error: &dyn std::error::Error = error.as_ref();
loop {
if let Some(agpm_error) = current_error.downcast_ref::<AgpmError>() {
return create_error_context(agpm_error);
}
if let Some(template_error) =
current_error.downcast_ref::<crate::templating::TemplateError>()
{
let formatted = template_error.format_with_context();
return ErrorContext::new(AgpmError::Other {
message: formatted.clone(),
})
.with_suggestion("Check your template syntax and variable declarations")
.with_details(formatted);
}
match current_error.source() {
Some(source) => current_error = source,
None => break,
}
}
if let Some(file_error) = error.downcast_ref::<FileOperationError>() {
if file_error.source.kind() == std::io::ErrorKind::PermissionDenied {
return ErrorContext::new(AgpmError::PermissionDenied {
operation: file_error.operation.to_string(),
path: file_error.file_path.to_string_lossy().to_string(),
})
.with_suggestion("Check file permissions and try running with appropriate privileges")
.with_details(format!(
"Permission denied for '{}' on path: {}",
file_error.operation,
file_error.file_path.display()
));
}
return ErrorContext::new(AgpmError::FileSystemError {
operation: file_error.operation.to_string(),
path: file_error.file_path.to_string_lossy().to_string(),
})
.with_suggestion("Check that the path exists and you have the necessary permissions")
.with_details(format!(
"Failed to {} at path: {}",
file_error.operation,
file_error.file_path.display()
));
}
if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
match io_error.kind() {
std::io::ErrorKind::PermissionDenied => {
return create_error_context(&AgpmError::PermissionDenied {
operation: "file access".to_string(),
path: "file path not specified in error context".to_string(),
});
}
std::io::ErrorKind::NotFound => {
return create_error_context(&AgpmError::FileSystemError {
operation: "file not found".to_string(),
path: "file path not specified in error context".to_string(),
});
}
std::io::ErrorKind::AlreadyExists => {
return create_error_context(&AgpmError::FileSystemError {
operation: "file creation".to_string(),
path: "file path not specified in error context".to_string(),
});
}
_ => {
return ErrorContext::new(AgpmError::FileSystemError {
operation: "file operation".to_string(),
path: "unknown path".to_string(),
})
.with_suggestion("Check file permissions and disk space")
.with_details(format!("IO error: {}", io_error));
}
}
}
let mut current_error: &dyn std::error::Error = error.as_ref();
loop {
let error_msg = current_error.to_string();
if error_msg.contains("No tags found") || error_msg.contains("No tag found") {
return ErrorContext::new(AgpmError::Other {
message: error_msg.clone(),
})
.with_suggestion("Check available tags with 'git tag -l' in the source repository, or adjust your version constraint")
.with_details("No tags match the requested version constraint");
}
match current_error.source() {
Some(source) => current_error = source,
None => break,
}
}
let error_msg = error.to_string();
if TEMPLATE_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
return ErrorContext::new(AgpmError::Other {
message: format!("Template error: {}", error_msg),
})
.with_suggestion("Check your template syntax and variable names")
.with_details("Template rendering failed. Make sure all variables are defined and the syntax is correct.");
}
if NETWORK_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
return ErrorContext::new(AgpmError::NetworkError {
operation: "network request".to_string(),
reason: error_msg.clone(),
})
.with_suggestion("Check your internet connection and try again")
.with_details("A network operation failed. Please verify your connection and retry.");
}
if GIT_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
return ErrorContext::new(AgpmError::GitCommandError {
operation: "git operation".to_string(),
stderr: error_msg.clone(),
})
.with_suggestion("Ensure git is installed and configured correctly")
.with_details(
"A git operation failed. Check that git is in your PATH and properly configured.",
);
}
if PERMISSION_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
return ErrorContext::new(AgpmError::Other {
message: error_msg.clone(),
})
.with_suggestion("Check file permissions and try running with appropriate privileges")
.with_details("Permission was denied for the requested operation.");
}
ErrorContext::new(AgpmError::Other {
message: error_msg,
})
.with_suggestion("Check the error message above for more details")
.with_details("An unexpected error occurred. Please report this issue if it persists.")
}
pub fn create_error_context(error: &AgpmError) -> ErrorContext {
match &error {
AgpmError::GitNotFound => ErrorContext::new(AgpmError::GitNotFound)
.with_suggestion("Install git from https://git-scm.com/ or your package manager")
.with_details("AGPM requires git to be installed and available in your PATH"),
AgpmError::ManifestNotFound => ErrorContext::new(AgpmError::ManifestNotFound)
.with_suggestion("Run 'agpm init' to create a new manifest, or navigate to a directory with an existing agpm.toml")
.with_details("AGPM searches for agpm.toml in the current directory and parent directories"),
AgpmError::GitCommandError {
operation,
stderr,
} => {
let suggestion = match operation.as_str() {
"fetch" => "Check your internet connection and try again",
"checkout" => "Verify the branch, tag, or commit reference exists",
"pull" => "Check your git configuration and remote settings",
"clone" => "Verify the repository URL and your network connection",
_ => "Ensure git is properly configured and try again",
};
ErrorContext::new(AgpmError::GitCommandError {
operation: operation.clone(),
stderr: stderr.clone(),
})
.with_suggestion(suggestion)
.with_details(format!("Git {} operation failed: {}", operation, stderr))
}
AgpmError::GitCloneFailed {
url,
reason,
} => ErrorContext::new(AgpmError::GitCloneFailed {
url: url.clone(),
reason: reason.clone(),
})
.with_suggestion(format!("Verify the repository URL '{}' is correct and accessible", url))
.with_details(format!("Failed to clone repository: {}", reason)),
AgpmError::ResourceNotFound {
name,
} => ErrorContext::new(AgpmError::ResourceNotFound {
name: name.clone(),
})
.with_suggestion("Check that the resource is installed and available")
.with_details(format!("Resource '{}' not found", name)),
AgpmError::ResourceFileNotFound {
path,
source_name,
} => ErrorContext::new(AgpmError::ResourceFileNotFound {
path: path.clone(),
source_name: source_name.clone(),
})
.with_suggestion(format!(
"Check that '{}' exists in source '{}' and the version/tag is correct",
path, source_name
))
.with_details(format!("Resource file '{}' not found in source '{}'", path, source_name)),
AgpmError::ManifestParseError {
file,
reason,
} => ErrorContext::new(AgpmError::ManifestParseError {
file: file.clone(),
reason: reason.clone(),
})
.with_suggestion(format!("Check the syntax in '{}' - TOML format must be valid", file))
.with_details(format!("Failed to parse manifest file: {}", reason)),
AgpmError::FileSystemError {
operation,
path,
} => ErrorContext::new(AgpmError::FileSystemError {
operation: operation.clone(),
path: path.clone(),
})
.with_suggestion("Check that the path exists and you have the necessary permissions")
.with_details(format!("Failed to {} at path: {}", operation, path)),
AgpmError::PermissionDenied {
operation,
path,
} => ErrorContext::new(AgpmError::PermissionDenied {
operation: operation.clone(),
path: path.clone(),
})
.with_suggestion("Check file permissions and try running with appropriate privileges")
.with_details(format!("Permission denied for '{}' on path: {}", operation, path)),
_ => ErrorContext::new(AgpmError::Other {
message: error.to_string(),
})
.with_suggestion("Check the error message above for more details")
.with_details("An unexpected error occurred. Please report this issue if it persists."),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_user_friendly_error_io_permission_denied() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "Access denied");
let error = anyhow::Error::from(io_err);
let ctx = user_friendly_error(error);
assert!(matches!(ctx.error, AgpmError::PermissionDenied { .. }));
assert!(ctx.suggestion.is_some());
}
#[test]
fn test_user_friendly_error_io_not_found() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
let error = anyhow::Error::from(io_err);
let ctx = user_friendly_error(error);
assert!(matches!(ctx.error, AgpmError::FileSystemError { .. }));
assert!(ctx.suggestion.is_some());
}
#[test]
fn test_user_friendly_error_template_error() {
let error = anyhow::Error::msg("Failed to render template: variable 'foo' not found");
let ctx = user_friendly_error(error);
assert!(ctx.suggestion.is_some());
}
#[test]
fn test_user_friendly_error_network_error() {
let error = anyhow::Error::msg("Network connection failed");
let ctx = user_friendly_error(error);
assert!(matches!(ctx.error, AgpmError::NetworkError { .. }));
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("internet connection"));
}
#[test]
fn test_user_friendly_error_git_error() {
let error = anyhow::Error::msg("git command failed: repository not found");
let ctx = user_friendly_error(error);
assert!(matches!(ctx.error, AgpmError::GitCommandError { .. }));
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("git is installed"));
}
#[test]
fn test_user_friendly_error_fallback() {
let error = anyhow::Error::msg("Some completely unknown error type");
let ctx = user_friendly_error(error);
assert!(matches!(ctx.error, AgpmError::Other { .. }));
assert!(ctx.suggestion.is_some());
}
}