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;
39pub use spantrace::{OptionalSpanTrace, SpanTrace};
40#[cfg(feature = "tracing")]
41pub mod tracing;
42
43pub use traits::*;
44pub use welp::{Welp, WelpOptionExt, WelpResultExt};
45/// Private helpers used by macro-generated code. Not part of the public API.
46#[doc(hidden)]
47pub mod __private {
48    pub use crate::backtrace::CORE_SRC_PATH;
49    /// Autoref probe for capture deduplication.
50    ///
51    /// When the concrete source type implements `Diagnostic`, the high-priority
52    /// `CaptureFromExt` impl is selected and tries to extract existing traces.
53    /// For non-`Diagnostic` sources (e.g., `io::Error`), the low-priority
54    /// `CaptureFromFallback` impl is selected via autoref and does fresh capture.
55    pub struct CaptureProbe<'a, T: ?Sized>(pub &'a T);
56
57    /// High-priority: source implements `Diagnostic` → try extraction.
58    pub trait CaptureFromExt {
59        fn resolve<C: crate::Capturable>(&self) -> C;
60    }
61
62    impl<T: crate::Diagnostic> CaptureFromExt for CaptureProbe<'_, T> {
63        #[inline]
64        #[track_caller]
65        fn resolve<C: crate::Capturable>(&self) -> C {
66            C::capture_or_extract(self.0)
67        }
68    }
69
70    /// Low-priority: source doesn't implement `Diagnostic` → fresh capture.
71    pub trait CaptureFromFallback {
72        fn resolve<C: crate::Capturable>(&self) -> C;
73    }
74
75    impl<T: ?Sized> CaptureFromFallback for &CaptureProbe<'_, T> {
76        #[inline]
77        #[track_caller]
78        fn resolve<C: crate::Capturable>(&self) -> C {
79            C::capture()
80        }
81    }
82
83    /// Accessor-time autoref probe for forwarding a transparent wrapper's
84    /// `Diagnostic` accessors to its source.
85    ///
86    /// Unlike [`CaptureProbe`] (which runs at construction to decide
87    /// extract-vs-capture), this runs when `oopsie_*` is called: a transparent
88    /// variant delegates each accessor to its source's concrete type. When that
89    /// type implements `Diagnostic` the high-priority [`DiagForwardExt`] impl
90    /// forwards the call; otherwise the [`DiagForwardFallback`] impl (selected
91    /// via autoref) yields `None`. Works on stable, where the Provider-API
92    /// [`source_trace`] path would yield `None`.
93    pub struct DiagProbe<'a, T: ?Sized>(pub &'a T);
94
95    /// High-priority: source implements `Diagnostic` → forward the accessor.
96    pub trait DiagForwardExt<'a> {
97        fn fwd_code(&self) -> Option<crate::ErrorCode>;
98        fn fwd_help(&self) -> Option<crate::HelpText>;
99        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace>;
100        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace>;
101        fn fwd_location(&self) -> Option<&'static std::panic::Location<'static>>;
102        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8>;
103    }
104
105    impl<'a, T: crate::Diagnostic + ?Sized> DiagForwardExt<'a> for DiagProbe<'a, T> {
106        #[inline]
107        fn fwd_code(&self) -> Option<crate::ErrorCode> {
108            self.0.oopsie_error_code()
109        }
110        #[inline]
111        fn fwd_help(&self) -> Option<crate::HelpText> {
112            self.0.oopsie_help_text()
113        }
114        #[inline]
115        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace> {
116            self.0.oopsie_backtrace()
117        }
118        #[inline]
119        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace> {
120            self.0.oopsie_spantrace()
121        }
122        #[inline]
123        fn fwd_location(&self) -> Option<&'static std::panic::Location<'static>> {
124            self.0.oopsie_location()
125        }
126        #[inline]
127        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8> {
128            self.0.oopsie_exit_code()
129        }
130    }
131
132    /// Low-priority: source doesn't implement `Diagnostic` → nothing to forward.
133    pub trait DiagForwardFallback<'a> {
134        #[inline]
135        fn fwd_code(&self) -> Option<crate::ErrorCode> {
136            None
137        }
138        #[inline]
139        fn fwd_help(&self) -> Option<crate::HelpText> {
140            None
141        }
142        #[inline]
143        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace> {
144            None
145        }
146        #[inline]
147        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace> {
148            None
149        }
150        #[inline]
151        fn fwd_location(&self) -> Option<&'static std::panic::Location<'static>> {
152            None
153        }
154        #[inline]
155        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8> {
156            None
157        }
158    }
159
160    impl<'a, T: ?Sized> DiagForwardFallback<'a> for &DiagProbe<'a, T> {}
161
162    /// Pull the deepest `T` reachable from a source error via the Provider API.
163    ///
164    /// The generated `provide()` forwards to the source before providing its
165    /// own trace, and std's `Request` is first-wins, so the deepest provider in
166    /// the chain fills the slot — this surfaces the origin-most trace rather
167    /// than a wrap-site one. Trace accessors go through the typed
168    /// [`source_backtrace`] / `source_spantrace` wrappers, which add the
169    /// skip-empty filter on top of this lookup.
170    ///
171    /// Returns `None` without `unstable-error-generic-member-access`: descending
172    /// into a type-erased `dyn Error` source is not portable there, and the
173    /// caller falls back to the wrapper's own field.
174    #[inline]
175    #[must_use]
176    pub fn source_trace<'a, T: 'static>(
177        source: &'a (dyn std::error::Error + 'static),
178    ) -> Option<&'a T> {
179        #[cfg(feature = "unstable-error-generic-member-access")]
180        {
181            core::error::request_ref::<T>(source)
182        }
183        #[cfg(not(feature = "unstable-error-generic-member-access"))]
184        {
185            let _ = source;
186            None
187        }
188    }
189
190    /// [`source_trace`] for `Backtrace`, treating an empty trace as absent:
191    /// a trace that captured no frames is not "available" and must not shadow
192    /// a captured one further out.
193    #[inline]
194    #[must_use]
195    pub fn source_backtrace<'a>(
196        source: &'a (dyn std::error::Error + 'static),
197    ) -> Option<&'a crate::Backtrace> {
198        source_trace::<crate::Backtrace>(source).filter(|bt| bt.is_captured())
199    }
200
201    /// [`source_trace`] for `SpanTrace`, treating an empty trace as absent.
202    #[inline]
203    #[must_use]
204    pub fn source_spantrace<'a>(
205        source: &'a (dyn std::error::Error + 'static),
206    ) -> Option<&'a crate::SpanTrace> {
207        source_trace::<crate::SpanTrace>(source).filter(|st| st.is_captured())
208    }
209
210    pub use crate::marker::{TraceMarker, restore_marker, set_marker};
211
212    /// The exit code a type-erased source declares, reached through the Provider
213    /// API. Returns `None` on stable (descending into a `dyn Error` is not
214    /// portable there).
215    #[inline]
216    #[must_use]
217    pub fn source_exit_code(
218        source: &(dyn std::error::Error + 'static),
219    ) -> Option<core::num::NonZeroU8> {
220        #[cfg(feature = "unstable-error-generic-member-access")]
221        {
222            core::error::request_value::<core::num::NonZeroU8>(source)
223        }
224        #[cfg(not(feature = "unstable-error-generic-member-access"))]
225        {
226            let _ = source;
227            None
228        }
229    }
230
231    /// Build a `NonZeroU8` from a value the macro already validated to be in
232    /// `1..=255`. Panics at const-eval if the invariant is ever broken, so the
233    /// generated code stays `unsafe`-free.
234    #[inline]
235    #[must_use]
236    pub const fn nonzero_u8_unchecked(value: u8) -> core::num::NonZeroU8 {
237        match core::num::NonZeroU8::new(value) {
238            Some(n) => n,
239            None => panic!("exit code must be non-zero"),
240        }
241    }
242
243    /// Fixed-capacity const string builder: concatenate string slices and base-10
244    /// `usize`s in a `const` context, then borrow the result as `&str` — e.g. to
245    /// feed a const `panic!("{}", …)` with a computed message. `N` is the byte
246    /// capacity; callers size it from the pieces (`Σ part.len() + 20·#usize`) so a
247    /// push never runs past the buffer.
248    pub struct ConstStr<const N: usize> {
249        buf: [u8; N],
250        len: usize,
251    }
252
253    impl<const N: usize> ConstStr<N> {
254        #[inline]
255        #[must_use]
256        pub const fn new() -> Self {
257            Self {
258                buf: [0; N],
259                len: 0,
260            }
261        }
262
263        /// Append a string slice (its UTF-8 bytes verbatim).
264        #[inline]
265        #[must_use]
266        pub const fn str(mut self, s: &str) -> Self {
267            let b = s.as_bytes();
268            let mut i = 0;
269            while i < b.len() {
270                self.buf[self.len] = b[i];
271                self.len += 1;
272                i += 1;
273            }
274            self
275        }
276
277        /// Append a `usize` in base 10.
278        #[inline]
279        #[must_use]
280        pub const fn usize(mut self, mut n: usize) -> Self {
281            let mut digits = [0u8; 20];
282            let mut count = if n == 0 {
283                digits[0] = b'0';
284                1
285            } else {
286                0
287            };
288            while n > 0 {
289                digits[count] = b'0' + (n % 10) as u8;
290                n /= 10;
291                count += 1;
292            }
293            while count > 0 {
294                count -= 1;
295                self.buf[self.len] = digits[count];
296                self.len += 1;
297            }
298            self
299        }
300
301        /// Borrow the accumulated bytes as `&str`. Only valid UTF-8 and ASCII digits
302        /// are ever written, so the error branch is unreachable in correct use.
303        #[inline]
304        #[must_use]
305        pub const fn as_str(&self) -> &str {
306            match core::str::from_utf8(self.buf.split_at(self.len).0) {
307                Ok(s) => s,
308                Err(_) => panic!("ConstStr: built a non-utf8 message"),
309            }
310        }
311    }
312
313    impl<const N: usize> Default for ConstStr<N> {
314        fn default() -> Self {
315            Self::new()
316        }
317    }
318
319    #[cfg(feature = "chrono")]
320    pub use chrono;
321}
322
323macro_rules! impl_string_newtypes {
324    ($($(#[$meta:meta])* $ident:ident,)* $(,)?) => { $(
325        #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
326        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
327        #[cfg_attr(feature = "serde", serde(transparent))]
328        #[repr(transparent)]
329        $(#[$meta])*
330        pub struct $ident(Cow<'static, str>);
331
332        impl $ident {
333            #[doc = concat!("Build a new `", stringify!($ident), "` from a `&'static str`.")]
334            #[must_use]
335            #[inline]
336            pub const fn from_static(s: &'static str) -> Self {
337                Self(Cow::Borrowed(s))
338            }
339
340            #[doc = concat!("Build a new `", stringify!($ident), "` from a `String`.")]
341            #[must_use]
342            #[inline]
343            pub const fn from_string(s: String) -> Self {
344                Self(Cow::Owned(s))
345            }
346
347            /// Borrow the underlying string.
348            #[must_use]
349            #[inline]
350            pub const fn as_str(&self) -> &str {
351                match &self.0 {
352                    Cow::Borrowed(s) => s,
353                    Cow::Owned(s) => s.as_str(),
354                }
355            }
356
357            #[doc = concat!("Consume the `", stringify!($ident), "` and return the underlying `Cow`.")]
358            #[must_use]
359            #[inline]
360            pub fn into_inner(self) -> Cow<'static, str> {
361                self.0
362            }
363        }
364
365        impl Deref for $ident {
366            type Target = str;
367
368            #[inline]
369            fn deref(&self) -> &Self::Target {
370                self.as_str()
371            }
372        }
373
374        impl Borrow<str> for $ident {
375            #[inline]
376            fn borrow(&self) -> &str {
377                self.as_str()
378            }
379        }
380
381        impl From<&'static str> for $ident {
382            #[inline]
383            fn from(s: &'static str) -> Self {
384                Self::from_static(s)
385            }
386        }
387
388        impl From<String> for $ident {
389            #[inline]
390            fn from(s: String) -> Self {
391                Self::from_string(s)
392            }
393        }
394
395        impl std::fmt::Display for $ident {
396            #[inline]
397            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398                self.0.fmt(f)
399            }
400        }
401
402    )* };
403}
404
405impl_string_newtypes!(
406    /// An opaque error code, used for programmatic handling and matching.
407    ErrorCode,
408    /// User-facing help text, intended to be shown in diagnostics.
409    HelpText,
410);
411
412#[cfg(test)]
413mod const_str_tests {
414    use crate::__private::ConstStr;
415
416    #[test]
417    fn builds_interleaved_string() {
418        let s = ConstStr::<64>::new()
419            .str("`E` is ")
420            .usize(80)
421            .str(" bytes, must be ≤ 64");
422        assert_eq!(s.as_str(), "`E` is 80 bytes, must be ≤ 64");
423    }
424
425    #[test]
426    fn formats_zero_and_large() {
427        assert_eq!(ConstStr::<8>::new().as_str(), "");
428        assert_eq!(ConstStr::<32>::new().usize(0).as_str(), "0");
429        assert_eq!(
430            ConstStr::<32>::new().usize(18446744073709551615).as_str(),
431            "18446744073709551615"
432        );
433    }
434
435    #[test]
436    fn usable_in_const_context() {
437        const M: &str = {
438            const C: ConstStr<16> = ConstStr::new().str("n=").usize(42);
439            C.as_str()
440        };
441        assert_eq!(M, "n=42");
442    }
443}