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}