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]
name = "how"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
[features]
default = []
backtrace = []
termination = []
clone-with-caveats = []
termination = ["dep:ansee"]
[dependencies]
ansee = { git = "https://git.pfaff.dev/michael/ansee", optional = true }
[[example]]
name = "output"

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

@ -21,7 +21,10 @@ where
#[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))
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
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.
///
/// 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,81 @@ 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()
}
}
#[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 for How {
type Output = Self;
#[inline(always)]
#[track_caller]
#[must_use]
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
}
}
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

@ -29,7 +29,7 @@ where
}
match self {
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 {
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::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
}