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}