error_rail/context/
mod.rs

1//! Helpers for attaching rich context metadata to errors.
2//!
3//! Key features:
4//! - [`with_context`] / [`with_context_result`] wrap any `Result` while preserving
5//!   structured [`ErrorContext`] values.
6//! - [`ErrorPipeline`] enables chaining operations, collecting pending contexts,
7//!   and mapping/recovering errors before finalizing into a `ComposableError`.
8//! - Convenience builders such as [`context_fn`], [`accumulate_context`], and
9//!   [`context_accumulator`] let you compose context-aware closures.
10//!
11//! See the crate-level docs for a high-level overview of when to prefer these
12//! utilities over bare `Result` transformations.
13
14use crate::traits::IntoErrorContext;
15use crate::types::composable_error::ComposableError;
16use crate::types::{BoxedComposableResult, ErrorContext, ErrorPipeline};
17use std::fmt::Display;
18
19/// Wraps an error with a single context entry.
20///
21/// Creates a new [`ComposableError`] containing the given error and context.
22///
23/// # Arguments
24///
25/// * `error` - The core error to wrap
26/// * `context` - Context information to attach
27///
28/// # Examples
29///
30/// ```
31/// use error_rail::{with_context, ErrorContext};
32///
33/// let err = with_context("io failed", ErrorContext::tag("disk"));
34/// assert_eq!(err.context().len(), 1);
35/// ```
36#[inline]
37pub fn with_context<E, C>(error: E, context: C) -> ComposableError<E>
38where
39    C: IntoErrorContext,
40{
41    ComposableError::new(error).with_context(context)
42}
43
44/// Transforms a `Result` by adding context to any error.
45///
46/// On `Err`, wraps the error in a boxed [`ComposableError`] with the provided context.
47/// On `Ok`, returns the success value unchanged.
48///
49/// # Arguments
50///
51/// * `result` - The result to transform
52/// * `context` - Context to attach on error
53///
54/// # Examples
55///
56/// ```
57/// use error_rail::{with_context_result, ErrorContext};
58///
59/// let result: Result<i32, &str> = Err("failed");
60/// let enriched = with_context_result(result, ErrorContext::tag("auth"));
61/// assert!(enriched.is_err());
62/// ```
63#[inline]
64pub fn with_context_result<T, E, C>(result: Result<T, E>, context: C) -> BoxedComposableResult<T, E>
65where
66    C: IntoErrorContext,
67{
68    result.map_err(|e| Box::new(with_context(e, context)))
69}
70
71/// Creates a reusable closure that wraps errors with a fixed context.
72///
73/// Returns a function that can be used with `map_err` to attach the same
74/// context to multiple error paths.
75///
76/// # Arguments
77///
78/// * `context` - The context to attach (must be `Clone`)
79///
80/// # Examples
81///
82/// ```
83/// use error_rail::{context_fn, ErrorContext};
84///
85/// let add_tag = context_fn(ErrorContext::tag("network"));
86/// let err = add_tag("timeout");
87/// assert_eq!(err.context().len(), 1);
88/// ```
89#[inline]
90pub fn context_fn<E, C>(context: C) -> impl Fn(E) -> ComposableError<E>
91where
92    C: IntoErrorContext + Clone,
93{
94    move |error| with_context(error, context.clone())
95}
96
97/// Creates an [`ErrorPipeline`] from a result.
98///
99/// Convenience function equivalent to `ErrorPipeline::new(result)`.
100///
101/// # Arguments
102///
103/// * `result` - The result to wrap
104///
105/// # Examples
106///
107/// ```
108/// use error_rail::{error_pipeline, context};
109///
110/// let pipeline = error_pipeline::<u32, &str>(Err("failed"))
111///     .with_context(context!("step failed"));
112/// ```
113#[inline]
114pub fn error_pipeline<T, E>(result: Result<T, E>) -> ErrorPipeline<T, E> {
115    ErrorPipeline::new(result)
116}
117
118/// Wraps an error with multiple context entries at once.
119///
120/// Creates a [`ComposableError`] and attaches all provided contexts.
121///
122/// # Arguments
123///
124/// * `error` - The core error to wrap
125/// * `contexts` - Iterator of contexts to attach
126///
127/// # Examples
128///
129/// ```
130/// use error_rail::{accumulate_context, ErrorContext};
131///
132/// let contexts = vec![
133///     ErrorContext::tag("db"),
134///     ErrorContext::new("connection failed"),
135/// ];
136/// let err = accumulate_context("timeout", contexts);
137/// assert_eq!(err.context().len(), 2);
138/// ```
139pub fn accumulate_context<E, I, C>(error: E, contexts: I) -> ComposableError<E>
140where
141    I: IntoIterator<Item = C>,
142    C: IntoErrorContext,
143{
144    let context_vec: Vec<ErrorContext> = contexts
145        .into_iter()
146        .map(|c| c.into_error_context())
147        .collect();
148
149    ComposableError::new(error).with_contexts(context_vec)
150}
151
152/// Creates a reusable closure that wraps errors with multiple contexts.
153///
154/// Returns a function that attaches all provided contexts to any error.
155///
156/// # Arguments
157///
158/// * `contexts` - Iterator of contexts to attach (must be `Clone`)
159///
160/// # Examples
161///
162/// ```
163/// use error_rail::{context_accumulator, ErrorContext};
164///
165/// let contexts = vec![ErrorContext::tag("auth"), ErrorContext::tag("api")];
166/// let add_contexts = context_accumulator(contexts);
167/// let err = add_contexts("unauthorized");
168/// assert_eq!(err.context().len(), 2);
169/// ```
170pub fn context_accumulator<E, I, C>(contexts: I) -> impl Fn(E) -> ComposableError<E>
171where
172    I: IntoIterator<Item = C> + Clone,
173    C: IntoErrorContext + Clone,
174{
175    move |error| accumulate_context(error, contexts.clone())
176}
177
178/// Formats a [`ComposableError`] as a human-readable error chain.
179///
180/// Returns a string representation showing all contexts and the core error.
181///
182/// # Arguments
183///
184/// * `error` - The composable error to format
185///
186/// # Examples
187///
188/// ```
189/// use error_rail::{ComposableError, ErrorContext, format_error_chain};
190///
191/// let err = ComposableError::new("failed")
192///     .with_context(ErrorContext::tag("db"));
193/// let chain = format_error_chain(&err);
194/// assert!(chain.contains("failed"));
195/// ```
196pub fn format_error_chain<E>(error: &ComposableError<E>) -> String
197where
198    E: Display,
199{
200    error.error_chain()
201}
202
203/// Extracts all context entries from a [`ComposableError`].
204///
205/// Returns a vector containing all attached contexts.
206///
207/// # Arguments
208///
209/// * `error` - The composable error to extract from
210///
211/// # Examples
212///
213/// ```
214/// use error_rail::{ComposableError, ErrorContext, extract_context};
215///
216/// let err = ComposableError::new("failed")
217///     .with_context(ErrorContext::tag("db"));
218/// let contexts = extract_context(&err);
219/// assert_eq!(contexts.len(), 1);
220/// ```
221pub fn extract_context<E>(error: &ComposableError<E>) -> Vec<ErrorContext> {
222    error.context()
223}