Skip to main content

rustica/error/
context.rs

1//! # Error Context Management and Functional Pipelines
2//!
3//! This module provides utilities for managing error context and creating
4//! functional error handling pipelines. It includes context accumulation,
5//! error transformation chains, and composable error handling patterns.
6
7use crate::error::{
8    ComposableResult,
9    types::{BoxedComposableResult, ComposableError, IntoErrorContext},
10};
11use smallvec::SmallVec;
12use std::fmt::Display;
13
14/// Adds context to any error type, creating a ComposableError.
15///
16/// This function provides a convenient way to add contextual information
17/// to any error, wrapping it in a ComposableError structure that supports
18/// context accumulation and error chaining.
19///
20/// # Type Parameters
21///
22/// * `E`: The original error type
23/// * `C`: The context type (must implement IntoErrorContext)
24///
25/// # Arguments
26///
27/// * `error`: The error to add context to
28/// * `context`: The context information to add
29///
30/// # Examples
31///
32/// ```rust
33/// use rustica::error::with_context;
34///
35/// let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file.txt");
36/// let contextual_error = with_context(io_error, "Failed to load configuration");
37///
38/// assert!(contextual_error.context().len() > 0);
39/// ```
40#[inline]
41pub fn with_context<E, C>(error: E, context: C) -> ComposableError<E>
42where
43    C: IntoErrorContext,
44{
45    ComposableError::new(error).with_context(context)
46}
47
48/// Adds context to a Result, converting errors to ComposableError.
49///
50/// This function transforms a `Result<T, E>` into a `Result<T, ComposableError<E>>`,
51/// adding the specified context to any error that occurs. Success values pass
52/// through unchanged.
53///
54/// # Type Parameters
55///
56/// * `T`: The success type
57/// * `E`: The original error type
58/// * `C`: The context type (must implement IntoErrorContext)
59///
60/// # Arguments
61///
62/// * `result`: The Result to add context to
63/// * `context`: The context information to add
64///
65/// # Examples
66///
67/// ```rust
68/// use rustica::error::with_context_result;
69///
70/// fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
71///     s.parse()
72/// }
73///
74/// let result = parse_number("not_a_number");
75/// let contextual = with_context_result(result, "Failed to parse user input");
76///
77/// match contextual {
78///     Ok(_) => panic!("Expected error"),
79///     Err(composable) => {
80///         assert_eq!(composable.context().len(), 1);
81///         assert!(composable.context()[0].contains("Failed to parse user input"));
82///     }
83/// }
84/// ```
85#[inline]
86pub fn with_context_result<T, E, C>(result: Result<T, E>, context: C) -> BoxedComposableResult<T, E>
87where
88    C: IntoErrorContext,
89{
90    result.map_err(|e| Box::new(with_context(e, context)))
91}
92
93/// Creates a context function that can be applied lazily.
94///
95/// This function returns a closure that, when called with an error,
96/// adds the specified context. This is useful for creating reusable
97/// context transformations and building error handling pipelines.
98///
99/// # Type Parameters
100///
101/// * `C`: The context type (must implement IntoErrorContext)
102///
103/// # Arguments
104///
105/// * `context`: The context information to add
106///
107/// # Examples
108///
109/// ```rust
110/// use rustica::error::context_fn;
111///
112/// let add_db_context = context_fn("Database operation failed");
113///
114/// let error = "Connection refused";
115/// let contextual_error = add_db_context(error);
116///
117/// assert_eq!(contextual_error.context().len(), 1);
118/// assert!(contextual_error.context()[0].contains("Database operation failed"));
119/// ```
120#[inline]
121pub fn context_fn<E, C>(context: C) -> impl Fn(E) -> ComposableError<E>
122where
123    C: IntoErrorContext + Clone,
124{
125    move |error| with_context(error, context.clone())
126}
127
128/// A functional error handling pipeline builder.
129///
130/// `ErrorPipeline` allows you to build composable error handling chains
131/// that can transform, contextualize, and recover from errors in a
132/// functional style. Each operation returns a new pipeline that can
133/// be further composed.
134///
135/// # Type Parameters
136///
137/// * `T`: The success type
138/// * `E`: The current error type
139///
140/// # Examples
141///
142/// ```rust
143/// use rustica::error::{ErrorPipeline, ComposableError};
144///
145/// let result: Result<i32, &str> = Err("parse error");
146///
147/// let processed = ErrorPipeline::new(result)
148///     .with_context("Failed to process input")
149///     .map_error(|e| format!("Error: {}", e))
150///     .recover(|_| Ok(42))
151///     .finish();
152///
153/// assert_eq!(processed, Ok(42));
154/// ```
155pub struct ErrorPipeline<T, E> {
156    result: Result<T, E>,
157    pending_contexts: SmallVec<[String; 4]>,
158}
159
160impl<T, E> ErrorPipeline<T, E> {
161    /// Creates a new error pipeline from a Result.
162    ///
163    /// # Arguments
164    ///
165    /// * `result`: The initial Result to process
166    ///
167    /// # Examples
168    ///
169    /// ```rust
170    /// use rustica::error::ErrorPipeline;
171    ///
172    /// let result: Result<i32, &str> = Ok(42);
173    /// let pipeline = ErrorPipeline::new(result);
174    /// ```
175    #[inline]
176    pub fn new(result: Result<T, E>) -> Self {
177        Self {
178            result,
179            pending_contexts: SmallVec::new(),
180        }
181    }
182
183    /// Adds context to errors in the pipeline.
184    ///
185    /// This buffers the context without transforming the error type,
186    /// enabling efficient deep pipeline operations. Contexts are only
187    /// applied when the pipeline is finished.
188    ///
189    /// # Arguments
190    ///
191    /// * `context`: The context to add
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// use rustica::error::ErrorPipeline;
197    ///
198    /// let result: Result<i32, &str> = Err("failed");
199    /// let pipeline = ErrorPipeline::new(result)
200    ///     .with_context("Operation failed")
201    ///     .with_context("Database error");
202    /// ```
203    #[inline]
204    pub fn with_context<C>(mut self, context: C) -> Self
205    where
206        C: IntoErrorContext,
207    {
208        if self.result.is_ok() {
209            return self;
210        }
211
212        let ctx_str = context.into_error_context().message().to_string();
213        self.pending_contexts.push(ctx_str);
214        self
215    }
216
217    /// Maps the error type to a new type.
218    ///
219    /// This applies a transformation function to any error in the pipeline,
220    /// allowing for error type conversions and transformations.
221    ///
222    /// # Type Parameters
223    ///
224    /// * `F`: The mapping function type
225    /// * `NewE`: The new error type
226    ///
227    /// # Arguments
228    ///
229    /// * `f`: The function to apply to errors
230    ///
231    /// # Examples
232    ///
233    /// ```rust
234    /// use rustica::error::ErrorPipeline;
235    ///
236    /// let result: Result<i32, i32> = Err(404);
237    /// let pipeline = ErrorPipeline::new(result)
238    ///     .map_error(|code| format!("HTTP Error: {}", code));
239    /// ```
240    #[inline]
241    pub fn map_error<F, NewE>(self, f: F) -> ErrorPipeline<T, NewE>
242    where
243        F: FnOnce(E) -> NewE,
244    {
245        ErrorPipeline {
246            result: self.result.map_err(f),
247            pending_contexts: self.pending_contexts,
248        }
249    }
250
251    /// Applies a recovery function to errors.
252    ///
253    /// This allows the pipeline to recover from errors by providing
254    /// alternative values or alternative computations.
255    ///
256    /// # Type Parameters
257    ///
258    /// * `F`: The recovery function type
259    ///
260    /// # Arguments
261    ///
262    /// * `recovery`: The function to apply for error recovery
263    ///
264    /// # Examples
265    ///
266    /// ```rust
267    /// use rustica::error::ErrorPipeline;
268    ///
269    /// let result: Result<i32, &str> = Err("failed");
270    /// let pipeline = ErrorPipeline::new(result)
271    ///     .recover(|_error| Ok(0)); // Provide default value
272    /// ```
273    #[inline]
274    pub fn recover<F>(self, recovery: F) -> ErrorPipeline<T, E>
275    where
276        F: FnOnce(E) -> Result<T, E>,
277    {
278        ErrorPipeline {
279            result: self.result.or_else(recovery),
280            pending_contexts: self.pending_contexts,
281        }
282    }
283
284    /// Chains another operation that might fail.
285    ///
286    /// This is similar to `Result::and_then`, allowing you to chain
287    /// operations that might produce errors.
288    ///
289    /// # Type Parameters
290    ///
291    /// * `U`: The new success type
292    /// * `F`: The chaining function type
293    ///
294    /// # Arguments
295    ///
296    /// * `f`: The function to apply to success values
297    ///
298    /// # Examples
299    ///
300    /// ```rust
301    /// use rustica::error::ErrorPipeline;
302    ///
303    /// let result: Result<i32, &str> = Ok(42);
304    /// let pipeline = ErrorPipeline::new(result)
305    ///     .and_then(|x| if x > 0 { Ok(x * 2) } else { Err("negative") });
306    /// ```
307    #[inline]
308    pub fn and_then<U, F>(self, f: F) -> ErrorPipeline<U, E>
309    where
310        F: FnOnce(T) -> Result<U, E>,
311    {
312        ErrorPipeline {
313            result: self.result.and_then(f),
314            pending_contexts: self.pending_contexts,
315        }
316    }
317
318    /// Maps the success value to a new type.
319    ///
320    /// This applies a transformation function to any success value
321    /// in the pipeline, leaving errors unchanged.
322    ///
323    /// # Type Parameters
324    ///
325    /// * `U`: The new success type
326    /// * `F`: The mapping function type
327    ///
328    /// # Arguments
329    ///
330    /// * `f`: The function to apply to success values
331    ///
332    /// # Examples
333    ///
334    /// ```rust
335    /// use rustica::error::ErrorPipeline;
336    ///
337    /// let result: Result<i32, &str> = Ok(42);
338    /// let pipeline = ErrorPipeline::new(result)
339    ///     .map(|x| x.to_string());
340    /// ```
341    #[inline]
342    pub fn map<U, F>(self, f: F) -> ErrorPipeline<U, E>
343    where
344        F: FnOnce(T) -> U,
345    {
346        ErrorPipeline {
347            result: self.result.map(f),
348            pending_contexts: self.pending_contexts,
349        }
350    }
351
352    /// Finishes the pipeline and returns the final result with applied contexts.
353    ///
354    /// This is the terminal operation of the pipeline that applies
355    /// all buffered contexts to any error and returns a **boxed** `ComposableError`.
356    /// This avoids clippy warnings about large error types and is recommended
357    /// for most use cases.
358    ///
359    /// If you want to avoid boxing (e.g., when `E` is small), use [`finish_without_box()`](Self::finish_without_box).
360    ///
361    /// # Examples
362    ///
363    /// ```rust
364    /// use rustica::error::ErrorPipeline;
365    ///
366    /// let result: Result<i32, &str> = Ok(42);
367    /// let final_result = ErrorPipeline::new(result)
368    ///     .map(|x| x * 2)
369    ///     .with_context("Processing data")
370    ///     .finish();
371    ///
372    /// assert_eq!(final_result, Ok(84));
373    /// ```
374    #[inline]
375    pub fn finish(self) -> BoxedComposableResult<T, E> {
376        match self.result {
377            Ok(v) => Ok(v),
378            Err(e) => {
379                let composable = ComposableError::new(e).with_contexts(self.pending_contexts);
380                Err(Box::new(composable))
381            },
382        }
383    }
384
385    /// Finishes the pipeline and returns the final result without boxing.
386    ///
387    /// This is an alternative to [`finish()`](Self::finish) that returns an
388    /// **unboxed** `ComposableError`. Use this when:
389    /// - The error type `E` is small (e.g., `i32`, `&str`)
390    /// - You want to avoid heap allocation
391    /// - You're okay with clippy warnings about large error types
392    ///
393    /// For most use cases, prefer [`finish()`](Self::finish) to avoid clippy warnings.
394    ///
395    /// # Examples
396    ///
397    /// ```rust
398    /// use rustica::error::{ErrorPipeline, ComposableResult};
399    ///
400    /// let result: Result<i32, i32> = Err(404);
401    /// let final_result: ComposableResult<i32, i32> = ErrorPipeline::new(result)
402    ///     .with_context("Request failed")
403    ///     .finish_without_box();
404    ///
405    /// match final_result {
406    ///     Ok(_) => panic!("Expected error"),
407    ///     Err(e) => assert_eq!(e.core_error(), &404),
408    /// }
409    /// ```
410    #[inline]
411    #[allow(clippy::result_large_err)]
412    pub fn finish_without_box(self) -> ComposableResult<T, E> {
413        match self.result {
414            Ok(v) => Ok(v),
415            Err(e) => {
416                let composable = ComposableError::new(e).with_contexts(self.pending_contexts);
417                Err(composable)
418            },
419        }
420    }
421}
422
423/// Creates an error handling pipeline from a Result.
424///
425/// This is a convenience function that creates an `ErrorPipeline`
426/// from a Result, enabling functional error handling patterns.
427///
428/// # Type Parameters
429///
430/// * `T`: The success type
431/// * `E`: The error type
432///
433/// # Arguments
434///
435/// * `result`: The Result to create a pipeline from
436///
437/// # Examples
438///
439/// ```rust
440/// use rustica::error::error_pipeline;
441///
442/// let result: Result<i32, &str> = Err("failed");
443/// let processed = error_pipeline(result)
444///     .with_context("Operation failed")
445///     .recover(|_| Ok(0))
446///     .finish();
447///
448/// assert_eq!(processed, Ok(0));
449/// ```
450#[inline]
451pub fn error_pipeline<T, E>(result: Result<T, E>) -> ErrorPipeline<T, E> {
452    ErrorPipeline::new(result)
453}
454
455/// Accumulates context from multiple sources into a single error.
456///
457/// This function takes an error and multiple context sources,
458/// creating a ComposableError with all context information
459/// accumulated in order.
460///
461/// # Type Parameters
462///
463/// * `E`: The error type
464/// * `I`: The iterator type for contexts
465/// * `C`: The context item type
466///
467/// # Arguments
468///
469/// * `error`: The base error
470/// * `contexts`: An iterator of context information
471///
472/// # Examples
473///
474/// ```rust
475/// use rustica::error::accumulate_context;
476///
477/// let error = "core error";
478/// let contexts = vec!["step 1 failed", "step 2 failed", "operation failed"];
479/// let accumulated = accumulate_context(error, contexts);
480///
481/// assert_eq!(accumulated.context().len(), 3);
482/// ```
483pub fn accumulate_context<E, I, C>(error: E, contexts: I) -> ComposableError<E>
484where
485    I: IntoIterator<Item = C>,
486    C: IntoErrorContext,
487{
488    let context_strings: Vec<String> = contexts
489        .into_iter()
490        .map(|c| c.into_error_context().message().to_string())
491        .collect();
492
493    ComposableError::new(error).with_contexts(context_strings)
494}
495
496/// Creates a context accumulator function.
497///
498/// This returns a function that can accumulate multiple contexts
499/// onto an error. The returned function can be reused for multiple
500/// errors with the same context pattern.
501///
502/// # Type Parameters
503///
504/// * `I`: The iterator type for contexts
505/// * `C`: The context item type
506///
507/// # Arguments
508///
509/// * `contexts`: The contexts to accumulate
510///
511/// # Examples
512///
513/// ```rust
514/// use rustica::error::context_accumulator;
515///
516/// let contexts = vec!["database error", "user operation failed"];
517/// let accumulator = context_accumulator(contexts);
518///
519/// let error1 = "connection timeout";
520/// let error2 = "query failed";
521///
522/// let contextual1 = accumulator(error1);
523/// let contextual2 = accumulator(error2);
524///
525/// // Both errors now have the same context stack
526/// assert_eq!(contextual1.context().len(), 2);
527/// assert_eq!(contextual2.context().len(), 2);
528/// ```
529pub fn context_accumulator<E, I, C>(contexts: I) -> impl Fn(E) -> ComposableError<E>
530where
531    I: IntoIterator<Item = C> + Clone,
532    C: IntoErrorContext + Clone,
533{
534    move |error| accumulate_context(error, contexts.clone())
535}
536
537/// Formats an error with its full context chain.
538///
539/// This function creates a human-readable string representation
540/// of an error and all its context information, formatted as
541/// a chain from most recent context to core error.
542///
543/// # Type Parameters
544///
545/// * `E`: The error type (must implement Display)
546///
547/// # Arguments
548///
549/// * `error`: The ComposableError to format
550///
551/// # Examples
552///
553/// ```rust
554/// use rustica::error::{ComposableError, format_error_chain};
555///
556/// let error = ComposableError::new("file not found")
557///     .with_context("failed to load config".to_string())
558///     .with_context("application startup failed".to_string());
559///
560/// let formatted = format_error_chain(&error);
561/// assert!(formatted.contains("application startup failed"));
562/// assert!(formatted.contains("failed to load config"));
563/// assert!(formatted.contains("file not found"));
564/// ```
565pub fn format_error_chain<E>(error: &ComposableError<E>) -> String
566where
567    E: Display,
568{
569    error.error_chain()
570}
571
572/// Extracts all context information from a ComposableError.
573///
574/// This function returns a vector of all context strings in the
575/// order they were added (most recent first).
576///
577/// # Type Parameters
578///
579/// * `E`: The error type
580///
581/// # Arguments
582///
583/// * `error`: The ComposableError to extract context from
584///
585/// # Examples
586///
587/// ```rust
588/// use rustica::error::{ComposableError, extract_context};
589///
590/// let error = ComposableError::new("error")
591///     .with_context("context 1".to_string())
592///     .with_context("context 2".to_string());
593///
594/// let contexts = extract_context(&error);
595/// assert_eq!(contexts.len(), 2);
596/// assert_eq!(contexts[0], "context 2"); // Most recent first
597/// assert_eq!(contexts[1], "context 1");
598/// ```
599pub fn extract_context<E>(error: &ComposableError<E>) -> Vec<String> {
600    error.context()
601}