use std::cell::RefCell;
pub type Result<T> = std::result::Result<T, Error>;
thread_local! {
static LAST_ERROR: RefCell<Option<Error>> = const { RefCell::new(None) };
}
#[derive(Debug, Clone)]
pub struct Error {
message: String,
}
impl Error {
pub fn new(variant: &str, message: impl Into<String>) -> Self {
Self {
message: format!("{}: {}", variant, message.into()),
}
}
pub fn from_error<E: std::error::Error>(e: E) -> Self {
let debug = format!("{:?}", e);
let variant = debug
.split(['(', '{'])
.next()
.and_then(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
})
.unwrap_or("Unknown");
Self::new(variant, e.to_string())
}
pub fn message(&self) -> &str {
&self.message
}
pub fn variant(&self) -> Option<&str> {
self.message.split_once(": ").map(|(v, _)| v)
}
pub fn details(&self) -> Option<&str> {
self.message.split_once(": ").map(|(_, d)| d)
}
pub fn null_parameter(param: impl Into<String>) -> Self {
Self::new("NullParameter", param.into())
}
pub fn string_too_long(param: impl Into<String>) -> Self {
Self::new("StringTooLong", param.into())
}
pub fn untracked_pointer(ptr: u64) -> Self {
Self::new("UntrackedPointer", format!("0x{:x}", ptr))
}
pub fn wrong_pointer_type(ptr: u64) -> Self {
Self::new("WrongPointerType", format!("0x{:x}", ptr))
}
pub fn mutex_poisoned() -> Self {
Self::new("MutexPoisoned", "thread panic detected")
}
pub fn invalid_buffer_size(size: usize, param: &str) -> Self {
Self::new("InvalidBufferSize", format!("{} for '{}'", size, param))
}
pub fn other(msg: impl Into<String>) -> Self {
Self::new("Other", msg.into())
}
pub fn last_message() -> Option<String> {
LAST_ERROR.with(|prev| prev.borrow().as_ref().map(|e| e.message.clone()))
}
pub fn set_last(self) {
LAST_ERROR.with(|prev| *prev.borrow_mut() = Some(self));
}
pub fn take_last() -> Option<Error> {
LAST_ERROR.with(|prev| prev.borrow_mut().take())
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = Error::new("TestError", "test message");
assert_eq!(err.message(), "TestError: test message");
assert_eq!(err.variant(), Some("TestError"));
assert_eq!(err.details(), Some("test message"));
}
#[test]
fn test_null_parameter_error() {
let err = Error::null_parameter("input_ptr");
assert_eq!(err.variant(), Some("NullParameter"));
assert!(err.message().contains("NullParameter"));
assert!(err.details().unwrap().contains("input_ptr"));
}
#[test]
fn test_string_too_long_error() {
let err = Error::string_too_long("name");
assert_eq!(err.variant(), Some("StringTooLong"));
assert!(err.details().unwrap().contains("name"));
}
#[test]
fn test_untracked_pointer_error() {
let err = Error::untracked_pointer(0xdeadbeef);
assert_eq!(err.variant(), Some("UntrackedPointer"));
assert!(err.details().unwrap().contains("0xdeadbeef"));
}
#[test]
fn test_wrong_pointer_type_error() {
let err = Error::wrong_pointer_type(0x12345678);
assert_eq!(err.variant(), Some("WrongPointerType"));
assert!(err.details().unwrap().contains("0x12345678"));
}
#[test]
fn test_mutex_poisoned_error() {
let err = Error::mutex_poisoned();
assert_eq!(err.variant(), Some("MutexPoisoned"));
assert!(err.details().unwrap().contains("thread panic"));
}
#[test]
fn test_invalid_buffer_size_error() {
let err = Error::invalid_buffer_size(1000, "data");
assert_eq!(err.variant(), Some("InvalidBufferSize"));
assert!(err.details().unwrap().contains("1000"));
assert!(err.details().unwrap().contains("data"));
}
#[test]
fn test_other_error() {
let err = Error::other("custom message");
assert_eq!(err.variant(), Some("Other"));
assert_eq!(err.details(), Some("custom message"));
}
#[test]
fn test_from_error() {
#[derive(Debug)]
enum TestError {
Parse(String),
Validate,
}
impl std::fmt::Display for TestError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
TestError::Parse(s) => write!(f, "parse failed: {}", s),
TestError::Validate => write!(f, "validation failed"),
}
}
}
impl std::error::Error for TestError {}
let test_err = TestError::Parse("bad input".to_string());
let err = Error::from_error(test_err);
assert_eq!(err.variant(), Some("Parse"));
assert!(err.details().unwrap().contains("parse failed"));
let test_err2 = TestError::Validate;
let err2 = Error::from_error(test_err2);
assert_eq!(err2.variant(), Some("Validate"));
assert!(err2.details().unwrap().contains("validation failed"));
}
#[test]
fn test_last_error_storage() {
let err = Error::new("TestError", "test message");
err.set_last();
let msg = Error::last_message();
assert_eq!(msg, Some("TestError: test message".to_string()));
}
#[test]
fn test_last_error_none() {
Error::take_last();
assert_eq!(Error::last_message(), None);
}
#[test]
fn test_take_last_clears_error() {
Error::new("Temporary", "temp").set_last();
let err = Error::take_last();
assert!(err.is_some());
assert_eq!(err.unwrap().variant(), Some("Temporary"));
assert_eq!(Error::last_message(), None);
}
#[test]
fn test_display_trait() {
let err = Error::new("DisplayTest", "test message");
let displayed = format!("{}", err);
assert_eq!(displayed, "DisplayTest: test message");
}
#[test]
fn test_debug_trait() {
let err = Error::new("DebugTest", "test message");
let debugged = format!("{:?}", err);
assert!(debugged.contains("DebugTest: test message"));
}
#[test]
fn test_error_trait() {
let err = Error::new("TraitTest", "test");
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_thread_local_isolation() {
use std::thread;
Error::new("MainThread", "main").set_last();
let handle = thread::spawn(|| {
assert_eq!(Error::last_message(), None);
Error::new("SpawnedThread", "spawned").set_last();
assert!(Error::last_message().is_some());
});
handle.join().unwrap();
assert!(Error::last_message().unwrap().contains("MainThread"));
}
#[test]
fn test_error_overwrite() {
Error::new("First", "first message").set_last();
Error::new("Second", "second message").set_last();
let msg = Error::last_message().unwrap();
assert!(msg.contains("Second"));
assert!(msg.contains("second message"));
}
#[test]
fn test_variant_and_details_extraction() {
let err = Error::new("MyError", "something went wrong");
assert_eq!(err.variant(), Some("MyError"));
assert_eq!(err.details(), Some("something went wrong"));
assert_eq!(err.message(), "MyError: something went wrong");
}
#[test]
fn test_variant_with_colon_in_details() {
let err = Error::new("IoError", "file not found: /path/to/file");
assert_eq!(err.variant(), Some("IoError"));
assert_eq!(err.details(), Some("file not found: /path/to/file"));
}
}