Compare commits

...

3 Commits

Author SHA1 Message Date
Michael Pfaff 6ad97e1d76
Bump version 2023-06-29 01:51:56 -04:00
Michael Pfaff efa1c3fa37
Refactoring, update docs, API adjustments 2023-06-29 01:51:08 -04:00
Michael Pfaff 9950d96522
Improved output and API
- New and improved output format
- Adjusted context API to go along with it
- Nicer output for termination
- Better example
- Better handling of backtraces
- Removed a silly field that is never used
- `How::location` now returns the actual location instead of the context
  element
- `Location::caller` is now additionally captured for every call to
  `How::context`
- Added a feature that implements `Clone` for `How` (by deferring to the
  existing `clone_without_backtrace` method)
2023-06-29 00:00:42 -04:00
11 changed files with 235 additions and 187 deletions

View File

@ -1,14 +1,17 @@
[package]
name = "how"
version = "0.3.0"
version = "0.5.0"
edition = "2021"
[features]
default = []
backtrace = []
termination = []
clone-with-caveats = []
termination = ["dep:ansee"]
[dependencies]
ansee = { git = "https://git.pfaff.dev/michael/ansee", optional = true }
typeid-cast = "0.1"
[[example]]
name = "output"

View File

@ -1,19 +1,40 @@
# *How*
A seriously contextual error library that focuses on how you got there. Designed to make debugging parser logic easier, *how* enables you to concisely capture any and all context that could possibly have contributed to an error.
A seriously contextual error library that focuses on how you got there. Designed to make debugging easier, *how* enables you to concisely capture any and all context that could possibly have contributed to an error.
## Getting started
Thanks to *how*'s minimal set of public types whose names are all unique from those of most crates (aside from other error handling libraries), you can safely use star imports anywhere you want to use *how*.
A basic example takes just 2 imports:
```rust,should_panic
use how::*;
use how::{How, Result};
fn main() -> Result<()> {
Err(How::new("TODO: implement amazing new program"))
}
```
[`How`] intentionally omits a [`From`] implementation for [`std::error::Error`] to discourage the creation of [`How`]s with no caller context. Instead, the [`Explain`] trait is implemented for all `Result`[^2] and provides a convenient [`context`](Explain::context) function.
But usually you'll want to [attach some information](Explain::context) too:
```rust,should_panic
use how::{How, Explain, Result};
[^2]: Where `E` is either [`How`] or implements [`std::error::Error`]. Errors that don't implement [`std::error::Error`] (usually in order to be permitted to implement [`From`] for any type that implements [`std::error::Error`]) can only be, are not, and will not be, supported manually.
fn main() -> Result<()> {
Err(How::new("TODO: implement amazing new program")
.context("I plan to do it eventually™"))
}
```
And you'll probably want to get [nicer output](TerminationResult) when you return an error from `main` (this one requires `feature = "termination"`):
```rust,should_panic,compile_fail
use how::{How, Explain, Result, TerminationResult};
fn main() -> TerminationResult<()> {
Err(How::new("TODO: implement amazing new program")
.context("I plan to do it eventually™"))
.into()
}
```
[`How`] intentionally omits a [`From`] implementation for [`Error`](std::error::Error) to discourage the creation of [`How`]s with no caller context. Instead, the [`Explain`] trait is implemented for all [`Result`][^1] and [`Option`] and provides a convenient [`context`](Explain::context) function.
[^1]: Where `E` is either [`How`] or implements [`Error + 'static`](std::error::Error).

View File

@ -1,12 +1,18 @@
use how::*;
fn main0() -> Result<()> {
Err(How::new("The engine broke down")
.context("While driving down a road".chain(format!("Where the road is {}", "Main Street")))
.context(
"While driving to a location".chain(format!("Where the location is {}", "the Mall")),
)
.context(format!("While in the car with {:?}", ["Mom", "Dad"])))?
Err(How::new(
"The engine broke down"
.with("Remember: you aren't good with cars")
.with("Suggestion: call a tow truck"),
)
.context(
"While driving down a road"
.with(format!("Where the road is {}", "Main Street"))
.with(format!("And the speed is {}", "86 km/h")),
)
.context("While driving to a location".with(format!("Where the location is {}", "the Mall")))
.context(format!("While in the car with {:?}", ["Mom", "Dad"])))?
}
fn main() -> TerminationResult {

View File

@ -1,10 +1,11 @@
use std::borrow::Cow;
use std::fmt;
use std::panic::Location;
/// Provides context furthering the explanation of *how* you got to an error.
#[derive(Debug, Clone)]
pub struct Context(ContextInner);
pub struct Context(pub(crate) ContextInner);
impl fmt::Display for Context {
#[inline(always)]
@ -14,15 +15,16 @@ impl fmt::Display for Context {
}
#[derive(Debug, Clone)]
enum ContextInner {
pub(crate) enum ContextInner {
Elem(ContextElem),
Compound(Vec<ContextElem>),
}
#[derive(Debug, Clone)]
enum ContextElem {
pub(crate) enum ContextElem {
Str(&'static str),
String(String),
Location(Location<'static>),
}
impl fmt::Display for ContextInner {
@ -34,7 +36,7 @@ impl fmt::Display for ContextInner {
if let Some(elem) = elems.next() {
fmt::Display::fmt(elem, f)?;
for elem in elems {
write!(f, "\n{elem}")?;
write!(f, "\n- {elem}")?;
}
}
Ok(())
@ -48,6 +50,7 @@ impl fmt::Display for ContextElem {
match self {
Self::Str(s) => f.write_str(s),
Self::String(s) => f.write_str(s),
Self::Location(l) => write!(f, "At {l}"),
}
}
}
@ -56,11 +59,11 @@ pub trait IntoContext {
fn into_context(self) -> Context;
#[inline(always)]
fn chain(self, other: impl IntoContext) -> Context
fn with(self, other: impl IntoContext) -> Context
where
Self: Sized,
{
self.into_context().chain(other)
self.into_context().with(other)
}
}
@ -70,24 +73,24 @@ impl IntoContext for Context {
self
}
/// Chains another piece of context that is equal from a hierarchical perspective.
/// Chains another piece of context that is a child from a hierarchical perspective.
#[track_caller]
#[inline]
fn chain(self, other: impl IntoContext) -> Self {
fn with(self, other: impl IntoContext) -> Self {
let other = other.into_context().0;
Context(ContextInner::Compound(match self.0 {
ContextInner::Compound(mut elems) => {
match other.into_context().0 {
match other {
ContextInner::Elem(elem) => elems.push(elem),
ContextInner::Compound(mut elems1) => elems.append(&mut elems1),
};
elems
}
ContextInner::Elem(elem) => {
match other.into_context().0 {
ContextInner::Elem(elem1) => vec![elem, elem1],
ContextInner::Compound(mut elems) => {
elems.insert(0, elem);
elems
}
ContextInner::Elem(elem) => match other {
ContextInner::Elem(elem1) => vec![elem, elem1],
ContextInner::Compound(mut elems) => {
elems.insert(0, elem);
elems
}
},
}))
@ -118,6 +121,20 @@ impl IntoContext for Cow<'static, str> {
}
}
impl<'a> IntoContext for &'a Location<'static> {
#[inline]
fn into_context(self) -> Context {
Location::into_context(*self)
}
}
impl IntoContext for Location<'static> {
#[inline]
fn into_context(self) -> Context {
Context(ContextInner::Elem(ContextElem::Location(self)))
}
}
impl<C, F> IntoContext for F
where
C: IntoContext,

View File

@ -1,27 +1,63 @@
use crate::{How, IntoContext, IntoResultHow};
use std::panic::Location;
crate::seal!(pub(crate) private::Sealed);
use crate::{How, IntoContext};
pub trait Explain: Sealed {
pub trait Explain {
type Output;
#[track_caller]
#[must_use]
fn context(self, context: impl IntoContext) -> Self::Output;
}
impl<T, E> Sealed for Result<T, E> where Result<T, E>: IntoResultHow {}
impl<T> Sealed for Option<T> where Option<T>: IntoResultHow {}
impl Explain for How {
type Output = Self;
#[track_caller]
#[inline]
fn context(mut self, context: impl IntoContext) -> Self {
use crate::context::*;
let mut context = context.into_context();
let loc = ContextElem::Location(*Location::caller());
match context.0 {
ContextInner::Elem(elem) => {
context.0 = ContextInner::Compound(vec![elem, loc]);
}
ContextInner::Compound(ref mut vec) => {
vec.insert(1, loc);
}
}
self.push_context(context);
self
}
}
impl<T, E> Explain for Result<T, E>
where
Result<T, E>: IntoResultHow,
E: std::error::Error + 'static,
{
type Output = Result<<Self as IntoResultHow>::T, How>;
type Output = Result<T, How>;
#[inline(always)]
#[track_caller]
fn context(self, context: impl IntoContext) -> Self::Output {
self.into_result_how().map_err(#[inline(never)] move |e| e.context(context))
#[cold]
#[track_caller]
fn into_and_context<E, C>(e: E, c: C) -> How
where
E: std::error::Error + 'static,
C: IntoContext,
{
match typeid_cast::cast(e) {
Ok(e) => e,
Err(e) => How::new(e.to_string()),
}
.context(c)
}
match self {
Ok(t) => Ok(t),
Err(e) => Err(into_and_context(e, context)),
}
}
}
@ -34,7 +70,7 @@ impl<T> Explain for Option<T> {
// TODO: maybe add a feature for the extra "Option::None" context
match self {
Some(t) => Ok(t),
None => Err(How::new(context))
None => Err(How::new(context)),
}
//self.into_result_how().map_err(#[inline(never)] move |e| e.context(context))
}

View File

@ -1,94 +1,97 @@
use core::panic::Location;
#[cfg(feature = "backtrace")]
use std::backtrace::BacktraceStatus;
use crate::*;
use crate::report::report_write;
/// Does not implement [`std::error::Error`] to allow a [`From`] implementation for all other error types.
/// The error type.
///
/// By default, does not implement [`Clone`] because [`std::backtrace::Backtrace`] does not
/// implement [`Clone`]. However, the `clone-with-caveats` feature may be used to enable a
/// [`Clone`] impl that sets the cloned `backtrace` to [`std::backtrace::Backtrace::disabled`].
pub struct How(Box<HowInner>);
struct HowInner {
/// When true, the error will cause branchers to abort.
classified: bool,
location: &'static Location<'static>,
#[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace,
backtrace: Backtrace,
// TODO: consider storing this vec inline (sharing the allocation with rest of the struct.
// Probably move after `backtrace`)
context: Vec<Context>,
}
// will be replaced with std::backtrace::Backtrace if and when it is Clone
#[cfg(feature = "backtrace")]
#[derive(Debug)]
enum Backtrace {
Disabled,
Unsupported,
Other(BacktraceStatus, String),
}
impl How {
#[must_use]
#[inline(never)]
#[track_caller]
pub fn new(context: impl IntoContext) -> Self {
let location = Location::caller();
Self(Box::new(HowInner {
classified: false,
context: {
let mut vec = Vec::with_capacity(4);
vec.push(format!("At {}", std::panic::Location::caller()).into_context());
vec.push(context.into_context());
vec
},
location,
context: Vec::with_capacity(4),
#[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace::capture(),
backtrace: {
let bt = std::backtrace::Backtrace::capture();
match bt.status() {
BacktraceStatus::Disabled => Backtrace::Disabled,
BacktraceStatus::Unsupported => Backtrace::Unsupported,
status => Backtrace::Other(status, bt.to_string()),
}
},
}))
.context(context)
}
#[must_use]
pub fn clone_without_backtrace(&self) -> Self {
Self(Box::new(HowInner {
classified: self.0.classified,
location: self.0.location,
context: self.0.context.clone(),
#[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace::disabled(),
backtrace: Backtrace::Disabled,
}))
}
#[inline]
#[must_use]
pub fn classified(mut self) -> Self {
self.0.classified = true;
self
}
#[inline]
#[must_use]
pub const fn is_classified(&self) -> bool {
self.0.classified
}
pub fn location(&self) -> &Context {
// SAFETY: we only ever push values into context, and the constructor ensures that there
// are at least 2 values in context.
let o = self.0.context.get(0);
if cfg!(debug_assertions) {
o.unwrap()
} else {
#[allow(unsafe_code)]
unsafe { o.unwrap_unchecked() }
}
pub fn location(&self) -> &'static Location {
self.0.location
}
pub fn top(&self) -> &Context {
// SAFETY: we only ever push values into context, and the constructor ensures that there
// are at least 2 values in context.
let o = self.0.context.get(1);
// is at least 1 value in context.
let o = self.0.context.iter().next();
if cfg!(debug_assertions) {
o.unwrap()
} else {
#[allow(unsafe_code)]
unsafe { o.unwrap_unchecked() }
unsafe {
o.unwrap_unchecked()
}
}
}
pub fn bottom(&self) -> &Context {
// SAFETY: we only ever push values into context, and the constructor ensures that there
// are at least 2 values in context.
// is at least 1 value in context.
let o = self.0.context.iter().next_back();
if cfg!(debug_assertions) {
o.unwrap()
} else {
#[allow(unsafe_code)]
unsafe { o.unwrap_unchecked() }
unsafe {
o.unwrap_unchecked()
}
}
}
@ -98,43 +101,62 @@ impl How {
fn fmt_debug_alternate(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut b = f.debug_struct(std::any::type_name::<Self>());
let b = b
.field("classified", &self.0.classified)
.field("context", &(&self.0.context));
b.field("location", &(&self.0.location));
b.field("context", &(&self.0.context));
#[cfg(feature = "backtrace")]
let b = b.field("backtrace", &self.0.backtrace);
b.finish()
}
}
impl explain::Sealed for How {}
impl Explain for How {
type Output = Self;
#[inline(always)]
#[must_use]
fn context(mut self, context: impl IntoContext) -> Self {
self.0.context.push(context.into_context());
self
pub(crate) fn push_context(&mut self, context: Context) {
self.0.context.push(context);
}
}
#[cfg(feature = "clone-with-caveats")]
impl Clone for How {
#[inline]
fn clone(&self) -> Self {
self.clone_without_backtrace()
}
fn clone_from(&mut self, source: &Self) {
self.0.location = source.0.location;
self.0.context.clone_from(&source.0.context);
#[cfg(feature = "backtrace")]
{
self.0.backtrace = Backtrace::Disabled;
}
}
}
impl std::error::Error for How {}
impl std::fmt::Display for How {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut opts = ReportOpts::default();
let mut ctxs = self.0.context.iter().rev();
let ctx = ctxs.next().expect("`How` created with no context.");
report_write!(f, &opts, "{ctx}")?;
for ctx in ctxs {
report_write!(f, &opts, "\n")?;
let opts = ReportOpts::default();
for (i, ctx) in self.0.context.iter().enumerate() {
if i != 0 {
f.write_str("\n")?;
}
report_write!(f, &opts.indent().next(), "{ctx}")?;
opts = opts.indent();
}
#[cfg(feature = "backtrace")]
{
opts = opts.indent();
report_write!(f, &opts, "\n{}", self.0.backtrace)?;
use std::backtrace::BacktraceStatus::*;
let bt = &self.0.backtrace;
match bt {
Backtrace::Unsupported => f.write_str("\nI'd like to show you a backtrace,\n but it's not supported on your platform")?,
Backtrace::Disabled => f.write_str("\nIf you'd like a backtrace,\n try again with RUST_BACKTRACE=1")?,
Backtrace::Other(status, bt) => {
f.write_str(if *status == Captured {
"\nHere is the backtrace:"
} else {
"\nI can't tell if backtraces are working,\n but I'll give it a go:"
})?;
report_write!(f, &opts, "\n{}", bt)?;
}
}
}
Ok(())
}

View File

@ -1,62 +0,0 @@
use crate::How;
mod private {
use crate::How;
#[doc(hidden)]
pub trait IntoResultHow: Sized {
type T;
fn into_result_how(self) -> Result<Self::T, How>;
}
}
pub(crate) use private::IntoResultHow;
impl<T, E> IntoResultHow for Result<T, E>
where
E: std::error::Error,
{
type T = T;
#[inline(always)]
#[track_caller]
fn into_result_how(self) -> Result<Self::T, How> {
#[inline(never)]
#[track_caller]
fn into<E: std::error::Error>(e: E) -> How {
How::new(e.to_string())
}
match self {
Ok(t) => Ok(t),
Err(e) => Err(into(e))
}
}
}
impl<T> IntoResultHow for Result<T, How> {
type T = T;
#[inline(always)]
fn into_result_how(self) -> Result<Self::T, How> {
self
}
}
impl<T> IntoResultHow for Option<T> {
type T = T;
#[inline(always)]
#[track_caller]
fn into_result_how(self) -> Result<Self::T, How> {
#[inline(never)]
#[track_caller]
fn into() -> How {
How::new("Option::None")
}
match self {
Some(t) => Ok(t),
None => Err(into())
}
}
}

View File

@ -1,8 +1,8 @@
#![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
mod sealed;
pub(crate) use sealed::seal;
#![feature(auto_traits)]
#![feature(doc_auto_cfg)]
#![feature(negative_impls)]
mod context;
pub use context::{Context, IntoContext};
@ -10,9 +10,6 @@ pub use context::{Context, IntoContext};
mod report;
pub use report::{Report, ReportOpts};
mod into;
pub(crate) use into::IntoResultHow;
mod explain;
pub use explain::Explain;
@ -25,3 +22,6 @@ mod how;
pub use self::how::How;
pub type Result<T, E = How> = std::result::Result<T, E>;
#[cfg(test)]
mod test;

View File

@ -1,15 +0,0 @@
/// Like [sealed](https://crates.io/crates/sealed) but not a procmacro.
#[doc(hidden)]
#[macro_export]
macro_rules! __sealed__seal {
($vis:vis $mod:ident::$trait:ident$(<$($gen:ident),+>)?) => {
#[doc(hidden)]
mod $mod {
#[doc(hidden)]
pub trait $trait$(<$($gen),+>)? {}
}
#[doc(hidden)]
$vis use $mod::$trait;
};
}
pub use __sealed__seal as seal;

View File

@ -14,7 +14,11 @@ impl Termination for TerminationResult {
Self::Ok => ExitCode::SUCCESS,
Self::Err(e) => {
use std::io::Write;
let _ = writeln!(std::io::stderr(), "{e}");
let _ = writeln!(
std::io::stderr(),
concat!(ansee::styled!(bold, italic, "But, how?"), "\n{}"),
e
);
ExitCode::FAILURE
}

16
src/test.rs Normal file
View File

@ -0,0 +1,16 @@
use super::*;
#[test]
fn test_io_result_context() {
let r: Result<()> =
Err(std::io::Error::new(std::io::ErrorKind::Other, "foo error")).context("bar reason");
assert!(r.is_err());
}
#[test]
fn test_option_context() {
let r = Some(69).context("not nice");
assert!(r.is_ok());
let r = None::<i32>.context("not nice");
assert!(r.is_err());
}