Skip to main content

oopsie_core/
lib.rs

1//! Core error types and utilities.
2
3#![warn(missing_docs)]
4#![cfg_attr(
5    feature = "unstable-error-generic-member-access",
6    feature(error_generic_member_access)
7)]
8// Doctests that use `#[derive(Oopsie)]` will see the macro emit a `provide`
9// impl when the unstable feature is active; inject the corresponding language
10// feature flag so those doctests compile under `--features unstable`.
11#![cfg_attr(
12    feature = "unstable-error-generic-member-access",
13    doc(test(attr(feature(error_generic_member_access))))
14)]
15
16mod backtrace;
17mod chain;
18mod diagnostic;
19#[cfg(feature = "serde")]
20pub mod erased;
21#[cfg(feature = "extras")]
22pub mod extras;
23mod marker;
24mod spantrace;
25#[cfg(feature = "test-utils")]
26pub mod test_utils;
27mod traits;
28mod welp;
29
30use std::borrow::{Borrow, Cow};
31use std::ops::Deref;
32
33pub use backtrace::{
34    Backtrace, RustBacktrace, clear_rust_backtrace_override, rust_backtrace, rust_panic_backtrace,
35    set_rust_backtrace_override, with_rust_backtrace_override,
36};
37pub use chain::{Chain, ErrorChainExt};
38pub use diagnostic::Diagnostic;
39/// Private helpers used by macro-generated code. Not part of the public API.
40#[doc(hidden)]
41pub mod __private {
42    pub use crate::backtrace::CORE_SRC_PATH;
43    /// Autoref probe for capture deduplication.
44    ///
45    /// When the concrete source type implements `Diagnostic`, the high-priority
46    /// `CaptureFromExt` impl is selected and tries to extract existing traces.
47    /// For non-`Diagnostic` sources (e.g., `io::Error`), the low-priority
48    /// `CaptureFromFallback` impl is selected via autoref and does fresh capture.
49    pub struct CaptureProbe<'a, T: ?Sized>(pub &'a T);
50
51    /// High-priority: source implements `Diagnostic` → try extraction.
52    pub trait CaptureFromExt {
53        fn resolve<C: crate::Capturable>(&self) -> C;
54    }
55
56    impl<T: crate::Diagnostic> CaptureFromExt for CaptureProbe<'_, T> {
57        #[inline]
58        #[track_caller]
59        fn resolve<C: crate::Capturable>(&self) -> C {
60            C::capture_or_extract(self.0)
61        }
62    }
63
64    /// Low-priority: source doesn't implement `Diagnostic` → fresh capture.
65    pub trait CaptureFromFallback {
66        fn resolve<C: crate::Capturable>(&self) -> C;
67    }
68
69    impl<T: ?Sized> CaptureFromFallback for &CaptureProbe<'_, T> {
70        #[inline]
71        #[track_caller]
72        fn resolve<C: crate::Capturable>(&self) -> C {
73            C::capture()
74        }
75    }
76
77    /// Accessor-time autoref probe for forwarding a transparent wrapper's
78    /// `Diagnostic` accessors to its source.
79    ///
80    /// Unlike [`CaptureProbe`] (which runs at construction to decide
81    /// extract-vs-capture), this runs when `oopsie_*` is called: a transparent
82    /// variant delegates each accessor to its source's concrete type. When that
83    /// type implements `Diagnostic` the high-priority [`DiagForwardExt`] impl
84    /// forwards the call; otherwise the [`DiagForwardFallback`] impl (selected
85    /// via autoref) yields `None`. Works on stable, where the Provider-API
86    /// [`source_trace`] path would yield `None`.
87    pub struct DiagProbe<'a, T: ?Sized>(pub &'a T);
88
89    /// High-priority: source implements `Diagnostic` → forward the accessor.
90    pub trait DiagForwardExt<'a> {
91        fn fwd_code(&self) -> Option<crate::ErrorCode>;
92        fn fwd_help(&self) -> Option<crate::HelpText>;
93        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace>;
94        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace>;
95        fn fwd_location(&self) -> Option<&'static std::panic::Location<'static>>;
96        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8>;
97    }
98
99    impl<'a, T: crate::Diagnostic + ?Sized> DiagForwardExt<'a> for DiagProbe<'a, T> {
100        #[inline]
101        fn fwd_code(&self) -> Option<crate::ErrorCode> {
102            self.0.oopsie_error_code()
103        }
104        #[inline]
105        fn fwd_help(&self) -> Option<crate::HelpText> {
106            self.0.oopsie_help_text()
107        }
108        #[inline]
109        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace> {
110            self.0.oopsie_backtrace()
111        }
112        #[inline]
113        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace> {
114            self.0.oopsie_spantrace()
115        }
116        #[inline]
117        fn fwd_location(&self) -> Option<&'static std::panic::Location<'static>> {
118            self.0.oopsie_location()
119        }
120        #[inline]
121        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8> {
122            self.0.oopsie_exit_code()
123        }
124    }
125
126    /// Low-priority: source doesn't implement `Diagnostic` → nothing to forward.
127    pub trait DiagForwardFallback<'a> {
128        #[inline]
129        fn fwd_code(&self) -> Option<crate::ErrorCode> {
130            None
131        }
132        #[inline]
133        fn fwd_help(&self) -> Option<crate::HelpText> {
134            None
135        }
136        #[inline]
137        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace> {
138            None
139        }
140        #[inline]
141        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace> {
142            None
143        }
144        #[inline]
145        fn fwd_location(&self) -> Option<&'static std::panic::Location<'static>> {
146            None
147        }
148        #[inline]
149        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8> {
150            None
151        }
152    }
153
154    impl<'a, T: ?Sized> DiagForwardFallback<'a> for &DiagProbe<'a, T> {}
155
156    /// Pull the deepest `T` reachable from a source error via the Provider API.
157    ///
158    /// The generated `provide()` forwards to the source before providing its
159    /// own trace, and std's `Request` is first-wins, so the deepest provider in
160    /// the chain fills the slot — this surfaces the origin-most trace rather
161    /// than a wrap-site one. Trace accessors go through the typed
162    /// [`source_backtrace`] / `source_spantrace` wrappers, which add the
163    /// skip-empty filter on top of this lookup.
164    ///
165    /// Returns `None` without `unstable-error-generic-member-access`: descending
166    /// into a type-erased `dyn Error` source is not portable there, and the
167    /// caller falls back to the wrapper's own field.
168    #[inline]
169    #[must_use]
170    pub fn source_trace<'a, T: 'static>(
171        source: &'a (dyn std::error::Error + 'static),
172    ) -> Option<&'a T> {
173        #[cfg(feature = "unstable-error-generic-member-access")]
174        {
175            core::error::request_ref::<T>(source)
176        }
177        #[cfg(not(feature = "unstable-error-generic-member-access"))]
178        {
179            let _ = source;
180            None
181        }
182    }
183
184    /// [`source_trace`] for `Backtrace`, treating an empty trace as absent:
185    /// a trace that captured no frames is not "available" and must not shadow
186    /// a captured one further out.
187    #[inline]
188    #[must_use]
189    pub fn source_backtrace<'a>(
190        source: &'a (dyn std::error::Error + 'static),
191    ) -> Option<&'a crate::Backtrace> {
192        source_trace::<crate::Backtrace>(source).filter(|bt| bt.is_captured())
193    }
194
195    /// [`source_trace`] for `SpanTrace`, treating an empty trace as absent.
196    #[inline]
197    #[must_use]
198    pub fn source_spantrace<'a>(
199        source: &'a (dyn std::error::Error + 'static),
200    ) -> Option<&'a crate::SpanTrace> {
201        source_trace::<crate::SpanTrace>(source).filter(|st| st.is_captured())
202    }
203
204    pub use crate::marker::{TraceMarker, restore_marker, set_marker};
205
206    /// The exit code a type-erased source declares, reached through the Provider
207    /// API. Returns `None` on stable (descending into a `dyn Error` is not
208    /// portable there).
209    #[inline]
210    #[must_use]
211    pub fn source_exit_code(
212        source: &(dyn std::error::Error + 'static),
213    ) -> Option<core::num::NonZeroU8> {
214        #[cfg(feature = "unstable-error-generic-member-access")]
215        {
216            core::error::request_value::<core::num::NonZeroU8>(source)
217        }
218        #[cfg(not(feature = "unstable-error-generic-member-access"))]
219        {
220            let _ = source;
221            None
222        }
223    }
224
225    /// Build a `NonZeroU8` from a value the macro already validated to be in
226    /// `1..=255`. Panics at const-eval if the invariant is ever broken, so the
227    /// generated code stays `unsafe`-free.
228    #[inline]
229    #[must_use]
230    pub const fn nonzero_u8_unchecked(value: u8) -> core::num::NonZeroU8 {
231        match core::num::NonZeroU8::new(value) {
232            Some(n) => n,
233            None => panic!("exit code must be non-zero"),
234        }
235    }
236
237    #[cfg(feature = "chrono")]
238    pub use chrono;
239}
240
241pub use spantrace::{OptionalSpanTrace, SpanTrace};
242#[cfg(all(feature = "tracing", feature = "serde"))]
243use tracing_error::ErrorLayer;
244#[cfg(all(feature = "tracing", feature = "serde"))]
245use tracing_subscriber::fmt::format::JsonFields;
246#[cfg(all(feature = "tracing", feature = "serde"))]
247use tracing_subscriber::registry::LookupSpan;
248pub use traits::*;
249pub use welp::{Welp, WelpOptionExt, WelpResultExt};
250
251macro_rules! impl_string_newtypes {
252    ($($(#[$meta:meta])* $ident:ident,)* $(,)?) => { $(
253        #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
254        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
255        #[cfg_attr(feature = "serde", serde(transparent))]
256        #[repr(transparent)]
257        $(#[$meta])*
258        pub struct $ident(Cow<'static, str>);
259
260        impl $ident {
261            #[doc = concat!("Build a new `", stringify!($ident), "` from a `&'static str`.")]
262            #[must_use]
263            #[inline]
264            pub const fn from_static(s: &'static str) -> Self {
265                Self(Cow::Borrowed(s))
266            }
267
268            #[doc = concat!("Build a new `", stringify!($ident), "` from a `String`.")]
269            #[must_use]
270            #[inline]
271            pub const fn from_string(s: String) -> Self {
272                Self(Cow::Owned(s))
273            }
274
275            /// Borrow the underlying string.
276            #[must_use]
277            #[inline]
278            pub const fn as_str(&self) -> &str {
279                match &self.0 {
280                    Cow::Borrowed(s) => s,
281                    Cow::Owned(s) => s.as_str(),
282                }
283            }
284
285            #[doc = concat!("Consume the `", stringify!($ident), "` and return the underlying `Cow`.")]
286            #[must_use]
287            #[inline]
288            pub fn into_inner(self) -> Cow<'static, str> {
289                self.0
290            }
291        }
292
293        impl Deref for $ident {
294            type Target = str;
295
296            #[inline]
297            fn deref(&self) -> &Self::Target {
298                self.as_str()
299            }
300        }
301
302        impl Borrow<str> for $ident {
303            #[inline]
304            fn borrow(&self) -> &str {
305                self.as_str()
306            }
307        }
308
309        impl From<&'static str> for $ident {
310            #[inline]
311            fn from(s: &'static str) -> Self {
312                Self::from_static(s)
313            }
314        }
315
316        impl From<String> for $ident {
317            #[inline]
318            fn from(s: String) -> Self {
319                Self::from_string(s)
320            }
321        }
322
323        impl std::fmt::Display for $ident {
324            #[inline]
325            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326                self.0.fmt(f)
327            }
328        }
329
330    )* };
331}
332
333impl_string_newtypes!(
334    /// An opaque error code, used for programmatic handling and matching.
335    ErrorCode,
336    /// User-facing help text, intended to be shown in diagnostics.
337    HelpText,
338);
339
340/// Construct a `tracing_error::ErrorLayer` configured to format span fields
341/// as JSON.
342///
343/// Equivalent to `ErrorLayer::new(JsonFields::default())` but doesn't require
344/// the caller to depend on `tracing-subscriber` directly.
345#[cfg(all(feature = "tracing", feature = "serde"))]
346#[inline]
347#[must_use]
348pub fn json_error_layer<S>() -> ErrorLayer<S, JsonFields>
349where
350    S: tracing::Subscriber + for<'span> LookupSpan<'span>,
351{
352    ErrorLayer::new(JsonFields::default())
353}