heron_rebuild_workflow/
error.rs

1use anyhow::Result;
2use colored::Colorize;
3
4use crate::WorkflowStrings;
5
6/// For re-throwing after we've printed a list of errors to the user.
7#[derive(Debug, thiserror::Error)]
8#[error("{0} failed due to {1} errors")]
9pub struct AggregatedErrors(pub String, pub usize);
10
11/// impl this for error traits that rely on Workflow strings for their message.
12pub trait Recap: std::fmt::Debug + Send + Sync {
13    fn recap(&self, wf: &WorkflowStrings) -> Result<Option<String>>;
14}
15
16/// Wrap errors in this struct at the call site so they can use the WorkflowStrings
17/// object in the recap.
18#[derive(Debug, thiserror::Error)]
19#[error("{e:?}")]
20pub struct Recapper {
21    e: Box<dyn Recap>,
22}
23
24impl Recapper {
25    pub fn new(e: impl Recap + 'static) -> Self {
26        Self { e: Box::new(e) }
27    }
28}
29
30// in future we can add a `warnings` field, too.
31pub struct Errors {
32    errors: Vec<anyhow::Error>,
33}
34
35impl Default for Errors {
36    fn default() -> Self {
37        Self {
38            // ideally we won't have any,
39            // and we don't mind reallocating if we're already in an error state:
40            errors: Vec::with_capacity(0),
41        }
42    }
43}
44
45impl Errors {
46    pub fn add_context(&mut self, e: anyhow::Error, msg: String) {
47        log::trace!("{msg}: {e:?}");
48        self.errors.push(e.context(msg));
49    }
50
51    pub fn add(&mut self, e: anyhow::Error) {
52        log::trace!("error: {e:?}");
53        self.errors.push(e);
54    }
55
56    /// Print full list of errors to stderr, fail w/ an aggregated error
57    /// if there were one or more errors.
58    pub fn print_recap(&self, label: &str, wf: &WorkflowStrings) -> Result<()> {
59        if self.errors.is_empty() {
60            Ok(())
61        } else {
62            eprintln!("\n{} {}:\n", "Encountered errors while".red(), label.red());
63            for e in &self.errors {
64                use anyhow::Context;
65                recap(e, wf).context("Unable to print error list due to errors while printing")?;
66            }
67            Err(AggregatedErrors(label.to_owned(), self.errors.len()).into())
68        }
69    }
70}
71
72fn recap(e: &anyhow::Error, wf: &WorkflowStrings) -> Result<()> {
73    eprint!("{}: ", "ERROR".red());
74
75    handle_recapper_anyhow(e, wf)?;
76    for cause in e.chain().skip(1) {
77        eprint!("\nCaused by:\n\t");
78        handle_recapper_dyn(cause, wf)?;
79    }
80    eprintln!();
81    Ok(())
82}
83
84// both anyhow::Error and std Error have a fn called `downcast_ref`, but they aren't the
85// same method, so we need two fns to handle them.
86fn handle_recapper_dyn(e: &(dyn std::error::Error + 'static), wf: &WorkflowStrings) -> Result<()> {
87    if let Some(recapper) = e.downcast_ref::<Recapper>() {
88        if let Some(msg) = recapper.e.recap(wf)? {
89            eprintln!("{}", msg);
90            return Ok(());
91        }
92    }
93    eprintln!("{}", e);
94    Ok(())
95}
96
97fn handle_recapper_anyhow(e: &anyhow::Error, wf: &WorkflowStrings) -> Result<()> {
98    if let Some(recapper) = e.downcast_ref::<Recapper>() {
99        if let Some(msg) = recapper.e.recap(wf)? {
100            eprintln!("{}", msg);
101            return Ok(());
102        }
103    }
104    eprintln!("{}", e);
105    Ok(())
106}