use std::any::Any;
use std::fmt::{self, Debug, Display};
use std::sync::Arc;
pub trait GoishError: Display + Debug + Send + Sync + 'static {
fn Error(&self) -> String { format!("{}", self) }
fn Unwrap(&self) -> error { nil }
fn as_any(&self) -> &dyn Any;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GoError {
msg: String,
source: Option<Box<GoError>>,
msg_includes_source: bool,
}
impl GoError {
fn new(msg: impl Into<String>) -> Self {
GoError { msg: msg.into(), source: None, msg_includes_source: false }
}
}
impl Display for GoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.msg)?;
if !self.msg_includes_source {
if let Some(ref src) = self.source {
write!(f, ": {}", src)?;
}
}
Ok(())
}
}
impl std::error::Error for GoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_deref().map(|s| s as &dyn std::error::Error)
}
}
#[derive(Debug, Clone)]
enum ErrorKind {
Builtin(GoError),
Custom(Arc<dyn GoishError>),
}
#[derive(Debug, Clone, Default)]
pub struct error(Option<ErrorKind>);
impl PartialEq for error {
fn eq(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(None, None) => true,
(Some(ErrorKind::Builtin(a)), Some(ErrorKind::Builtin(b))) => a == b,
(Some(ErrorKind::Custom(a)), Some(ErrorKind::Custom(b))) => Arc::ptr_eq(a, b),
_ => false,
}
}
}
impl Eq for error {}
impl error {
pub fn is_nil(&self) -> bool { self.0.is_none() }
pub fn Error(&self) -> String {
match &self.0 {
Some(ErrorKind::Builtin(e)) => format!("{}", e),
Some(ErrorKind::Custom(a)) => a.Error(),
None => panic!("runtime error: invalid memory address or nil pointer dereference"),
}
}
pub fn downcast_ref<T: GoishError>(&self) -> Option<&T> {
match &self.0 {
Some(ErrorKind::Custom(a)) => a.as_any().downcast_ref::<T>(),
_ => None,
}
}
}
impl Display for error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
Some(ErrorKind::Builtin(e)) => Display::fmt(e, f),
Some(ErrorKind::Custom(a)) => Display::fmt(&**a, f),
None => write!(f, "<nil>"),
}
}
}
pub trait IsNil {
fn is_nil(&self) -> bool;
}
impl IsNil for error {
fn is_nil(&self) -> bool { self.0.is_none() }
}
#[allow(non_upper_case_globals)]
pub const nil: error = error(None);
#[macro_export]
macro_rules! var {
({ $( $name:ident = $expr:expr );+ $(;)? }) => {
$( $crate::var!($name = $expr); )+
};
({ $( $name:ident $t:ty = $expr:expr );+ $(;)? }) => {
$( $crate::var!($name $t = $expr); )+
};
($name:ident = $expr:expr) => {
#[allow(non_snake_case)]
pub fn $name() -> $crate::errors::error {
static __ONCE: ::std::sync::OnceLock<$crate::errors::error>
= ::std::sync::OnceLock::new();
__ONCE.get_or_init(|| $expr).clone()
}
};
($name:ident $t:ty = $expr:expr) => {
#[allow(non_snake_case)]
pub fn $name() -> $t {
static __ONCE: ::std::sync::OnceLock<$t>
= ::std::sync::OnceLock::new();
__ONCE.get_or_init(|| $expr).clone()
}
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! static_err {
($name:ident = $msg:expr) => {
$crate::var!($name = $crate::errors::New($msg));
};
}
pub fn New(msg: &str) -> error {
error(Some(ErrorKind::Builtin(GoError::new(msg))))
}
#[allow(non_snake_case)]
pub fn FromDyn<T: GoishError>(e: T) -> error {
error(Some(ErrorKind::Custom(Arc::new(e))))
}
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn New_with_source(msg: &str, source: error) -> error {
let source_builtin = match source.0 {
Some(ErrorKind::Builtin(g)) => Some(g),
Some(ErrorKind::Custom(a)) => Some(GoError::new(a.Error())),
None => None,
};
match source_builtin {
Some(inner) => error(Some(ErrorKind::Builtin(GoError {
msg: msg.to_string(),
source: Some(Box::new(inner)),
msg_includes_source: true,
}))),
None => New(msg),
}
}
pub fn Wrap(err: error, msg: &str) -> error {
let inner = match err.0 {
Some(ErrorKind::Builtin(g)) => g,
Some(ErrorKind::Custom(a)) => GoError::new(a.Error()),
None => return nil,
};
error(Some(ErrorKind::Builtin(GoError {
msg: msg.to_string(),
source: Some(Box::new(inner)),
msg_includes_source: false,
})))
}
pub fn Is(err: &error, target: &error) -> bool {
if target.0.is_none() { return err.0.is_none(); }
let mut cur = err.clone();
loop {
if cur == *target { return true; }
if let (Some(ErrorKind::Builtin(c)), Some(ErrorKind::Builtin(t))) =
(&cur.0, &target.0)
{
if c.msg == t.msg { return true; }
}
let next = Unwrap(cur.clone());
if next == nil { return false; }
cur = next;
}
}
pub fn Unwrap(err: error) -> error {
match err.0 {
Some(ErrorKind::Builtin(g)) => match g.source {
Some(src) => error(Some(ErrorKind::Builtin(*src))),
None => nil,
},
Some(ErrorKind::Custom(a)) => a.Unwrap(),
None => nil,
}
}
pub fn Join(errs: &[error]) -> error {
let msgs: Vec<String> = errs
.iter()
.filter_map(|e| match &e.0 {
Some(ErrorKind::Builtin(g)) => Some(g.msg.clone()),
Some(ErrorKind::Custom(a)) => Some(a.Error()),
None => None,
})
.collect();
if msgs.is_empty() {
return nil;
}
let joined = msgs.join("\n");
error(Some(ErrorKind::Builtin(GoError::new(joined))))
}
pub fn Append(err: error, more: error) -> error {
if err == nil { return more; }
if more == nil { return err; }
Join(&[err, more])
}
pub fn As(err: &error, target: &mut error) -> bool {
let target_msg = match &target.0 {
Some(ErrorKind::Builtin(g)) => g.msg.clone(),
Some(ErrorKind::Custom(a)) => a.Error(),
None => return false,
};
let mut cur_opt = match &err.0 {
Some(ErrorKind::Builtin(g)) => Some(g.clone()),
Some(ErrorKind::Custom(a)) => Some(GoError::new(a.Error())),
None => None,
};
while let Some(e) = cur_opt {
if e.msg == target_msg {
*target = error(Some(ErrorKind::Builtin(e)));
return true;
}
cur_opt = e.source.map(|b| *b);
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn var_macro_error_sentinel() {
crate::var!(ErrMockSentinel = crate::errors::New("mock: short read"));
let a = ErrMockSentinel();
let b = ErrMockSentinel();
assert_eq!(format!("{}", a), "mock: short read");
assert!(crate::errors::Is(&a, &b));
}
#[test]
fn var_macro_typed() {
crate::var!(TheAnswer i64 = 42i64);
assert_eq!(TheAnswer(), 42);
assert_eq!(TheAnswer(), TheAnswer());
}
#[test]
fn static_err_compat() {
crate::static_err!(ErrOldStyle = "old style");
assert_eq!(format!("{}", ErrOldStyle()), "old style");
}
#[test]
fn new_displays_message() {
let e = New("boom");
assert_eq!(format!("{}", e), "boom");
}
#[test]
fn nil_displays_as_nil() {
assert_eq!(format!("{}", nil), "<nil>");
}
#[test]
fn nil_equality() {
let n: error = nil;
assert!(n == nil);
assert!(New("x") != nil);
}
#[test]
fn wrap_chains() {
let inner = New("disk full");
let outer = Wrap(inner, "save failed");
assert_eq!(format!("{}", outer), "save failed: disk full");
}
#[test]
fn wrap_nil_returns_nil() {
assert!(Wrap(nil, "ctx") == nil);
}
#[test]
fn is_walks_chain() {
let sentinel = New("not found");
let wrapped = Wrap(sentinel.clone(), "lookup");
assert!(Is(&wrapped, &sentinel));
assert!(!Is(&wrapped, &New("other")));
}
#[test]
fn unwrap_returns_inner_or_nil() {
let inner = New("inner");
let outer = Wrap(inner, "outer");
assert_eq!(format!("{}", Unwrap(outer)), "inner");
assert!(Unwrap(New("solo")) == nil);
}
#[test]
fn join_combines_messages() {
let e = Join(&[New("a"), New("b"), nil, New("c")]);
assert_eq!(format!("{}", e), "a\nb\nc");
}
#[test]
fn join_of_nils_is_nil() {
assert!(Join(&[nil, nil]) == nil);
assert!(Join(&[]) == nil);
}
#[test]
fn as_finds_wrapped_sentinel() {
let sentinel = New("not found");
let wrapped = Wrap(sentinel.clone(), "lookup");
let mut target = New("not found");
assert!(As(&wrapped, &mut target));
assert_eq!(format!("{}", target), "not found");
let mut target = New("other");
assert!(!As(&wrapped, &mut target));
}
}