errgonomic/functions/
writeln_error.rs

1use crate::{ErrVec, Prefixer, write_to_named_temp_file};
2use std::error::Error;
3use std::io;
4use std::io::{Write, stderr};
5
6/// Writes a human-readable error trace to the provided writer and persists the full debug output to a temp file.
7///
8/// This is useful for CLI tools that want a concise error trace on stderr and a path to a full report.
9pub fn writeln_error_to_writer_and_file(error: &(dyn Error + 'static), writer: &mut dyn Write) -> Result<(), io::Error> {
10    writeln_error_to_writer(error, writer, true)?;
11    writeln!(writer)?;
12    let error_debug = format!("{error:#?}");
13    let result = write_to_named_temp_file(error_debug.as_bytes());
14    match result {
15        Ok((_file, path_buf)) => {
16            writeln!(writer, "See the full error report:\nless {}", path_buf.display())
17        }
18        Err(other_error) => {
19            writeln!(writer, "{other_error:#?}")
20        }
21    }
22}
23
24/// Writes a human-readable error trace to the provided writer.
25///
26/// When the error is an [`ErrVec`], each element is rendered as a nested bullet list.
27pub fn writeln_error_to_writer(error: &(dyn Error + 'static), writer: &mut dyn Write, is_top_level: bool) -> Result<(), io::Error> {
28    let source = error;
29    if let Some(err_vec) = source.downcast_ref::<ErrVec>() {
30        if is_top_level {
31            writeln!(writer, "- {error}")?;
32        }
33        err_vec.inner.iter().try_for_each(|err| {
34            let mut prefixer = error_prefixer(writer);
35            writeln_error_to_writer(err.as_ref(), &mut prefixer, false)
36        })
37    } else {
38        writeln!(writer, "- {error}")?;
39        if let Some(source_new) = source.source() {
40            writeln_error_to_writer(source_new, writer, false)
41        } else {
42            Ok(())
43        }
44    }
45}
46
47/// Writes an error trace to stderr and, if possible, includes a path to the full error report.
48pub fn eprintln_error(error: &(dyn Error + 'static)) {
49    let mut stderr = stderr().lock();
50    let result = writeln_error_to_writer_and_file(error, &mut stderr);
51    match result {
52        Ok(()) => (),
53        Err(err) => eprintln!("failed to write to stderr: {err:#?}"),
54    }
55}
56
57/// Builds a [`Prefixer`] suitable for nested error bullet lists.
58pub fn error_prefixer(writer: &mut dyn Write) -> Prefixer<'_> {
59    Prefixer::new("  * ", "    ", writer)
60}
61
62#[cfg(test)]
63mod tests {
64    use crate::functions::writeln_error::tests::JsonSchemaNewError::InputMustBeObject;
65    use crate::{ErrVec, writeln_error_to_writer};
66    use thiserror::Error;
67
68    #[test]
69    fn must_write_error() {
70        let error = CliRunError::CommandRunFailed {
71            source: CommandRunError::I18nUpdateRunFailed {
72                source: I18nUpdateRunError::UpdateRowsFailed {
73                    source: vec![
74                        UpdateRowError::I18nRequestFailed {
75                            source: I18nRequestError::JsonSchemaNewFailed {
76                                source: InputMustBeObject {
77                                    input: "foo".to_string(),
78                                },
79                            },
80                            row: Row::new("Foo"),
81                        },
82                        UpdateRowError::I18nRequestFailed {
83                            source: I18nRequestError::RequestSendFailed {
84                                source: tokio::io::Error::new(tokio::io::ErrorKind::AddrNotAvailable, "server at 239.143.73.1 did not respond"),
85                            },
86                            row: Row::new("Bar"),
87                        },
88                    ]
89                    .into(),
90                },
91            },
92        };
93        let mut output = Vec::new();
94        writeln_error_to_writer(&error, &mut output, true).unwrap();
95        let string = String::from_utf8(output).unwrap();
96        assert_eq!(string, include_str!("writeln_error/fixtures/must_write_error.txt"))
97    }
98
99    #[derive(Error, Debug)]
100    pub enum CliRunError {
101        #[error("failed to run CLI command")]
102        CommandRunFailed { source: CommandRunError },
103    }
104
105    #[derive(Error, Debug)]
106    pub enum CommandRunError {
107        #[error("failed to run i18n update command")]
108        I18nUpdateRunFailed { source: I18nUpdateRunError },
109    }
110
111    #[derive(Error, Debug)]
112    pub enum I18nUpdateRunError {
113        #[error("failed to update {len} rows", len = source.len())]
114        UpdateRowsFailed { source: ErrVec },
115    }
116
117    #[derive(Error, Debug)]
118    pub enum UpdateRowError {
119        #[error("failed to send an i18n request for row '{row}'", row = row.name)]
120        I18nRequestFailed { source: I18nRequestError, row: Row },
121    }
122
123    #[derive(Error, Debug)]
124    pub enum I18nRequestError {
125        #[error("failed to construct a JSON schema")]
126        JsonSchemaNewFailed { source: JsonSchemaNewError },
127        #[error("failed to send a request")]
128        RequestSendFailed { source: tokio::io::Error },
129    }
130
131    #[derive(Error, Debug)]
132    pub enum JsonSchemaNewError {
133        #[error("input must be an object")]
134        InputMustBeObject { input: String },
135    }
136
137    #[derive(Debug)]
138    pub struct Row {
139        name: String,
140    }
141
142    impl Row {
143        pub fn new(name: impl Into<String>) -> Self {
144            Self {
145                name: name.into(),
146            }
147        }
148    }
149}