easy_error/
lib.rs

1//! # Easy-error
2//!
3//! This crate is a lightweight error handling library meant to play well with
4//! the standard `Error` trait. It is designed for quick prototyping or for
5//! Command-line applications where any error will simply bubble up to the user.
6//! There are four major components of this crate:
7//!
8//! 1. A basic, string-based error type that is meant for either quick
9//!    prototyping or human-facing errors.
10//! 2. A nice way to iterate over the causes of an error.
11//! 3. Some macros that make returning errors slightly more ergonomic.
12//! 4. A "termination" type that produces nicely formatted error messages when
13//!    returned from the `main` function.
14//!
15//! ## Rust Version Requirements
16//!
17//! The current version requires **Rustc 1.46 or newer**.  In general, this
18//! crate will be compilable with the Rustc version available on the oldest
19//! supported Ubuntu LTS release.  Any change that requires a newer version of
20//! Rustc than what is available on the oldest supported Ubuntu LTS will
21//! be considered a breaking change.
22//!
23//! ## Example
24//!
25//! ```no_run
26//! use std::{fs::File, io::Read};
27//! use easy_error::{bail, ensure, Error, ResultExt, Terminator};
28//!
29//! fn from_file() -> Result<i32, Error> {
30//!     let file_name = "example.txt";
31//!     let mut file = File::open(file_name).context("Could not open file")?;
32//!
33//!     let mut contents = String::new();
34//!     file.read_to_string(&mut contents).context("Unable to read file")?;
35//!
36//!     contents.trim().parse().context("Could not parse file")
37//! }
38//!
39//! fn validate(value: i32) -> Result<(), Error> {
40//!     ensure!(value > 0, "Value must be greater than zero (found {})", value);
41//!
42//!     if value % 2 == 1 {
43//!         bail!("Only even numbers can be used");
44//!     }
45//!
46//!     Ok(())
47//! }
48//!
49//! fn main() -> Result<(), Terminator> {
50//!     let value = from_file().context("Unable to get value from file")?;
51//!     validate(value).context("Value is not acceptable")?;
52//!
53//!     println!("Value = {}", value);
54//!     Ok(())
55//! }
56//! ```
57
58// Just bunches of Clippy lints.
59#![deny(clippy::all)]
60#![warn(clippy::nursery)]
61#![warn(clippy::pedantic)]
62#![allow(clippy::use_self)] // I rather like the name repetition
63#![allow(clippy::missing_errors_doc)] // This is an error handling library, errors are implied.
64#![warn(unknown_lints)]
65
66use std::{
67	error,
68	fmt::{self, Display, Formatter},
69	panic::Location,
70	string::ToString,
71};
72
73mod macros;
74mod terminator;
75pub use terminator::Terminator;
76
77pub type Result<T> = std::result::Result<T, Error>;
78
79/// An error that is a human-targetted string plus an optional cause.
80#[derive(Debug)]
81pub struct Error
82{
83	/// The human-targetting error string.
84	pub ctx: String,
85
86	/// The location of the error.
87	pub location: &'static Location<'static>,
88
89	/// The optional cause of the error.
90	pub cause: Option<Box<dyn error::Error + Send + 'static>>,
91}
92
93impl Error
94{
95	/// Create a new error with the given cause.
96	#[allow(clippy::needless_pass_by_value)] // `T: ToString` implies `&T: ToString`
97	#[track_caller]
98	pub fn new<S, E>(ctx: S, cause: E) -> Error
99	where
100		S: ToString,
101		E: error::Error + Send + 'static,
102	{
103		let ctx = ctx.to_string();
104		let location = Location::caller();
105		let cause: Option<Box<dyn error::Error + Send + 'static>> = Some(Box::new(cause));
106
107		Error { ctx, location, cause }
108	}
109}
110
111impl Display for Error
112{
113	fn fmt(&self, f: &mut Formatter) -> fmt::Result
114	{
115		write!(f, "{} ({})", self.ctx, self.location)
116	}
117}
118
119impl error::Error for Error
120{
121	fn description(&self) -> &str { &self.ctx }
122
123	fn source(&self) -> Option<&(dyn error::Error + 'static)>
124	{
125		self.cause.as_ref().map(|c| &**c as _)
126	}
127}
128
129/// Extension methods to the `Result` type.
130pub trait ResultExt<T>
131{
132	/// Adds some context to the error.
133	#[track_caller]
134	fn context<S: ToString>(self, ctx: S) -> Result<T>;
135
136	/// Adds context to the error, evaluating the context function only if there
137	/// is an `Err`.
138	#[track_caller]
139	fn with_context<S: ToString, F: FnOnce() -> S>(self, ctx_fn: F) -> Result<T>;
140}
141
142impl<T, E> ResultExt<T> for std::result::Result<T, E>
143where
144	E: error::Error + Send + 'static,
145{
146	fn context<S: ToString>(self, ctx: S) -> Result<T>
147	{
148		let location = Location::caller();
149		self.map_err(|e| Error { ctx: ctx.to_string(), location, cause: Some(Box::new(e)) })
150	}
151
152	fn with_context<S: ToString, F: FnOnce() -> S>(self, ctx_fn: F) -> Result<T>
153	{
154		let location = Location::caller();
155		self.map_err(|e| Error { ctx: ctx_fn().to_string(), location, cause: Some(Box::new(e)) })
156	}
157}
158
159/// Extension methods to `Error` types.
160pub trait ErrorExt: error::Error
161{
162	fn iter_chain(&self) -> Causes;
163
164	fn iter_causes(&self) -> Causes { Causes { cause: self.iter_chain().nth(1) } }
165
166	fn find_root_cause(&self) -> &(dyn error::Error + 'static)
167	{
168		self.iter_chain().last().expect("source chain should at least contain original error")
169	}
170}
171
172impl<E: error::Error + 'static> ErrorExt for E
173{
174	fn iter_chain(&self) -> Causes { Causes { cause: Some(self) } }
175}
176
177impl ErrorExt for dyn error::Error
178{
179	fn iter_chain(&self) -> Causes { Causes { cause: Some(self) } }
180}
181
182/// An iterator over the causes of an error.
183// Add the `must_use` tag to please Clippy. I really doubt there will ever be a situation where
184// someone creates a `Causes` iterator and doesn't consume it but we might as well warn them.
185#[must_use = "iterators are lazy and do nothing unless consumed"]
186pub struct Causes<'a>
187{
188	/// The next cause to display.
189	cause: Option<&'a (dyn error::Error + 'static)>,
190}
191
192impl<'a> Iterator for Causes<'a>
193{
194	type Item = &'a (dyn error::Error + 'static);
195
196	fn next(&mut self) -> Option<Self::Item>
197	{
198		let cause = self.cause.take();
199		self.cause = cause.and_then(error::Error::source);
200
201		cause
202	}
203}
204
205/// Creates an error message from the provided string.
206#[inline]
207#[allow(clippy::needless_pass_by_value)] // `T: ToString` implies `&T: ToString`
208#[track_caller]
209pub fn err_msg<S: ToString>(ctx: S) -> Error
210{
211	Error { ctx: ctx.to_string(), location: Location::caller(), cause: None }
212}