Seems good

This commit is contained in:
Michael Pfaff 2022-07-21 20:40:44 -04:00
parent fdb14c22ec
commit a3058961f8
Signed by: michael
GPG Key ID: CF402C4A012AA9D4
10 changed files with 306 additions and 154 deletions

View File

@ -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" ]

View File

@ -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.

14
examples/output.rs Normal file
View File

@ -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()
}

View File

@ -1,36 +1,24 @@
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;
for ctx in ctxs {
ctx.fmt(f, &opts)?;
f.write_char('\n')?;
opts = opts.next();
let mut ctxs = ctxs.iter();
if let Some(ctx) = ctxs.next() {
write!(f, "{ctx}")?;
for ctx in ctxs {
write!(f, "\n{ctx}")?;
}
}
Ok(())
}
}
Ok(())
}
}
@ -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()
}
}

View File

@ -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))
}
}

35
src/into.rs Normal file
View File

@ -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
}
}

View File

@ -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())
}
}

View File

@ -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(' ')?;
}
Ok(())
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)?;
}
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}"))
}
<T as std::fmt::Display>::fmt(self, f)?;
Ok(())
}
}

15
src/sealed.rs Normal file
View File

@ -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;

31
src/termination.rs Normal file
View File

@ -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),
}
}
}