1use crate::core::error::ErrorContext;
8use anyhow::{Context, Result};
9use std::path::Path;
10
11pub fn file_error_context(operation: &str, path: &Path) -> ErrorContext {
34 use crate::core::AgpmError;
35
36 ErrorContext {
37 error: AgpmError::FileSystemError {
38 operation: operation.to_string(),
39 path: path.display().to_string(),
40 },
41 suggestion: match operation {
42 "read" => Some("Check that the file exists and you have read permissions".to_string()),
43 "write" => Some("Check that you have write permissions for this location".to_string()),
44 "create" => Some(
45 "Check that the parent directory exists and you have write permissions".to_string(),
46 ),
47 "delete" => {
48 Some("Check that the file exists and you have delete permissions".to_string())
49 }
50 _ => None,
51 },
52 details: Some(format!("File path: {}", path.display())),
53 }
54}
55
56pub fn git_error_context(command: &str, repo: Option<&str>) -> ErrorContext {
71 use crate::core::AgpmError;
72
73 ErrorContext {
74 error: AgpmError::GitCommandError {
75 operation: command.to_string(),
76 stderr: format!("Git {command} operation failed"),
77 },
78 suggestion: match command {
79 "clone" => {
80 Some("Check your network connection and that the repository exists".to_string())
81 }
82 "fetch" | "pull" => {
83 Some("Check your network connection and repository access".to_string())
84 }
85 "checkout" => Some("Ensure the branch or tag exists in the repository".to_string()),
86 "status" => Some("Ensure you're in a valid git repository".to_string()),
87 _ => Some("Check that git is installed and accessible".to_string()),
88 },
89 details: repo.map(|r| format!("Repository: {r}")),
90 }
91}
92
93pub fn manifest_error_context(operation: &str, details: Option<&str>) -> ErrorContext {
108 use crate::core::AgpmError;
109
110 let error = match operation {
111 "load" => AgpmError::ManifestNotFound,
112 "parse" => AgpmError::ManifestParseError {
113 file: "agpm.toml".to_string(),
114 reason: details.unwrap_or("Invalid TOML syntax").to_string(),
115 },
116 "validate" => AgpmError::ManifestValidationError {
117 reason: details.unwrap_or("Validation failed").to_string(),
118 },
119 _ => AgpmError::Other {
120 message: format!("Manifest operation '{operation}' failed"),
121 },
122 };
123
124 ErrorContext {
125 error,
126 suggestion: match operation {
127 "load" => Some("Check that agpm.toml exists in the project directory".to_string()),
128 "parse" => Some("Check that agpm.toml contains valid TOML syntax".to_string()),
129 "validate" => {
130 Some("Ensure all required fields are present in the manifest".to_string())
131 }
132 _ => None,
133 },
134 details: details.map(std::string::ToString::to_string),
135 }
136}
137
138pub fn dependency_error_context(dependency: &str, reason: &str) -> ErrorContext {
153 use crate::core::AgpmError;
154
155 ErrorContext {
156 error: AgpmError::InvalidDependency {
157 name: dependency.to_string(),
158 reason: reason.to_string(),
159 },
160 suggestion: Some("Try running 'agpm update' to update dependencies".to_string()),
161 details: Some(reason.to_string()),
162 }
163}
164
165pub fn network_error_context(operation: &str, url: Option<&str>) -> ErrorContext {
180 use crate::core::AgpmError;
181
182 ErrorContext {
183 error: AgpmError::NetworkError {
184 operation: operation.to_string(),
185 reason: format!("Network {operation} failed"),
186 },
187 suggestion: Some("Check your internet connection and try again".to_string()),
188 details: url.map(|u| format!("URL: {u}")),
189 }
190}
191
192pub fn config_error_context(config_type: &str, issue: &str) -> ErrorContext {
207 use crate::core::AgpmError;
208
209 ErrorContext {
210 error: AgpmError::ConfigError {
211 message: format!("Configuration error in {config_type} config: {issue}"),
212 },
213 suggestion: match config_type {
214 "global" => Some("Check ~/.agpm/config.toml for correct settings".to_string()),
215 "project" => Some("Check agpm.toml in your project directory".to_string()),
216 "mcp" => Some("Check .mcp.json for valid MCP server configurations".to_string()),
217 _ => None,
218 },
219 details: Some(issue.to_string()),
220 }
221}
222
223pub fn permission_error_context(resource: &str, operation: &str) -> ErrorContext {
238 use crate::core::AgpmError;
239
240 ErrorContext {
241 error: AgpmError::PermissionDenied {
242 operation: operation.to_string(),
243 path: resource.to_string(),
244 },
245 suggestion: Some(format!("Check that you have {operation} permissions for: {resource}")),
246 details: if cfg!(windows) {
247 Some("On Windows, you may need to run as Administrator".to_string())
248 } else {
249 Some("On Unix systems, you may need to use sudo or change file permissions".to_string())
250 },
251 }
252}
253
254pub trait ErrorContextExt<T> {
256 fn file_context(self, operation: &str, path: &Path) -> Result<T>;
258
259 fn git_context(self, command: &str, repo: Option<&str>) -> Result<T>;
261
262 fn manifest_context(self, operation: &str, details: Option<&str>) -> Result<T>;
264
265 fn dependency_context(self, dependency: &str, reason: &str) -> Result<T>;
267
268 fn network_context(self, operation: &str, url: Option<&str>) -> Result<T>;
270}
271
272impl<T, E> ErrorContextExt<T> for std::result::Result<T, E>
273where
274 E: std::error::Error + Send + Sync + 'static,
275{
276 fn file_context(self, operation: &str, path: &Path) -> Result<T> {
277 self.with_context(|| file_error_context(operation, path))
278 }
279
280 fn git_context(self, command: &str, repo: Option<&str>) -> Result<T> {
281 self.with_context(|| git_error_context(command, repo))
282 }
283
284 fn manifest_context(self, operation: &str, details: Option<&str>) -> Result<T> {
285 self.with_context(|| manifest_error_context(operation, details))
286 }
287
288 fn dependency_context(self, dependency: &str, reason: &str) -> Result<T> {
289 self.with_context(|| dependency_error_context(dependency, reason))
290 }
291
292 fn network_context(self, operation: &str, url: Option<&str>) -> Result<T> {
293 self.with_context(|| network_error_context(operation, url))
294 }
295}
296
297#[macro_export]
311macro_rules! error_context {
312 (error: $err:expr) => {
313 $crate::core::error::ErrorContext {
314 error: $err,
315 suggestion: None,
316 details: None,
317 }
318 };
319 (error: $err:expr, suggestion: $sug:expr) => {
320 $crate::core::error::ErrorContext {
321 error: $err,
322 suggestion: Some($sug.to_string()),
323 details: None,
324 }
325 };
326 (error: $err:expr, suggestion: $sug:expr, details: $det:expr) => {
327 $crate::core::error::ErrorContext {
328 error: $err,
329 suggestion: Some($sug.to_string()),
330 details: Some($det.to_string()),
331 }
332 };
333 (error: $err:expr, details: $det:expr) => {
334 $crate::core::error::ErrorContext {
335 error: $err,
336 suggestion: None,
337 details: Some($det.to_string()),
338 }
339 };
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn test_file_error_context() {
348 let context = file_error_context("read", Path::new("/tmp/test.txt"));
349 assert!(matches!(context.error, crate::core::AgpmError::FileSystemError { .. }));
350 assert!(context.suggestion.is_some());
351 assert!(context.details.unwrap().contains("/tmp/test.txt"));
352 }
353
354 #[test]
355 fn test_git_error_context() {
356 let context = git_error_context("clone", Some("https://github.com/test/repo"));
357 assert!(matches!(context.error, crate::core::AgpmError::GitCommandError { .. }));
358 assert!(context.suggestion.unwrap().contains("network"));
359 assert!(context.details.unwrap().contains("github.com"));
360 }
361
362 #[test]
363 fn test_error_context_macro() {
364 use crate::core::AgpmError;
365
366 let context = error_context! {
367 error: AgpmError::Other { message: "Test error".to_string() },
368 suggestion: "Test suggestion",
369 details: "Test details"
370 };
371 assert!(matches!(context.error, AgpmError::Other { .. }));
372 assert_eq!(context.suggestion.unwrap(), "Test suggestion");
373 assert_eq!(context.details.unwrap(), "Test details");
374 }
375
376 #[test]
377 fn test_permission_error_context() {
378 let context = permission_error_context("/usr/local", "write");
379 assert!(matches!(context.error, crate::core::AgpmError::PermissionDenied { .. }));
380 assert!(context.suggestion.unwrap().contains("write permissions"));
381 assert!(context.details.is_some());
382 }
383
384 #[test]
385 fn test_manifest_error_context_all_operations() {
386 let context = manifest_error_context("load", None);
388 assert!(matches!(context.error, crate::core::AgpmError::ManifestNotFound));
389 assert!(context.suggestion.unwrap().contains("agpm.toml exists"));
390
391 let context = manifest_error_context("parse", Some("Syntax error at line 10"));
393 assert!(matches!(context.error, crate::core::AgpmError::ManifestParseError { .. }));
394 assert!(context.suggestion.unwrap().contains("valid TOML syntax"));
395 assert_eq!(context.details.unwrap(), "Syntax error at line 10");
396
397 let context = manifest_error_context("validate", Some("Missing required field"));
399 assert!(matches!(context.error, crate::core::AgpmError::ManifestValidationError { .. }));
400 assert!(context.suggestion.unwrap().contains("required fields"));
401 assert_eq!(context.details.unwrap(), "Missing required field");
402
403 let context = manifest_error_context("unknown", None);
405 assert!(matches!(context.error, crate::core::AgpmError::Other { .. }));
406 assert!(context.suggestion.is_none());
407 }
408
409 #[test]
410 fn test_dependency_error_context() {
411 let context = dependency_error_context("test-agent", "Version not found");
412 assert!(matches!(context.error, crate::core::AgpmError::InvalidDependency { .. }));
413 assert!(context.suggestion.unwrap().contains("agpm update"));
414 assert_eq!(context.details.unwrap(), "Version not found");
415 }
416
417 #[test]
418 fn test_network_error_context() {
419 let context = network_error_context("download", Some("https://example.com/file"));
420 assert!(matches!(context.error, crate::core::AgpmError::NetworkError { .. }));
421 assert!(context.suggestion.unwrap().contains("internet connection"));
422 assert!(context.details.unwrap().contains("example.com"));
423 }
424
425 #[test]
426 fn test_config_error_context_types() {
427 let context = config_error_context("global", "Invalid format");
429 assert!(matches!(context.error, crate::core::AgpmError::ConfigError { .. }));
430 assert!(context.suggestion.unwrap().contains("~/.agpm/config.toml"));
431
432 let context = config_error_context("project", "Missing dependency");
434 assert!(context.suggestion.unwrap().contains("agpm.toml"));
435
436 let context = config_error_context("mcp", "Invalid server");
438 assert!(context.suggestion.unwrap().contains(".mcp.json"));
439
440 let context = config_error_context("unknown", "Some issue");
442 assert!(context.suggestion.is_none());
443 }
444
445 #[test]
446 fn test_file_error_context_operations() {
447 let context = file_error_context("read", Path::new("/test/file.txt"));
449 assert!(context.suggestion.unwrap().contains("read permissions"));
450
451 let context = file_error_context("write", Path::new("/test/file.txt"));
453 assert!(context.suggestion.unwrap().contains("write permissions"));
454
455 let context = file_error_context("create", Path::new("/test/file.txt"));
457 assert!(context.suggestion.unwrap().contains("parent directory"));
458
459 let context = file_error_context("delete", Path::new("/test/file.txt"));
461 assert!(context.suggestion.unwrap().contains("delete permissions"));
462
463 let context = file_error_context("unknown", Path::new("/test/file.txt"));
465 assert!(context.suggestion.is_none());
466 }
467
468 #[test]
469 fn test_git_error_context_commands() {
470 let context = git_error_context("clone", Some("repo.git"));
472 assert!(context.suggestion.unwrap().contains("repository exists"));
473
474 let context = git_error_context("fetch", None);
476 assert!(context.suggestion.unwrap().contains("repository access"));
477
478 let context = git_error_context("pull", Some("origin"));
480 assert!(context.suggestion.unwrap().contains("repository access"));
481
482 let context = git_error_context("checkout", Some("branch"));
484 assert!(context.suggestion.unwrap().contains("branch or tag exists"));
485
486 let context = git_error_context("status", None);
488 assert!(context.suggestion.unwrap().contains("valid git repository"));
489
490 let context = git_error_context("unknown", None);
492 assert!(context.suggestion.unwrap().contains("git is installed"));
493 }
494
495 #[test]
496 fn test_error_context_ext_trait() {
497 use std::io;
498
499 let result: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::NotFound, "test"));
501 let result = result.file_context("read", Path::new("/test.txt"));
502 assert!(result.is_err());
503
504 let result: Result<(), io::Error> = Err(io::Error::other("test"));
506 let result = result.git_context("clone", Some("repo"));
507 assert!(result.is_err());
508
509 let result: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::InvalidData, "test"));
511 let result = result.manifest_context("parse", Some("details"));
512 assert!(result.is_err());
513
514 let result: Result<(), io::Error> = Err(io::Error::other("test"));
516 let result = result.dependency_context("dep", "reason");
517 assert!(result.is_err());
518
519 let result: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::TimedOut, "test"));
521 let result = result.network_context("fetch", Some("url"));
522 assert!(result.is_err());
523 }
524
525 #[test]
526 fn test_permission_error_context_platforms() {
527 let context = permission_error_context("/path", "execute");
528 assert!(context.details.is_some());
529
530 #[cfg(windows)]
531 assert!(context.details.unwrap().contains("Administrator"));
532
533 #[cfg(not(windows))]
534 assert!(context.details.unwrap().contains("sudo"));
535 }
536
537 #[test]
538 fn test_error_context_macro_variants() {
539 use crate::core::AgpmError;
540
541 let context = error_context! {
543 error: AgpmError::Other { message: "Error only".to_string() }
544 };
545 assert!(context.suggestion.is_none());
546 assert!(context.details.is_none());
547
548 let context = error_context! {
550 error: AgpmError::Other { message: "Error".to_string() },
551 suggestion: "Do this"
552 };
553 assert_eq!(context.suggestion.unwrap(), "Do this");
554 assert!(context.details.is_none());
555
556 let context = error_context! {
558 error: AgpmError::Other { message: "Error".to_string() },
559 details: "More info"
560 };
561 assert!(context.suggestion.is_none());
562 assert_eq!(context.details.unwrap(), "More info");
563 }
564}