ohno/
core.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use std::backtrace::{Backtrace, BacktraceStatus};
5use std::borrow::Cow;
6use std::error::Error as StdError;
7use std::fmt;
8
9use super::source::Source;
10use super::trace_info::TraceInfo;
11
12/// Internal error data that is boxed to keep `OhnoCore` lightweight.
13#[derive(Debug)]
14pub struct Inner {
15    pub(super) source: Source,
16    pub(super) backtrace: Backtrace,
17    pub(super) context: Vec<TraceInfo>,
18}
19
20/// Core error type that wraps source errors, captures backtraces, and holds context messages.
21///
22/// `OhnoCore` is the foundation of the ohno error handling system. It can wrap any error
23/// type while providing automatic backtrace capture and context stacking capabilities.
24///
25/// The internal error data is boxed to keep the `Err` variant in `Result` small. This minimizes
26/// cases where the `Err` is larger than the `Ok` variant. If the error only contains a
27/// `OhnoCore` field, the size of `Err` will be equivalent to that of a raw pointer.
28///
29/// # Examples
30///
31/// ```rust
32/// use std::io;
33///
34/// use ohno::{ErrorTraceExt, OhnoCore};
35///
36/// // Create from a string message
37/// let error = OhnoCore::from("something went wrong")
38///     .error_trace("while processing request")
39///     .error_trace("in user handler");
40///
41/// // Wrap an existing error
42/// let io_error = io::Error::new(io::ErrorKind::NotFound, "file.txt");
43/// let wrapped = OhnoCore::from(io_error).error_trace("failed to load config");
44/// ```
45pub struct OhnoCore {
46    pub(super) data: Box<Inner>,
47}
48
49impl OhnoCore {
50    /// Creates a new `OhnoCore` with no source (useful when using display override).
51    ///
52    /// Automatically captures a backtrace at the point of creation.
53    ///
54    /// # Examples
55    ///
56    /// ```rust
57    /// let error = ohno::OhnoCore::new();
58    /// ```
59    #[must_use]
60    pub fn new() -> Self {
61        Self::from_source(Source::None)
62    }
63
64    /// Creates a new `OhnoCore` wrapping an existing error.
65    ///
66    /// The wrapped error becomes the source in the error chain. Backtrace capture
67    /// is disabled assuming the source error already has one.
68    ///
69    /// # Examples
70    ///
71    /// ```rust
72    /// use std::io;
73    ///
74    /// use ohno::OhnoCore;
75    ///
76    /// let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
77    /// let wrapped = OhnoCore::without_backtrace(io_error);
78    /// ```
79    pub fn without_backtrace(error: impl Into<Box<dyn StdError + Send + Sync + 'static>>) -> Self {
80        Self {
81            data: Box::new(Inner {
82                source: Source::Error(error.into()),
83                backtrace: Backtrace::disabled(),
84                context: Vec::new(),
85            }),
86        }
87    }
88
89    fn from_source(source: Source) -> Self {
90        Self {
91            data: Box::new(Inner {
92                source,
93                backtrace: Backtrace::capture(),
94                context: Vec::new(),
95            }),
96        }
97    }
98
99    /// Returns the source error if this error wraps another error.
100    ///
101    /// # Examples
102    ///
103    /// ```rust
104    /// use std::io;
105    ///
106    /// use ohno::OhnoCore;
107    ///
108    /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file.txt");
109    /// let wrapped = OhnoCore::from(io_error);
110    ///
111    /// assert!(wrapped.source().is_some());
112    /// ```
113    #[must_use]
114    pub fn source(&self) -> Option<&(dyn StdError + 'static)> {
115        match &self.data.source {
116            Source::Error(source) => Some(source.as_ref()),
117            Source::Transparent(source) => source.source(),
118            Source::None => None,
119        }
120    }
121
122    /// Returns whether this error has a captured backtrace.
123    ///
124    /// # Examples
125    ///
126    /// ```rust
127    /// use ohno::OhnoCore;
128    ///
129    /// let error = OhnoCore::from("test error");
130    /// // Backtrace capture depends on RUST_BACKTRACE environment variable
131    /// println!("Has backtrace: {}", error.has_backtrace());
132    /// ```
133    #[must_use]
134    pub fn has_backtrace(&self) -> bool {
135        matches!(self.data.backtrace.status(), BacktraceStatus::Captured)
136    }
137
138    /// Returns a reference to the backtrace regardless of capture status.
139    ///
140    /// This method always returns a reference to the internal backtrace,
141    /// even if it wasn't captured (in which case it will be empty/disabled).
142    pub fn backtrace(&self) -> &Backtrace {
143        &self.data.backtrace
144    }
145
146    /// Returns an iterator over the context information in reverse order (most recent first).
147    pub fn context_iter(&self) -> impl Iterator<Item = &TraceInfo> {
148        self.data.context.iter().rev()
149    }
150
151    /// Returns an iterator over just the context messages in reverse order (most recent first).
152    pub fn context_messages(&self) -> impl Iterator<Item = &str> {
153        self.data.context.iter().rev().map(|ctx| ctx.message.as_ref())
154    }
155
156    /// Formats the main error message without backtrace or error traces.
157    #[must_use]
158    pub fn format_message(&self, default_message: &str, override_message: Option<Cow<'_, str>>) -> String {
159        MessageFormatter {
160            core: self,
161            default_message,
162            override_message,
163        }
164        .to_string()
165    }
166
167    /// Formats the error with an optional custom message override.
168    ///
169    /// This method is used internally by the Display implementation and by
170    /// derived Error types that want to override the main error message.
171    ///
172    /// # Errors
173    ///
174    /// This function returns a `fmt::Error` if writing to the formatter fails.
175    pub fn format_error(&self, f: &mut fmt::Formatter<'_>, default_message: &str, override_message: Option<Cow<'_, str>>) -> fmt::Result {
176        let m = MessageFormatter {
177            core: self,
178            default_message,
179            override_message,
180        };
181
182        std::fmt::Display::fmt(&m, f)?;
183
184        for ctx in &self.data.context {
185            write!(f, "\n> {ctx}")?;
186        }
187
188        if matches!(self.data.backtrace.status(), BacktraceStatus::Captured) {
189            write!(f, "\n\nBacktrace:\n{}", self.data.backtrace)?;
190        }
191
192        Ok(())
193    }
194}
195
196impl std::fmt::Debug for OhnoCore {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        f.debug_struct("OhnoCore")
199            .field("source", &self.data.source)
200            .field("backtrace", &self.data.backtrace)
201            .field("context", &self.data.context)
202            .finish()
203    }
204}
205
206impl fmt::Display for OhnoCore {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        self.format_error(f, "", None)
209    }
210}
211
212impl Default for OhnoCore {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218impl<T> From<T> for OhnoCore
219where
220    T: Into<Box<dyn StdError + Send + Sync>>,
221{
222    fn from(value: T) -> Self {
223        // StringError is a private error type and cannot be referenced directly
224        if is_string_error(&value) {
225            Self::from_source(Source::Transparent(value.into()))
226        } else {
227            Self::from_source(Source::Error(value.into()))
228        }
229    }
230}
231
232const STR_TYPE_IDS: [typeid::ConstTypeId; 3] = [
233    typeid::ConstTypeId::of::<&str>(),
234    typeid::ConstTypeId::of::<String>(),
235    typeid::ConstTypeId::of::<Cow<'_, str>>(),
236];
237
238fn is_string_error<T>(_: &T) -> bool {
239    let typeid_of_t = typeid::of::<T>();
240    STR_TYPE_IDS.iter().any(|&id| id == typeid_of_t)
241}
242
243/// Helper struct for formatting error messages in a consistent way.
244struct MessageFormatter<'a> {
245    core: &'a OhnoCore,
246    default_message: &'a str,
247    override_message: Option<Cow<'a, str>>,
248}
249
250impl fmt::Display for MessageFormatter<'_> {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        const CAUSED_BY: &str = "caused by:";
253
254        let MessageFormatter {
255            core,
256            default_message,
257            override_message,
258        } = self;
259
260        match (override_message, &core.data.source) {
261            (Some(msg), Source::Transparent(source) | Source::Error(source)) => {
262                write!(f, "{msg}\n{CAUSED_BY} {source}")
263            }
264            (Some(msg), Source::None) => write!(f, "{msg}"),
265            (None, Source::Transparent(source) | Source::Error(source)) => write!(f, "{source}"),
266            (None, Source::None) => write!(f, "{default_message}"),
267        }
268    }
269}
270
271#[cfg_attr(coverage_nightly, coverage(off))]
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::error_trace::ErrorTrace;
276
277    #[test]
278    fn test_default() {
279        let error = OhnoCore::default();
280        assert!(matches!(error.data.source, Source::None));
281    }
282
283    #[test]
284    fn test_format_error() {
285        let error = OhnoCore::from("test error");
286        let result = error.to_string();
287        assert!(result.contains("test error"));
288    }
289
290    #[test]
291    fn test_new() {
292        let error = OhnoCore::new();
293        assert!(matches!(error.data.source, Source::None));
294        assert!(error.data.context.is_empty());
295    }
296
297    #[test]
298    fn test_from_string() {
299        let error = OhnoCore::from("msg");
300        assert!(error.source().is_none());
301        if let Source::Transparent(source) = &error.data.source {
302            assert_eq!(source.to_string(), "msg");
303        }
304        assert!(matches!(&error.data.source, Source::Transparent(_)), "expected transparent source");
305    }
306
307    #[test]
308    fn test_caused_by_without_backtrace() {
309        let io_error = std::io::Error::other("io error");
310        let error = OhnoCore::without_backtrace(io_error);
311        assert!(matches!(error.data.source, Source::Error(_)));
312        assert!(!error.has_backtrace());
313        assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
314    }
315
316    #[test]
317    fn test_caused_by() {
318        let io_error = std::io::Error::other("io error");
319        let error = OhnoCore::from(io_error);
320        assert!(matches!(error.data.source, Source::Error(_)));
321        assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
322    }
323
324    #[test]
325    fn test_from_boxed_error() {
326        let io_error = std::io::Error::other("io error");
327        let boxed: Box<dyn StdError + Send + Sync> = Box::new(io_error);
328        let error = OhnoCore::from(boxed);
329        assert!(matches!(error.data.source, Source::Error(_)));
330        assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
331    }
332
333    #[test]
334    fn test_from_boxed_error_2() {
335        let io_error = std::io::Error::other("io error");
336        let boxed: Box<dyn StdError + Send + Sync> = Box::new(io_error);
337        let error: OhnoCore = boxed.into();
338        assert!(matches!(error.data.source, Source::Error(_)));
339        assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
340    }
341
342    #[test]
343    fn test_context_iter_and_messages() {
344        let mut error = OhnoCore::from("msg");
345        error.add_error_trace(TraceInfo::new("ctx1"));
346        error.add_error_trace(TraceInfo::new("ctx2"));
347        let messages: Vec<_> = error.context_messages().collect();
348        assert_eq!(messages, vec!["ctx2", "ctx1"]);
349    }
350
351    #[test]
352    fn test_display_and_debug() {
353        let error = OhnoCore::from("msg");
354        let display = format!("{error}");
355        assert!(display.starts_with("msg"));
356        let debug = format!("{error:?}");
357        assert!(debug.contains("OhnoCore"));
358    }
359
360    #[test]
361    fn test_from_string_impls() {
362        let s = "abc";
363        let error1: OhnoCore = s.into();
364        assert!(error1.to_string().starts_with("abc"));
365        assert!(matches!(error1.data.source, Source::Transparent(_)));
366
367        let error2: OhnoCore = String::from("def").into();
368        assert!(error2.to_string().starts_with("def"));
369        assert!(matches!(error2.data.source, Source::Transparent(_)));
370
371        let error3: OhnoCore = Cow::Borrowed("ghi").into();
372        assert!(error3.to_string().starts_with("ghi"));
373        assert!(matches!(error3.data.source, Source::Transparent(_)));
374    }
375
376    #[test]
377    fn test_from_boxed_error_impl() {
378        let io_error = std::io::Error::other("io error");
379        let boxed: Box<dyn StdError + Send + Sync> = Box::new(io_error);
380        let error: OhnoCore = boxed.into();
381        assert!(matches!(error.data.source, Source::Error(_)));
382        assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
383    }
384
385    #[test]
386    fn test_from_io_error_impl() {
387        let io_error = std::io::Error::other("io error");
388        let error: OhnoCore = io_error.into();
389        assert!(matches!(error.data.source, Source::Error(_)));
390        assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
391    }
392
393    #[test]
394    #[cfg_attr(miri, ignore)] // unsupported operation: `GetCurrentDirectoryW` not available when isolation is enabled
395    fn force_backtrace_capture() {
396        let mut error = OhnoCore::from("test error with backtrace");
397        error.data.backtrace = Backtrace::force_capture();
398
399        assert!(error.has_backtrace());
400        let backtrace = error.backtrace();
401        assert_eq!(backtrace.status(), BacktraceStatus::Captured);
402        let display = format!("{error}");
403        assert!(display.starts_with("test error with backtrace\n\nBacktrace:\n"));
404    }
405
406    #[test]
407    fn no_backtrace_capture() {
408        let mut error = OhnoCore::from("test error without backtrace");
409        error.data.backtrace = Backtrace::disabled();
410        assert!(!error.has_backtrace());
411        assert_eq!(error.backtrace().status(), BacktraceStatus::Disabled);
412        let display = format!("{error}");
413        assert_eq!(display, "test error without backtrace");
414    }
415
416    #[test]
417    fn is_string_error_test() {
418        assert!(is_string_error(&"a string slice"));
419        assert!(is_string_error(&String::from("a string")));
420        assert!(is_string_error(&Cow::Borrowed("a string slice")));
421        assert!(is_string_error(&Cow::<'static, str>::Owned(String::from("a string"))));
422        assert!(!is_string_error(&std::io::Error::other("an io error")));
423    }
424}