rustica/utils/
error_utils.rs

1//! # Error Handling Utilities
2//!
3//! This module provides standardized error handling utilities for working with
4//! functional programming patterns in Rust. It builds upon the abstractions in
5//! the rest of the library, particularly focusing on error handling.
6//!
7//! ## Key Features
8//!
9//! - **Error Transformation**: Convert between different error representations
10//! - **Error Collection**: Accumulate all errors or short-circuit on first error
11//! - **Standardized Error Interface**: The `WithError` trait provides a unified
12//!   approach to handling errors
13//! - **Custom Error Types**: Create application-specific error types with context
14//!
15//! ## Categories of Utilities
16//!
17//! - **Core Error Traits**: `WithError` trait and its implementations
18//! - **Sequence/Traverse Functions**: Collect and transform collections of fallible operations
19//! - **Error Type Conversions**: Convert between `Result`, `Either`, and `Validated`
20//! - **Chainable Error Handling**: Extension methods for smoother error handling
21//! - **Custom Error Types**: Tools for creating application-specific errors
22//!
23//! ## Getting Started
24//!
25//! The most commonly used functions in this module are:
26//!
27//! - `sequence`: Convert a collection of `Result`s into a `Result` of collection
28//! - `traverse`: Apply a fallible function to every element in a collection
29//! - `traverse_validated`: Apply a fallible function and collect all errors
30//! - `error_with_context`: Create contextualized error messages
31
32use crate::datatypes::either::Either;
33use crate::datatypes::validated::Validated;
34use crate::prelude::HKT;
35use smallvec::SmallVec;
36use std::fmt::Debug;
37
38// ===== Core Error Traits =====
39
40/// Error handling trait for types that can fail with a specific error type.
41///
42/// This trait provides a common interface for working with different error handling
43/// types such as `Result`, `Either`, and `Validated`. It defines methods for
44/// transforming errors and converting to standard Rust `Result` types.
45///
46/// # Type Parameters
47///
48/// * `E`: The error type that can occur in the fallible computation
49///
50/// # Examples
51///
52/// Using `WithError` with `Result`:
53///
54/// ```rust
55/// use rustica::utils::error_utils::WithError;
56/// use std::io;
57///
58/// // Define a function that works with any type implementing WithError
59/// fn log_and_transform_error<T, E>(value: T) -> T::ErrorOutput<String>
60/// where
61///     T: WithError<E>,
62///     E: std::fmt::Display + Clone,
63/// {
64///     value.fmap_error(|e| format!("Error occurred: {}", e))
65/// }
66///
67/// // Use with a Result
68/// let result: Result<i32, String> = Err("file not found".to_string());
69/// let transformed = log_and_transform_error(result);
70/// assert!(transformed.is_err());
71/// ```
72pub trait WithError<E>: HKT {
73    /// The successful value type
74    type Success;
75
76    /// The output type when mapping the error to a new type
77    type ErrorOutput<G>;
78
79    /// Maps a function over the error, transforming the error type.
80    ///
81    /// This is similar to `map_err` for `Result`, but generalized to work with
82    /// any error handling type.
83    ///
84    /// # Type Parameters
85    ///
86    /// * `F`: Function type that transforms error `E` to error `G`
87    /// * `G`: The new error type after transformation
88    ///
89    /// # Examples
90    ///
91    /// ```rust
92    /// use rustica::utils::error_utils::WithError;
93    ///
94    /// let result: Result<i32, &str> = Err("not found");
95    /// let transformed = result.fmap_error(|e| format!("Error: {}", e));
96    /// assert_eq!(transformed, Err("Error: not found".to_string()));
97    /// ```
98    fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
99    where
100        F: Fn(E) -> G,
101        G: Clone;
102
103    /// Converts this type to a Result.
104    ///
105    /// This provides a way to standardize error handling by converting any
106    /// error handling type to a Rust `Result`.
107    ///
108    /// # Examples
109    ///
110    /// ```rust
111    /// use rustica::utils::error_utils::WithError;
112    /// use rustica::datatypes::either::Either;
113    ///
114    /// let either: Either<&str, i32> = Either::left("error");
115    /// let result = either.to_result();
116    /// assert_eq!(result, Err("error"));
117    /// ```
118    fn to_result(self) -> Result<Self::Success, E>;
119}
120
121impl<T, E: Clone> WithError<E> for Result<T, E> {
122    type Success = T;
123    type ErrorOutput<G> = Result<T, G>;
124
125    fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
126    where
127        F: FnOnce(E) -> G,
128    {
129        match self {
130            Ok(t) => Ok(t),
131            Err(e) => Err(f(e)),
132        }
133    }
134
135    fn to_result(self) -> Result<Self::Success, E> {
136        self
137    }
138}
139
140impl<T, E> WithError<E> for Either<E, T> {
141    type Success = T;
142    type ErrorOutput<G> = Either<G, T>;
143
144    fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
145    where
146        F: FnOnce(E) -> G,
147    {
148        match self {
149            Either::Left(e) => Either::Left(f(e)),
150            Either::Right(t) => Either::Right(t),
151        }
152    }
153
154    fn to_result(self) -> Result<Self::Success, E> {
155        match self {
156            Either::Left(e) => Err(e),
157            Either::Right(t) => Ok(t),
158        }
159    }
160}
161
162impl<T: Clone, E: Clone> WithError<E> for Validated<E, T> {
163    type Success = T;
164    type ErrorOutput<G> = Validated<G, T>;
165
166    fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
167    where
168        F: Fn(E) -> G,
169        G: Clone,
170        T: Clone,
171    {
172        match self {
173            Validated::Valid(t) => Validated::Valid(t),
174            Validated::Invalid(e) => Validated::Invalid(e.into_iter().map(f).collect()),
175        }
176    }
177
178    fn to_result(self) -> Result<Self::Success, E> {
179        match self {
180            Validated::Valid(t) => Ok(t),
181            Validated::Invalid(e) => Err(e.into_iter().next().unwrap()),
182        }
183    }
184}
185
186// ===== Sequence and Traverse Functions =====
187
188/// Specialization of `sequence_result` for standardizing error handling.
189///
190/// This function converts a collection of results into a result containing
191/// a collection of values, short-circuiting on the first error encountered.
192///
193/// # Type Parameters
194///
195/// * `A`: The success type in the Result
196/// * `E`: The error type in the Result
197///
198/// # Examples
199///
200/// ```rust
201/// use rustica::utils::error_utils::sequence;
202///
203/// // All results are Ok, so the final result is Ok containing all values
204/// let results: Vec<Result<i32, &str>> = vec![Ok(1), Ok(2), Ok(3)];
205/// assert_eq!(sequence(results), Ok(vec![1, 2, 3]));
206///
207/// // One result is Err, so the final result is Err containing that error
208/// let results: Vec<Result<i32, &str>> = vec![Ok(1), Err("error"), Ok(3)];
209/// assert_eq!(sequence(results), Err("error"));
210///
211/// // Empty collection gives an empty success collection
212/// let empty_results: Vec<Result<i32, &str>> = vec![];
213/// assert_eq!(sequence(empty_results), Ok(vec![]));
214/// ```
215#[inline]
216pub fn sequence<A, E>(collection: Vec<Result<A, E>>) -> Result<Vec<A>, E> {
217    sequence_result(collection)
218}
219
220/// Specialization of `traverse_result` for standardizing error handling.
221///
222/// This function applies a function that might fail to each element of a collection,
223/// collecting the results if everything succeeds, or returning the first error.
224///
225/// # Type Parameters
226///
227/// * `A`: The input item type
228/// * `B`: The success type in the Result
229/// * `E`: The error type in the Result
230/// * `F`: The transformation function type
231///
232/// # Examples
233///
234/// ```rust
235/// use rustica::utils::error_utils::traverse;
236///
237/// // Define a fallible parsing function with explicit error type
238/// let parse_int = |s: &str| s.parse::<i32>().map_err(|_| "parse error");
239///
240/// // When all inputs are valid, returns a collection of successful results
241/// let inputs: Vec<&str> = vec!["1", "2", "3"];
242/// let result: Result<Vec<i32>, &str> = traverse(inputs, parse_int);
243/// assert_eq!(result, Ok(vec![1, 2, 3]));
244///
245/// // When any input is invalid, returns the first error
246/// let inputs: Vec<&str> = vec!["1", "not_a_number", "3"];
247/// let result: Result<Vec<i32>, &str> = traverse(inputs, parse_int);
248/// assert_eq!(result, Err("parse error"));
249///
250/// // Empty collection gives an empty success collection
251/// let empty_inputs: Vec<&str> = vec![];
252/// let result: Result<Vec<i32>, &str> = traverse(empty_inputs, parse_int);
253/// assert_eq!(result, Ok(vec![]));
254/// ```
255#[inline]
256pub fn traverse<A, B, E, F>(collection: impl IntoIterator<Item = A>, f: F) -> Result<Vec<B>, E>
257where
258    F: FnMut(A) -> Result<B, E>,
259{
260    traverse_result(collection, f)
261}
262
263/// Applies a function that might fail to each element, collecting all errors.
264///
265/// Unlike `traverse`, this continues processing even after encountering errors,
266/// collecting all errors that occur throughout the entire collection.
267///
268/// # Type Parameters
269///
270/// * `A`: The input item type
271/// * `B`: The success type in the Validated
272/// * `E`: The error type in the Validated
273/// * `F`: The transformation function type
274///
275/// # Examples
276///
277/// ```rust
278/// use rustica::utils::error_utils::traverse_validated;
279/// use rustica::datatypes::validated::Validated;
280/// use rustica::traits::identity::Identity;
281///
282/// // Define a fallible parsing function
283/// let parse_int = |s: &str| -> Result<i32, String> {
284///     s.parse::<i32>().map_err(|_| format!("'{}' is not a valid number", s))
285/// };
286///
287/// // Process a collection with multiple errors
288/// let inputs: Vec<&str> = vec!["1", "not_a_number", "3", "also_not_a_number"];
289/// let result: Validated<String, Vec<i32>> = traverse_validated(inputs, parse_int);
290///
291/// // Verify that the result is invalid and contains the expected number of errors
292/// assert!(result.is_invalid());
293/// assert_eq!(result.errors().len(), 2);
294/// assert!(result.errors()[0].contains("not_a_number"));
295/// assert!(result.errors()[1].contains("also_not_a_number"));
296///
297/// // Process a collection with no errors
298/// let valid_inputs: Vec<&str> = vec!["1", "2", "3"];
299/// let valid_result: Validated<String, Vec<i32>> = traverse_validated(valid_inputs, parse_int);
300/// assert!(valid_result.is_valid());
301/// assert_eq!(valid_result.unwrap(), vec![1, 2, 3]);
302///
303pub fn traverse_validated<A, B, E, F>(
304    collection: impl IntoIterator<Item = A>, mut f: F,
305) -> Validated<E, Vec<B>>
306where
307    F: FnMut(A) -> Result<B, E>,
308    E: Clone,
309{
310    let mut values = Vec::new();
311    let mut errors = SmallVec::<[E; 8]>::new();
312    let mut had_error = false;
313
314    for item in collection {
315        match f(item) {
316            Ok(value) => values.push(value),
317            Err(error) => {
318                had_error = true;
319                errors.push(error);
320            },
321        }
322    }
323
324    if had_error {
325        Validated::Invalid(errors)
326    } else {
327        Validated::Valid(values)
328    }
329}
330
331/// Converts a collection of `WithError` values into a Result.
332///
333/// This function generalizes the `sequence` function to work with any type
334/// that implements the `WithError` trait, not just `Result`.
335///
336/// # Type Parameters
337///
338/// * `C`: The container type that implements `WithError`
339/// * `T`: The success type
340/// * `E`: The error type
341///
342/// # Examples
343///
344/// ```rust
345/// use rustica::utils::error_utils::{sequence_with_error, WithError};
346/// use rustica::datatypes::either::Either;
347///
348/// // Create a collection of Either values (all successful)
349/// let results: Vec<Either<&str, i32>> = vec![
350///     Either::right(1),
351///     Either::right(2),
352///     Either::right(3),
353/// ];
354/// assert_eq!(sequence_with_error(results), Ok(vec![1, 2, 3]));
355///
356/// // Create a collection of Either values (with one failure)
357/// let results: Vec<Either<&str, i32>> = vec![
358///     Either::right(1),
359///     Either::left("error"),
360///     Either::right(3),
361/// ];
362/// assert_eq!(sequence_with_error::<Either<&str, i32>, i32, &str>(results), Err("error"));
363/// ```
364#[inline]
365pub fn sequence_with_error<C, T, E>(collection: Vec<C>) -> Result<Vec<T>, E>
366where
367    C: WithError<E>,
368    C::Success: Clone + Into<T>,
369    E: Clone,
370{
371    let mut values = Vec::with_capacity(collection.len());
372
373    for item in collection {
374        match item.to_result() {
375            Ok(value) => values.push(value.into()),
376            Err(error) => return Err(error),
377        }
378    }
379
380    Ok(values)
381}
382
383// ===== Error Type Conversions =====
384
385/// Transforms a Result into an Either.
386///
387/// This is a convenience function for converting between error handling types.
388///
389/// # Type Parameters
390///
391/// * `T`: The success type
392/// * `E`: The error type
393///
394/// # Examples
395///
396/// ```rust
397/// use rustica::utils::error_utils::result_to_either;
398/// use rustica::datatypes::either::Either;
399///
400/// // Convert a successful Result to Either
401/// let ok_result: Result<i32, &str> = Ok(42);
402/// let either_right: Either<&str, i32> = result_to_either(ok_result);
403/// assert_eq!(either_right, Either::right(42));
404///
405/// // Convert an error Result to Either
406/// let err_result: Result<i32, &str> = Err("error");
407/// let either_left: Either<&str, i32> = result_to_either(err_result);
408/// assert_eq!(either_left, Either::left("error"));
409/// ```
410#[inline]
411pub fn result_to_either<T, E>(result: Result<T, E>) -> Either<E, T> {
412    match result {
413        Ok(value) => Either::Right(value),
414        Err(error) => Either::Left(error),
415    }
416}
417
418/// Transforms an Either into a Result.
419///
420/// This is a convenience function for converting between error handling types.
421///
422/// # Type Parameters
423///
424/// * `T`: The success type
425/// * `E`: The error type
426///
427/// # Examples
428///
429/// ```rust
430/// use rustica::utils::error_utils::either_to_result;
431/// use rustica::datatypes::either::Either;
432///
433/// // Convert a right Either to Result
434/// let right: Either<&str, i32> = Either::right(42);
435/// let ok_result: Result<i32, &str> = either_to_result(right);
436/// assert_eq!(ok_result, Ok(42));
437///
438/// // Convert a left Either to Result
439/// let left: Either<&str, i32> = Either::left("error");
440/// let err_result: Result<i32, &str> = either_to_result(left);
441/// assert_eq!(err_result, Err("error"));
442/// ```
443#[inline]
444pub fn either_to_result<T, E>(either: Either<E, T>) -> Result<T, E> {
445    match either {
446        Either::Left(error) => Err(error),
447        Either::Right(value) => Ok(value),
448    }
449}
450
451// ===== Chainable Error Handling =====
452
453/// A chainable error handling extension trait.
454///
455/// This trait adds convenient methods to Result for more ergonomic error
456/// handling patterns. It provides conversions to other error handling
457/// types and additional utility methods.
458///
459/// # Examples
460///
461/// ```rust
462/// use rustica::utils::error_utils::ResultExt;
463/// use rustica::datatypes::either::Either;
464///
465/// // Convert a Result to an Either
466/// let result: Result<i32, &str> = Ok(42);
467/// let either: Either<&str, i32> = result.to_either();
468/// assert_eq!(either, Either::right(42));
469///
470/// // Use unwrap_or_default with a specific error type
471/// let result: Result<String, i32> = Err(404);
472/// let default_value: String = result.unwrap_or_default();
473/// assert_eq!(default_value, String::default());
474///
475/// // Transform both success and error types
476/// let result: Result<i32, &str> = Ok(10);
477/// let transformed: Result<String, usize> = result.bimap(
478///     |v| v.to_string(),      // Success mapper
479///     |e| e.len(),            // Error mapper
480/// );
481/// assert_eq!(transformed, Ok("10".to_string()));
482/// ```
483pub trait ResultExt<T, E> {
484    /// Converts a Result to a Validated.
485    fn to_validated(self) -> Validated<E, T>;
486
487    /// Converts a Result to an Either.
488    fn to_either(self) -> Either<E, T>;
489
490    /// Returns the contained value or the default for type T.
491    fn unwrap_or_default(self) -> T
492    where
493        T: Default;
494
495    /// Maps both the success and error types.
496    fn bimap<F, G, U, E2>(self, success_map: F, error_map: G) -> Result<U, E2>
497    where
498        F: FnOnce(T) -> U,
499        G: FnOnce(E) -> E2;
500}
501
502impl<T, E> ResultExt<T, E> for Result<T, E> {
503    fn to_validated(self) -> Validated<E, T> {
504        use smallvec::smallvec;
505        match self {
506            Ok(value) => Validated::Valid(value),
507            Err(error) => Validated::Invalid(smallvec![error]),
508        }
509    }
510
511    fn to_either(self) -> Either<E, T> {
512        result_to_either(self)
513    }
514
515    fn unwrap_or_default(self) -> T
516    where
517        T: Default,
518    {
519        self.unwrap_or_else(|_| T::default())
520    }
521
522    fn bimap<F, G, U, E2>(self, success_map: F, error_map: G) -> Result<U, E2>
523    where
524        F: FnOnce(T) -> U,
525        G: FnOnce(E) -> E2,
526    {
527        match self {
528            Ok(value) => Ok(success_map(value)),
529            Err(error) => Err(error_map(error)),
530        }
531    }
532}
533
534// ===== Custom Error Types =====
535
536/// A custom error type with optional context.
537///
538/// This is useful for creating specialized error types that carry
539/// additional context. The error has a main message and optional
540/// contextual information to help with debugging.
541///
542/// # Type Parameters
543///
544/// * `M`: The type of the error message
545/// * `C`: The type of the context information
546///
547/// # Examples
548///
549/// ```rust
550/// use rustica::utils::error_utils::{AppError, error_with_context};
551///
552/// // Create an error with context
553/// let error: AppError<&str, &str> = error_with_context("File not found", "trying to open config.json");
554/// assert_eq!(error.message(), &"File not found");
555/// assert_eq!(error.context(), Some(&"trying to open config.json"));
556///
557/// // Get a formatted display of the error
558/// let error_display = format!("{}", error);
559/// assert!(error_display.contains("File not found"));
560/// assert!(error_display.contains("config.json"));
561/// ```
562#[derive(Debug, Clone)]
563pub struct AppError<M, C = ()> {
564    message: M,
565    context: Option<C>,
566}
567
568impl<M: PartialEq, C: PartialEq> PartialEq for AppError<M, C> {
569    fn eq(&self, other: &Self) -> bool {
570        self.message == other.message && self.context == other.context
571    }
572}
573
574impl<M: Eq + PartialEq, C: Eq + PartialEq> Eq for AppError<M, C> {}
575
576impl<M, C> AppError<M, C> {
577    /// Creates a new error with just a message.
578    ///
579    /// # Arguments
580    ///
581    /// * `message`: The error message
582    #[inline]
583    pub fn new(message: M) -> Self {
584        AppError {
585            message,
586            context: None,
587        }
588    }
589
590    /// Creates a new error with a message and context.
591    ///
592    /// # Arguments
593    ///
594    /// * `message`: The error message
595    /// * `context`: The contextual information about the error
596    #[inline]
597    pub fn with_context(message: M, context: C) -> Self {
598        AppError {
599            message,
600            context: Some(context),
601        }
602    }
603
604    /// Returns a reference to the error message.
605    #[inline]
606    pub fn message(&self) -> &M {
607        &self.message
608    }
609
610    /// Returns a reference to the error context, if any.
611    #[inline]
612    pub fn context(&self) -> Option<&C> {
613        self.context.as_ref()
614    }
615
616    /// Maps the error message to a new type.
617    ///
618    /// # Type Parameters
619    ///
620    /// * `F`: The function type
621    /// * `N`: The new message type
622    #[inline]
623    pub fn map<F, N>(self, f: F) -> AppError<N, C>
624    where
625        F: FnOnce(M) -> N,
626    {
627        AppError {
628            message: f(self.message),
629            context: self.context,
630        }
631    }
632
633    /// Maps the error context to a new type.
634    ///
635    /// # Type Parameters
636    ///
637    /// * `F`: The function type
638    /// * `D`: The new context type
639    #[inline]
640    pub fn map_context<F, D>(self, f: F) -> AppError<M, D>
641    where
642        F: FnOnce(Option<C>) -> Option<D>,
643    {
644        AppError {
645            message: self.message,
646            context: f(self.context),
647        }
648    }
649}
650
651impl<M: Debug, C: Debug> std::fmt::Display for AppError<M, C> {
652    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
653        match &self.context {
654            Some(context) => write!(f, "{:?} (Context: {:?})", self.message, context),
655            None => write!(f, "{:?}", self.message),
656        }
657    }
658}
659
660impl<M: Debug + Clone, C: Debug + Clone> std::error::Error for AppError<M, C> {}
661
662/// Creates an error with a message.
663///
664/// This is a convenience function for creating an `AppError`.
665///
666/// # Type Parameters
667///
668/// * `M`: The type of the error message
669///
670/// # Examples
671///
672/// ```rust
673/// use rustica::utils::error_utils::error;
674///
675/// // Create a simple error with just a message
676/// let error = error("File not found");
677/// assert_eq!(*error.message(), "File not found");
678/// assert_eq!(error.context(), None::<&()>);
679///
680/// // The error implements Display and Error traits
681/// let error_display = format!("{}", error);
682/// assert!(error_display.contains("File not found"));
683/// ```
684#[inline]
685pub fn error<M>(message: M) -> AppError<M> {
686    AppError::new(message)
687}
688
689/// Creates an error with a message and context.
690///
691/// This is a convenience function for creating an `AppError` with context.
692///
693/// # Type Parameters
694///
695/// * `M`: The type of the error message
696/// * `C`: The type of the context information
697///
698/// # Examples
699///
700/// ```rust
701/// use rustica::utils::error_utils::error_with_context;
702///
703/// // Create an error with context
704/// let error = error_with_context("File not found", "trying to open config.json");
705/// assert_eq!(error.message(), &"File not found");
706/// assert_eq!(error.context(), Some(&"trying to open config.json"));
707///
708/// // Map the message to a new type
709/// let mapped = error.map(|msg| format!("Error: {}", msg));
710/// assert_eq!(mapped.message(), &"Error: File not found");
711/// ```
712#[inline]
713pub fn error_with_context<M, C>(message: M, context: C) -> AppError<M, C> {
714    AppError::with_context(message, context)
715}
716
717// ===== Private Implementation Functions =====
718
719// Sequence implementation for Result
720#[inline]
721fn sequence_result<A, E>(collection: Vec<Result<A, E>>) -> Result<Vec<A>, E> {
722    let mut values = Vec::with_capacity(collection.len());
723
724    for item in collection {
725        match item {
726            Ok(value) => values.push(value),
727            Err(error) => return Err(error),
728        }
729    }
730
731    Ok(values)
732}
733// Traverse implementation for Result
734#[inline]
735fn traverse_result<A, B, E, F>(
736    collection: impl IntoIterator<Item = A>, mut f: F,
737) -> Result<Vec<B>, E>
738where
739    F: FnMut(A) -> Result<B, E>,
740{
741    let mut values = Vec::new();
742
743    for item in collection {
744        match f(item) {
745            Ok(value) => values.push(value),
746            Err(error) => return Err(error),
747        }
748    }
749
750    Ok(values)
751}