bizerror/lib.rs
1//! # `BizError` - Structured Business Error Handling for Rust
2//!
3//! A lightweight, flexible business error handling library that provides
4//! structured error codes and contextual information while maintaining full
5//! compatibility with Rust's error ecosystem.
6//!
7//! ## 🎯 Design Philosophy
8//!
9//! **90/10 Principle**: 90% of error handling scenarios only need error codes,
10//! while 10% require detailed context information.
11//!
12//! - **Minimal Core**: `BizError` trait contains only essential business error
13//! identification
14//! - **Optional Context**: Use `ContextualError` wrapper only when detailed
15//! context is needed
16//! - **Zero Overhead**: Basic usage scenarios have no additional performance
17//! cost
18//! - **Full Compatibility**: Seamlessly integrates with thiserror and the
19//! entire Rust error ecosystem
20//!
21//! ## 🚀 Basic Usage with Derive Macro
22//!
23//! The simplest way to use `BizError` is with the derive macro:
24//!
25//! ```rust
26//! use bizerror::BizError;
27//!
28//! #[derive(BizError, thiserror::Error)]
29//! pub enum ApiError {
30//! #[bizcode(4001)]
31//! #[error("Invalid input: {field}")]
32//! ValidationError { field: String },
33//!
34//! #[bizcode(8001)]
35//! #[error("Database connection failed")]
36//! DatabaseError(#[from] std::io::Error),
37//!
38//! #[bizcode(8006)]
39//! #[error("Request timeout")]
40//! Timeout,
41//! }
42//!
43//! // Use the error
44//! let error = ApiError::ValidationError { field: "email".to_string() };
45//! assert_eq!(error.code(), 4001);
46//! assert_eq!(error.name(), "ValidationError");
47//! assert_eq!(error.to_string(), "Invalid input: email"); // Uses Display implementation
48//! ```
49//!
50//! ## 🏗️ Automatic Code Assignment
51//!
52//! You can configure automatic code assignment for variants without explicit
53//! codes:
54//!
55//! ```rust
56//! use bizerror::BizError;
57//!
58//! #[derive(BizError, thiserror::Error)]
59//! #[bizconfig(auto_start = 1000, auto_increment = 10)]
60//! pub enum ServiceError {
61//! #[error("Auto-assigned code")]
62//! AutoError1, // code: 1000
63//!
64//! #[bizcode(2001)]
65//! #[error("Explicit code")]
66//! ExplicitError, // code: 2001
67//!
68//! #[error("Another auto-assigned")]
69//! AutoError2, // code: 1010
70//! }
71//! ```
72//!
73//! ## 🔧 Advanced Usage with Context
74//!
75//! For scenarios requiring detailed context information:
76//!
77//! ```rust
78//! use bizerror::*;
79//!
80//! #[derive(BizError, thiserror::Error)]
81//! pub enum ApiError {
82//! #[bizcode(8001)]
83//! #[error("Database connection failed")]
84//! DatabaseError(#[from] std::io::Error),
85//! }
86//!
87//! fn load_user_config() -> Result<String, ContextualError<ApiError>> {
88//! std::fs::read_to_string("config.json")
89//! .with_context("Loading user configuration")
90//! }
91//!
92//! # fn example_usage() {
93//! match load_user_config() {
94//! Ok(config) => println!("Config loaded: {}", config),
95//! Err(e) => {
96//! println!("Error code: {}", e.code());
97//! println!("Context: {}", e.context());
98//! println!("Location: {}", e.location());
99//! }
100//! }
101//! # }
102//! ```
103//!
104//! ## 📊 Custom Code Types
105//!
106//! You can use different types for error codes:
107//!
108//! ```rust
109//! use bizerror::BizError;
110//!
111//! // String codes
112//! #[derive(BizError, thiserror::Error)]
113//! #[bizconfig(code_type = "&'static str")]
114//! pub enum StringError {
115//! #[bizcode("USER_NOT_FOUND")]
116//! #[error("User not found")]
117//! UserNotFound,
118//!
119//! #[error("Auto string code")]
120//! AutoString, // code: "0"
121//! }
122//!
123//! // Signed integer codes
124//! #[derive(BizError, thiserror::Error)]
125//! #[bizconfig(code_type = "i32", auto_start = -100)]
126//! pub enum SignedError {
127//! #[error("Negative code")]
128//! NegativeCode, // code: -100
129//! }
130//! ```
131//!
132//! ## 🎨 Structured Debug Output
133//!
134//! The derive macro automatically generates structured debug output:
135//!
136//! ```rust
137//! # use bizerror::BizError;
138//! # #[derive(BizError, thiserror::Error)]
139//! # pub enum ApiError {
140//! # #[bizcode(4001)]
141//! # #[error("Invalid input: {field}")]
142//! # ValidationError { field: String },
143//! # }
144//! let error = ApiError::ValidationError { field: "email".to_string() };
145//! println!("{:?}", error);
146//! // Output: ApiError { variant: "ValidationError", code: 4001, message: "Invalid input: email" }
147//! ```
148//!
149//! ## 🔗 Error Chains and Context
150//!
151//! Build comprehensive error chains with context:
152//!
153//! ```rust
154//! use bizerror::*;
155//!
156//! #[derive(BizError, thiserror::Error)]
157//! pub enum ServiceError {
158//! #[bizcode(8001)]
159//! #[error("Database error: {0}")]
160//! DatabaseError(#[from] std::io::Error),
161//! }
162//!
163//! fn complex_operation() -> Result<String, ContextualError<ServiceError>> {
164//! // Multiple layers of context
165//! std::fs::read_to_string("data.json")
166//! .with_context("Loading configuration")
167//! .and_then(|_| {
168//! std::fs::read_to_string("user.json")
169//! .with_context("Loading user data")
170//! })
171//! }
172//! ```
173//!
174//! ## 🏆 Best Practices
175//!
176//! 1. **Use meaningful error codes**: Group related errors by code ranges
177//! - 1000-1999: Validation errors
178//! - 2000-2999: Authentication errors
179//! - 8000-8999: System errors
180//!
181//! 2. **Leverage automatic assignment**: Use `bizconfig` for consistent code
182//! spacing
183//!
184//! 3. **Add context sparingly**: Only use `ContextualError` when you need
185//! detailed debugging
186//!
187//! 4. **Chain errors properly**: Use `#[from]` for automatic conversions
188//!
189//! 5. **Document error codes**: Include code meanings in your API documentation
190
191use core::panic::Location;
192use std::{
193 borrow::Cow,
194 error::Error,
195};
196
197// Re-export the BizError derive macro
198pub use bizerror_impl::BizError;
199
200/// Core business error trait
201///
202/// This trait provides the essential functionality for business error
203/// identification:
204/// - `code()`: Returns a unique business error code
205/// - `name()`: Returns the error type name (typically the enum variant name)
206///
207/// For error messages, use the standard `Display` trait implementation.
208///
209/// ## Example Implementation
210///
211/// ```rust
212/// use std::error::Error;
213///
214/// use bizerror::BizError;
215///
216/// #[derive(Debug)]
217/// pub struct CustomError {
218/// code: u32,
219/// message: String,
220/// }
221///
222/// impl std::fmt::Display for CustomError {
223/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224/// write!(f, "{}", self.message)
225/// }
226/// }
227///
228/// impl Error for CustomError {}
229///
230/// impl BizError for CustomError {
231/// type CodeType = u32;
232///
233/// fn code(&self) -> Self::CodeType {
234/// self.code
235/// }
236///
237/// fn name(&self) -> &str {
238/// "CustomError"
239/// }
240/// }
241/// ```
242pub trait BizError: Error + Send + Sync + 'static {
243 /// The type of the error code
244 ///
245 /// Can be any type that implements `Copy + Display + Debug + Send + Sync +
246 /// 'static`. Common choices include:
247 /// - `u32` or `u16` for numeric codes
248 /// - `&'static str` for string codes
249 /// - `i32` for signed numeric codes
250 type CodeType: Copy
251 + std::fmt::Display
252 + std::fmt::Debug
253 + Send
254 + Sync
255 + std::hash::Hash
256 + PartialEq
257 + Eq
258 + 'static;
259
260 /// Get the business error code
261 ///
262 /// This should return a unique identifier for this specific error type.
263 /// The code should be consistent across different instances of the same
264 /// error variant.
265 fn code(&self) -> Self::CodeType;
266
267 /// Get the error type name
268 ///
269 /// This typically returns the enum variant name for derived
270 /// implementations. For custom implementations, this should return a
271 /// consistent, descriptive name.
272 fn name(&self) -> &str;
273}
274
275/// Contextual error wrapper (only used when detailed context is needed)
276///
277/// This wrapper allows you to add context information and automatic location
278/// tracking to any `BizError` without changing the original error type.
279///
280/// ## When to Use
281///
282/// Use `ContextualError` when you need:
283/// - Detailed debugging information
284/// - Location tracking for where the error occurred
285/// - Additional context about the operation that failed
286/// - Multiple layers of context in error chains
287///
288/// ## Example
289///
290/// ```rust
291/// use bizerror::*;
292///
293/// #[derive(BizError, thiserror::Error)]
294/// pub enum ServiceError {
295/// #[bizcode(8001)]
296/// #[error("Database connection failed")]
297/// DatabaseError(#[from] std::io::Error),
298/// }
299///
300/// fn load_config() -> Result<String, ContextualError<ServiceError>> {
301/// std::fs::read_to_string("config.json")
302/// .with_context("Loading application configuration")
303/// }
304/// ```
305pub struct ContextualError<E: BizError> {
306 error: E,
307 context: Cow<'static, str>, // Avoids allocation for static strings,
308 location: &'static Location<'static>,
309}
310
311impl<E: BizError> ContextualError<E> {
312 /// Create a new contextual error with automatic location tracking
313 ///
314 /// The location is automatically captured using `#[track_caller]`,
315 /// providing precise information about where the error context was added.
316 #[track_caller]
317 pub fn new(error: E, context: impl Into<String>) -> Self {
318 Self {
319 error,
320 context: Cow::Owned(context.into()),
321 location: Location::caller(),
322 }
323 }
324
325 /// Get the original error
326 ///
327 /// This provides access to the underlying `BizError` instance.
328 pub const fn inner(&self) -> &E {
329 &self.error
330 }
331
332 /// Get the context
333 ///
334 /// Returns the contextual information that was added to this error.
335 pub fn context(&self) -> &str {
336 &self.context
337 }
338
339 /// Get the location
340 ///
341 /// Returns the location where the context was added to this error.
342 pub const fn location(&self) -> &'static Location<'static> {
343 self.location
344 }
345
346 /// Add additional context to the existing context
347 ///
348 /// This method appends new context information to the existing context,
349 /// creating a layered context description.
350 ///
351 /// # Example
352 ///
353 /// ```rust
354 /// use bizerror::*;
355 ///
356 /// #[derive(BizError, thiserror::Error)]
357 /// pub enum MyError {
358 /// #[bizcode(8001)]
359 /// #[error("IO error")]
360 /// IoError,
361 /// }
362 ///
363 /// let error = MyError::IoError;
364 /// let contextual = error.with_context("Loading file");
365 /// let layered = contextual.add_context("During startup");
366 /// assert_eq!(layered.context(), "Loading file -> During startup");
367 /// ```
368 #[track_caller]
369 #[must_use]
370 pub fn add_context(self, additional: impl Into<String>) -> Self {
371 let new_context = format!("{} -> {}", self.context, additional.into());
372 Self {
373 error: self.error,
374 context: Cow::Owned(new_context),
375 location: Location::caller(),
376 }
377 }
378
379 /// Unwrap the contextual error, returning the inner error
380 ///
381 /// This method consumes the `ContextualError` and returns the underlying
382 /// business error, discarding the context information.
383 ///
384 /// # Example
385 ///
386 /// ```rust
387 /// use bizerror::*;
388 ///
389 /// #[derive(BizError, thiserror::Error)]
390 /// pub enum MyError {
391 /// #[bizcode(8001)]
392 /// #[error("IO error")]
393 /// IoError,
394 /// }
395 ///
396 /// let error = MyError::IoError;
397 /// let contextual = error.with_context("Some context");
398 /// let original = contextual.into_inner();
399 /// // original is now MyError::IoError again
400 /// ```
401 pub fn into_inner(self) -> E {
402 self.error
403 }
404
405 /// Find the first error in the chain of a specific type
406 ///
407 /// This method traverses the error chain and returns the first error
408 /// of the specified type. Useful for extracting specific error types
409 /// from a complex error chain.
410 ///
411 /// # Example
412 ///
413 /// ```rust
414 /// use std::io;
415 ///
416 /// use bizerror::*;
417 ///
418 /// #[derive(BizError, thiserror::Error)]
419 /// pub enum MyError {
420 /// #[bizcode(8001)]
421 /// #[error("IO error: {0}")]
422 /// IoError(#[from] io::Error),
423 /// }
424 ///
425 /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
426 /// let my_error = MyError::IoError(io_error);
427 /// let contextual = my_error.with_context("Loading config");
428 ///
429 /// // Find the original io::Error in the chain
430 /// let found_io_error = contextual.find_root::<io::Error>();
431 /// assert!(found_io_error.is_some());
432 /// ```
433 pub fn find_root<T>(&self) -> Option<&T>
434 where
435 T: Error + 'static,
436 {
437 let mut current: &dyn Error = self;
438 while let Some(source) = current.source() {
439 if let Some(target) = source.downcast_ref::<T>() {
440 return Some(target);
441 }
442 current = source;
443 }
444 None
445 }
446
447 /// Count the depth of the error chain
448 ///
449 /// Returns the number of errors in the chain, including this error.
450 /// Useful for understanding the complexity of error propagation.
451 ///
452 /// # Example
453 ///
454 /// ```rust
455 /// use bizerror::*;
456 /// use std::io;
457 ///
458 /// #[derive(BizError, thiserror::Error)]
459 /// pub enum MyError {
460 /// #[bizcode(8001)]
461 /// #[error("IO error: {0}")]
462 /// IoError(#[from] io::Error),
463 /// }
464 ///
465 /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
466 /// let my_error = MyError::IoError(io_error);
467 /// let contextual = my_error.with_context("Loading config");
468 ///
469 /// assert_eq!(contextual.chain_depth(), 3); // ContextualError -> MyError -> io::Error
470 /// ```
471 pub fn chain_depth(&self) -> usize {
472 let mut depth = 1;
473 let mut current: &dyn Error = self;
474 while let Some(source) = current.source() {
475 depth += 1;
476 current = source;
477 }
478 depth
479 }
480
481 /// Get the root cause message of the error chain
482 ///
483 /// Returns the deepest error message in the chain, which is typically the
484 /// original cause of the error.
485 ///
486 /// # Example
487 ///
488 /// ```rust
489 /// use std::io;
490 ///
491 /// use bizerror::*;
492 ///
493 /// #[derive(BizError, thiserror::Error)]
494 /// pub enum MyError {
495 /// #[bizcode(8001)]
496 /// #[error("IO error: {0}")]
497 /// IoError(#[from] io::Error),
498 /// }
499 ///
500 /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
501 /// let my_error = MyError::IoError(io_error);
502 /// let contextual = my_error.with_context("Loading config");
503 ///
504 /// let root_cause = contextual.root_cause_message();
505 /// assert_eq!(root_cause, "file not found");
506 /// ```
507 pub fn root_cause_message(&self) -> String {
508 let mut current: &dyn Error = self;
509 while let Some(source) = current.source() {
510 current = source;
511 }
512 current.to_string()
513 }
514
515 /// Collect all error messages in the chain
516 ///
517 /// Returns a vector of all error messages in the chain, from the current
518 /// error to the root cause. Useful for comprehensive error reporting.
519 ///
520 /// # Example
521 ///
522 /// ```rust
523 /// use std::io;
524 ///
525 /// use bizerror::*;
526 ///
527 /// #[derive(BizError, thiserror::Error)]
528 /// pub enum MyError {
529 /// #[bizcode(8001)]
530 /// #[error("IO error: {0}")]
531 /// IoError(#[from] io::Error),
532 /// }
533 ///
534 /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
535 /// let my_error = MyError::IoError(io_error);
536 /// let contextual = my_error.with_context("Loading config");
537 ///
538 /// let chain = contextual.error_chain_messages();
539 /// assert_eq!(chain.len(), 3);
540 /// ```
541 pub fn error_chain_messages(&self) -> Vec<String> {
542 let mut chain = vec![self.to_string()];
543 let mut current = self.source();
544 while let Some(source) = current {
545 chain.push(source.to_string());
546 current = source.source();
547 }
548 chain
549 }
550
551 /// Check if the error chain contains a specific error type
552 ///
553 /// Returns true if any error in the chain is of the specified type.
554 /// Useful for conditional error handling.
555 ///
556 /// # Example
557 ///
558 /// ```rust
559 /// use std::io;
560 ///
561 /// use bizerror::*;
562 ///
563 /// #[derive(BizError, thiserror::Error)]
564 /// pub enum MyError {
565 /// #[bizcode(8001)]
566 /// #[error("IO error: {0}")]
567 /// IoError(#[from] io::Error),
568 /// }
569 ///
570 /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
571 /// let my_error = MyError::IoError(io_error);
572 /// let contextual = my_error.with_context("Loading config");
573 ///
574 /// assert!(contextual.contains_error::<io::Error>());
575 /// assert!(!contextual.contains_error::<std::fmt::Error>());
576 /// ```
577 pub fn contains_error<T>(&self) -> bool
578 where
579 T: Error + 'static,
580 {
581 self.find_root::<T>().is_some()
582 }
583
584 /// Check if the error chain contains a specific business error code
585 ///
586 /// Returns true if any `BizError` in the chain has the specified code.
587 /// Useful for conditional error handling based on business error codes.
588 ///
589 /// # Example
590 ///
591 /// ```rust
592 /// use bizerror::*;
593 ///
594 /// #[derive(BizError, thiserror::Error)]
595 /// pub enum MyError {
596 /// #[bizcode(8001)]
597 /// #[error("IO error")]
598 /// IoError,
599 ///
600 /// #[bizcode(8002)]
601 /// #[error("Network error")]
602 /// NetworkError,
603 /// }
604 ///
605 /// let error = MyError::IoError;
606 /// let contextual = error.with_context("Operation failed");
607 ///
608 /// assert!(contextual.chain_contains_code(8001));
609 /// assert!(!contextual.chain_contains_code(8002));
610 /// ```
611 pub fn chain_contains_code<C>(&self, code: C) -> bool
612 where
613 C: PartialEq<E::CodeType> + Copy,
614 {
615 let mut current: &dyn Error = self;
616 loop {
617 if let Some(biz_error) = current.downcast_ref::<E>() &&
618 code == biz_error.code()
619 {
620 return true;
621 }
622 if let Some(contextual) = current.downcast_ref::<Self>() &&
623 code == contextual.error.code()
624 {
625 return true;
626 }
627 if let Some(source) = current.source() {
628 current = source;
629 } else {
630 break;
631 }
632 }
633 false
634 }
635}
636
637impl<E: BizError> std::fmt::Debug for ContextualError<E> {
638 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
639 f.debug_struct("ContextualError")
640 .field("type", &self.error.name())
641 .field("code", &self.error.code())
642 .field("message", &self.error.to_string())
643 .field("context", &self.context.as_ref())
644 .field(
645 "location",
646 &format!(
647 "{}:{}:{}",
648 self.location.file(),
649 self.location.line(),
650 self.location.column()
651 ),
652 )
653 .finish()
654 }
655}
656
657impl<E: BizError> std::fmt::Display for ContextualError<E> {
658 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659 write!(f, "{}\nContext: {}", self.error, self.context)
660 }
661}
662
663impl<E: BizError> Error for ContextualError<E> {
664 fn source(&self) -> Option<&(dyn Error + 'static)> {
665 Some(&self.error)
666 }
667}
668
669impl<E: BizError> BizError for ContextualError<E> {
670 type CodeType = E::CodeType;
671
672 fn code(&self) -> Self::CodeType {
673 self.error.code()
674 }
675
676 fn name(&self) -> &str {
677 self.error.name()
678 }
679}
680
681/// Result extension trait (simplified)
682///
683/// Provides convenient methods to add business context to any Result.
684/// This trait is automatically implemented for all `Result<T, E>` types
685/// where `E` implements `Error`.
686///
687/// ## Core Methods
688///
689/// - `with_context()` - Add context and convert to `ContextualError`
690/// - `map_biz()` - Simple error type conversion
691/// - `with_context_if()` - Conditional context addition
692///
693/// ## Example
694///
695/// ```rust
696/// use bizerror::*;
697///
698/// #[derive(BizError, thiserror::Error)]
699/// pub enum MyError {
700/// #[bizcode(8001)]
701/// #[error("IO error: {0}")]
702/// IoError(#[from] std::io::Error),
703/// }
704///
705/// fn read_file() -> Result<String, ContextualError<MyError>> {
706/// std::fs::read_to_string("important.txt")
707/// .with_context("Reading critical configuration file")
708/// }
709/// ```
710pub trait ResultExt<T, E> {
711 /// Add contextual information and convert to `ContextualError`
712 ///
713 /// This method allows you to add context to any `Result` that contains
714 /// an error that can be converted to your business error type.
715 ///
716 /// The context is captured with automatic location tracking.
717 fn with_context<B>(
718 self,
719 context: impl Into<String>,
720 ) -> Result<T, ContextualError<B>>
721 where
722 B: BizError + From<E>;
723
724 /// Convert error type without adding context
725 ///
726 /// This is a convenience method that converts the error type to a business
727 /// error. It's equivalent to `.map_err(B::from)`.
728 fn map_biz<B>(self) -> Result<T, B>
729 where
730 B: BizError + From<E>;
731
732 /// Add context conditionally
733 ///
734 /// This method adds context only when the condition is true.
735 /// If the condition is false, it still converts the error type but without
736 /// context.
737 fn with_context_if<B>(
738 self,
739 condition: bool,
740 context: impl Into<String>,
741 ) -> Result<T, ContextualError<B>>
742 where
743 B: BizError + From<E>;
744
745 /// Chain operations with error conversion
746 ///
747 /// This method allows you to chain operations while converting errors
748 /// to business error types. It's a convenience method that combines
749 /// `and_then` with automatic error type conversion.
750 ///
751 /// # Example
752 ///
753 /// ```rust
754 /// use bizerror::*;
755 ///
756 /// #[derive(BizError, thiserror::Error)]
757 /// pub enum MyError {
758 /// #[bizcode(8001)]
759 /// #[error("IO error: {0}")]
760 /// IoError(#[from] std::io::Error),
761 /// }
762 ///
763 /// let initial_err: Result<u32, std::io::Error> = Err(std::io::Error::new(
764 /// std::io::ErrorKind::BrokenPipe,
765 /// "pipe broken",
766 /// ));
767 /// let chained_err: Result<String, MyError> =
768 /// initial_err.and_then_biz(|val| Ok(format!("Value is {val}")));
769 /// assert!(chained_err.is_err()); // true
770 /// ```
771 fn and_then_biz<U, F, B>(self, f: F) -> Result<U, B>
772 where
773 F: FnOnce(T) -> Result<U, B>,
774 B: BizError + From<E>;
775}
776
777impl<T, E: Error + 'static> ResultExt<T, E> for Result<T, E> {
778 #[track_caller]
779 fn with_context<B>(
780 self,
781 context: impl Into<String>,
782 ) -> Result<T, ContextualError<B>>
783 where
784 B: BizError + From<E>,
785 {
786 self.map_err(|e| ContextualError::new(B::from(e), context))
787 }
788
789 fn map_biz<B>(self) -> Result<T, B>
790 where
791 B: BizError + From<E>,
792 {
793 self.map_err(|e| B::from(e))
794 }
795
796 fn with_context_if<B>(
797 self,
798 condition: bool,
799 context: impl Into<String>,
800 ) -> Result<T, ContextualError<B>>
801 where
802 B: BizError + From<E>,
803 {
804 if condition {
805 self.with_context(context)
806 } else {
807 self.map_err(|e| {
808 ContextualError::new(B::from(e), "no context".to_string())
809 })
810 }
811 }
812
813 fn and_then_biz<U, F, B>(self, f: F) -> Result<U, B>
814 where
815 F: FnOnce(T) -> Result<U, B>,
816 B: BizError + From<E>,
817 {
818 match self {
819 Ok(t) => f(t),
820 Err(e) => Err(B::from(e)),
821 }
822 }
823}
824
825/// `BizError` extension trait
826///
827/// Provides convenient methods for adding context to business errors.
828/// This trait is automatically implemented for all types that implement
829/// `BizError`.
830///
831/// ## Example
832///
833/// ```rust
834/// use bizerror::*;
835///
836/// #[derive(BizError, thiserror::Error)]
837/// pub enum ApiError {
838/// #[bizcode(4001)]
839/// #[error("Validation failed")]
840/// ValidationError,
841/// }
842///
843/// let error = ApiError::ValidationError;
844/// let contextual = error.with_context("Processing user registration");
845/// ```
846pub trait BizErrorExt: BizError + Sized {
847 /// Add context with automatic location tracking
848 ///
849 /// This method wraps the error in a `ContextualError` with the provided
850 /// context and automatic location tracking.
851 #[track_caller]
852 fn with_context(self, context: impl Into<String>) -> ContextualError<Self> {
853 ContextualError::new(self, context)
854 }
855}
856
857impl<T: BizError> BizErrorExt for T {}
858
859/// Option extension trait
860///
861/// Provides convenient methods to convert `Option` to `Result` with business
862/// errors. This trait is automatically implemented for all `Option<T>` types.
863///
864/// ## Example
865///
866/// ```rust
867/// use bizerror::*;
868///
869/// #[derive(BizError, thiserror::Error)]
870/// pub enum MyError {
871/// #[bizcode(4001)]
872/// #[error("Value not found")]
873/// NotFound,
874/// }
875///
876/// let value: Option<String> = None;
877/// let result = value.ok_or_biz(MyError::NotFound);
878/// assert!(result.is_err());
879/// ```
880pub trait OptionExt<T> {
881 /// Convert `Option<T>` to `Result<T, B>` with a business error
882 ///
883 /// This is a convenience method that converts `None` to an error
884 /// of the specified business error type.
885 ///
886 /// # Example
887 ///
888 /// ```rust
889 /// use bizerror::*;
890 ///
891 /// #[derive(BizError, thiserror::Error)]
892 /// pub enum MyError {
893 /// #[bizcode(4001)]
894 /// #[error("User not found")]
895 /// UserNotFound,
896 /// }
897 ///
898 /// fn find_user(id: u32) -> Option<String> {
899 /// None // simulate not found
900 /// }
901 ///
902 /// let result = find_user(123).ok_or_biz(MyError::UserNotFound);
903 /// assert!(result.is_err());
904 /// assert_eq!(result.unwrap_err().code(), 4001);
905 /// ```
906 fn ok_or_biz<B>(self, error: B) -> Result<T, B>
907 where
908 B: BizError;
909}
910
911impl<T> OptionExt<T> for Option<T> {
912 fn ok_or_biz<B>(self, error: B) -> Result<T, B>
913 where
914 B: BizError,
915 {
916 self.ok_or(error)
917 }
918}
919
920/// Business errors collection for aggregating multiple errors
921///
922/// This type is useful for scenarios where you need to collect all errors
923/// instead of failing on the first one, such as form validation or batch
924/// processing.
925///
926/// ## Example
927///
928/// ```rust
929/// use bizerror::*;
930///
931/// #[derive(BizError, thiserror::Error)]
932/// pub enum ValidationError {
933/// #[bizcode(4001)]
934/// #[error("Invalid email: {email}")]
935/// InvalidEmail { email: String },
936///
937/// #[bizcode(4002)]
938/// #[error("Password too short")]
939/// PasswordTooShort,
940/// }
941///
942/// fn validate_user(
943/// email: &str,
944/// password: &str,
945/// ) -> Result<(), BizErrors<ValidationError>> {
946/// let mut errors = BizErrors::new();
947///
948/// if !email.contains('@') {
949/// errors.push_simple(ValidationError::InvalidEmail {
950/// email: email.to_string(),
951/// });
952/// }
953///
954/// if password.len() < 8 {
955/// errors.push_simple(ValidationError::PasswordTooShort);
956/// }
957///
958/// if errors.is_empty() {
959/// Ok(())
960/// } else {
961/// Err(errors)
962/// }
963/// }
964/// ```
965pub struct BizErrors<E: BizError> {
966 errors: Vec<ContextualError<E>>,
967}
968
969impl<E: BizError> BizErrors<E> {
970 /// Create a new empty error collection
971 pub const fn new() -> Self {
972 Self { errors: Vec::new() }
973 }
974
975 /// Create a new error collection with the given capacity
976 pub fn with_capacity(capacity: usize) -> Self {
977 Self {
978 errors: Vec::with_capacity(capacity),
979 }
980 }
981
982 /// Add a contextual error to the collection
983 pub fn push(&mut self, error: ContextualError<E>) {
984 self.errors.push(error);
985 }
986
987 /// Add a simple business error to the collection
988 ///
989 /// The error will be wrapped in a `ContextualError` with minimal context.
990 #[track_caller]
991 pub fn push_simple(&mut self, error: E) {
992 self.errors.push(ContextualError::new(error, ""));
993 }
994
995 /// Add a business error with context to the collection
996 #[track_caller]
997 pub fn push_with_context(&mut self, error: E, context: impl Into<String>) {
998 self.errors.push(ContextualError::new(error, context));
999 }
1000
1001 /// Get the number of errors in the collection
1002 pub const fn len(&self) -> usize {
1003 self.errors.len()
1004 }
1005
1006 /// Check if the error collection is empty
1007 pub const fn is_empty(&self) -> bool {
1008 self.errors.is_empty()
1009 }
1010
1011 /// Get an iterator over the errors
1012 pub fn iter(&self) -> impl Iterator<Item = &ContextualError<E>> {
1013 self.errors.iter()
1014 }
1015
1016 /// Get a reference to the errors vector
1017 pub fn as_slice(&self) -> &[ContextualError<E>] {
1018 &self.errors
1019 }
1020
1021 /// Convert into the underlying errors vector
1022 pub fn into_vec(self) -> Vec<ContextualError<E>> {
1023 self.errors
1024 }
1025
1026 /// Get the first error in the collection
1027 pub fn first(&self) -> Option<&ContextualError<E>> {
1028 self.errors.first()
1029 }
1030
1031 /// Get the last error in the collection
1032 pub fn last(&self) -> Option<&ContextualError<E>> {
1033 self.errors.last()
1034 }
1035
1036 /// Collect successful results and errors from an iterator
1037 ///
1038 /// Returns a tuple containing all successful values and optionally
1039 /// the collected errors (if any occurred).
1040 ///
1041 /// # Example
1042 ///
1043 /// ```rust
1044 /// use bizerror::*;
1045 ///
1046 /// #[derive(BizError, thiserror::Error)]
1047 /// pub enum ProcessError {
1048 /// #[bizcode(5001)]
1049 /// #[error("Invalid value: {value}")]
1050 /// InvalidValue { value: i32 },
1051 /// }
1052 ///
1053 /// let results: Vec<Result<i32, ContextualError<ProcessError>>> = vec![
1054 /// Ok(1),
1055 /// Ok(2),
1056 /// Err(ProcessError::InvalidValue { value: 3 }
1057 /// .with_context("Processing item 3")),
1058 /// Ok(4),
1059 /// Err(ProcessError::InvalidValue { value: 5 }
1060 /// .with_context("Processing item 5")),
1061 /// ];
1062 ///
1063 /// let (successes, errors) = BizErrors::collect_from(results.into_iter());
1064 /// assert_eq!(successes, vec![1, 2, 4]);
1065 /// assert!(errors.is_some());
1066 /// assert_eq!(errors.unwrap().len(), 2);
1067 /// ```
1068 pub fn collect_from<T, I>(iter: I) -> (Vec<T>, Option<Self>)
1069 where
1070 I: Iterator<Item = Result<T, ContextualError<E>>>,
1071 {
1072 let mut successes = Vec::new();
1073 let mut errors = Self::new();
1074
1075 for result in iter {
1076 match result {
1077 Ok(value) => successes.push(value),
1078 Err(error) => errors.push(error),
1079 }
1080 }
1081
1082 let errors = if errors.is_empty() {
1083 None
1084 } else {
1085 Some(errors)
1086 };
1087
1088 (successes, errors)
1089 }
1090
1091 /// Collect all errors from an iterator of Results
1092 ///
1093 /// Returns `None` if no errors occurred, or `Some(BizErrors)` with all
1094 /// errors.
1095 ///
1096 /// # Example
1097 ///
1098 /// ```rust
1099 /// use bizerror::*;
1100 ///
1101 /// #[derive(BizError, thiserror::Error)]
1102 /// pub enum ValidationError {
1103 /// #[bizcode(4001)]
1104 /// #[error("Invalid field")]
1105 /// InvalidField,
1106 /// }
1107 ///
1108 /// let results: Vec<Result<(), ContextualError<ValidationError>>> = vec![
1109 /// Ok(()),
1110 /// Err(ValidationError::InvalidField.with_context("Field 1")),
1111 /// Err(ValidationError::InvalidField.with_context("Field 2")),
1112 /// ];
1113 ///
1114 /// let errors = BizErrors::collect_errors(results.into_iter());
1115 /// assert!(errors.is_some());
1116 /// assert_eq!(errors.unwrap().len(), 2);
1117 /// ```
1118 pub fn collect_errors<T, I>(iter: I) -> Option<Self>
1119 where
1120 I: Iterator<Item = Result<T, ContextualError<E>>>,
1121 {
1122 let mut errors = Self::new();
1123
1124 for result in iter {
1125 if let Err(error) = result {
1126 errors.push(error);
1127 }
1128 }
1129
1130 if errors.is_empty() {
1131 None
1132 } else {
1133 Some(errors)
1134 }
1135 }
1136
1137 /// Check if any error in the collection has the specified code
1138 pub fn contains_code<C>(&self, code: C) -> bool
1139 where
1140 C: PartialEq<E::CodeType> + Copy,
1141 {
1142 self.errors.iter().any(|error| code == error.code())
1143 }
1144
1145 /// Get all unique error codes in the collection
1146 pub fn error_codes(&self) -> Vec<E::CodeType> {
1147 let mut codes: Vec<E::CodeType> =
1148 self.errors.iter().map(BizError::code).collect();
1149 codes.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1150 codes.dedup();
1151 codes
1152 }
1153
1154 /// Filter errors by a predicate
1155 ///
1156 /// Returns an iterator over the errors that satisfy the given predicate.
1157 ///
1158 /// # Example
1159 ///
1160 /// ```rust
1161 /// use bizerror::*;
1162 ///
1163 /// #[derive(BizError, thiserror::Error)]
1164 /// pub enum MyError {
1165 /// #[bizcode(4001)]
1166 /// #[error("Validation error")]
1167 /// ValidationError,
1168 ///
1169 /// #[bizcode(8001)]
1170 /// #[error("System error")]
1171 /// SystemError,
1172 /// }
1173 ///
1174 /// let mut errors = BizErrors::new();
1175 /// errors.push_simple(MyError::ValidationError);
1176 /// errors.push_simple(MyError::SystemError);
1177 ///
1178 /// // Filter only validation errors (4xxx codes)
1179 /// let validation_errors: Vec<_> = errors
1180 /// .filter(|e| e.code() >= 4000 && e.code() < 5000)
1181 /// .collect();
1182 /// assert_eq!(validation_errors.len(), 1);
1183 /// ```
1184 pub fn filter<F>(
1185 &self,
1186 predicate: F,
1187 ) -> impl Iterator<Item = &ContextualError<E>>
1188 where
1189 F: Fn(&ContextualError<E>) -> bool,
1190 {
1191 self.errors.iter().filter(move |e| predicate(*e))
1192 }
1193}
1194
1195impl<E: BizError> Default for BizErrors<E> {
1196 fn default() -> Self {
1197 Self::new()
1198 }
1199}
1200
1201impl<E: BizError> IntoIterator for BizErrors<E> {
1202 type Item = ContextualError<E>;
1203 type IntoIter = std::vec::IntoIter<Self::Item>;
1204 fn into_iter(self) -> Self::IntoIter {
1205 self.errors.into_iter()
1206 }
1207}
1208
1209impl<E: BizError> std::fmt::Debug for BizErrors<E> {
1210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1211 if self.errors.is_empty() {
1212 f.debug_struct("BizErrors").field("count", &0).finish()
1213 } else if self.errors.len() == 1 {
1214 f.debug_struct("BizErrors")
1215 .field("count", &1)
1216 .field("error", &self.errors[0])
1217 .finish()
1218 } else {
1219 let mut debug_struct = f.debug_struct("BizErrors");
1220 debug_struct.field("count", &self.errors.len());
1221
1222 let codes: Vec<_> =
1223 self.errors.iter().map(BizError::code).collect();
1224 debug_struct.field("codes", &codes);
1225
1226 // Show first few errors for detailed view
1227 if self.errors.len() <= 3 {
1228 debug_struct.field("errors", &self.errors);
1229 } else {
1230 debug_struct.field("first_3_errors", &&self.errors[0..3]);
1231 debug_struct.field(
1232 "note",
1233 &format!("... and {} more", self.errors.len() - 3),
1234 );
1235 }
1236
1237 debug_struct.finish()
1238 }
1239 }
1240}
1241
1242impl<E: BizError> std::fmt::Display for BizErrors<E> {
1243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1244 if self.errors.is_empty() {
1245 write!(f, "No errors")
1246 } else if self.errors.len() == 1 {
1247 write!(f, "{}", self.errors[0])
1248 } else {
1249 writeln!(
1250 f,
1251 "Multiple errors occurred ({} total):",
1252 self.errors.len()
1253 )?;
1254 for (i, error) in self.errors.iter().enumerate() {
1255 writeln!(f, " {}. {}", i + 1, error)?;
1256 }
1257 Ok(())
1258 }
1259 }
1260}
1261
1262impl<E: BizError> Error for BizErrors<E> {
1263 fn source(&self) -> Option<&(dyn Error + 'static)> {
1264 // Return the first error as the source
1265 self.errors.first().map(|e| e as &dyn Error)
1266 }
1267}
1268
1269impl<E: BizError> BizError for BizErrors<E> {
1270 type CodeType = E::CodeType;
1271
1272 fn code(&self) -> Self::CodeType {
1273 // Return the code of the first error
1274 self.errors
1275 .first()
1276 .map_or_else(|| panic!("BizErrors is empty"), BizError::code)
1277 }
1278
1279 fn name(&self) -> &'static str {
1280 "BizErrors"
1281 }
1282}
1283
1284impl<'a, E: BizError> IntoIterator for &'a BizErrors<E> {
1285 type Item = &'a ContextualError<E>;
1286 type IntoIter = std::slice::Iter<'a, ContextualError<E>>;
1287
1288 fn into_iter(self) -> Self::IntoIter {
1289 self.errors.iter()
1290 }
1291}
1292
1293// Allow collecting Results into BizErrors
1294impl<E: BizError> FromIterator<ContextualError<E>> for BizErrors<E> {
1295 fn from_iter<T: IntoIterator<Item = ContextualError<E>>>(iter: T) -> Self {
1296 Self {
1297 errors: iter.into_iter().collect(),
1298 }
1299 }
1300}
1301
1302impl<E: BizError> FromIterator<E> for BizErrors<E> {
1303 #[track_caller]
1304 fn from_iter<T: IntoIterator<Item = E>>(iter: T) -> Self {
1305 Self {
1306 errors: iter
1307 .into_iter()
1308 .map(|e| ContextualError::new(e, ""))
1309 .collect(),
1310 }
1311 }
1312}