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)
This commit is contained in:
Michael Pfaff 2023-06-29 00:00:42 -04:00
parent 35bb62320f
commit 9950d96522
Signed by: michael
GPG Key ID: CF402C4A012AA9D4
7 changed files with 156 additions and 83 deletions

View File

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

View File

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

View File

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

View File

@ -21,7 +21,10 @@ where
#[inline(always)] #[inline(always)]
#[track_caller] #[track_caller]
fn context(self, context: impl IntoContext) -> Self::Output { fn context(self, context: impl IntoContext) -> Self::Output {
self.into_result_how().map_err(#[inline(never)] move |e| e.context(context)) self.into_result_how().map_err(
#[inline(never)]
move |e| e.context(context),
)
} }
} }
@ -34,7 +37,7 @@ impl<T> Explain for Option<T> {
// TODO: maybe add a feature for the extra "Option::None" context // TODO: maybe add a feature for the extra "Option::None" context
match self { match self {
Some(t) => Ok(t), 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)) //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::*;
use crate::report::report_write; use crate::report::report_write;
/// Does not implement [`std::error::Error`] to allow a [`From`] implementation for all other error types. /// Does not implement [`std::error::Error`] to allow a [`From`] implementation for all other error types.
///
/// 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>); pub struct How(Box<HowInner>);
struct HowInner { struct HowInner {
/// When true, the error will cause branchers to abort. location: &'static Location<'static>,
classified: bool,
#[cfg(feature = "backtrace")] #[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace, backtrace: Backtrace,
// TODO: consider storing this vec inline (sharing the allocation with rest of the struct. // TODO: consider storing this vec inline (sharing the allocation with rest of the struct.
// Probably move after `backtrace`) // Probably move after `backtrace`)
context: Vec<Context>, 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 { impl How {
#[must_use] #[must_use]
#[inline(never)] #[inline(never)]
#[track_caller] #[track_caller]
pub fn new(context: impl IntoContext) -> Self { pub fn new(context: impl IntoContext) -> Self {
let location = Location::caller();
Self(Box::new(HowInner { Self(Box::new(HowInner {
classified: false, location,
context: { context: Vec::with_capacity(4),
let mut vec = Vec::with_capacity(4);
vec.push(format!("At {}", std::panic::Location::caller()).into_context());
vec.push(context.into_context());
vec
},
#[cfg(feature = "backtrace")] #[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] #[must_use]
pub fn clone_without_backtrace(&self) -> Self { pub fn clone_without_backtrace(&self) -> Self {
Self(Box::new(HowInner { Self(Box::new(HowInner {
classified: self.0.classified, location: self.0.location,
context: self.0.context.clone(), context: self.0.context.clone(),
#[cfg(feature = "backtrace")] #[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace::disabled(), backtrace: Backtrace::Disabled,
})) }))
} }
#[inline] pub fn location(&self) -> &'static Location {
#[must_use] self.0.location
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 top(&self) -> &Context { pub fn top(&self) -> &Context {
// SAFETY: we only ever push values into context, and the constructor ensures that there // 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.get(1); let o = self.0.context.iter().next();
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
o.unwrap() o.unwrap()
} else { } else {
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe { o.unwrap_unchecked() } unsafe {
o.unwrap_unchecked()
}
} }
} }
pub fn bottom(&self) -> &Context { pub fn bottom(&self) -> &Context {
// SAFETY: we only ever push values into context, and the constructor ensures that there // 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(); let o = self.0.context.iter().next_back();
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
o.unwrap() o.unwrap()
} else { } else {
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe { o.unwrap_unchecked() } unsafe {
o.unwrap_unchecked()
}
} }
} }
@ -98,43 +101,81 @@ impl How {
fn fmt_debug_alternate(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 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 mut b = f.debug_struct(std::any::type_name::<Self>());
let b = b b.field("location", &(&self.0.location));
.field("classified", &self.0.classified) b.field("context", &(&self.0.context));
.field("context", &(&self.0.context));
#[cfg(feature = "backtrace")] #[cfg(feature = "backtrace")]
let b = b.field("backtrace", &self.0.backtrace); let b = b.field("backtrace", &self.0.backtrace);
b.finish() b.finish()
} }
} }
#[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 explain::Sealed for How {} impl explain::Sealed for How {}
impl Explain for How { impl Explain for How {
type Output = Self; type Output = Self;
#[inline(always)] #[inline(always)]
#[track_caller]
#[must_use] #[must_use]
fn context(mut self, context: impl IntoContext) -> Self { fn context(mut self, context: impl IntoContext) -> Self {
self.0.context.push(context.into_context()); use 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.0.context.push(context);
self self
} }
} }
impl std::fmt::Display for How { impl std::fmt::Display for How {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut opts = ReportOpts::default(); let opts = ReportOpts::default();
let mut ctxs = self.0.context.iter().rev(); for (i, ctx) in self.0.context.iter().enumerate() {
let ctx = ctxs.next().expect("`How` created with no context."); if i != 0 {
report_write!(f, &opts, "{ctx}")?; f.write_str("\n")?;
for ctx in ctxs { }
report_write!(f, &opts, "\n")?;
report_write!(f, &opts.indent().next(), "{ctx}")?; report_write!(f, &opts.indent().next(), "{ctx}")?;
opts = opts.indent();
} }
#[cfg(feature = "backtrace")] #[cfg(feature = "backtrace")]
{ {
opts = opts.indent(); use std::backtrace::BacktraceStatus::*;
report_write!(f, &opts, "\n{}", self.0.backtrace)?; 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(()) Ok(())
} }

View File

@ -29,7 +29,7 @@ where
} }
match self { match self {
Ok(t) => Ok(t), Ok(t) => Ok(t),
Err(e) => Err(into(e)) Err(e) => Err(into(e)),
} }
} }
} }
@ -56,7 +56,7 @@ impl<T> IntoResultHow for Option<T> {
} }
match self { match self {
Some(t) => Ok(t), Some(t) => Ok(t),
None => Err(into()) None => Err(into()),
} }
} }
} }

View File

@ -14,7 +14,11 @@ impl Termination for TerminationResult {
Self::Ok => ExitCode::SUCCESS, Self::Ok => ExitCode::SUCCESS,
Self::Err(e) => { Self::Err(e) => {
use std::io::Write; 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 ExitCode::FAILURE
} }