Skip to main content

rajac_base/
cli.rs

1use crate::error::RajacError;
2use crate::result::RajacResult;
3use std::fmt::Write as _;
4use std::process::ExitCode;
5
6/// What does `try_main` provide for CLI binaries?
7///
8/// It runs a fallible entrypoint, prints a readable error report on failure,
9/// and converts the outcome into a process exit code.
10pub fn try_main(run: impl FnOnce() -> RajacResult<()>) -> ExitCode {
11    match run() {
12        Ok(()) => ExitCode::SUCCESS,
13        Err(error) => {
14            eprint!("{}", format_cli_error("operation failed", &error));
15            ExitCode::FAILURE
16        }
17    }
18}
19
20/// What does `try_main_with_headline` provide?
21///
22/// It behaves like [`try_main`], but lets a binary choose a more specific
23/// headline for its top-level error report.
24pub fn try_main_with_headline(headline: &str, run: impl FnOnce() -> RajacResult<()>) -> ExitCode {
25    match run() {
26        Ok(()) => ExitCode::SUCCESS,
27        Err(error) => {
28            eprint!("{}", format_cli_error(headline, &error));
29            ExitCode::FAILURE
30        }
31    }
32}
33
34/// What does `format_cli_error` return?
35///
36/// It returns a stable, human-readable rendering of a [`RajacError`] that is
37/// suitable for printing from a command-line binary.
38pub fn format_cli_error(headline: &str, error: &RajacError) -> String {
39    let mut rendered = String::new();
40    let _ = writeln!(&mut rendered, "\u{1b}[1;31m━━ {}\u{1b}[0m", headline);
41    error
42        .write_to(&mut rendered)
43        .expect("error rendering should not fail");
44
45    let mut causes = Vec::new();
46    let mut current = error.source();
47    while let Some(cause) = current {
48        causes.push((cause.kind().to_string(), cause.location()));
49        current = cause.source();
50    }
51
52    if !causes.is_empty() {
53        rendered.push('\n');
54        rendered.push_str("\u{1b}[1;33m━━ cause chain\u{1b}[0m\n");
55        for (cause, location) in &causes {
56            let _ = writeln!(&mut rendered, "  • {}", cause);
57            let _ = writeln!(
58                &mut rendered,
59                "    at {}:{}:{}",
60                location.file(),
61                location.line(),
62                location.column()
63            );
64        }
65    }
66
67    rendered
68}
69
70#[cfg(test)]
71mod tests {
72    use crate::error::RajacError;
73    use expect_test::expect;
74
75    use super::format_cli_error;
76
77    #[test]
78    fn format_cli_error_renders_headline_and_cause_chain() {
79        let error = RajacError::message("failed to verify")
80            .with_source(RajacError::message("missing reference output"));
81
82        expect!([r#"
83            ━━ verification failed
84            × error failed to verify
85              at crates/base/src/cli.rs:79:21
86            caused by: missing reference output
87                 at crates/base/src/cli.rs:80:26
88
89            ━━ cause chain
90              • missing reference output
91                at crates/base/src/cli.rs:80:26
92        "#])
93        .assert_eq(&crate::unansi(&format_cli_error(
94            "verification failed",
95            &error,
96        )));
97    }
98}