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}