Skip to main content

rama_error/ext/
mod.rs

1use core::fmt;
2
3#[cfg(feature = "std")]
4mod backtrace;
5
6mod context;
7pub(super) mod opaque;
8
9use crate::{
10    BoxError,
11    extra::OpaqueError,
12    std::{Box, String},
13};
14
15/// Extends the `Result` and `Option` types with methods for adding context to errors.
16///
17/// See the [module level documentation](crate) for more information.
18pub trait ErrorContext: private::SealedErrorContext {
19    /// The resulting context type after adding context to the contained error.
20    type Context;
21
22    /// The resulting context type after turning the error into an opaque error
23    type OpaqueContext;
24
25    /// Return a err variant for [`Self::Context`] as [`BoxError`].
26    fn into_box_error(self) -> Self::Context;
27
28    /// Return a err variant for [`Self::OpaqueContext`] as [`OpaqueError`].
29    fn into_opaque_error(self) -> Self::OpaqueContext;
30
31    /// Add context to the contained error.
32    fn context<M>(self, value: M) -> Self::Context
33    where
34        M: fmt::Debug + fmt::Display + Send + Sync + 'static;
35
36    /// Add context to the contained error,
37    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
38    fn context_hex<M>(self, value: M) -> Self::Context
39    where
40        M: fmt::Debug + Send + Sync + 'static;
41
42    /// Add context to the contained error,
43    /// using [`fmt::Debug`] as [`fmt::Display`].
44    fn context_debug<M>(self, value: M) -> Self::Context
45    where
46        M: fmt::Debug + Send + Sync + 'static;
47
48    /// Add keyed context to the contained error.
49    fn context_field<M>(self, key: &'static str, value: M) -> Self::Context
50    where
51        M: fmt::Debug + fmt::Display + Send + Sync + 'static;
52
53    /// Same as [`Self::context_field`] but using a string-like value,
54    /// this is useful in case you need to pass a string slice which is borrowed
55    /// and thus cannot be passed as part of 'static error.
56    fn context_str_field<M>(self, key: &'static str, value: M) -> Self::Context
57    where
58        M: Into<String>;
59
60    /// Add keyed context to the contained error
61    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
62    fn context_hex_field<M>(self, key: &'static str, value: M) -> Self::Context
63    where
64        M: fmt::Debug + Send + Sync + 'static;
65
66    /// Add keyed context to the contained error
67    /// using [`fmt::Debug`] as [`fmt::Display`].
68    fn context_debug_field<M>(self, key: &'static str, value: M) -> Self::Context
69    where
70        M: fmt::Debug + Send + Sync + 'static;
71
72    /// Lazily add a context to the contained error, if it exists.
73    fn with_context<C, F>(self, cb: F) -> Self::Context
74    where
75        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
76        F: FnOnce() -> C;
77
78    /// Lazily add a context to the contained error, if it exists.
79    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
80    fn with_context_hex<C, F>(self, cb: F) -> Self::Context
81    where
82        C: fmt::Debug + Send + Sync + 'static,
83        F: FnOnce() -> C;
84
85    /// Lazily add a context to the contained error, if it exists.
86    /// using [`fmt::Debug`] as [`fmt::Display`].
87    fn with_context_debug<C, F>(self, cb: F) -> Self::Context
88    where
89        C: fmt::Debug + Send + Sync + 'static,
90        F: FnOnce() -> C;
91
92    /// Lazily add keyed context to the contained error, if it exists.
93    fn with_context_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
94    where
95        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
96        F: FnOnce() -> C;
97
98    /// Same as [`Self::with_context_field`] but using a string-like value,
99    /// this is useful in case you need to pass a string slice which is borrowed
100    /// and thus cannot be passed as part of 'static error.
101    fn with_context_str_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
102    where
103        C: Into<String>,
104        F: FnOnce() -> C;
105
106    /// Lazily add keyed context to the contained error, if it exists
107    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
108    fn with_context_hex_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
109    where
110        C: fmt::Debug + Send + Sync + 'static,
111        F: FnOnce() -> C;
112
113    /// Lazily add keyed context to the contained error, if it exists
114    /// using [`fmt::Debug`] as [`fmt::Display`].
115    fn with_context_debug_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
116    where
117        C: fmt::Debug + Send + Sync + 'static,
118        F: FnOnce() -> C;
119}
120
121impl<T, E: Into<BoxError>> ErrorContext for Result<T, E> {
122    type Context = Result<T, BoxError>;
123    type OpaqueContext = Result<T, OpaqueError>;
124
125    #[inline(always)]
126    fn into_box_error(self) -> Self::Context {
127        self.map_err(Into::into)
128    }
129
130    #[inline(always)]
131    fn into_opaque_error(self) -> Self::OpaqueContext {
132        self.map_err(OpaqueError::from_box_error)
133    }
134
135    #[inline(always)]
136    fn context<M>(self, value: M) -> Self::Context
137    where
138        M: fmt::Debug + fmt::Display + Send + Sync + 'static,
139    {
140        self.map_err(|error| error.context(value))
141    }
142
143    #[inline(always)]
144    fn context_hex<M>(self, value: M) -> Self::Context
145    where
146        M: fmt::Debug + Send + Sync + 'static,
147    {
148        self.map_err(|error| error.context_hex(value))
149    }
150
151    #[inline(always)]
152    fn context_debug<M>(self, value: M) -> Self::Context
153    where
154        M: fmt::Debug + Send + Sync + 'static,
155    {
156        self.map_err(|error| error.context_debug(value))
157    }
158
159    #[inline(always)]
160    fn context_field<M>(self, key: &'static str, value: M) -> Self::Context
161    where
162        M: fmt::Debug + fmt::Display + Send + Sync + 'static,
163    {
164        self.map_err(|error| error.context_field(key, value))
165    }
166
167    #[inline(always)]
168    fn context_str_field<M>(self, key: &'static str, value: M) -> Self::Context
169    where
170        M: Into<String>,
171    {
172        self.map_err(|error| error.context_str_field(key, value))
173    }
174
175    #[inline(always)]
176    fn context_hex_field<M>(self, key: &'static str, value: M) -> Self::Context
177    where
178        M: fmt::Debug + Send + Sync + 'static,
179    {
180        self.map_err(|error| error.context_hex_field(key, value))
181    }
182
183    #[inline(always)]
184    fn context_debug_field<M>(self, key: &'static str, value: M) -> Self::Context
185    where
186        M: fmt::Debug + Send + Sync + 'static,
187    {
188        self.map_err(|error| error.context_debug_field(key, value))
189    }
190
191    #[inline(always)]
192    fn with_context<C, F>(self, cb: F) -> Self::Context
193    where
194        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
195        F: FnOnce() -> C,
196    {
197        self.map_err(|error| error.with_context(cb))
198    }
199
200    #[inline(always)]
201    fn with_context_hex<C, F>(self, cb: F) -> Self::Context
202    where
203        C: fmt::Debug + Send + Sync + 'static,
204        F: FnOnce() -> C,
205    {
206        self.map_err(|error| error.with_context_hex(cb))
207    }
208
209    #[inline(always)]
210    fn with_context_debug<C, F>(self, cb: F) -> Self::Context
211    where
212        C: fmt::Debug + Send + Sync + 'static,
213        F: FnOnce() -> C,
214    {
215        self.map_err(|error| error.with_context_debug(cb))
216    }
217
218    #[inline(always)]
219    fn with_context_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
220    where
221        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
222        F: FnOnce() -> C,
223    {
224        self.map_err(|error| error.with_context_field(key, cb))
225    }
226
227    #[inline(always)]
228    fn with_context_str_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
229    where
230        C: Into<String>,
231        F: FnOnce() -> C,
232    {
233        self.map_err(|error| error.with_context_str_field(key, cb))
234    }
235
236    #[inline(always)]
237    fn with_context_hex_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
238    where
239        C: fmt::Debug + Send + Sync + 'static,
240        F: FnOnce() -> C,
241    {
242        self.map_err(|error| error.with_context_hex_field(key, cb))
243    }
244
245    #[inline(always)]
246    fn with_context_debug_field<C, F>(self, key: &'static str, cb: F) -> Self::Context
247    where
248        C: fmt::Debug + Send + Sync + 'static,
249        F: FnOnce() -> C,
250    {
251        self.map_err(|error| error.with_context_debug_field(key, cb))
252    }
253}
254
255/// Generates the [`ErrorContext`] methods for [`Option<T>`], which all share the
256/// same shape: forward to the matching [`BoxError`] method, tagging the missing
257/// value with its type name.
258macro_rules! forward_none_context {
259    ($(
260        fn $name:ident < $($gen:ident : [$($bound:tt)*]),+ > ( $($arg:ident : $argty:ty),* ) => ( $($carg:expr),* );
261    )+) => {
262        $(
263            fn $name < $($gen),+ > (self, $($arg : $argty),*) -> Self::Context
264            where $($gen : $($bound)*),+
265            {
266                match self {
267                    Some(value) => Ok(value),
268                    None => Err(BoxError::from_static_str("Option is None")
269                        .context_debug_field("type", core::any::type_name::<Self>())
270                        .$name($($carg),*)),
271                }
272            }
273        )+
274    };
275}
276
277impl<T> ErrorContext for Option<T> {
278    type Context = Result<T, BoxError>;
279    type OpaqueContext = Result<T, OpaqueError>;
280
281    fn into_box_error(self) -> Self::Context {
282        match self {
283            Some(value) => Ok(value),
284            None => Err(BoxError::from_static_str("Option is None")
285                .context_debug_field("type", core::any::type_name::<Self>())),
286        }
287    }
288
289    #[inline(always)]
290    fn into_opaque_error(self) -> Self::OpaqueContext {
291        self.into_box_error().into_opaque_error()
292    }
293
294    forward_none_context! {
295        fn context<M: [fmt::Debug + fmt::Display + Send + Sync + 'static]>(value: M) => (value);
296        fn context_hex<M: [fmt::Debug + Send + Sync + 'static]>(value: M) => (value);
297        fn context_debug<M: [fmt::Debug + Send + Sync + 'static]>(value: M) => (value);
298        fn context_field<M: [fmt::Debug + fmt::Display + Send + Sync + 'static]>(key: &'static str, value: M) => (key, value);
299        fn context_str_field<M: [Into<String>]>(key: &'static str, value: M) => (key, value);
300        fn context_hex_field<M: [fmt::Debug + Send + Sync + 'static]>(key: &'static str, value: M) => (key, value);
301        fn context_debug_field<M: [fmt::Debug + Send + Sync + 'static]>(key: &'static str, value: M) => (key, value);
302        fn with_context<C: [fmt::Debug + fmt::Display + Send + Sync + 'static], F: [FnOnce() -> C]>(cb: F) => (cb);
303        fn with_context_hex<C: [fmt::Debug + Send + Sync + 'static], F: [FnOnce() -> C]>(cb: F) => (cb);
304        fn with_context_debug<C: [fmt::Debug + Send + Sync + 'static], F: [FnOnce() -> C]>(cb: F) => (cb);
305        fn with_context_field<C: [fmt::Debug + fmt::Display + Send + Sync + 'static], F: [FnOnce() -> C]>(key: &'static str, cb: F) => (key, cb);
306        fn with_context_str_field<C: [Into<String>], F: [FnOnce() -> C]>(key: &'static str, cb: F) => (key, cb);
307        fn with_context_hex_field<C: [fmt::Debug + Send + Sync + 'static], F: [FnOnce() -> C]>(key: &'static str, cb: F) => (key, cb);
308        fn with_context_debug_field<C: [fmt::Debug + Send + Sync + 'static], F: [FnOnce() -> C]>(key: &'static str, cb: F) => (key, cb);
309    }
310}
311
312/// Extends the `Error` type with methods for working with errors.
313///
314/// See the [module level documentation](crate) for more information.
315pub trait ErrorExt: private::SealedErrorExt {
316    /// Return self as [`BoxError`] without additional context.
317    fn into_box_error(self) -> BoxError;
318
319    /// Return self as [`OpaqueError`] without additional context.
320    ///
321    /// Pretty much always you'll want [`Self::into_box_error`] instead,
322    /// but [`OpaqueError`] can be useful as a last-resort, should you
323    /// run into higher-rank lifetime issues while spawning (tokio) tasks...
324    fn into_opaque_error(self) -> OpaqueError;
325
326    /// Wrap the error in a context.
327    fn context<M>(self, value: M) -> BoxError
328    where
329        M: fmt::Debug + fmt::Display + Send + Sync + 'static;
330
331    /// Wrap the error in a context,
332    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
333    fn context_hex<M>(self, value: M) -> BoxError
334    where
335        M: fmt::Debug + Send + Sync + 'static;
336
337    /// Wrap the error in a context,
338    /// using [`fmt::Debug`] as [`fmt::Display`].
339    fn context_debug<M>(self, value: M) -> BoxError
340    where
341        M: fmt::Debug + Send + Sync + 'static;
342
343    /// Wrap the error in a keyed context.
344    fn context_field<M>(self, key: &'static str, value: M) -> BoxError
345    where
346        M: fmt::Debug + fmt::Display + Send + Sync + 'static;
347
348    /// Same as [`Self::context_field`] but using a string-like value,
349    /// this is useful in case you need to pass a string slice which is borrowed
350    /// and thus cannot be passed as part of 'static error.
351    fn context_str_field<M>(self, key: &'static str, value: M) -> BoxError
352    where
353        M: Into<String>;
354
355    /// Wrap the error in a keyed context,
356    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
357    fn context_hex_field<M>(self, key: &'static str, value: M) -> BoxError
358    where
359        M: fmt::Debug + Send + Sync + 'static;
360
361    /// Wrap the error in a keyed context,
362    /// using [`fmt::Debug`] as [`fmt::Display`].
363    fn context_debug_field<M>(self, key: &'static str, value: M) -> BoxError
364    where
365        M: fmt::Debug + Send + Sync + 'static;
366
367    /// Lazily wrap the error with a context.
368    fn with_context<C, F>(self, cb: F) -> BoxError
369    where
370        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
371        F: FnOnce() -> C;
372
373    /// Lazily wrap the error with a context,
374    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
375    fn with_context_hex<C, F>(self, cb: F) -> BoxError
376    where
377        C: fmt::Debug + Send + Sync + 'static,
378        F: FnOnce() -> C;
379
380    /// Lazily wrap the error with a context,
381    /// using [`fmt::Debug`] as [`fmt::Display`].
382    fn with_context_debug<C, F>(self, cb: F) -> BoxError
383    where
384        C: fmt::Debug + Send + Sync + 'static,
385        F: FnOnce() -> C;
386
387    /// Lazily wrap the error with keyed context.
388    fn with_context_field<C, F>(self, key: &'static str, cb: F) -> BoxError
389    where
390        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
391        F: FnOnce() -> C;
392
393    /// Same as [`Self::with_context_field`] but using a string-like value,
394    /// this is useful in case you need to pass a string slice which is borrowed
395    /// and thus cannot be passed as part of 'static error.
396    fn with_context_str_field<C, F>(self, key: &'static str, cb: F) -> BoxError
397    where
398        C: Into<String>,
399        F: FnOnce() -> C;
400
401    /// Lazily wrap the error with keyed context
402    /// using [`fmt::LowerHex`] as [`fmt::Debug`] and [`fmt::Display`].
403    fn with_context_hex_field<C, F>(self, key: &'static str, cb: F) -> BoxError
404    where
405        C: fmt::Debug + Send + Sync + 'static,
406        F: FnOnce() -> C;
407
408    /// Lazily wrap the error with keyed context
409    /// using [`fmt::Debug`] as [`fmt::Display`].
410    fn with_context_debug_field<C, F>(self, key: &'static str, cb: F) -> BoxError
411    where
412        C: fmt::Debug + Send + Sync + 'static,
413        F: FnOnce() -> C;
414
415    #[cfg(feature = "std")]
416    /// Add a [`Backtrace`][std::backtrace::Backtrace] to the error.
417    fn backtrace(self) -> BoxError;
418}
419
420impl<Error: Into<BoxError>> ErrorExt for Error {
421    #[inline(always)]
422    fn into_box_error(self) -> BoxError {
423        self.into()
424    }
425
426    #[inline(always)]
427    fn into_opaque_error(self) -> OpaqueError {
428        OpaqueError::from_box_error(self)
429    }
430
431    fn context<M>(self, value: M) -> BoxError
432    where
433        M: fmt::Debug + fmt::Display + Send + Sync + 'static,
434    {
435        let mut err = self.into();
436
437        if let Some(existing) = err.downcast_mut::<self::context::ErrorWithContext>() {
438            existing.insert_value(value);
439            return err;
440        }
441
442        let mut wrapped = self::context::ErrorWithContext::new(err);
443        wrapped.insert_value(value);
444        Box::new(wrapped)
445    }
446
447    #[inline(always)]
448    fn context_hex<M>(self, value: M) -> BoxError
449    where
450        M: fmt::Debug + Send + Sync + 'static,
451    {
452        self.context(self::context::HexContextValue(value))
453    }
454
455    #[inline(always)]
456    fn context_debug<M>(self, value: M) -> BoxError
457    where
458        M: fmt::Debug + Send + Sync + 'static,
459    {
460        self.context(self::context::DebugContextValue(value))
461    }
462
463    fn context_field<M>(self, key: &'static str, value: M) -> BoxError
464    where
465        M: fmt::Debug + fmt::Display + Send + Sync + 'static,
466    {
467        let mut err = self.into();
468
469        if let Some(existing) = err.downcast_mut::<self::context::ErrorWithContext>() {
470            existing.insert_key_value(key, value);
471            return err;
472        }
473
474        let mut wrapped = self::context::ErrorWithContext::new(err);
475        wrapped.insert_key_value(key, value);
476        Box::new(wrapped)
477    }
478
479    fn context_str_field<M>(self, key: &'static str, value: M) -> BoxError
480    where
481        M: Into<String>,
482    {
483        let mut err = self.into();
484
485        if let Some(existing) = err.downcast_mut::<self::context::ErrorWithContext>() {
486            existing.insert_key_value_str(key, value);
487            return err;
488        }
489
490        let mut wrapped = self::context::ErrorWithContext::new(err);
491        wrapped.insert_key_value_str(key, value);
492        Box::new(wrapped)
493    }
494
495    #[inline(always)]
496    fn context_hex_field<M>(self, key: &'static str, value: M) -> BoxError
497    where
498        M: fmt::Debug + Send + Sync + 'static,
499    {
500        self.context_field(key, self::context::HexContextValue(value))
501    }
502
503    #[inline(always)]
504    fn context_debug_field<M>(self, key: &'static str, value: M) -> BoxError
505    where
506        M: fmt::Debug + Send + Sync + 'static,
507    {
508        self.context_field(key, self::context::DebugContextValue(value))
509    }
510
511    #[inline(always)]
512    fn with_context<C, F>(self, cb: F) -> BoxError
513    where
514        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
515        F: FnOnce() -> C,
516    {
517        self.context(cb())
518    }
519
520    #[inline(always)]
521    fn with_context_hex<C, F>(self, cb: F) -> BoxError
522    where
523        C: fmt::Debug + Send + Sync + 'static,
524        F: FnOnce() -> C,
525    {
526        self.context(self::context::HexContextValue(cb()))
527    }
528
529    #[inline(always)]
530    fn with_context_debug<C, F>(self, cb: F) -> BoxError
531    where
532        C: fmt::Debug + Send + Sync + 'static,
533        F: FnOnce() -> C,
534    {
535        self.context(self::context::DebugContextValue(cb()))
536    }
537
538    #[inline(always)]
539    fn with_context_field<C, F>(self, key: &'static str, cb: F) -> BoxError
540    where
541        C: fmt::Debug + fmt::Display + Send + Sync + 'static,
542        F: FnOnce() -> C,
543    {
544        self.context_field(key, cb())
545    }
546
547    #[inline(always)]
548    fn with_context_str_field<C, F>(self, key: &'static str, cb: F) -> BoxError
549    where
550        C: Into<String>,
551        F: FnOnce() -> C,
552    {
553        self.context_str_field(key, cb())
554    }
555
556    #[inline(always)]
557    fn with_context_hex_field<C, F>(self, key: &'static str, cb: F) -> BoxError
558    where
559        C: fmt::Debug + Send + Sync + 'static,
560        F: FnOnce() -> C,
561    {
562        self.context_field(key, self::context::HexContextValue(cb()))
563    }
564
565    #[inline(always)]
566    fn with_context_debug_field<C, F>(self, key: &'static str, cb: F) -> BoxError
567    where
568        C: fmt::Debug + Send + Sync + 'static,
569        F: FnOnce() -> C,
570    {
571        self.context_field(key, self::context::DebugContextValue(cb()))
572    }
573
574    #[cfg(feature = "std")]
575    fn backtrace(self) -> BoxError {
576        let source = self.into();
577        Box::new(self::backtrace::ErrorWithBacktrace::new(source))
578    }
579}
580pub trait BoxErrorExt: private::SealedBoxErrorExt {
581    /// Create a [`BoxError`] from a static str.
582    ///
583    /// Use this instead of `"msg".context()` or
584    /// some other method that turns it into a BoxError...
585    /// Because the std rust library turns this into a `String` otherwise...
586    fn from_static_str(e: &'static str) -> Self;
587}
588
589impl BoxErrorExt for BoxError {
590    fn from_static_str(e: &'static str) -> Self {
591        OpaqueError::from_static_str(e).into_box_error()
592    }
593}
594
595mod private {
596    use crate::BoxError;
597
598    // These sealed traits document intent rather than enforce hard closure:
599    // because the blanket impls cover every type that satisfies the public
600    // trait bounds, any downstream type that meets those bounds will satisfy
601    // the seal too. The seal exists to discourage hand-rolled impls and to
602    // keep the surface listed in one place — not as an unbypassable boundary.
603    pub trait SealedErrorContext {}
604
605    impl<T, E> SealedErrorContext for Result<T, E> where E: Into<crate::BoxError> {}
606    impl<T> SealedErrorContext for Option<T> {}
607
608    pub trait SealedErrorExt {}
609
610    impl<Error: Into<crate::BoxError>> SealedErrorExt for Error {}
611
612    pub trait SealedBoxErrorExt {}
613    impl SealedBoxErrorExt for BoxError {}
614}
615
616#[cfg(test)]
617mod tests {
618    use core::cell::Cell;
619
620    use super::*;
621
622    use crate::StdError;
623
624    #[derive(Debug, Clone)]
625    struct BoomError;
626
627    impl fmt::Display for BoomError {
628        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
629            write!(f, "boom")
630        }
631    }
632
633    impl core::error::Error for BoomError {}
634
635    #[test]
636    fn result_context_adds_context_to_error() {
637        let res: Result<(), BoomError> = Err(BoomError);
638
639        let err = res.context("ctx").unwrap_err();
640        let s = format!("{err}");
641
642        assert!(s.starts_with("boom"), "got: {s:?}");
643        assert!(s.contains(" | "), "got: {s:?}");
644        assert!(s.contains(r#""ctx""#), "got: {s:?}");
645    }
646
647    #[test]
648    fn result_context_field_adds_keyed_context_to_error() {
649        let res: Result<(), BoomError> = Err(BoomError);
650
651        let err = res.context_field("path", "/a,b/c").unwrap_err();
652        let s = format!("{err}");
653
654        assert!(s.starts_with("boom"), "got: {s:?}");
655        assert!(s.contains(r#"path="/a,b/c""#), "got: {s:?}");
656    }
657
658    #[test]
659    fn result_with_context_is_lazy_and_called_once() {
660        let res: Result<(), BoomError> = Err(BoomError);
661
662        let calls = Cell::new(0);
663        let err = res
664            .with_context(|| {
665                calls.set(calls.get() + 1);
666                "lazy"
667            })
668            .unwrap_err();
669
670        assert_eq!(calls.get(), 1);
671
672        let s = format!("{err}");
673        assert!(s.contains(r#""lazy""#), "got: {s:?}");
674    }
675
676    #[test]
677    fn result_with_context_field_is_lazy_and_called_once() {
678        let res: Result<(), BoomError> = Err(BoomError);
679
680        let calls = Cell::new(0);
681        let err = res
682            .with_context_field("k", || {
683                calls.set(calls.get() + 1);
684                "v"
685            })
686            .unwrap_err();
687
688        assert_eq!(calls.get(), 1);
689
690        let s = format!("{err}");
691        assert!(s.contains(r#"k="v""#), "got: {s:?}");
692    }
693
694    #[test]
695    fn option_context_none_returns_error_with_context() {
696        let opt: Option<i32> = None;
697
698        let err = opt.context("missing").unwrap_err();
699        let s = format!("{err}");
700
701        assert!(s.starts_with("Option is None"), "got: {s:?}");
702        assert!(s.contains(r#""missing""#), "got: {s:?}");
703    }
704
705    #[test]
706    fn option_context_field_none_returns_error_with_keyed_context() {
707        let opt: Option<i32> = None;
708
709        let err = opt.context_field("user_id", 42).unwrap_err();
710        let s = format!("{err}");
711
712        assert!(s.starts_with("Option is None"), "got: {s:?}");
713        assert!(s.contains(r#"user_id="42""#), "got: {s:?}");
714    }
715
716    #[test]
717    fn option_with_context_none_is_lazy_and_called_once() {
718        let opt: Option<i32> = None;
719
720        let calls = Cell::new(0);
721        let err = opt
722            .with_context(|| {
723                calls.set(calls.get() + 1);
724                "lazy"
725            })
726            .unwrap_err();
727
728        assert_eq!(calls.get(), 1);
729
730        let s = format!("{err}");
731        assert!(s.contains(r#""lazy""#), "got: {s:?}");
732    }
733
734    #[test]
735    fn option_with_context_field_none_is_lazy_and_called_once() {
736        let opt: Option<i32> = None;
737
738        let calls = Cell::new(0);
739        let err = opt
740            .with_context_field("k", || {
741                calls.set(calls.get() + 1);
742                "v"
743            })
744            .unwrap_err();
745
746        assert_eq!(calls.get(), 1);
747
748        let s = format!("{err}");
749        assert!(s.contains(r#"k="v""#), "got: {s:?}");
750    }
751
752    #[test]
753    fn errorext_context_reuses_existing_context_wrapper() {
754        // First wrap
755        let err1: BoxError = BoomError.context("a");
756        // Second wrap should mutate existing wrapper, not create another layer
757        let err2: BoxError = err1.context("b");
758
759        let s = format!("{err2}");
760
761        // Both values should be present
762        assert!(s.contains(r#""a""#), "got: {s:?}");
763        assert!(s.contains(r#""b""#), "got: {s:?}");
764
765        // And there should be only one " | " separator (one context wrapper)
766        assert_eq!(s.matches(" | ").count(), 1, "got: {s:?}");
767    }
768
769    #[test]
770    fn errorext_context_field_reuses_existing_context_wrapper() {
771        let err1: BoxError = BoomError.context_field("k1", "v1");
772        let err2: BoxError = err1.context_field("k2", "v2");
773
774        let s = format!("{err2}");
775
776        assert!(s.contains(r#"k1="v1""#), "got: {s:?}");
777        assert!(s.contains(r#"k2="v2""#), "got: {s:?}");
778        assert_eq!(s.matches(" | ").count(), 1, "got: {s:?}");
779    }
780
781    #[test]
782    #[cfg(feature = "std")]
783    fn errorext_backtrace_wraps_error_and_preserves_source() {
784        let err: BoxError = BoomError.backtrace();
785
786        // Display default should be source only
787        assert_eq!(format!("{err}"), "boom");
788
789        // Alternate display should include "Backtrace:" label
790        let pretty = format!("{err:#}");
791        assert!(pretty.starts_with("boom\n"), "got: {pretty:?}");
792        assert!(
793            pretty.contains("\nBacktrace:\n") || pretty.contains("\nBacktrace:\r\n"),
794            "got: {pretty:?}"
795        );
796
797        // Source chain still points to underlying error
798        let src = err.source().expect("source exists");
799        assert_eq!(src.to_string(), "boom");
800    }
801
802    #[test]
803    fn option_with_context_str_field_none_includes_type_debug_field() {
804        let opt: Option<i32> = None;
805        let err = opt
806            .with_context_str_field("k", || "v".to_owned())
807            .unwrap_err();
808        let s = format!("{err}");
809
810        assert!(s.starts_with("Option is None"), "got: {s:?}");
811        // The `type` debug field is attached on every Option<None> error path,
812        // including the lazy str/hex/debug field variants.
813        assert!(s.contains("type="), "got: {s:?}");
814        assert!(s.contains("Option<i32>"), "got: {s:?}");
815        assert!(s.contains(r#"k="v""#), "got: {s:?}");
816    }
817
818    #[test]
819    fn option_with_context_hex_field_none_includes_type_debug_field() {
820        let opt: Option<i32> = None;
821        let err = opt.with_context_hex_field("addr", || 0xfeu8).unwrap_err();
822        let s = format!("{err}");
823
824        assert!(s.starts_with("Option is None"), "got: {s:?}");
825        assert!(s.contains("type="), "got: {s:?}");
826        assert!(s.contains("Option<i32>"), "got: {s:?}");
827        assert!(s.contains("addr="), "got: {s:?}");
828    }
829
830    #[test]
831    fn option_with_context_debug_field_none_includes_type_debug_field() {
832        let opt: Option<i32> = None;
833        let err = opt
834            .with_context_debug_field("payload", || ("a", 1u32))
835            .unwrap_err();
836        let s = format!("{err}");
837
838        assert!(s.starts_with("Option is None"), "got: {s:?}");
839        assert!(s.contains("type="), "got: {s:?}");
840        assert!(s.contains("Option<i32>"), "got: {s:?}");
841        assert!(s.contains("payload="), "got: {s:?}");
842    }
843
844    #[test]
845    fn errorcontext_for_result_converts_error_into_boxerror() {
846        // Ensure Into<BoxError> conversion path works.
847        #[derive(Debug)]
848        struct MyErr;
849
850        impl core::fmt::Display for MyErr {
851            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
852                write!(f, "myerr")
853            }
854        }
855
856        impl StdError for MyErr {}
857
858        // This requires that BoxError supports From<MyErr> via Into<BoxError>.
859        let res: Result<(), MyErr> = Err(MyErr);
860
861        let err = res.context("ctx").unwrap_err();
862        let s = format!("{err}");
863
864        assert!(s.starts_with("myerr"), "got: {s:?}");
865        assert!(s.contains(r#""ctx""#), "got: {s:?}");
866    }
867}