Skip to main content

suzunari_error/
stack_report.rs

1use crate::StackError;
2use core::fmt::{Debug, Display, Formatter};
3
4#[cfg(feature = "std")]
5use std::io::{Write, stderr};
6#[cfg(feature = "std")]
7use std::process::{ExitCode, Termination};
8
9/// Formats a [`StackError`] chain as a stack-trace-like report with type names and locations.
10///
11/// Wraps `Result<(), E>` and provides formatted output via `Display` (and `Debug`, which
12/// delegates to `Display`). Used at error display boundaries such as `main()`.
13///
14/// Create via `StackReport::from(error)`, `Result::<(), E>::into()`, or `error.into()`.
15///
16/// # Output Format
17///
18/// ```text
19/// Error: AppError::IoFailed: io failed, at src/main.rs:42:5
20/// Caused by (recent first):
21///   1| InfraError::Read: read failed, at src/infra.rs:10:9
22///   2| No such file or directory (os error 2)
23/// ```
24///
25/// The first line shows the top-level error with type name and location.
26/// StackError sources (with location) are listed first with numbering,
27/// then plain `Error::source()` chain entries (without location) follow.
28///
29/// With the `std` feature, implements [`Termination`] for use as the
30/// return type of `main()`. The [`#[suzunari_error::report]`](crate::report) macro
31/// can transform `fn() -> Result<(), E>` into `fn() -> StackReport<E>` automatically.
32///
33/// # Example
34///
35/// ```
36/// use suzunari_error::*;
37///
38/// #[suzunari_error]
39/// #[suzu(display("app error"))]
40/// struct AppError {
41///     source: std::io::Error,
42/// }
43///
44/// fn run() -> Result<(), AppError> {
45///     std::fs::read("/nonexistent").context(AppSnafu)?;
46///     Ok(())
47/// }
48///
49/// let err = run().unwrap_err();
50/// let report = StackReport::from(err);
51///
52/// let output = format!("{report}");
53/// assert!(output.contains("Error: AppError: app error"));
54/// assert!(output.contains("Caused by"));
55/// ```
56///
57/// # Notes
58///
59/// - Both `Display` and `Debug` produce an empty string for the `Ok` case.
60///   This is intentional — in the `Termination` use case, success should be silent.
61/// - **`Debug` delegates to `Display`** (same output). This intentionally
62///   deviates from the [C-DEBUG](https://rust-lang.github.io/api-guidelines/debuggability.html#c-debug)
63///   guideline because `Termination` calls `Debug::fmt` to produce the error
64///   output. Making `Debug` structural (e.g., `StackReport(Err(...))`) would
65///   render the terminal output useless. Since `StackReport` is a display
66///   boundary type (not a general-purpose data carrier), the human-readable
67///   format is appropriate for both traits.
68/// - `Display` output does **not** include a trailing newline. This matches
69///   the convention for `Display` implementations and avoids double newlines
70///   with `eprintln!("{report}")`. The `Termination` impl adds a trailing
71///   newline when writing to stderr.
72pub struct StackReport<E>(Result<(), E>);
73
74impl<E: StackError> From<Result<(), E>> for StackReport<E> {
75    fn from(result: Result<(), E>) -> Self {
76        Self(result)
77    }
78}
79
80impl<E: StackError> From<E> for StackReport<E> {
81    fn from(error: E) -> Self {
82        Self(Err(error))
83    }
84}
85
86impl<E: StackError> Debug for StackReport<E> {
87    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
88        Display::fmt(self, f)
89    }
90}
91
92impl<E: StackError> Display for StackReport<E> {
93    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
94        match &self.0 {
95            Ok(()) => Ok(()),
96            Err(e) => Display::fmt(&StackReportFormatter(e), f),
97        }
98    }
99}
100
101#[cfg(feature = "std")]
102impl<E: StackError> Termination for StackReport<E> {
103    fn report(self) -> ExitCode {
104        match self.0 {
105            Ok(()) => ExitCode::SUCCESS,
106            Err(e) => {
107                // Ignore write errors — stderr may be closed, and
108                // panicking here would mask the original error.
109                // Trailing `\n` is added here because Display omits it
110                // (Display convention: no trailing newline).
111                let _ = Write::write_fmt(
112                    &mut stderr(),
113                    format_args!("{}\n", StackReportFormatter(&e)),
114                );
115                ExitCode::FAILURE
116            }
117        }
118    }
119}
120
121/// Internal formatter that formats a StackError chain.
122struct StackReportFormatter<'a>(&'a dyn StackError);
123
124impl Display for StackReportFormatter<'_> {
125    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
126        let error = self.0;
127
128        // Top-level error with type name and location (no index).
129        // No trailing newline — Display convention.
130        write!(
131            f,
132            "Error: {}: {error}, at {}",
133            error.type_name(),
134            error.location()
135        )?;
136
137        // Check if there are any causes.
138        // source() suffices: the StackError contract guarantees that
139        // stack_source().is_some() implies source().is_some().
140        if error.source().is_none() {
141            return Ok(());
142        }
143
144        // Prefix each subsequent line with `\n` instead of appending trailing `\n`,
145        // so the overall output has no trailing newline.
146        write!(f, "\nCaused by (recent first):")?;
147
148        let mut index = 1;
149
150        // Phase 1: StackError chain (with location)
151        let mut current_stack: &dyn StackError = error;
152        while let Some(next) = current_stack.stack_source() {
153            // Invariant: stack_source() implies source() (StackError is a sub-chain of Error).
154            // In release builds this assertion is stripped; a broken impl would produce
155            // truncated output (missing causes) rather than a panic, which is preferable
156            // to crashing inside a Display formatter.
157            debug_assert!(
158                current_stack.source().is_some(),
159                "StackError::stack_source() returned Some but Error::source() returned None \
160                 for type {}. This indicates an incorrect StackError implementation.",
161                current_stack.type_name()
162            );
163            write!(
164                f,
165                "\n  {index}| {}: {next}, at {}",
166                next.type_name(),
167                next.location()
168            )?;
169            index += 1;
170            current_stack = next;
171        }
172
173        // Phase 2: Error chain (without location)
174        let mut current_error = current_stack.source();
175        while let Some(e) = current_error {
176            write!(f, "\n  {index}| {e}")?;
177            index += 1;
178            current_error = e.source();
179        }
180
181        Ok(())
182    }
183}