diff --git a/README.md b/README.md index 73cdbb1..a32e831 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,10 @@ Enhance Rust's `?` operator by appending file and line number details to errors, ## Features -- Appends file and line number to errors propagated with `?`. -- Integrates seamlessly with Rust's existing error handling. -- Helps locate and fix errors faster by pinpointing their origin. +- 📁 Appends file and line number to errors propagated with `?`. +- 🧩 Integrates seamlessly with Rust's existing error handling. +- 📍 Helps locate and fix errors faster by pinpointing their origin. +- 🚀 Supports `anyhow` error handling through the `anyhow` feature flag. ## Why `wherr`? @@ -29,10 +30,19 @@ Add the following to your `Cargo.toml`: wherr = "0.1" ``` +For `anyhow` support: + +```toml +[dependencies] +wherr = { version = "0.1", features = ["anyhow"] } +``` + ## Quick Start By simply annotating your functions with `#[wherr]`, errors propagated using `?` will also include the file and line number. +1. **Standard Usage**: Annotate functions with `#[wherr]`. + ```rust use wherr::wherr; @@ -58,6 +68,36 @@ This will highlight the difference `wherr` makes. Your error will now indicate e error at wherr/examples/with_wherr.rs:5 ``` +2. **With `anyhow`**: + +Ensure you've added the `anyhow` feature as shown in the installation section. Then: + +```rust +use anyhow::{Context, Result}; +use wherr::wherr; + +#[wherr] +fn concat_files(path1: &str, path2: &str) -> Result { + let mut content1 = std::fs::read_to_string(path1).with_context(|| format!("Failed to read {}", path1))?; + let content2 = std::fs::read_to_string(path2).with_context(|| format!("Failed to read {}", path2))?; + + content1.push_str(&content2); + Ok(content1) +} +``` + +Run the provided example: + +```sh +cargo run --features=anyhow --example anyhow +``` + +This will highlight the difference `wherr` makes. Your error will now indicate exactly where the issue arises: + +``` +error at wherr/examples/anyhow.rs:6 +``` + ## Behind the Scenes The `#[wherr]` notation is a [proc_macro_attribute](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros), which allows for code transformations at compile time. diff --git a/wherr-macro/Cargo.toml b/wherr-macro/Cargo.toml index 7b7b4d5..ff9bae1 100644 --- a/wherr-macro/Cargo.toml +++ b/wherr-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wherr-macro" -version = "0.1.6" +version = "0.1.7" edition = "2021" authors = ["Joel Jakobsson "] categories = ["rust-patterns"] diff --git a/wherr/Cargo.toml b/wherr/Cargo.toml index 902e0d9..dfe93e3 100644 --- a/wherr/Cargo.toml +++ b/wherr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wherr" -version = "0.1.6" +version = "0.1.7" edition = "2021" authors = ["Joel Jakobsson "] categories = ["rust-patterns"] @@ -9,5 +9,18 @@ keywords = ["error", "error-handling"] license = "MIT OR Apache-2.0" repository = "https://github.com/joelonsql/wherr" +[features] +anyhow = ["dep:anyhow"] + [dependencies] wherr-macro = "0.1" +anyhow = { version = "1.0", optional = true } + +[[example]] +name = "anyhow" +required-features = ["anyhow"] + +[[test]] +name = "anyhow_error_tests" +path = "tests/anyhow_error_tests.rs" +required-features = ["anyhow"] diff --git a/wherr/examples/anyhow.rs b/wherr/examples/anyhow.rs new file mode 100644 index 0000000..de9c383 --- /dev/null +++ b/wherr/examples/anyhow.rs @@ -0,0 +1,16 @@ +use anyhow::{Context, Result}; +use wherr::wherr; + +#[wherr] +fn concat_files(path1: &str, path2: &str) -> Result { + let mut content1 = std::fs::read_to_string(path1).with_context(|| format!("Failed to read {}", path1))?; + let content2 = std::fs::read_to_string(path2).with_context(|| format!("Failed to read {}", path2))?; + + content1.push_str(&content2); + Ok(content1) +} + +fn main() { + let content = concat_files("file1.txt", "file2.txt").expect("Failed to concatenate the files"); + println!("Concatenated content:\n{content}"); +} diff --git a/wherr/examples/with_wherr.rs b/wherr/examples/with_wherr.rs index 429248d..9f4dca2 100644 --- a/wherr/examples/with_wherr.rs +++ b/wherr/examples/with_wherr.rs @@ -11,5 +11,5 @@ fn concat_files(path1: &str, path2: &str) -> Result Result { fn main() { let content = concat_files("file1.txt", "file2.txt").expect("Failed to concatenate the files"); - println!("Concatenated content:\n{}", content); + println!("Concatenated content:\n{content}"); } diff --git a/wherr/src/lib.rs b/wherr/src/lib.rs index 5e43c87..5342c91 100644 --- a/wherr/src/lib.rs +++ b/wherr/src/lib.rs @@ -9,12 +9,20 @@ use std::fmt; // Re-export the procedural macro from the `wherr_macro` crate. pub use wherr_macro::wherr; +#[cfg(feature = "anyhow")] +use anyhow::Error as AnyhowError; + +#[cfg(feature = "anyhow")] +type Wherror = AnyhowError; +#[cfg(not(feature = "anyhow"))] +type Wherror = Box; + /// Represents an error that includes file and line number metadata. /// /// This error struct wraps around any error and provides a consistent interface to access the original error /// and the file and line where it originated from. pub struct Wherr { - pub inner: Box, + pub inner: Wherror, pub file: &'static str, pub line: u32, } @@ -26,7 +34,7 @@ impl Wherr { /// * `err`: The original error to wrap. /// * `file`: The file where the error occurred. /// * `line`: The line number where the error occurred. - pub fn new(err: Box, file: &'static str, line: u32) -> Self { + pub fn new(err: Wherror, file: &'static str, line: u32) -> Self { Wherr { inner: err, file, @@ -71,18 +79,18 @@ pub fn wherrapper( result: Result, file: &'static str, line: u32, -) -> Result> +) -> Result where - E: Into>, + E: Into, { match result { Ok(val) => Ok(val), Err(err) => { - let boxed_err: Box = err.into(); - if boxed_err.is::() { - Err(boxed_err) + let error: Wherror = err.into(); + if error.is::() { + Err(error) } else { - Err(Box::new(Wherr::new(boxed_err, file, line))) + Err(Wherr::new(error, file, line).into()) } } } diff --git a/wherr/tests/anyhow_error_tests.rs b/wherr/tests/anyhow_error_tests.rs new file mode 100644 index 0000000..97ace30 --- /dev/null +++ b/wherr/tests/anyhow_error_tests.rs @@ -0,0 +1,76 @@ +use wherr::{wherr, wherrapper, Wherr}; +use anyhow::{Error, Result}; + +#[test] +fn test_wherr_new() { + let error_message = "Test error"; + let err = anyhow::Error::new(std::io::Error::new(std::io::ErrorKind::Other, error_message)); + let wherr = Wherr::new(err, "test.rs", 42); + + assert_eq!(wherr.file, "test.rs"); + assert_eq!(wherr.line, 42); + assert_eq!(wherr.inner.to_string(), error_message); +} + +#[test] +fn test_wherr_display() { + let error_message = "Test error"; + let err = anyhow::Error::new(std::io::Error::new(std::io::ErrorKind::Other, error_message)); + let wherr = Wherr::new(err, "test.rs", 42); + + assert_eq!( + format!("{}", wherr), + format!("{}\nerror at test.rs:42", error_message) + ); +} + +#[test] +fn test_wherrapper() { + let error_message = "Test error"; + let err = std::io::Error::new(std::io::ErrorKind::Other, error_message); + let result: Result<(), _> = Err(err.into()); // Convert the error to anyhow::Error + + match wherrapper::<(), anyhow::Error>(result, "test.rs", 42) { + Ok(_) => panic!("Expected an error"), + Err(err) => { + let wherr = err.downcast::().expect("Expected a Wherr error"); + assert_eq!(wherr.file, "test.rs"); + assert_eq!(wherr.line, 42); + assert_eq!(wherr.inner.to_string(), error_message); + } + } +} + +#[wherr] +fn f3() -> Result<()> { + i64::from_str_radix("not a decimal number", 10).map_err(Error::new)?; + + Ok(()) +} + +#[wherr] +fn f2() -> Result<()> { + f3()?; + + Ok(()) +} + +#[wherr] +fn f1() -> Result<()> { + f2()?; + + Ok(()) +} + +#[test] +fn test_wherr_macro() { + match f1() { + Ok(_) => panic!("Expected an error"), + Err(err) => { + let wherr = err.downcast::().expect("Expected a Wherr error"); + assert_eq!(wherr.file, "wherr/tests/anyhow_error_tests.rs"); + assert_eq!(wherr.line, 46); + assert_eq!(wherr.inner.to_string(), "invalid digit found in string"); + } + } +} diff --git a/wherr/tests/integration_tests.rs b/wherr/tests/box_error_tests.rs similarity index 94% rename from wherr/tests/integration_tests.rs rename to wherr/tests/box_error_tests.rs index 94e06ad..c2f3100 100644 --- a/wherr/tests/integration_tests.rs +++ b/wherr/tests/box_error_tests.rs @@ -1,4 +1,4 @@ -extern crate wherr; +#![cfg(not(feature = "anyhow"))] use wherr::{wherr, wherrapper, Wherr}; @@ -69,7 +69,7 @@ fn test_wherr_macro() { Ok(_) => panic!("Expected an error"), Err(err) => { let wherr = err.downcast::().expect("Expected a Wherr error"); - assert_eq!(wherr.file, "wherr/tests/integration_tests.rs"); + assert_eq!(wherr.file, "wherr/tests/box_error_tests.rs"); assert_eq!(wherr.line, 47); assert_eq!(wherr.inner.to_string(), "invalid digit found in string"); }