Skip to main content

aoc_runtime/
error.rs

1//! The error type the binary reports.
2//!
3//! Each module owns a typed error describing what can go wrong inside it; this
4//! enum is the union the command handlers return.
5
6use crate::{
7    aoc::ApiError, config::ConfigError, env::EnvError, process::ProcessError, resolve::ResolveError,
8};
9use std::{io, path::Path, path::PathBuf};
10
11/// Anything that can stop a command from completing.
12#[derive(Debug, thiserror::Error)]
13pub enum Error {
14    /// The environment could not be inspected.
15    #[error(transparent)]
16    Env(#[from] EnvError),
17
18    /// Configuration could not be loaded.
19    #[error(transparent)]
20    Config(#[from] ConfigError),
21
22    /// Arguments could not be resolved into a plan.
23    #[error(transparent)]
24    Resolve(#[from] ResolveError),
25
26    /// A child process failed.
27    #[error(transparent)]
28    Process(#[from] ProcessError),
29
30    /// A request to Advent of Code failed.
31    #[error(transparent)]
32    Api(#[from] ApiError),
33
34    /// The project directory does not exist yet.
35    #[error("project does not exist: {path}\n\ncreate it with `aoc init`")]
36    ProjectMissing {
37        /// The expected project directory.
38        path: PathBuf,
39    },
40
41    /// The project directory already exists.
42    #[error("project already exists: {path}")]
43    ProjectExists {
44        /// The existing project directory.
45        path: PathBuf,
46    },
47
48    /// A filesystem operation failed.
49    #[error("failed to {action}: {path}")]
50    Io {
51        /// What was being attempted, phrased as a verb.
52        action: &'static str,
53        /// The path involved.
54        path: PathBuf,
55        /// The underlying I/O error.
56        #[source]
57        source: io::Error,
58    },
59}
60
61/// Attaches the path and the attempted operation to an I/O failure.
62pub trait IoResultExt<T> {
63    /// Converts an I/O error into [`Error::Io`].
64    ///
65    /// # Errors
66    ///
67    /// Returns [`Error::Io`] whenever the receiver is an error.
68    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
81/// Renders an error and its causes to standard error.
82pub 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}