use crate::core::error::ErrorContext;
use anyhow::{Context, Result};
use std::path::Path;
pub fn file_error_context(operation: &str, path: &Path) -> ErrorContext {
use crate::core::AgpmError;
ErrorContext {
error: AgpmError::FileSystemError {
operation: operation.to_string(),
path: path.display().to_string(),
},
suggestion: match operation {
"read" => Some("Check that the file exists and you have read permissions".to_string()),
"write" => Some("Check that you have write permissions for this location".to_string()),
"create" => Some(
"Check that the parent directory exists and you have write permissions".to_string(),
),
"delete" => {
Some("Check that the file exists and you have delete permissions".to_string())
}
_ => None,
},
details: Some(format!("File path: {}", path.display())),
}
}
pub fn git_error_context(command: &str, repo: Option<&str>) -> ErrorContext {
use crate::core::AgpmError;
ErrorContext {
error: AgpmError::GitCommandError {
operation: command.to_string(),
stderr: format!("Git {command} operation failed"),
},
suggestion: match command {
"clone" => {
Some("Check your network connection and that the repository exists".to_string())
}
"fetch" | "pull" => {
Some("Check your network connection and repository access".to_string())
}
"checkout" => Some("Ensure the branch or tag exists in the repository".to_string()),
"status" => Some("Ensure you're in a valid git repository".to_string()),
_ => Some("Check that git is installed and accessible".to_string()),
},
details: repo.map(|r| format!("Repository: {r}")),
}
}
pub fn manifest_error_context(operation: &str, details: Option<&str>) -> ErrorContext {
use crate::core::AgpmError;
let error = match operation {
"load" => AgpmError::ManifestNotFound,
"parse" => AgpmError::ManifestParseError {
file: "agpm.toml".to_string(),
reason: details.unwrap_or("Invalid TOML syntax").to_string(),
},
"validate" => AgpmError::ManifestValidationError {
reason: details.unwrap_or("Validation failed").to_string(),
},
_ => AgpmError::Other {
message: format!("Manifest operation '{operation}' failed"),
},
};
ErrorContext {
error,
suggestion: match operation {
"load" => Some("Check that agpm.toml exists in the project directory".to_string()),
"parse" => Some("Check that agpm.toml contains valid TOML syntax".to_string()),
"validate" => {
Some("Ensure all required fields are present in the manifest".to_string())
}
_ => None,
},
details: details.map(std::string::ToString::to_string),
}
}
pub fn dependency_error_context(dependency: &str, reason: &str) -> ErrorContext {
use crate::core::AgpmError;
ErrorContext {
error: AgpmError::InvalidDependency {
name: dependency.to_string(),
reason: reason.to_string(),
},
suggestion: Some("Try running 'agpm update' to update dependencies".to_string()),
details: Some(reason.to_string()),
}
}
pub fn network_error_context(operation: &str, url: Option<&str>) -> ErrorContext {
use crate::core::AgpmError;
ErrorContext {
error: AgpmError::NetworkError {
operation: operation.to_string(),
reason: format!("Network {operation} failed"),
},
suggestion: Some("Check your internet connection and try again".to_string()),
details: url.map(|u| format!("URL: {u}")),
}
}
pub fn config_error_context(config_type: &str, issue: &str) -> ErrorContext {
use crate::core::AgpmError;
ErrorContext {
error: AgpmError::ConfigError {
message: format!("Configuration error in {config_type} config: {issue}"),
},
suggestion: match config_type {
"global" => Some("Check ~/.agpm/config.toml for correct settings".to_string()),
"project" => Some("Check agpm.toml in your project directory".to_string()),
"mcp" => Some("Check .mcp.json for valid MCP server configurations".to_string()),
_ => None,
},
details: Some(issue.to_string()),
}
}
pub fn permission_error_context(resource: &str, operation: &str) -> ErrorContext {
use crate::core::AgpmError;
ErrorContext {
error: AgpmError::PermissionDenied {
operation: operation.to_string(),
path: resource.to_string(),
},
suggestion: Some(format!("Check that you have {operation} permissions for: {resource}")),
details: if cfg!(windows) {
Some("On Windows, you may need to run as Administrator".to_string())
} else {
Some("On Unix systems, you may need to use sudo or change file permissions".to_string())
},
}
}
pub trait ErrorContextExt<T> {
fn file_context(self, operation: &str, path: &Path) -> Result<T>;
fn git_context(self, command: &str, repo: Option<&str>) -> Result<T>;
fn manifest_context(self, operation: &str, details: Option<&str>) -> Result<T>;
fn dependency_context(self, dependency: &str, reason: &str) -> Result<T>;
fn network_context(self, operation: &str, url: Option<&str>) -> Result<T>;
}
impl<T, E> ErrorContextExt<T> for std::result::Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn file_context(self, operation: &str, path: &Path) -> Result<T> {
self.with_context(|| file_error_context(operation, path))
}
fn git_context(self, command: &str, repo: Option<&str>) -> Result<T> {
self.with_context(|| git_error_context(command, repo))
}
fn manifest_context(self, operation: &str, details: Option<&str>) -> Result<T> {
self.with_context(|| manifest_error_context(operation, details))
}
fn dependency_context(self, dependency: &str, reason: &str) -> Result<T> {
self.with_context(|| dependency_error_context(dependency, reason))
}
fn network_context(self, operation: &str, url: Option<&str>) -> Result<T> {
self.with_context(|| network_error_context(operation, url))
}
}
#[macro_export]
macro_rules! error_context {
(error: $err:expr) => {
$crate::core::error::ErrorContext {
error: $err,
suggestion: None,
details: None,
}
};
(error: $err:expr, suggestion: $sug:expr) => {
$crate::core::error::ErrorContext {
error: $err,
suggestion: Some($sug.to_string()),
details: None,
}
};
(error: $err:expr, suggestion: $sug:expr, details: $det:expr) => {
$crate::core::error::ErrorContext {
error: $err,
suggestion: Some($sug.to_string()),
details: Some($det.to_string()),
}
};
(error: $err:expr, details: $det:expr) => {
$crate::core::error::ErrorContext {
error: $err,
suggestion: None,
details: Some($det.to_string()),
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_error_context() {
let context = file_error_context("read", Path::new("/tmp/test.txt"));
assert!(matches!(context.error, crate::core::AgpmError::FileSystemError { .. }));
assert!(context.suggestion.is_some());
assert!(context.details.unwrap().contains("/tmp/test.txt"));
}
#[test]
fn test_git_error_context() {
let context = git_error_context("clone", Some("https://github.com/test/repo"));
assert!(matches!(context.error, crate::core::AgpmError::GitCommandError { .. }));
assert!(context.suggestion.unwrap().contains("network"));
assert!(context.details.unwrap().contains("github.com"));
}
#[test]
fn test_error_context_macro() {
use crate::core::AgpmError;
let context = error_context! {
error: AgpmError::Other { message: "Test error".to_string() },
suggestion: "Test suggestion",
details: "Test details"
};
assert!(matches!(context.error, AgpmError::Other { .. }));
assert_eq!(context.suggestion.unwrap(), "Test suggestion");
assert_eq!(context.details.unwrap(), "Test details");
}
#[test]
fn test_permission_error_context() {
let context = permission_error_context("/usr/local", "write");
assert!(matches!(context.error, crate::core::AgpmError::PermissionDenied { .. }));
assert!(context.suggestion.unwrap().contains("write permissions"));
assert!(context.details.is_some());
}
#[test]
fn test_manifest_error_context_all_operations() {
let context = manifest_error_context("load", None);
assert!(matches!(context.error, crate::core::AgpmError::ManifestNotFound));
assert!(context.suggestion.unwrap().contains("agpm.toml exists"));
let context = manifest_error_context("parse", Some("Syntax error at line 10"));
assert!(matches!(context.error, crate::core::AgpmError::ManifestParseError { .. }));
assert!(context.suggestion.unwrap().contains("valid TOML syntax"));
assert_eq!(context.details.unwrap(), "Syntax error at line 10");
let context = manifest_error_context("validate", Some("Missing required field"));
assert!(matches!(context.error, crate::core::AgpmError::ManifestValidationError { .. }));
assert!(context.suggestion.unwrap().contains("required fields"));
assert_eq!(context.details.unwrap(), "Missing required field");
let context = manifest_error_context("unknown", None);
assert!(matches!(context.error, crate::core::AgpmError::Other { .. }));
assert!(context.suggestion.is_none());
}
#[test]
fn test_dependency_error_context() {
let context = dependency_error_context("test-agent", "Version not found");
assert!(matches!(context.error, crate::core::AgpmError::InvalidDependency { .. }));
assert!(context.suggestion.unwrap().contains("agpm update"));
assert_eq!(context.details.unwrap(), "Version not found");
}
#[test]
fn test_network_error_context() {
let context = network_error_context("download", Some("https://example.com/file"));
assert!(matches!(context.error, crate::core::AgpmError::NetworkError { .. }));
assert!(context.suggestion.unwrap().contains("internet connection"));
assert!(context.details.unwrap().contains("example.com"));
}
#[test]
fn test_config_error_context_types() {
let context = config_error_context("global", "Invalid format");
assert!(matches!(context.error, crate::core::AgpmError::ConfigError { .. }));
assert!(context.suggestion.unwrap().contains("~/.agpm/config.toml"));
let context = config_error_context("project", "Missing dependency");
assert!(context.suggestion.unwrap().contains("agpm.toml"));
let context = config_error_context("mcp", "Invalid server");
assert!(context.suggestion.unwrap().contains(".mcp.json"));
let context = config_error_context("unknown", "Some issue");
assert!(context.suggestion.is_none());
}
#[test]
fn test_file_error_context_operations() {
let context = file_error_context("read", Path::new("/test/file.txt"));
assert!(context.suggestion.unwrap().contains("read permissions"));
let context = file_error_context("write", Path::new("/test/file.txt"));
assert!(context.suggestion.unwrap().contains("write permissions"));
let context = file_error_context("create", Path::new("/test/file.txt"));
assert!(context.suggestion.unwrap().contains("parent directory"));
let context = file_error_context("delete", Path::new("/test/file.txt"));
assert!(context.suggestion.unwrap().contains("delete permissions"));
let context = file_error_context("unknown", Path::new("/test/file.txt"));
assert!(context.suggestion.is_none());
}
#[test]
fn test_git_error_context_commands() {
let context = git_error_context("clone", Some("repo.git"));
assert!(context.suggestion.unwrap().contains("repository exists"));
let context = git_error_context("fetch", None);
assert!(context.suggestion.unwrap().contains("repository access"));
let context = git_error_context("pull", Some("origin"));
assert!(context.suggestion.unwrap().contains("repository access"));
let context = git_error_context("checkout", Some("branch"));
assert!(context.suggestion.unwrap().contains("branch or tag exists"));
let context = git_error_context("status", None);
assert!(context.suggestion.unwrap().contains("valid git repository"));
let context = git_error_context("unknown", None);
assert!(context.suggestion.unwrap().contains("git is installed"));
}
#[test]
fn test_error_context_ext_trait() {
use std::io;
let result: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::NotFound, "test"));
let result = result.file_context("read", Path::new("/test.txt"));
assert!(result.is_err());
let result: Result<(), io::Error> = Err(io::Error::other("test"));
let result = result.git_context("clone", Some("repo"));
assert!(result.is_err());
let result: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::InvalidData, "test"));
let result = result.manifest_context("parse", Some("details"));
assert!(result.is_err());
let result: Result<(), io::Error> = Err(io::Error::other("test"));
let result = result.dependency_context("dep", "reason");
assert!(result.is_err());
let result: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::TimedOut, "test"));
let result = result.network_context("fetch", Some("url"));
assert!(result.is_err());
}
#[test]
fn test_permission_error_context_platforms() {
let context = permission_error_context("/path", "execute");
assert!(context.details.is_some());
#[cfg(windows)]
assert!(context.details.unwrap().contains("Administrator"));
#[cfg(not(windows))]
assert!(context.details.unwrap().contains("sudo"));
}
#[test]
fn test_error_context_macro_variants() {
use crate::core::AgpmError;
let context = error_context! {
error: AgpmError::Other { message: "Error only".to_string() }
};
assert!(context.suggestion.is_none());
assert!(context.details.is_none());
let context = error_context! {
error: AgpmError::Other { message: "Error".to_string() },
suggestion: "Do this"
};
assert_eq!(context.suggestion.unwrap(), "Do this");
assert!(context.details.is_none());
let context = error_context! {
error: AgpmError::Other { message: "Error".to_string() },
details: "More info"
};
assert!(context.suggestion.is_none());
assert_eq!(context.details.unwrap(), "More info");
}
}