use std::backtrace::BacktraceStatus;
use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt;
use super::EnrichmentEntry;
use super::backtrace::Backtrace;
use super::source::Source;
#[derive(Debug, Clone)]
pub struct Inner {
pub(super) source: Source,
pub(super) backtrace: Backtrace,
pub(super) enrichment: Vec<EnrichmentEntry>,
}
#[derive(Clone)]
pub struct OhnoCore {
pub(super) data: Box<Inner>,
}
impl OhnoCore {
#[must_use]
pub fn new() -> Self {
Self::from_source(Source::None)
}
pub fn without_backtrace(error: impl Into<Box<dyn StdError + Send + Sync + 'static>>) -> Self {
Self {
data: Box::new(Inner {
source: Source::Error(error.into().into()),
backtrace: Backtrace::disabled(),
enrichment: Vec::new(),
}),
}
}
fn from_source(source: Source) -> Self {
Self {
data: Box::new(Inner {
source,
backtrace: Backtrace::capture(),
enrichment: Vec::new(),
}),
}
}
#[must_use]
pub fn source(&self) -> Option<&(dyn StdError + 'static)> {
match &self.data.source {
Source::Error(source) => Some(source.as_ref()),
Source::Transparent(source) => source.source(),
Source::None => None,
}
}
#[must_use]
pub fn has_backtrace(&self) -> bool {
matches!(self.data.backtrace.status(), BacktraceStatus::Captured)
}
pub fn backtrace(&self) -> &std::backtrace::Backtrace {
self.data.backtrace.as_backtrace()
}
pub fn enrichments(&self) -> impl Iterator<Item = &EnrichmentEntry> {
self.data.enrichment.iter()
}
pub fn enrichment_messages(&self) -> impl Iterator<Item = &str> {
self.data.enrichment.iter().map(|ctx| ctx.message.as_ref())
}
#[must_use]
pub fn format_message(&self, default_message: &str, override_message: Option<Cow<'_, str>>) -> String {
MessageFormatter {
core: self,
default_message,
override_message,
}
.to_string()
}
pub fn format_error(&self, f: &mut fmt::Formatter<'_>, default_message: &str, override_message: Option<Cow<'_, str>>) -> fmt::Result {
let m = MessageFormatter {
core: self,
default_message,
override_message,
};
std::fmt::Display::fmt(&m, f)?;
for ctx in &self.data.enrichment {
write!(f, "\n> {ctx}")?;
}
if matches!(self.data.backtrace.status(), BacktraceStatus::Captured) {
write!(f, "\n\nBacktrace:\n{}", self.data.backtrace.as_backtrace())?;
}
Ok(())
}
}
impl std::fmt::Debug for OhnoCore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OhnoCore")
.field("source", &self.data.source)
.field("backtrace", &self.data.backtrace)
.field("enrichment", &self.data.enrichment)
.finish()
}
}
impl fmt::Display for OhnoCore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.format_error(f, "", None)
}
}
impl Default for OhnoCore {
fn default() -> Self {
Self::new()
}
}
impl<T> From<T> for OhnoCore
where
T: Into<Box<dyn StdError + Send + Sync>>,
{
fn from(value: T) -> Self {
if is_string_error(&value) {
Self::from_source(Source::Transparent(value.into().into()))
} else {
Self::from_source(Source::Error(value.into().into()))
}
}
}
const STR_TYPE_IDS: [typeid::ConstTypeId; 3] = [
typeid::ConstTypeId::of::<&str>(),
typeid::ConstTypeId::of::<String>(),
typeid::ConstTypeId::of::<Cow<'_, str>>(),
];
fn is_string_error<T>(_: &T) -> bool {
let typeid_of_t = typeid::of::<T>();
STR_TYPE_IDS.iter().any(|&id| id == typeid_of_t)
}
struct MessageFormatter<'a> {
core: &'a OhnoCore,
default_message: &'a str,
override_message: Option<Cow<'a, str>>,
}
impl fmt::Display for MessageFormatter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const CAUSED_BY: &str = "caused by:";
let MessageFormatter {
core,
default_message,
override_message,
} = self;
match (override_message, &core.data.source) {
(Some(msg), Source::Transparent(source) | Source::Error(source)) => {
write!(f, "{msg}\n{CAUSED_BY} {source}")
}
(Some(msg), Source::None) => write!(f, "{msg}"),
(None, Source::Transparent(source) | Source::Error(source)) => write!(f, "{source}"),
(None, Source::None) => write!(f, "{default_message}"),
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::enrichable::Enrichable;
#[test]
fn test_default() {
let error = OhnoCore::default();
assert!(matches!(error.data.source, Source::None));
}
#[test]
fn test_format_error() {
let error = OhnoCore::from("test error");
let result = error.to_string();
assert!(result.contains("test error"));
}
#[test]
fn test_new() {
let error = OhnoCore::new();
assert!(matches!(error.data.source, Source::None));
assert!(error.data.enrichment.is_empty());
}
#[test]
fn test_from_string() {
let error = OhnoCore::from("msg");
assert!(error.source().is_none());
if let Source::Transparent(source) = &error.data.source {
assert_eq!(source.to_string(), "msg");
}
assert!(matches!(&error.data.source, Source::Transparent(_)), "expected transparent source");
}
#[test]
fn test_caused_by_without_backtrace() {
let io_error = std::io::Error::other("io error");
let error = OhnoCore::without_backtrace(io_error);
assert!(matches!(error.data.source, Source::Error(_)));
assert!(!error.has_backtrace());
assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
}
#[test]
fn test_caused_by() {
let io_error = std::io::Error::other("io error");
let error = OhnoCore::from(io_error);
assert!(matches!(error.data.source, Source::Error(_)));
assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
}
#[test]
fn test_from_boxed_error() {
let io_error = std::io::Error::other("io error");
let boxed: Box<dyn StdError + Send + Sync> = Box::new(io_error);
let error = OhnoCore::from(boxed);
assert!(matches!(error.data.source, Source::Error(_)));
assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
}
#[test]
fn test_from_boxed_error_2() {
let io_error = std::io::Error::other("io error");
let boxed: Box<dyn StdError + Send + Sync> = Box::new(io_error);
let error: OhnoCore = boxed.into();
assert!(matches!(error.data.source, Source::Error(_)));
assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
}
#[test]
fn test_enrichment_iter_and_messages() {
let mut error = OhnoCore::from("msg");
error.add_enrichment(EnrichmentEntry::new("ctx1", "test.rs", 1));
error.add_enrichment(EnrichmentEntry::new("ctx2", "test.rs", 2));
let messages: Vec<_> = error.enrichment_messages().collect();
assert_eq!(messages, vec!["ctx1", "ctx2"]);
}
#[test]
fn test_display_and_debug() {
let error = OhnoCore::from("msg");
let display = format!("{error}");
assert!(display.starts_with("msg"));
let debug = format!("{error:?}");
assert!(debug.contains("OhnoCore"));
}
#[test]
fn test_from_string_impls() {
let s = "abc";
let error1: OhnoCore = s.into();
assert!(error1.to_string().starts_with("abc"));
assert!(matches!(error1.data.source, Source::Transparent(_)));
let error2: OhnoCore = String::from("def").into();
assert!(error2.to_string().starts_with("def"));
assert!(matches!(error2.data.source, Source::Transparent(_)));
let error3: OhnoCore = Cow::Borrowed("ghi").into();
assert!(error3.to_string().starts_with("ghi"));
assert!(matches!(error3.data.source, Source::Transparent(_)));
}
#[test]
fn test_from_boxed_error_impl() {
let io_error = std::io::Error::other("io error");
let boxed: Box<dyn StdError + Send + Sync> = Box::new(io_error);
let error: OhnoCore = boxed.into();
assert!(matches!(error.data.source, Source::Error(_)));
assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
}
#[test]
fn test_from_io_error_impl() {
let io_error = std::io::Error::other("io error");
let error: OhnoCore = io_error.into();
assert!(matches!(error.data.source, Source::Error(_)));
assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
}
#[test]
#[cfg_attr(miri, ignore)] fn force_backtrace_capture() {
let mut error = OhnoCore::from("test error with backtrace");
error.data.backtrace = Backtrace::force_capture();
assert!(error.has_backtrace());
let backtrace = error.backtrace();
assert_eq!(backtrace.status(), BacktraceStatus::Captured);
let display = format!("{error}");
assert!(display.starts_with("test error with backtrace\n\nBacktrace:\n"));
}
#[test]
fn no_backtrace_capture() {
let mut error = OhnoCore::from("test error without backtrace");
error.data.backtrace = Backtrace::disabled();
assert!(!error.has_backtrace());
assert_eq!(error.backtrace().status(), BacktraceStatus::Disabled);
let display = format!("{error}");
assert_eq!(display, "test error without backtrace");
}
#[test]
fn is_string_error_test() {
assert!(is_string_error(&"a string slice"));
assert!(is_string_error(&String::from("a string")));
assert!(is_string_error(&Cow::Borrowed("a string slice")));
assert!(is_string_error(&Cow::<'static, str>::Owned(String::from("a string"))));
assert!(!is_string_error(&std::io::Error::other("an io error")));
}
}