Seems good
This commit is contained in:
parent
fdb14c22ec
commit
a3058961f8
|
@ -1,10 +1,15 @@
|
|||
[package]
|
||||
name = "how"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
backtrace = []
|
||||
termination = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
[[example]]
|
||||
name = "output"
|
||||
required-features = [ "termination" ]
|
||||
|
|
20
README.md
20
README.md
|
@ -1,3 +1,19 @@
|
|||
# How
|
||||
# *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 parser logic 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*.
|
||||
|
||||
```rust,should_panic
|
||||
use how::*;
|
||||
|
||||
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.
|
||||
|
||||
[^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.
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
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"])))?
|
||||
}
|
||||
|
||||
fn main() -> TerminationResult {
|
||||
main0().into()
|
||||
}
|
|
@ -1,38 +1,26 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use crate::{Report, ReportFmt};
|
||||
|
||||
/// Provides context furthering the explanation of *how* you got to an error.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context(ContextInner);
|
||||
|
||||
impl Context {
|
||||
pub fn chain(mut self, other: impl IntoContext) -> Self {
|
||||
if let ContextInner::Compound(ref mut vec) = self.0 {
|
||||
vec.push(other.into_context());
|
||||
} else {
|
||||
self = Context(ContextInner::Compound(vec![self, other.into_context()]))
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Report for Context {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>, opts: &ReportFmt) -> std::fmt::Result {
|
||||
use std::fmt::Write;
|
||||
impl std::fmt::Display for Context {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.0 {
|
||||
ContextInner::String(ref s) => s.fmt(f, opts)?,
|
||||
ContextInner::String(ref s) => f.write_str(s),
|
||||
ContextInner::Compound(ref ctxs) => {
|
||||
let mut opts = *opts;
|
||||
let mut ctxs = ctxs.iter();
|
||||
if let Some(ctx) = ctxs.next() {
|
||||
write!(f, "{ctx}")?;
|
||||
for ctx in ctxs {
|
||||
ctx.fmt(f, &opts)?;
|
||||
f.write_char('\n')?;
|
||||
opts = opts.next();
|
||||
}
|
||||
write!(f, "\n{ctx}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ContextInner {
|
||||
|
@ -43,7 +31,7 @@ enum ContextInner {
|
|||
pub trait IntoContext {
|
||||
fn into_context(self) -> Context;
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
fn chain(self, other: impl IntoContext) -> Context
|
||||
where
|
||||
Self: Sized,
|
||||
|
@ -57,6 +45,20 @@ impl IntoContext for Context {
|
|||
fn into_context(self) -> Context {
|
||||
self
|
||||
}
|
||||
|
||||
/// Chains another piece of context that is equal from a hierarchical perspective.
|
||||
// TODO: should this inline? Would the compiler be allowed to fold the allocations into a
|
||||
// single one with inlining?
|
||||
fn chain(self, other: impl IntoContext) -> Self {
|
||||
let items = match self {
|
||||
Context(ContextInner::Compound(mut items)) => {
|
||||
items.push(other.into_context());
|
||||
items
|
||||
}
|
||||
_ => vec![self, other.into_context()],
|
||||
};
|
||||
Context(ContextInner::Compound(items))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoContext for String {
|
||||
|
@ -74,6 +76,7 @@ impl IntoContext for &'static str {
|
|||
}
|
||||
|
||||
impl IntoContext for Cow<'static, str> {
|
||||
// TODO: should this always inline?
|
||||
#[inline]
|
||||
fn into_context(self) -> Context {
|
||||
Context(ContextInner::String(self))
|
||||
|
@ -90,18 +93,3 @@ where
|
|||
self().into_context()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToContext {
|
||||
fn to_context(&self) -> Context;
|
||||
}
|
||||
|
||||
impl<C, F> ToContext for F
|
||||
where
|
||||
C: IntoContext,
|
||||
F: Fn() -> C,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn to_context(&self) -> Context {
|
||||
self().into_context()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,24 @@
|
|||
use crate::{How, IntoContext};
|
||||
use crate::{How, IntoContext, IntoResultHow};
|
||||
|
||||
pub trait Explain: Sized {
|
||||
type T;
|
||||
crate::seal!(pub(crate) private::Sealed);
|
||||
|
||||
fn explained(self) -> Result<Self::T, How>;
|
||||
pub trait Explain: Sealed {
|
||||
type Output;
|
||||
|
||||
fn context(self, context: impl IntoContext) -> Result<Self::T, How> {
|
||||
self.explained()
|
||||
.context(context)
|
||||
}
|
||||
#[must_use]
|
||||
fn context(self, context: impl IntoContext) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<T, E> Explain for Result<T, E> where E: std::error::Error {
|
||||
type T = T;
|
||||
impl<T, E> Sealed for Result<T, E> where Result<T, E>: IntoResultHow {}
|
||||
|
||||
fn explained(self) -> Result<Self::T, How> {
|
||||
self.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Explain for Result<T, How> {
|
||||
type T = T;
|
||||
|
||||
fn explained(self) -> Result<Self::T, How> {
|
||||
self
|
||||
}
|
||||
impl<T, E> Explain for Result<T, E>
|
||||
where
|
||||
Result<T, E>: IntoResultHow,
|
||||
{
|
||||
type Output = Result<<Self as IntoResultHow>::T, How>;
|
||||
|
||||
#[inline(always)]
|
||||
fn context(self, context: impl IntoContext) -> Result<Self::T, How> {
|
||||
self.map_err(|e| e.context(context))
|
||||
fn context(self, context: impl IntoContext) -> Self::Output {
|
||||
self.into_result_how().map_err(|e| e.context(context))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
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]
|
||||
fn into_result_how(self) -> Result<Self::T, How> {
|
||||
self.map_err(|e| How::new(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResultHow for Result<T, How> {
|
||||
type T = T;
|
||||
|
||||
#[inline(always)]
|
||||
fn into_result_how(self) -> Result<Self::T, How> {
|
||||
self
|
||||
}
|
||||
}
|
112
src/lib.rs
112
src/lib.rs
|
@ -1,87 +1,123 @@
|
|||
#![doc = include_str!("README.md")]
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(feature = "backtrace", feature(backtrace))]
|
||||
|
||||
mod sealed;
|
||||
pub(crate) use sealed::seal;
|
||||
|
||||
mod context;
|
||||
pub use context::{Context, IntoContext, ToContext};
|
||||
pub use context::{Context, IntoContext};
|
||||
|
||||
mod report;
|
||||
pub use report::{Report, ReportFmt};
|
||||
use report::{report_write, Indentation};
|
||||
use report::report_write;
|
||||
pub use report::{Report, ReportOpts};
|
||||
|
||||
mod into;
|
||||
pub(crate) use into::IntoResultHow;
|
||||
|
||||
mod explain;
|
||||
pub use explain::Explain;
|
||||
|
||||
#[cfg(feature = "termination")]
|
||||
mod termination;
|
||||
#[cfg(feature = "termination")]
|
||||
pub use termination::TerminationResult;
|
||||
|
||||
pub type Result<T, E = How> = std::result::Result<T, E>;
|
||||
|
||||
/// Does not implement [`std::error::Error`] to allow a [`From`] implementation for all other error types.
|
||||
#[derive(Debug)]
|
||||
pub struct How {
|
||||
pub struct How(Box<HowInner>);
|
||||
|
||||
struct HowInner {
|
||||
/// When true, the error will cause branchers to abort.
|
||||
classified: bool,
|
||||
// TODO: consider storing this vec inline (sharing the allocation with rest of the struct.
|
||||
// Probably move after `backtrace`)
|
||||
context: Vec<Context>,
|
||||
#[cfg(feature = "backtrace")]
|
||||
backtrace: std::backtrace::Backtrace,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for How {
|
||||
fn fmt(&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);
|
||||
#[cfg(feature = "backtrace")]
|
||||
let b = b.field("backtrace", &self.0.backtrace);
|
||||
b.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl How {
|
||||
#[must_use]
|
||||
pub fn new(context: impl IntoContext) -> Self {
|
||||
Self {
|
||||
Self(Box::new(HowInner {
|
||||
classified: false,
|
||||
context: vec![context.into_context()],
|
||||
context: {
|
||||
let mut vec = Vec::with_capacity(2);
|
||||
vec.push(context.into_context());
|
||||
vec
|
||||
},
|
||||
#[cfg(feature = "backtrace")]
|
||||
backtrace: std::backtrace::Backtrace::capture(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn clone_without_backtrace(&self) -> Self {
|
||||
Self {
|
||||
classified: self.classified,
|
||||
context: self.context.clone(),
|
||||
Self(Box::new(HowInner {
|
||||
classified: self.0.classified,
|
||||
context: self.0.context.clone(),
|
||||
#[cfg(feature = "backtrace")]
|
||||
backtrace: std::backtrace::Backtrace::disabled(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn classified(mut self) -> Self {
|
||||
self.classified = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn context(mut self, context: impl IntoContext) -> Self {
|
||||
self.context.push(context.into_context());
|
||||
#[must_use]
|
||||
pub fn classified(mut self) -> Self {
|
||||
self.0.classified = true;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn is_classified(&self) -> bool {
|
||||
self.classified
|
||||
self.0.classified
|
||||
}
|
||||
}
|
||||
|
||||
impl explain::Sealed for How {}
|
||||
|
||||
impl Explain for How {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
fn context(mut self, context: impl IntoContext) -> Self {
|
||||
self.0.context.push(context.into_context());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for How {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut opts = ReportFmt::default().indent_first(false);
|
||||
report_write!(f, &opts, "Parsing failed")?;
|
||||
for context in self.context.iter().rev() {
|
||||
write!(f, "\n{}└ ", Indentation(opts.indentation()))?;
|
||||
context.fmt(f, &opts)?;
|
||||
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└ ")?;
|
||||
report_write!(f, &opts.indent().next(), "{ctx}")?;
|
||||
opts = opts.indent();
|
||||
}
|
||||
#[cfg(feature = "backtrace")]
|
||||
{
|
||||
write!(f, "\n{}", self.backtrace)?;
|
||||
};
|
||||
opts = opts.indent();
|
||||
report_write!(f, &opts, "\n{}", self.0.backtrace)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<E> for How
|
||||
where
|
||||
E: std::error::Error,
|
||||
{
|
||||
fn from(value: E) -> Self {
|
||||
Self::new(value.to_string())
|
||||
}
|
||||
}
|
||||
|
|
119
src/report.rs
119
src/report.rs
|
@ -1,87 +1,108 @@
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ReportFmt {
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ReportOpts {
|
||||
indentation: usize,
|
||||
is_first: bool,
|
||||
indent_first: bool,
|
||||
was_nl: bool,
|
||||
}
|
||||
|
||||
impl ReportFmt {
|
||||
impl ReportOpts {
|
||||
#[inline]
|
||||
pub fn indent(mut self) -> Self {
|
||||
self.indentation += 1;
|
||||
self.next_line()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn next_line(mut self) -> Self {
|
||||
self.was_nl = true;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn next(mut self) -> Self {
|
||||
self.is_first = false;
|
||||
self.was_nl = false;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn indent_first(mut self, indent_first: bool) -> Self {
|
||||
self.indent_first = indent_first;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the amount of indentation.
|
||||
#[inline]
|
||||
pub fn indentation(&self) -> usize {
|
||||
self.indentation
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn should_indent(&self) -> bool {
|
||||
self.indent_first || !self.is_first
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReportFmt {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indentation: 0,
|
||||
indent_first: true,
|
||||
is_first: true,
|
||||
}
|
||||
fn should_indent(&self) -> bool {
|
||||
self.was_nl
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! report_write {
|
||||
($f:expr, $opts:expr, $msg:literal$(, $($tt:tt)+)?) => {
|
||||
<std::fmt::Arguments<'_> as $crate::Report>::fmt(&format_args!($msg$(, $($tt)+)?), $f, $opts)
|
||||
<::std::fmt::Arguments<'_> as $crate::Report>::fmt(&::std::format_args!($msg$(, $($tt)+)?), $f, $opts)
|
||||
};
|
||||
}
|
||||
pub(crate) use report_write;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Indentation(pub usize);
|
||||
|
||||
impl std::fmt::Display for Indentation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use std::fmt::Write;
|
||||
for _ in 0..self.0 {
|
||||
f.write_char(' ')?;
|
||||
fn write_indent(n: usize, f: &mut impl Write) -> std::fmt::Result {
|
||||
for _ in 0..n {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A more flexible formatting type that is a cross between [`std::fmt::Debug`] and
|
||||
/// [`std::fmt::Display`].
|
||||
/// `Report` should format the output in a manner suitable for debugging, similar to [`std::fmt::Debug`] in terms of detail, but similar to [`std::fmt::Display`] in terms of readability. The options passed to [`Self::fmt`] must be respected by all implementations.
|
||||
pub trait Report {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>, opts: &ReportFmt) -> std::fmt::Result;
|
||||
/// Formats the value using the given formatter and options.
|
||||
fn fmt(&self, f: &mut impl Write, opts: &ReportOpts) -> std::fmt::Result;
|
||||
}
|
||||
|
||||
impl<T> Report for T
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>, opts: &ReportFmt) -> std::fmt::Result {
|
||||
if opts.should_indent() {
|
||||
write!(f, "{}", Indentation(opts.indentation()))?;
|
||||
fn fmt(&self, f: &mut impl Write, opts: &ReportOpts) -> std::fmt::Result {
|
||||
use std::fmt::Error;
|
||||
struct IndentedWrite<'a, W: Write> {
|
||||
f: &'a mut W,
|
||||
n: usize,
|
||||
}
|
||||
impl<'a, W> Write for IndentedWrite<'a, W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write_str(&mut self, s: &str) -> Result<(), Error> {
|
||||
// TODO: any room for optimization?
|
||||
// iterates over the lines where each str ends with the line terminator.
|
||||
// after giving this a bit of thought I think it is best to indent after any
|
||||
// trailing newline.
|
||||
let mut ss = s.split_inclusive('\n');
|
||||
if let Some(mut s) = ss.next() {
|
||||
self.f.write_str(s)?;
|
||||
for s_next in ss {
|
||||
write_indent(self.n, &mut self.f)?;
|
||||
self.f.write_str(s_next)?;
|
||||
s = s_next;
|
||||
}
|
||||
if matches!(s.chars().rev().next(), Some('\n')) {
|
||||
write_indent(self.n, &mut self.f)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_char(&mut self, c: char) -> Result<(), Error> {
|
||||
self.f.write_char(c)?;
|
||||
if c == '\n' {
|
||||
write_indent(self.n, &mut self.f)?;
|
||||
}
|
||||
<T as std::fmt::Display>::fmt(self, f)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
if opts.indentation > 0 {
|
||||
if opts.should_indent() {
|
||||
write_indent(opts.indentation, f)?;
|
||||
}
|
||||
IndentedWrite {
|
||||
f,
|
||||
n: opts.indentation,
|
||||
}
|
||||
.write_fmt(format_args!("{self}"))
|
||||
} else {
|
||||
f.write_fmt(format_args!("{self}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/// 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;
|
|
@ -0,0 +1,31 @@
|
|||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use crate::How;
|
||||
|
||||
pub enum TerminationResult {
|
||||
Ok,
|
||||
Err(How),
|
||||
}
|
||||
|
||||
impl Termination for TerminationResult {
|
||||
fn report(self) -> ExitCode {
|
||||
match self {
|
||||
Self::Ok => ExitCode::SUCCESS,
|
||||
Self::Err(e) => {
|
||||
use std::io::Write;
|
||||
let _ = writeln!(std::io::stderr(), "{e}");
|
||||
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<(), How>> for TerminationResult {
|
||||
fn from(value: Result<(), How>) -> Self {
|
||||
match value {
|
||||
Ok(()) => Self::Ok,
|
||||
Err(e) => Self::Err(e),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue