1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
//! This library provides an error wrapper which adds a description to its specific instance. //! //! ### Examples //! //! For example, you want to create file on the given path and write here a given string. //! Let's forget for a moment that [`std::fs::write`] exists and do it ourselves: //! ``` //! # use std::{io, fs::File, path::Path}; //! use std::io::Write; //! fn create_and_write(path: &Path, content: &str) -> Result<(), io::Error> { //! let mut file = File::create(path)?; //! write!(file, "{}", content)?; //! file.sync_all() //! } //! ``` //! Here are three distinct sources of error, and it might not always be obvious //! which of them is the real one in particular case. That's how it is handled with `describe_err`: //! ``` //! # use std::{io, fs::File, path::Path}; //! use std::io::Write; //! use describe_err::{describing, describe, Described}; //! //! fn create_and_write(path: &Path, content: &str) -> Result<(), Described<io::Error>> { //! let mut file = describing!(File::create(path))?; //! write!(file, "{}", content).map_err(describe("Cannot write to file"))?; //! describing!(file.sync_all()) //! } //! ``` //! //! Here you can see two ways to use the library: //! //! - By explicitly providing the description with [`describe`]. //! This function returns the closure, which maps an incoming error to `Described` instance. //! - By wrapping the `Result`-producing operation in [`describing!`] macro, //! which will describe the error with the stringified content. //! //! And here's how will be used the generated output: //! ``` //! # use std::{io, fs::File, path::{Path, PathBuf}}; //! # use std::io::Write; //! # use describe_err::{describing, describe, Described}; //! # //! # fn create_and_write(path: &Path, content: &str) -> Result<(), Described<io::Error>> { //! # let mut file = describing!(File::create(path))?; //! # write!(file, "{}", content).map_err(describe("Cannot write to file"))?; //! # describing!(file.sync_all()) //! # } //! fn main() { //! let path = PathBuf::from("/tmp/nonexistent/path"); //! let res = create_and_write(&path, "arbitrary content"); //! let err = res.unwrap_err(); //! assert_eq!(err.to_string(), "File::create(path): No such file or directory (os error 2)"); //! } //! ``` //! As you can see, the command which produced an error is right here, in the error itself. use thiserror::Error; use std::error; /// An error wrapper with description. /// /// This struct can hold every error, with the only restriction that this error /// must be `'static` to support downcasting through [`source`][std::error::Error::source]. /// /// When converting this wrapper to string with `Display`, it will render colon-separated /// pair of description and original error: /// ``` /// use describe_err::{Described, describing}; /// fn fmt<E: std::error::Error + 'static>(err: &Described<E>) -> String { /// format!("{}: {}", err.description(), err.original()) /// } /// /// fn main() { /// // Let's create a simple error with auto-generated description... /// let res: Result<u32, _> = describing!("Not a number".parse()); /// // ...then unwrap it... /// let err = res.unwrap_err(); /// // and see that the formatting is indeed the same: /// assert_eq!(fmt(&err), format!("{}", err)); /// } /// ``` #[derive(Debug, Error)] #[error("{description}: {original}")] pub struct Described<E: error::Error + 'static> { description: String, #[source] original: E, } impl<E: error::Error + 'static> Described<E> { /// Directly retrieves an error description. pub fn description(&self) -> &str { &self.description } /// Directly retrieves an original error. /// /// This method is different from [`source`][std::error::Error::source], /// since it is generic and is known to return exactly the wrapped type, /// not a boxed trait object. This way you won't need any downcasting. pub fn original(&self) -> &E { &self.original } } /// Wrap an error with description. /// /// This method generates a closure to be passed into `map_err`: /// ``` /// use describe_err::describe; /// let description = "Parsing a not-a-number to number"; /// let err = "Not a number".parse::<u64>().map_err(describe(description)).unwrap_err(); /// assert_eq!(err.description(), description); /// ``` pub fn describe<E: error::Error>(description: impl Into<String>) -> impl FnOnce(E) -> Described<E> { let description = description.into(); |original| Described { description, original } } /// Wrap an error with an auto-generated description. /// /// This macro is essentially a wrapper around [`describe`]. It expands to the following: /// ``` /// # use describe_err::describe; /// # let result_expression = "123".parse::<u64>(); /// // let res = describing!(result_expression); /// let res = result_expression.map_err(describe("result_expression")); /// ``` /// The returned `Result` can be pattern-matched or propagated as usual. #[macro_export] macro_rules! describing { ($expr:expr) => {{ let expr: Result<_, _> = $expr; expr.map_err($crate::describe(stringify!($expr))) }}; } #[cfg(test)] mod tests { use super::*; use std::io; #[test] fn simple_error() { let err: Result<(), _> = Err(io::Error::new(io::ErrorKind::Other, "Inner error")).map_err(describe("Produced in test")); let err = err.unwrap_err(); assert_eq!(err.to_string(), "Produced in test: Inner error"); } fn returns_err() -> Result<(), io::Error> { Err(io::Error::new(io::ErrorKind::Other, "Inner error")) } #[test] fn macro_err() { let err = describing!(returns_err()).unwrap_err(); assert_eq!(err.to_string(), "returns_err(): Inner error"); } }