5 unstable releases
new 0.4.0 | Nov 27, 2024 |
---|---|
0.3.7 | Oct 5, 2024 |
0.3.6 | Jun 21, 2024 |
0.3.5 | Jun 21, 2024 |
0.1.0 | May 5, 2024 |
#20 in #shorthand
26 downloads per month
Used in 3 crates
68KB
1K
SLoC
Error Tree
error-tree
is a Rust procedural macro crate designed to simplify the creation and management of complex error hierarchies in Rust applications. It allows you to define nested error enums in a straightforward and declarative manner, automatically generating From
implementations and other boilerplate code to facilitate error handling across multiple layers of your application.
Features
- Simplified Error Definitions: Define multiple error enums with nested relationships in a concise syntax.
- Automatic
From
Implementations: Automatically generatesFrom
implementations for error conversions between different error types, even across multiple layers. - Customizable
Display
Implementations: Use the#[display("...")]
attribute to define customDisplay
messages for your error variants. - Custom
PartialEq
Implementations: Control equality comparisons using the#[cmp_neq]
attribute for specific variants. - Support for Common Traits: Supports deriving common traits like
Clone
andPartialEq
.
Installation
Add error-tree
to your Cargo.toml
:
[dependencies]
error-tree = "0.4.0"
Usage
Import the error_tree
macro and start defining your error enums using the error_tree!
macro:
use error_tree::error_tree;
error_tree! {
pub enum MyAppError {
#[display("An unexpected error occurred")]
UnexpectedError,
#[display("IO error: {inner}")]
IoError(std::io::Error),
#[display("Network error: {url}")]
NetworkError { url: String },
}
}
This macro will generate:
- The
MyAppError
enum with the specified variants. - Implementations of the
From
trait for converting from wrapped error types toMyAppError
. - A
Display
implementation based on the#[display("...")]
attributes.
Example: Defining Nested Error Enums
You can define multiple error enums and specify relationships between them. The macro will automatically generate the necessary From
implementations for error conversion.
use error_tree::error_tree;
use std::io;
error_tree! {
pub enum OuterError {
InnerError(InnerError),
#[display("An outer error occurred")]
OuterVariant,
}
pub enum InnerError {
#[display("An inner IO error: {inner}")]
IoError(io::Error),
#[display("A custom inner error")]
CustomError,
}
}
With this setup, you can convert an io::Error
directly into an OuterError
:
fn cause_io_error() -> Result<(), io::Error> {
Err(io::Error::new(io::ErrorKind::Other, "Disk not found"))
}
fn handle_error() -> Result<(), OuterError> {
cause_io_error()?;
Ok(())
}
fn main() {
match handle_error() {
Ok(_) => println!("Success"),
Err(e) => println!("Error occurred: {}", e),
}
}
Automatic From
Implementations
The macro generates From
implementations that allow seamless conversion between your error types:
impl From<io::Error> for InnerError {
fn from(inner: io::Error) -> Self {
InnerError::IoError(inner)
}
}
impl From<InnerError> for OuterError {
fn from(inner: InnerError) -> Self {
OuterError::InnerError(inner)
}
}
// This allows:
impl From<io::Error> for OuterError {
fn from(inner: io::Error) -> Self {
OuterError::InnerError(InnerError::IoError(inner))
}
}
Customizing Display
Messages
Use the #[display("...")]
attribute to define custom messages for your error variants:
error_tree! {
pub enum MyError {
#[display("Simple error occurred")]
SimpleError,
#[display("IO error occurred: {inner}")]
IoError(std::io::Error),
#[display("Data error: {data}")]
DataError { data: String },
}
}
Controlling PartialEq
Behavior
You can derive PartialEq
for your error enums and control comparison behavior for specific variants using the #[cmp_neq]
attribute:
error_tree! {
#[derive(PartialEq)]
pub enum MyError {
SimpleError,
#[cmp_neq]
NonComparableError(std::io::Error),
DataError { data: String },
}
}
let error1 = MyError::NonComparableError(io::Error::new(io::ErrorKind::Other, "Error"));
let error2 = MyError::NonComparableError(io::Error::new(io::ErrorKind::Other, "Error"));
assert_ne!(error1, error2); // Due to #[cmp_neq], these are not equal
Advanced Usage
Complex Error Hierarchies
error-tree
excels at handling complex error hierarchies. Here's an example adapted from the crate's tests:
use error_tree::error_tree;
use std::sync::mpsc;
#[derive(Debug)]
pub struct CpalStreamError;
#[derive(Debug)]
pub struct CpalDeviceNameError;
#[derive(Debug)]
pub struct CpalDevicesError;
#[derive(Debug)]
pub struct CpalHostUnavailable;
error_tree! {
pub enum PassiveAudioCaptureError {
FormatError,
DeviceError(DeviceError),
IOError(IOError),
HostError(HostError),
StreamError(StreamError),
ChannelError(ChannelError),
}
pub enum DeviceError {
DeviceNotAvailable { device_name: String },
Basic(CpalDevicesError),
NameError(CpalDeviceNameError),
}
pub enum IOError {
Basic(std::io::Error),
}
pub enum HostError {
HostUnavailable(CpalHostUnavailable),
}
pub enum StreamError {
StreamError(CpalStreamError),
}
pub enum ChannelError {
ChannelRecvError(mpsc::RecvError),
}
}
// Usage example
fn cause_device_error() -> Result<(), CpalDeviceNameError> {
Err(CpalDeviceNameError)
}
fn main() {
let result: Result<(), PassiveAudioCaptureError> = (|| {
cause_device_error()?;
Ok(())
})();
match result {
Ok(_) => println!("Success"),
Err(e) => println!("Error: {}", e),
}
}
In this example:
- Multiple error enums are defined, representing different components of an audio capture system.
- The macro generates
From
implementations, allowing errors from low-level components (likeCpalDeviceNameError
) to be converted into high-level errors (PassiveAudioCaptureError
) automatically. - This simplifies error handling in functions that may return errors from different layers.
Tests and Examples
The crate includes several tests demonstrating its capabilities:
- Clone Derivation Test: Ensures that enums defined with
#[derive(Clone)]
properly implement theClone
trait. - PartialEq Implementation Test: Verifies that custom
PartialEq
implementations respect the#[cmp_neq]
attribute. - Display Trait Implementation Test: Checks that custom
Display
messages are formatted correctly for different variants.
Limitations
- The macro assumes that the types used in your error variants are valid Rust types that implement necessary traits like
Debug
andDisplay
where appropriate. - For custom types used in wrapped variants, ensure that they implement
Debug
if you want the defaultDisplay
implementation to work correctly.
Contributing
Contributions are welcome! Please submit issues or pull requests on the GitHub repository.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Feel free to reach out if you have any questions or need assistance using error-tree
.
Appendix: Understanding the Macro's Generated Code
To help you understand what the error_tree!
macro generates, here's an overview based on the crate's code:
Error Enums and Variants
The macro processes your error enums and their variants, supporting:
- Basic Variants: Simple variants without data.
- Wrapped Variants: Variants that wrap another error type.
- Struct Variants: Variants with named fields.
From
Implementations
For each wrapped variant, the macro generates From
implementations to convert from the wrapped type to the enum containing it. It also generates transitive From
implementations to allow direct conversion from low-level errors to top-level errors in your hierarchy.
Example generated code:
impl From<std::io::Error> for IOError {
fn from(x: std::io::Error) -> Self {
IOError::Basic(x)
}
}
impl From<IOError> for PassiveAudioCaptureError {
fn from(x: IOError) -> Self {
PassiveAudioCaptureError::IOError(x)
}
}
// Transitive conversion
impl From<std::io::Error> for PassiveAudioCaptureError {
fn from(x: std::io::Error) -> Self {
PassiveAudioCaptureError::IOError(IOError::Basic(x))
}
}
Display
Implementations
The macro generates Display
implementations for your enums, using the #[display("...")]
attribute if provided. If no #[display("...")]
attribute is present, it defaults to displaying the variant name.
Custom PartialEq
Implementations
If you derive PartialEq
and use the #[cmp_neq]
attribute on specific variants, the macro generates a custom PartialEq
implementation that respects this attribute.
Attribute Handling
The macro carefully processes attributes to ensure that standard attributes like #[derive(...)]
are preserved and applied correctly to the generated enums.
Conclusion
error-tree
simplifies error handling in Rust applications by reducing boilerplate and providing a clear, declarative way to define complex error hierarchies. By automatically generating necessary implementations, it allows you to focus on your application's logic rather than repetitive error handling code.
Dependencies
~7MB
~110K SLoC