commit fdb14c22ecd1678c350afdb6442735ef0db0c539 Author: Michael Pfaff Date: Thu Jul 21 20:39:47 2022 -0400 Where was I? diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..33e1386 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "how" +version = "0.2.0" +edition = "2021" + +[features] +default = [] +backtrace = [] + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..44f4996 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 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. diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..77a85d9 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,107 @@ +use std::borrow::Cow; + +use crate::{Report, ReportFmt}; + +#[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; + match self.0 { + ContextInner::String(ref s) => s.fmt(f, opts)?, + ContextInner::Compound(ref ctxs) => { + let mut opts = *opts; + for ctx in ctxs { + ctx.fmt(f, &opts)?; + f.write_char('\n')?; + opts = opts.next(); + } + } + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +enum ContextInner { + String(Cow<'static, str>), + Compound(Vec), +} + +pub trait IntoContext { + fn into_context(self) -> Context; + + #[inline] + fn chain(self, other: impl IntoContext) -> Context + where + Self: Sized, + { + self.into_context().chain(other) + } +} + +impl IntoContext for Context { + #[inline(always)] + fn into_context(self) -> Context { + self + } +} + +impl IntoContext for String { + #[inline] + fn into_context(self) -> Context { + Context(ContextInner::String(self.into())) + } +} + +impl IntoContext for &'static str { + #[inline] + fn into_context(self) -> Context { + Context(ContextInner::String(self.into())) + } +} + +impl IntoContext for Cow<'static, str> { + #[inline] + fn into_context(self) -> Context { + Context(ContextInner::String(self)) + } +} + +impl IntoContext for F +where + C: IntoContext, + F: FnOnce() -> C, +{ + #[inline(always)] + fn into_context(self) -> Context { + 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 new file mode 100644 index 0000000..cdb2f3b --- /dev/null +++ b/src/explain.rs @@ -0,0 +1,33 @@ +use crate::{How, IntoContext}; + +pub trait Explain: Sized { + type T; + + fn explained(self) -> Result; + + fn context(self, context: impl IntoContext) -> Result { + self.explained() + .context(context) + } +} + +impl Explain for Result where E: std::error::Error { + type T = T; + + fn explained(self) -> Result { + self.map_err(Into::into) + } +} + +impl Explain for Result { + type T = T; + + fn explained(self) -> Result { + self + } + + #[inline(always)] + fn context(self, context: impl IntoContext) -> Result { + self.map_err(|e| e.context(context)) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..41b420f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,87 @@ +#![doc = include_str!("README.md")] + +#![cfg_attr(feature = "backtrace", feature(backtrace))] + +mod context; +pub use context::{Context, IntoContext, ToContext}; + +mod report; +pub use report::{Report, ReportFmt}; +use report::{report_write, Indentation}; + +mod explain; +pub use explain::Explain; + +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 { + /// When true, the error will cause branchers to abort. + classified: bool, + context: Vec, + #[cfg(feature = "backtrace")] + backtrace: std::backtrace::Backtrace, +} + +impl How { + pub fn new(context: impl IntoContext) -> Self { + Self { + classified: false, + context: vec![context.into_context()], + #[cfg(feature = "backtrace")] + backtrace: std::backtrace::Backtrace::capture(), + } + } + + pub fn clone_without_backtrace(&self) -> Self { + Self { + classified: self.classified, + context: self.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()); + self + } + + #[inline] + pub const fn is_classified(&self) -> bool { + self.classified + } +} + +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)?; + opts = opts.indent(); + } + #[cfg(feature = "backtrace")] + { + write!(f, "\n{}", self.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 new file mode 100644 index 0000000..9618954 --- /dev/null +++ b/src/report.rs @@ -0,0 +1,87 @@ +#[derive(Debug, Clone, Copy)] +pub struct ReportFmt { + indentation: usize, + is_first: bool, + indent_first: bool, +} + +impl ReportFmt { + #[inline] + pub fn indent(mut self) -> Self { + self.indentation += 1; + self + } + + #[inline] + pub fn next(mut self) -> Self { + self.is_first = 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, + } + } +} + +macro_rules! report_write { + ($f:expr, $opts:expr, $msg:literal$(, $($tt:tt)+)?) => { + as $crate::Report>::fmt(&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(()) + } +} + +/// A more flexible formatting type that is a cross between [`std::fmt::Debug`] and +/// [`std::fmt::Display`]. +pub trait Report { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>, opts: &ReportFmt) -> 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()))?; + } + ::fmt(self, f)?; + Ok(()) + } +}