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:
parent
35bb62320f
commit
9950d96522
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
149
src/how.rs
149
src/how.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue