diff --git a/Cargo.toml b/Cargo.toml index 33e1386..a4680b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" ] diff --git a/README.md b/README.md index 44f4996..be77c9c 100644 --- a/README.md +++ b/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. diff --git a/examples/output.rs b/examples/output.rs new file mode 100644 index 0000000..3639ccd --- /dev/null +++ b/examples/output.rs @@ -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() +} diff --git a/src/context.rs b/src/context.rs index 77a85d9..8152020 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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 ToContext for F -where - C: IntoContext, - F: Fn() -> C, -{ - #[inline(always)] - fn to_context(&self) -> Context { - self().into_context() - } -} diff --git a/src/explain.rs b/src/explain.rs index cdb2f3b..33632ef 100644 --- a/src/explain.rs +++ b/src/explain.rs @@ -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; +pub trait Explain: Sealed { + type Output; - fn context(self, context: impl IntoContext) -> Result { - self.explained() - .context(context) - } + #[must_use] + fn context(self, context: impl IntoContext) -> Self::Output; } -impl Explain for Result where E: std::error::Error { - type T = T; +impl Sealed for Result where Result: IntoResultHow {} - fn explained(self) -> Result { - self.map_err(Into::into) - } -} - -impl Explain for Result { - type T = T; - - fn explained(self) -> Result { - self - } +impl Explain for Result +where + Result: IntoResultHow, +{ + type Output = Result<::T, How>; #[inline(always)] - fn context(self, context: impl IntoContext) -> Result { - 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)) } } diff --git a/src/into.rs b/src/into.rs new file mode 100644 index 0000000..d10e564 --- /dev/null +++ b/src/into.rs @@ -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; + } +} + +pub(crate) use private::IntoResultHow; + +impl IntoResultHow for Result +where + E: std::error::Error, +{ + type T = T; + + #[inline] + fn into_result_how(self) -> Result { + self.map_err(|e| How::new(e.to_string())) + } +} + +impl IntoResultHow for Result { + type T = T; + + #[inline(always)] + fn into_result_how(self) -> Result { + self + } +} diff --git a/src/lib.rs b/src/lib.rs index 41b420f..8c9e949 100644 --- a/src/lib.rs +++ b/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 = std::result::Result; /// 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); + +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, #[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::()); + 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 From for How -where - E: std::error::Error, -{ - fn from(value: E) -> Self { - Self::new(value.to_string()) - } -} diff --git a/src/report.rs b/src/report.rs index 9618954..2f56a4e 100644 --- a/src/report.rs +++ b/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)+)?) => { - 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 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}")) } - ::fmt(self, f)?; - Ok(()) } } diff --git a/src/sealed.rs b/src/sealed.rs new file mode 100644 index 0000000..3bad213 --- /dev/null +++ b/src/sealed.rs @@ -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; diff --git a/src/termination.rs b/src/termination.rs new file mode 100644 index 0000000..15b9c11 --- /dev/null +++ b/src/termination.rs @@ -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> for TerminationResult { + fn from(value: Result<(), How>) -> Self { + match value { + Ok(()) => Self::Ok, + Err(e) => Self::Err(e), + } + } +}