1use crate::{
7 aoc::ApiError, config::ConfigError, env::EnvError, process::ProcessError, resolve::ResolveError,
8};
9use std::{io, path::Path, path::PathBuf};
10
11#[derive(Debug, thiserror::Error)]
13pub enum Error {
14 #[error(transparent)]
16 Env(#[from] EnvError),
17
18 #[error(transparent)]
20 Config(#[from] ConfigError),
21
22 #[error(transparent)]
24 Resolve(#[from] ResolveError),
25
26 #[error(transparent)]
28 Process(#[from] ProcessError),
29
30 #[error(transparent)]
32 Api(#[from] ApiError),
33
34 #[error("project does not exist: {path}\n\ncreate it with `aoc init`")]
36 ProjectMissing {
37 path: PathBuf,
39 },
40
41 #[error("project already exists: {path}")]
43 ProjectExists {
44 path: PathBuf,
46 },
47
48 #[error("failed to {action}: {path}")]
50 Io {
51 action: &'static str,
53 path: PathBuf,
55 #[source]
57 source: io::Error,
58 },
59}
60
61pub trait IoResultExt<T> {
63 fn io_context(self, action: &'static str, path: &Path) -> Result<T, Error>;
69}
70
71impl<T> IoResultExt<T> for Result<T, io::Error> {
72 fn io_context(self, action: &'static str, path: &Path) -> Result<T, Error> {
73 self.map_err(|source| Error::Io {
74 action,
75 path: path.to_path_buf(),
76 source,
77 })
78 }
79}
80
81pub fn report(error: &Error) {
83 use colored::Colorize as _;
84
85 eprintln!("{} {error}", "error:".red().bold());
86
87 let mut source = std::error::Error::source(error);
88 while let Some(cause) = source {
89 eprintln!(" {} {cause}", "caused by:".dimmed());
90 source = cause.source();
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn io_failures_name_the_action_and_path() {
100 let result: Result<(), io::Error> =
101 Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied"));
102
103 let error = result
104 .io_context(
105 "create project directory",
106 Path::new("/aoc/2024/day07/rust"),
107 )
108 .expect_err("the result is an error");
109
110 let message = error.to_string();
111 assert!(message.contains("create project directory"), "{message}");
112 assert!(message.contains("/aoc/2024/day07/rust"), "{message}");
113 }
114
115 #[test]
116 fn missing_and_existing_projects_read_clearly() {
117 let missing = Error::ProjectMissing {
118 path: PathBuf::from("/aoc/2024/day07/rust"),
119 };
120 let existing = Error::ProjectExists {
121 path: PathBuf::from("/aoc/2024/day07/rust"),
122 };
123
124 assert!(missing.to_string().contains("aoc init"));
125 assert!(existing.to_string().contains("already exists"));
126 }
127}