mod macros;
use core::{
any::{Any, TypeId},
fmt::{Debug, Display},
};
use std::{collections::HashSet, sync::LazyLock};
pub static LOG_LEVEL: LazyLock<String> = LazyLock::new(|| {
if let Ok(l) = std::env::var("RUC_LOG_LEVEL")
&& "ERROR" == l
{
return "ERROR".to_owned();
}
"INFO".to_owned()
});
static PID: LazyLock<u32> = LazyLock::new(std::process::id);
static PID_NS: LazyLock<String> =
LazyLock::new(|| get_pidns(*PID).unwrap_or_else(|_| "UNKNOWN".to_owned()));
pub type Result<T> = core::result::Result<T, Box<dyn RucError>>;
pub trait RucError: Display + Debug + Send {
fn type_id(&self) -> TypeId;
fn type_ids(&self) -> Vec<TypeId> {
let mut res = Vec::new();
res.push(self.type_id());
let mut current = self.cause();
while let Some(c) = current {
res.push(c.type_id());
current = c.cause();
}
res
}
fn lowest_type_id(&self) -> TypeId {
*self.type_ids().last().unwrap()
}
fn lowest_is_type(&self, e: &dyn Any) -> bool {
self.lowest_type_id() == e.type_id()
}
fn contains_type(&self, e: &dyn Any) -> bool {
self.type_ids().contains(&e.type_id())
}
fn msg_eq(&self, another: &dyn RucError) -> bool {
self.get_lowest_msg() == another.get_lowest_msg()
}
fn msg_has_overlap(&self, another: &dyn RucError) -> bool {
let mut self_list = HashSet::new();
self_list.insert(self.get_top_msg());
let mut b = self.cause();
while let Some(next) = b {
self_list.insert(next.get_top_msg());
b = next.cause();
}
if self_list.contains(&another.get_top_msg()) {
return true;
}
b = another.cause();
while let Some(next) = b {
if self_list.contains(&next.get_top_msg()) {
return true;
}
b = next.cause();
}
false
}
fn get_top_msg(&self) -> String;
fn get_lowest_msg(&self) -> String;
fn get_lowest_err(&self) -> &dyn RucError;
fn get_top_msg_with_dbginfo(&self) -> String;
fn cause(&self) -> Option<&dyn RucError> {
None
}
fn stringify_chain(&self, prefix: Option<&str>) -> String {
let mut res =
format!("{}{}: ", delimiter(), prefix.unwrap_or("ERROR"));
res.push_str(&self.get_top_msg_with_dbginfo());
let mut e = self.cause();
let mut indent_num = 0;
while let Some(c) = e {
let mut prefix = delimiter().to_owned();
(0..indent_num).for_each(|_| {
prefix.push_str(indent());
});
res.push_str(&prefix);
res.push_str("Caused By: ");
res.push_str(&c.get_top_msg_with_dbginfo().replace('\n', &prefix));
indent_num += 1;
e = c.cause();
}
res
}
#[inline(always)]
fn print_die(&self) -> ! {
self.print(None);
panic!();
}
#[inline(always)]
fn generate_log(&self, prefix: Option<&str>) -> String {
self.generate_log_custom(prefix)
}
fn generate_log_custom(&self, prefix: Option<&str>) -> String {
#[cfg(feature = "ansi")]
#[inline(always)]
fn generate_log_header(ns: &str, pid: u32) -> String {
format!(
"\n\x1b[31;01m# {time} [pid: {pid}] [pidns: {ns}]\x1b[00m",
time = crate::datetime!(),
pid = pid,
ns = ns,
)
}
#[cfg(not(feature = "ansi"))]
#[inline(always)]
fn generate_log_header(ns: &str, pid: u32) -> String {
format!(
"\n# {time} [pid: {pid}] [pidns: {ns}]",
time = crate::datetime!(),
pid = pid,
ns = ns,
)
}
let mut res = generate_log_header(&PID_NS, *PID);
res.push_str(&self.stringify_chain(prefix));
res
}
#[inline(always)]
fn print(&self, prefix: Option<&str>) {
let msg = self.generate_log(prefix);
eprintln!("{}", msg);
}
}
pub trait RucResult<T, E: Debug + Display + Send> {
fn c(self, msg: SimpleMsg<E>) -> Result<T>;
}
impl<T, E: Debug + Display + Send> RucResult<T, E> for Option<T> {
#[inline(always)]
fn c(self, msg: SimpleMsg<E>) -> Result<T> {
self.ok_or_else(|| SimpleError::new(msg, None).into())
}
}
impl<T, E: Debug + Display + Send, ERR: Display + Send + 'static>
RucResult<T, E> for core::result::Result<T, ERR>
{
#[inline(always)]
fn c(self, msg: SimpleMsg<E>) -> Result<T> {
self.map_err(|e| {
let err_str = e.to_string();
let any_e: Box<dyn Any + Send> = Box::new(e);
let cause: Box<dyn RucError> =
match any_e.downcast::<Box<dyn RucError>>() {
Ok(ruc_err) => *ruc_err,
Err(_) => {
let inner = SimpleMsg::new(
err_str, msg.file, msg.line, msg.column,
);
Box::new(SimpleError::new(inner, None))
}
};
SimpleError::new(msg, Some(cause)).into()
})
}
}
#[derive(Debug)]
pub struct SimpleError<E: Debug + Display + Send + 'static> {
msg: SimpleMsg<E>,
cause: Option<Box<dyn RucError>>,
}
impl<E: Debug + Display + Send + 'static> SimpleError<E> {
#[inline(always)]
pub fn new(msg: SimpleMsg<E>, cause: Option<Box<dyn RucError>>) -> Self {
SimpleError { msg, cause }
}
}
impl<E: Debug + Display + Send + 'static> Display for SimpleError<E> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.generate_log(None))
}
}
impl<E: Debug + Display + Send + 'static> From<SimpleError<E>>
for Box<dyn RucError>
{
fn from(e: SimpleError<E>) -> Box<dyn RucError> {
Box::new(e)
}
}
impl<E: Debug + Display + Send + 'static> std::error::Error
for SimpleError<E>
{
}
impl From<&'static str> for Box<dyn RucError> {
fn from(s: &'static str) -> Self {
crate::eg!(s)
}
}
impl From<String> for Box<dyn RucError> {
fn from(s: String) -> Self {
crate::eg!(s)
}
}
impl<E: Debug + Display + Send + 'static> RucError for SimpleError<E> {
fn type_id(&self) -> TypeId {
TypeId::of::<E>()
}
#[inline(always)]
fn get_top_msg(&self) -> String {
self.msg.err.to_string()
}
#[inline(always)]
fn get_lowest_msg(&self) -> String {
if let Some(next) = self.cause.as_ref() {
next.get_lowest_msg()
} else {
self.msg.err.to_string()
}
}
fn get_lowest_err(&self) -> &dyn RucError {
if let Some(next) = self.cause.as_ref() {
next.get_lowest_err()
} else {
self
}
}
#[inline(always)]
fn get_top_msg_with_dbginfo(&self) -> String {
self.msg.to_string()
}
#[inline(always)]
fn cause(&self) -> Option<&dyn RucError> {
self.cause.as_deref()
}
}
#[derive(Debug)]
pub struct SimpleMsg<E: Debug + Display + Send + 'static> {
pub err: E,
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl<E: Debug + Display + Send + 'static> SimpleMsg<E> {
#[inline(always)]
pub fn new(err: E, file: &'static str, line: u32, column: u32) -> Self {
SimpleMsg {
err,
file,
line,
column,
}
}
}
impl<E: Debug + Display + Send + 'static> Display for SimpleMsg<E> {
#[cfg(feature = "ansi")]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"\x1b[01m{0}\x1b[00m{4}{5}\x1b[01mfile:\x1b[00m {1}{4}{5}\x1b[01mline:\x1b[00m {2}{4}{6}\x1b[01mcolumn:\x1b[00m {3}",
self.err,
self.file,
self.line,
self.column,
delimiter(),
pretty()[0],
pretty()[1]
)
}
#[cfg(not(feature = "ansi"))]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{0}{4}{5}file: {1}{4}{5}line: {2}{4}{6}column: {3}",
self.err,
self.file,
self.line,
self.column,
delimiter(),
pretty()[0],
pretty()[1]
)
}
}
impl<E: Debug + Display + Send + 'static> From<SimpleMsg<E>>
for Box<dyn RucError>
{
fn from(m: SimpleMsg<E>) -> Self {
SimpleError::new(m, None).into()
}
}
#[inline(always)]
#[cfg(target_os = "linux")]
fn get_pidns(pid: u32) -> Result<String> {
std::fs::read_link(format!("/proc/{pid}/ns/pid"))
.c(crate::d!())
.map(|p| {
p.to_string_lossy()
.trim_start_matches("pid:[")
.trim_end_matches(']')
.to_owned()
})
}
#[inline(always)]
#[cfg(not(target_os = "linux"))]
#[allow(clippy::unnecessary_wraps)]
fn get_pidns(_pid: u32) -> Result<String> {
Ok("NULL".to_owned())
}
#[cfg(not(feature = "compact"))]
const fn delimiter() -> &'static str {
"\n"
}
#[cfg(feature = "compact")]
const fn delimiter() -> &'static str {
" 》"
}
#[cfg(not(feature = "compact"))]
const fn indent() -> &'static str {
" "
}
#[cfg(feature = "compact")]
const fn indent() -> &'static str {
""
}
#[cfg(all(not(feature = "compact"), feature = "ansi"))]
const fn pretty() -> [&'static str; 2] {
["├──", "└──"]
}
#[cfg(all(not(feature = "compact"), not(feature = "ansi")))]
const fn pretty() -> [&'static str; 2] {
["|--", "`--"]
}
#[cfg(feature = "compact")]
const fn pretty() -> [&'static str; 2] {
["", ""]
}
#[cfg(test)]
mod test {
use super::*;
use std::process;
#[test]
fn t_get_pidns() {
let ns_name = crate::pnk!(get_pidns(process::id()));
assert!(1 < ns_name.len());
}
#[test]
fn t_error_chain() {
let res: Result<i32> = Err(SimpleError::new(
SimpleMsg::new("***", "/tmp/xx.rs", 9, 90),
None,
)
.into());
println!(
"{}",
res.c(SimpleMsg::new("cat", "/tmp/xx.rs", 1, 10))
.c(SimpleMsg::new("dog", "/tmp/xx.rs", 2, 20))
.c(SimpleMsg::new("pig", "/tmp/xx.rs", 3, 30))
.unwrap_err()
.stringify_chain(None)
);
let e1: Box<dyn RucError> =
SimpleError::new(SimpleMsg::new("***", "/tmp/xx.rs", 9, 90), None)
.into();
let e2: Box<dyn RucError> =
SimpleError::new(SimpleMsg::new("***", "/tmp/xx.rs", 9, 90), None)
.into();
assert!(e1.msg_eq(e2.as_ref()));
assert!(e1.lowest_is_type(&""));
assert!(e2.lowest_is_type(&""));
assert_eq!(e2.lowest_type_id(), TypeId::of::<&str>());
}
#[test]
fn t_error_chain_ids() {
let e1 = SimpleError::new(SimpleMsg::new("root", "f", 1, 1), None);
let e2 = SimpleError::new(
SimpleMsg::new("cause", "f", 2, 2),
Some(Box::new(e1)),
);
let ids = e2.type_ids();
assert_eq!(ids.len(), 2);
}
#[test]
fn t_chain_string_error() {
let r: core::result::Result<(), String> =
Err("string error".to_owned());
let r = r.c(crate::d!("wrapping string"));
assert!(r.is_err());
let e = r.unwrap_err();
assert!(e.get_lowest_msg().contains("string error"));
}
#[test]
fn t_chain_str_error() {
let r: core::result::Result<(), &str> = Err("str error");
let r = r.c(crate::d!("wrapping str"));
assert!(r.is_err());
let e = r.unwrap_err();
assert!(e.get_lowest_msg().contains("str error"));
}
#[test]
fn t_chain_preserves_ruc_error() {
let l1 = || -> Result<()> { Err(crate::eg!("root cause")) };
let l2 = || -> Result<()> { l1().c(crate::d!("level 2")) };
let l3 = || -> Result<()> { l2().c(crate::d!("level 3")) };
let e = l3().unwrap_err();
let ids = e.type_ids();
assert_eq!(ids.len(), 3);
assert!(e.get_lowest_msg().contains("root cause"));
}
}