use std::backtrace::Backtrace;
use std::collections::HashMap;
use std::panic::Location;
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
struct CodexErrorData {
pub name: String,
pub cause: String,
pub suggestion: Option<String>,
#[cfg_attr(feature = "serde", serde(skip))]
pub location: &'static Location<'static>,
#[cfg_attr(feature = "serde", serde(skip))]
pub backtrace: Backtrace,
pub metadata: HashMap<String, String>,
}
#[cfg(feature = "serde")]
impl Default for CodexErrorData {
fn default() -> Self {
Self {
name: "Unknown Error".to_string(),
cause: "No cause provided".to_string(),
suggestion: None,
location: std::panic::Location::caller(),
backtrace: std::backtrace::Backtrace::disabled(),
metadata: std::collections::HashMap::new(),
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CodexError {
inner: Box<CodexErrorData>,
}
impl CodexError {
#[track_caller]
pub fn builder<N: Into<String>, C: Into<String>>(name: N, cause: C) -> Self {
Self {
inner: Box::new(CodexErrorData {
name: name.into(),
cause: cause.into(),
suggestion: None,
location: Location::caller(),
backtrace: Backtrace::capture(),
metadata: HashMap::new(),
}),
}
}
#[must_use]
pub fn with_suggestion<S: Into<String>>(mut self, suggestion: S) -> Self {
self.inner.suggestion = Some(suggestion.into());
self
}
#[must_use]
pub fn with_meta<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.inner.metadata.insert(key.into(), value.into());
self
}
#[inline]
#[must_use]
pub fn name(&self) -> &str {
&self.inner.name
}
#[inline]
#[must_use]
pub fn cause(&self) -> &str {
&self.inner.cause
}
#[inline]
#[must_use]
pub fn suggestion(&self) -> Option<&String> {
self.inner.suggestion.as_ref()
}
#[inline]
#[must_use]
pub fn metadata(&self) -> &HashMap<String, String> {
&self.inner.metadata
}
#[inline]
pub fn backtrace(&self) -> &Backtrace {
&self.inner.backtrace
}
#[inline]
#[must_use]
pub fn location(&self) -> &'static Location<'static> {
self.inner.location
}
}
impl std::fmt::Display for CodexError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.inner.name, self.inner.cause)?;
if let Some(sug) = &self.inner.suggestion {
write!(f, "\nSuggestion: {sug}")?;
}
Ok(())
}
}
impl std::error::Error for CodexError {}
impl From<std::io::Error> for CodexError {
fn from(err: std::io::Error) -> Self {
Self::builder("IO_ERROR", err.to_string()).with_meta("kind", format!("{:?}", err.kind()))
}
}
pub trait IntoCodex {
fn into_codex(self, name: &str) -> CodexError;
}
impl<E: std::error::Error> IntoCodex for E {
fn into_codex(self, name: &str) -> CodexError {
CodexError::builder(name, self.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codex_error_display_and_creation() {
let error = CodexError::builder("SYS_FAULT", "Insufficient memory")
.with_suggestion("Increase the swap limit")
.with_meta("process_id", "1234");
let error_string = format!("{error}");
assert!(error_string.contains("[SYS_FAULT] Insufficient memory"));
assert!(error_string.contains("Suggestion: Increase the swap limit"));
let loc = error.location();
assert!(loc.file().ends_with("error.rs"), "Should capture the current file name");
if let Some(result) = error.metadata().get("process_id") {
assert_eq!(result, "1234");
}
}
}