error_rail/traits/
result_ext.rs

1//! Extension trait for ergonomic context addition to `Result` types.
2//!
3//! This module provides [`ResultExt`], which adds convenient methods for
4//! attaching context to errors without verbose `.map_err()` chains.
5//!
6//! # Examples
7//!
8//! ```
9//! use error_rail::traits::ResultExt;
10//!
11//! fn load_config() -> Result<String, Box<error_rail::ComposableError<std::io::Error>>> {
12//!     std::fs::read_to_string("config.toml")
13//!         .ctx("loading configuration file")
14//! }
15//! ```
16
17use crate::traits::IntoErrorContext;
18use crate::types::alloc_type::{Box, String};
19use crate::types::{ComposableError, LazyContext};
20
21/// Extension trait for adding context to `Result` types ergonomically.
22///
23/// This trait provides a more natural API for error context compared to
24/// manual `.map_err()` chains, reducing boilerplate while maintaining
25/// full type safety.
26///
27/// # Performance
28///
29/// The [`ctx_with`](ResultExt::ctx_with) method uses lazy evaluation,
30/// meaning the closure is only executed when an error actually occurs.
31/// This provides the same 2.1x performance benefit as the `context!` macro.
32///
33/// # Examples
34///
35/// ## Basic Usage
36///
37/// ```
38/// use error_rail::traits::ResultExt;
39/// use error_rail::ComposableError;
40///
41/// fn read_file() -> Result<String, Box<ComposableError<std::io::Error>>> {
42///     std::fs::read_to_string("data.txt")
43///         .ctx("reading data file")
44/// }
45/// ```
46///
47/// ## Lazy Context (Recommended for Performance)
48///
49/// ```
50/// use error_rail::traits::ResultExt;
51/// use error_rail::ComposableError;
52///
53/// fn process(user_id: u64) -> Result<(), Box<ComposableError<&'static str>>> {
54///     let result: Result<(), &str> = Err("not found");
55///     result.ctx_with(|| format!("processing user {}", user_id))
56/// }
57/// ```
58///
59/// ## Chaining Multiple Contexts
60///
61/// ```
62/// use error_rail::traits::ResultExt;
63/// use error_rail::ComposableError;
64///
65/// fn complex_operation() -> Result<String, Box<ComposableError<std::io::Error>>> {
66///     std::fs::read_to_string("config.toml")
67///         .ctx("loading config")
68///         .map(|s| s.to_uppercase())
69/// }
70/// ```
71pub trait ResultExt<T, E> {
72    /// Adds a static context message to the error.
73    ///
74    /// This method wraps the error in a [`ComposableError`] with the given
75    /// context message, then boxes it for ergonomic return types.
76    ///
77    /// # Arguments
78    ///
79    /// * `msg` - A static string describing what operation was being performed.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use error_rail::traits::ResultExt;
85    ///
86    /// let result: Result<(), &str> = Err("failed");
87    /// let with_context = result.ctx("performing operation");
88    /// assert!(with_context.is_err());
89    /// ```
90    fn ctx<C: IntoErrorContext>(self, msg: C) -> Result<T, Box<ComposableError<E>>>;
91
92    /// Adds a lazily-evaluated context message to the error.
93    ///
94    /// The closure is only called if the `Result` is an `Err`, providing
95    /// optimal performance on success paths (2.1x faster than eager evaluation).
96    ///
97    /// # Arguments
98    ///
99    /// * `f` - A closure that produces the context message.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use error_rail::traits::ResultExt;
105    ///
106    /// let user_id = 42;
107    /// let result: Result<(), &str> = Err("not found");
108    /// let with_context = result.ctx_with(|| format!("user_id: {}", user_id));
109    /// ```
110    fn ctx_with<F>(self, f: F) -> Result<T, Box<ComposableError<E>>>
111    where
112        F: FnOnce() -> String;
113}
114
115impl<T, E> ResultExt<T, E> for Result<T, E> {
116    #[inline]
117    fn ctx<C: IntoErrorContext>(self, msg: C) -> Result<T, Box<ComposableError<E>>> {
118        self.map_err(|e| Box::new(ComposableError::new(e).with_context(msg)))
119    }
120
121    #[inline]
122    fn ctx_with<F>(self, f: F) -> Result<T, Box<ComposableError<E>>>
123    where
124        F: FnOnce() -> String,
125    {
126        self.map_err(|e| Box::new(ComposableError::new(e).with_context(LazyContext::new(f))))
127    }
128}
129
130/// Extension trait for adding context to already-boxed `ComposableError` results.
131///
132/// This trait allows chaining `.ctx()` calls on results that already contain
133/// a boxed `ComposableError`.
134///
135/// # Examples
136///
137/// ```
138/// use error_rail::traits::{ResultExt, BoxedResultExt};
139///
140/// fn inner() -> Result<(), Box<error_rail::ComposableError<&'static str>>> {
141///     Err("inner error").ctx("inner operation")
142/// }
143///
144/// fn outer() -> Result<(), Box<error_rail::ComposableError<&'static str>>> {
145///     inner().ctx_boxed("outer operation")
146/// }
147/// ```
148pub trait BoxedResultExt<T, E> {
149    /// Adds additional context to an already-boxed `ComposableError`.
150    fn ctx_boxed<C: IntoErrorContext>(self, msg: C) -> Self;
151
152    /// Adds lazily-evaluated context to an already-boxed `ComposableError`.
153    fn ctx_boxed_with<F>(self, f: F) -> Self
154    where
155        F: FnOnce() -> String;
156}
157
158impl<T, E> BoxedResultExt<T, E> for Result<T, Box<ComposableError<E>>> {
159    #[inline]
160    fn ctx_boxed<C: IntoErrorContext>(self, msg: C) -> Self {
161        self.map_err(|e| {
162            let inner = *e;
163            Box::new(inner.with_context(msg))
164        })
165    }
166
167    #[inline]
168    fn ctx_boxed_with<F>(self, f: F) -> Self
169    where
170        F: FnOnce() -> String,
171    {
172        self.map_err(|e| {
173            let inner = *e;
174            Box::new(inner.with_context(LazyContext::new(f)))
175        })
176    }
177}