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}