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