#[cfg(feature = "server")]
use axum::{
Json,
response::{IntoResponse, Response},
};
#[cfg(feature = "http")]
pub use http::StatusCode;
#[cfg(feature = "server")]
use serde::Serialize;
use std::{
any::Any,
backtrace::Backtrace,
error::Error as StdError,
fmt::{Debug, Display},
sync::{Arc, Mutex},
};
pub trait HostError: Any + StdError + Send + Sync + 'static {}
impl<T: Any + StdError + Send + Sync + 'static> HostError for T {}
pub enum Error {
Context { msg: String, source: Box<SError> },
HostLang(Box<dyn HostError>),
Client { msg: String, bt: Backtrace },
Internal(anyhow::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.format_context(f)? {
Error::Context { .. } => Ok(()),
Error::HostLang(e) => write!(f, "{}", e),
Error::Client { msg, .. } => write!(f, "Invalid Request: {}", msg),
Error::Internal(e) => write!(f, "{}", e),
}
}
}
impl Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.format_context(f)? {
Error::Context { .. } => Ok(()),
Error::HostLang(e) => write!(f, "{:?}", e),
Error::Client { msg, bt } => {
write!(f, "Invalid Request: {msg}\n\n{bt}\n")
}
Error::Internal(e) => write!(f, "{e:?}"),
}
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub type CError = Error;
pub type CResult<T> = Result<T>;
impl Error {
pub fn host(e: impl HostError) -> Self {
Self::HostLang(Box::new(e))
}
pub fn client(msg: impl Into<String>) -> Self {
Self::Client {
msg: msg.into(),
bt: Backtrace::capture(),
}
}
pub fn internal(e: impl Into<anyhow::Error>) -> Self {
Self::Internal(e.into())
}
pub fn internal_msg(msg: impl Into<String>) -> Self {
Self::Internal(anyhow::anyhow!("{}", msg.into()))
}
pub fn backtrace(&self) -> Option<&Backtrace> {
match self {
Error::Client { bt, .. } => Some(bt),
Error::Internal(e) => Some(e.backtrace()),
Error::Context { source, .. } => source.0.backtrace(),
Error::HostLang(_) => None,
}
}
pub fn without_contexts(&self) -> &Error {
match self {
Error::Context { source, .. } => source.0.without_contexts(),
other => other,
}
}
pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Context { source, .. } => Some(source.as_ref()),
Error::HostLang(e) => Some(e.as_ref()),
Error::Internal(e) => e.source(),
Error::Client { .. } => None,
}
}
pub fn context<C: Into<String>>(self, context: C) -> Self {
Self::Context {
msg: context.into(),
source: Box::new(SError(self)),
}
}
pub fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Self {
Self::Context {
msg: f().into(),
source: Box::new(SError(self)),
}
}
pub fn std_error(self) -> SError {
SError(self)
}
fn format_context(&self, f: &mut std::fmt::Formatter<'_>) -> Result<&Error, std::fmt::Error> {
let mut current = self;
if matches!(current, Error::Context { .. }) {
write!(f, "\nContext:\n")?;
let mut next_id = 1;
while let Error::Context { msg, source } = current {
writeln!(f, " {next_id}: {msg}")?;
current = source.inner();
next_id += 1;
}
}
Ok(current)
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source()
}
}
impl From<anyhow::Error> for Error {
fn from(e: anyhow::Error) -> Self {
Error::Internal(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Internal(e.into())
}
}
#[cfg(any(
feature = "server",
feature = "concur_control",
feature = "retryable",
feature = "batching",
feature = "http"
))]
impl From<tokio::task::JoinError> for Error {
fn from(e: tokio::task::JoinError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(any(
feature = "server",
feature = "concur_control",
feature = "retryable",
feature = "batching",
feature = "http"
))]
impl From<tokio::sync::oneshot::error::RecvError> for Error {
fn from(e: tokio::sync::oneshot::error::RecvError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "fingerprint")]
impl From<base64::DecodeError> for Error {
fn from(e: base64::DecodeError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "fingerprint")]
impl From<hex::FromHexError> for Error {
fn from(e: hex::FromHexError) -> Self {
Error::Internal(e.into())
}
}
impl From<ResidualError> for Error {
fn from(e: ResidualError) -> Self {
Error::Internal(anyhow::Error::from(e))
}
}
#[cfg(feature = "fingerprint")]
impl From<crate::fingerprint::FingerprinterError> for Error {
fn from(e: crate::fingerprint::FingerprinterError) -> Self {
Error::Internal(anyhow::Error::new(e))
}
}
impl From<ApiError> for Error {
fn from(e: ApiError) -> Self {
Error::Internal(e.err)
}
}
#[cfg(feature = "deserialize")]
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Internal(e.into())
}
}
#[cfg(any(
feature = "local-file",
feature = "google-drive",
feature = "azure",
feature = "s3"
))]
impl From<globset::Error> for Error {
fn from(e: globset::Error) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "regex")]
impl From<regex::Error> for Error {
fn from(e: regex::Error) -> Self {
Error::Internal(e.into())
}
}
impl<T> From<std::sync::PoisonError<T>> for Error {
fn from(e: std::sync::PoisonError<T>) -> Self {
Error::Internal(anyhow::anyhow!("Mutex poison error: {}", e))
}
}
#[cfg(feature = "chrono")]
impl From<chrono::ParseError> for Error {
fn from(e: chrono::ParseError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "uuid")]
impl From<uuid::Error> for Error {
fn from(e: uuid::Error) -> Self {
Error::Internal(e.into())
}
}
#[cfg(any(feature = "server", feature = "http", feature = "reqwest"))]
impl From<http::header::InvalidHeaderValue> for Error {
fn from(e: http::header::InvalidHeaderValue) -> Self {
Error::Internal(e.into())
}
}
impl From<std::num::ParseIntError> for Error {
fn from(e: std::num::ParseIntError) -> Self {
Error::Internal(e.into())
}
}
impl From<std::str::ParseBoolError> for Error {
fn from(e: std::str::ParseBoolError) -> Self {
Error::Internal(e.into())
}
}
impl From<std::fmt::Error> for Error {
fn from(e: std::fmt::Error) -> Self {
Error::Internal(e.into())
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(e: std::string::FromUtf8Error) -> Self {
Error::Internal(e.into())
}
}
impl From<std::borrow::Cow<'_, str>> for Error {
fn from(e: std::borrow::Cow<'_, str>) -> Self {
Error::Internal(anyhow::anyhow!("{}", e))
}
}
#[cfg(any(
feature = "server",
feature = "concur_control",
feature = "retryable",
feature = "batching"
))]
impl From<tokio::sync::AcquireError> for Error {
fn from(e: tokio::sync::AcquireError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(any(
feature = "server",
feature = "concur_control",
feature = "retryable",
feature = "batching"
))]
impl From<tokio::sync::watch::error::RecvError> for Error {
fn from(e: tokio::sync::watch::error::RecvError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "yaml")]
impl From<yaml_rust2::EmitError> for Error {
fn from(e: yaml_rust2::EmitError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "yaml")]
impl From<crate::yaml_ser::YamlSerializerError> for Error {
fn from(e: crate::yaml_ser::YamlSerializerError) -> Self {
Error::Internal(anyhow::Error::new(e))
}
}
#[cfg(feature = "reqwest")]
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "sqlx")]
impl From<sqlx::Error> for Error {
fn from(e: sqlx::Error) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "neo4rs")]
impl From<neo4rs::Error> for Error {
fn from(e: neo4rs::Error) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "openai")]
impl From<async_openai::error::OpenAIError> for Error {
fn from(e: async_openai::error::OpenAIError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "qdrant")]
impl From<qdrant_client::QdrantError> for Error {
fn from(e: qdrant_client::QdrantError) -> Self {
Error::Internal(anyhow::Error::msg(e.to_string()))
}
}
#[cfg(feature = "redis")]
impl From<redis::RedisError> for Error {
fn from(e: redis::RedisError) -> Self {
Error::Internal(e.into())
}
}
#[cfg(feature = "azure")]
impl From<azure_storage::Error> for Error {
fn from(e: azure_storage::Error) -> Self {
Error::Internal(anyhow::Error::msg(e.to_string()))
}
}
#[cfg(feature = "google-drive")]
impl From<google_drive3::Error> for Error {
fn from(e: google_drive3::Error) -> Self {
Error::Internal(anyhow::Error::msg(e.to_string()))
}
}
#[cfg(feature = "google-drive")]
impl From<google_drive3::hyper::Error> for Error {
fn from(e: google_drive3::hyper::Error) -> Self {
Error::Internal(e.into())
}
}
pub trait ContextExt<T> {
fn context<C: Into<String>>(self, context: C) -> Result<T>;
fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T>;
}
impl<T> ContextExt<T> for Result<T> {
fn context<C: Into<String>>(self, context: C) -> Result<T> {
self.map_err(|e| e.context(context))
}
fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
self.map_err(|e| e.with_context(f))
}
}
pub trait StdContextExt<T, E> {
fn context<C: Into<String>>(self, context: C) -> Result<T>;
fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T>;
}
impl<T, E: StdError + Send + Sync + 'static> StdContextExt<T, E> for Result<T, E> {
fn context<C: Into<String>>(self, context: C) -> Result<T> {
self.map_err(|e| Error::internal(e).context(context))
}
fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
self.map_err(|e| Error::internal(e).with_context(f))
}
}
impl<T> ContextExt<T> for Option<T> {
fn context<C: Into<String>>(self, context: C) -> Result<T> {
self.ok_or_else(|| Error::client(context))
}
fn with_context<C: Into<String>, F: FnOnce() -> C>(self, f: F) -> Result<T> {
self.ok_or_else(|| Error::client(f()))
}
}
#[cfg(feature = "server")]
impl IntoResponse for Error {
fn into_response(self) -> Response {
tracing::debug!("Error response:\n{:?}", self);
let (status_code, error_msg) = match &self {
Error::Client { msg, .. } => (StatusCode::BAD_REQUEST, msg.clone()),
Error::HostLang(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
Error::Context { .. } | Error::Internal(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self))
}
};
let error_response = ErrorResponse { error: error_msg };
(status_code, Json(error_response)).into_response()
}
}
#[macro_export]
macro_rules! client_bail {
( $fmt:literal $(, $($arg:tt)*)?) => {
return Err($crate::error::Error::client(format!($fmt $(, $($arg)*)?)))
};
}
#[macro_export]
macro_rules! client_error {
( $fmt:literal $(, $($arg:tt)*)?) => {
$crate::error::Error::client(format!($fmt $(, $($arg)*)?))
};
}
#[macro_export]
macro_rules! internal_bail {
( $fmt:literal $(, $($arg:tt)*)?) => {
return Err($crate::error::Error::internal_msg(format!($fmt $(, $($arg)*)?)))
};
}
#[macro_export]
macro_rules! internal_error {
( $fmt:literal $(, $($arg:tt)*)?) => {
$crate::error::Error::internal_msg(format!($fmt $(, $($arg)*)?))
};
}
pub struct SError(Error);
impl SError {
pub fn inner(&self) -> &Error {
&self.0
}
}
impl Display for SError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl Debug for SError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.0, f)
}
}
impl std::error::Error for SError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
}
struct ResidualErrorData {
message: String,
debug: String,
}
#[derive(Clone)]
pub struct ResidualError(Arc<ResidualErrorData>);
impl ResidualError {
pub fn new<Err: Display + Debug>(err: &Err) -> Self {
Self(Arc::new(ResidualErrorData {
message: err.to_string(),
debug: err.to_string(),
}))
}
}
impl Display for ResidualError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0.message)
}
}
impl Debug for ResidualError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0.debug)
}
}
impl StdError for ResidualError {}
enum SharedErrorState {
Error(Error),
ResidualErrorMessage(ResidualError),
}
#[derive(Clone)]
pub struct SharedError(Arc<Mutex<SharedErrorState>>);
impl SharedError {
pub fn new(err: Error) -> Self {
Self(Arc::new(Mutex::new(SharedErrorState::Error(err))))
}
fn extract_error(&self) -> Error {
let mut state = self.0.lock().unwrap();
let mut_state = &mut *state;
let residual_err = match mut_state {
SharedErrorState::ResidualErrorMessage(err) => {
return Error::internal(err.clone());
}
SharedErrorState::Error(err) => ResidualError::new(err),
};
let orig_state = std::mem::replace(
mut_state,
SharedErrorState::ResidualErrorMessage(residual_err),
);
let SharedErrorState::Error(err) = orig_state else {
panic!("Expected shared error state to hold Error");
};
err
}
}
impl Debug for SharedError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let state = self.0.lock().unwrap();
match &*state {
SharedErrorState::Error(err) => Debug::fmt(err, f),
SharedErrorState::ResidualErrorMessage(err) => Debug::fmt(err, f),
}
}
}
impl Display for SharedError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let state = self.0.lock().unwrap();
match &*state {
SharedErrorState::Error(err) => Display::fmt(err, f),
SharedErrorState::ResidualErrorMessage(err) => Display::fmt(err, f),
}
}
}
impl From<Error> for SharedError {
fn from(err: Error) -> Self {
Self(Arc::new(Mutex::new(SharedErrorState::Error(err))))
}
}
pub fn shared_ok<T>(value: T) -> std::result::Result<T, SharedError> {
Ok(value)
}
pub type SharedResult<T> = std::result::Result<T, SharedError>;
pub trait SharedResultExt<T> {
fn into_result(self) -> Result<T>;
}
impl<T> SharedResultExt<T> for std::result::Result<T, SharedError> {
fn into_result(self) -> Result<T> {
match self {
Ok(value) => Ok(value),
Err(err) => Err(err.extract_error()),
}
}
}
pub trait SharedResultExtRef<'a, T> {
fn into_result(self) -> Result<&'a T>;
}
impl<'a, T> SharedResultExtRef<'a, T> for &'a std::result::Result<T, SharedError> {
fn into_result(self) -> Result<&'a T> {
match self {
Ok(value) => Ok(value),
Err(err) => Err(err.extract_error()),
}
}
}
pub fn invariance_violation() -> anyhow::Error {
anyhow::anyhow!("Invariance violation")
}
#[derive(Debug)]
pub struct ApiError {
pub err: anyhow::Error,
#[cfg(feature = "http")]
pub status_code: StatusCode,
}
impl ApiError {
cfg_if::cfg_if! {
if #[cfg(feature = "http")] {
pub fn new(message: &str, status_code: StatusCode) -> Self {
Self {
err: anyhow::anyhow!("{}", message),
status_code,
}
}} else {
pub fn new(message: &str) -> Self {
Self {
err: anyhow::anyhow!("{}", message),
}
}
}
}
}
impl Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
Display::fmt(&self.err, f)
}
}
impl StdError for ApiError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.err.source()
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "server")] {
#[derive(Serialize)]
struct ErrorResponse {
error: String,
}
}
}
#[cfg(feature = "server")]
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
tracing::debug!("Internal server error:\n{:?}", self.err);
let error_response = ErrorResponse {
error: format!("{:?}", self.err),
};
(self.status_code, Json(error_response)).into_response()
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "http")] {
impl From<anyhow::Error> for ApiError {
fn from(err: anyhow::Error) -> ApiError {
if err.is::<ApiError>() {
return err.downcast::<ApiError>().unwrap();
}
Self {
err,
status_code: StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
} else {
impl From<anyhow::Error> for ApiError {
fn from(err: anyhow::Error) -> ApiError {
if err.is::<ApiError>() {
return err.downcast::<ApiError>().unwrap();
}
Self {
err,
}
}
}
}
}
impl From<Error> for ApiError {
fn from(err: Error) -> ApiError {
cfg_if::cfg_if! {
if #[cfg(feature = "http")] {
let status_code = match err.without_contexts() {
Error::Client { .. } => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
ApiError {
err: anyhow::Error::from(err.std_error()),
status_code,
}
} else {
ApiError {
err: anyhow::Error::from(err.std_error()),
}
}
}
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "http")] {
#[macro_export]
macro_rules! api_bail {
( $fmt:literal $(, $($arg:tt)*)?) => {
return Err($crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?), $crate::error::StatusCode::BAD_REQUEST).into())
};
}
} else {
#[macro_export]
macro_rules! api_bail {
( $fmt:literal $(, $($arg:tt)*)?) => {
return Err($crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?)).into())
};
}
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "http")] {
#[macro_export]
macro_rules! api_error {
( $fmt:literal $(, $($arg:tt)*)?) => {
$crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?), $crate::error::StatusCode::BAD_REQUEST)
};
}
} else {
#[macro_export]
macro_rules! api_error {
( $fmt:literal $(, $($arg:tt)*)?) => {
$crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?))
};
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::backtrace::BacktraceStatus;
use std::io;
#[derive(Debug)]
struct MockHostError(String);
impl Display for MockHostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MockHostError: {}", self.0)
}
}
impl StdError for MockHostError {}
#[test]
fn test_client_error_creation() {
let err = Error::client("invalid input");
assert!(matches!(&err, Error::Client { msg, .. } if msg == "invalid input"));
assert!(matches!(err.without_contexts(), Error::Client { .. }));
}
#[test]
fn test_internal_error_creation() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::Internal { .. }));
}
#[test]
fn test_internal_msg_error_creation() {
let err = Error::internal_msg("something went wrong");
assert!(matches!(err, Error::Internal { .. }));
assert_eq!(err.to_string(), "something went wrong");
}
#[test]
fn test_host_error_creation_and_detection() {
let mock = MockHostError("test error".to_string());
let err = Error::host(mock);
assert!(matches!(err.without_contexts(), Error::HostLang(_)));
if let Error::HostLang(host_err) = err.without_contexts() {
let any: &dyn Any = host_err.as_ref();
let downcasted = any.downcast_ref::<MockHostError>();
assert!(downcasted.is_some());
assert_eq!(downcasted.unwrap().0, "test error");
} else {
panic!("Expected HostLang variant");
}
}
#[test]
fn test_context_chaining() {
let inner = Error::client("base error");
let with_context: Result<()> = Err(inner);
let wrapped = ContextExt::context(
ContextExt::context(ContextExt::context(with_context, "layer 1"), "layer 2"),
"layer 3",
);
let err = wrapped.unwrap_err();
assert!(matches!(&err, Error::Context { msg, .. } if msg == "layer 3"));
if let Error::Context { source, .. } = &err {
assert!(
matches!(source.as_ref(), SError(Error::Context { msg, .. }) if msg == "layer 2")
);
}
assert_eq!(
err.to_string(),
"\nContext:\
\n 1: layer 3\
\n 2: layer 2\
\n 3: layer 1\
\nInvalid Request: base error"
);
}
#[test]
fn test_context_preserves_host_error() {
let mock = MockHostError("original python error".to_string());
let err = Error::host(mock);
let wrapped: Result<()> = Err(err);
let with_context = ContextExt::context(wrapped, "while processing request");
let final_err = with_context.unwrap_err();
assert!(matches!(final_err.without_contexts(), Error::HostLang(_)));
if let Error::HostLang(host_err) = final_err.without_contexts() {
let any: &dyn Any = host_err.as_ref();
let downcasted = any.downcast_ref::<MockHostError>();
assert!(downcasted.is_some());
assert_eq!(downcasted.unwrap().0, "original python error");
} else {
panic!("Expected HostLang variant");
}
}
#[test]
fn test_backtrace_captured_for_client_error() {
let err = Error::client("test");
let bt = err.backtrace();
assert!(bt.is_some());
let status = bt.unwrap().status();
assert!(
status == BacktraceStatus::Captured
|| status == BacktraceStatus::Disabled
|| status == BacktraceStatus::Unsupported
);
}
#[test]
fn test_backtrace_captured_for_internal_error() {
let err = Error::internal_msg("test internal");
let bt = err.backtrace();
assert!(bt.is_some());
}
#[test]
fn test_backtrace_traverses_context() {
let inner = Error::internal_msg("base");
let wrapped: Result<()> = Err(inner);
let with_context = ContextExt::context(wrapped, "context");
let err = with_context.unwrap_err();
let bt = err.backtrace();
assert!(bt.is_some());
}
#[test]
fn test_option_context_ext() {
let opt: Option<i32> = None;
let result = opt.context("value was missing");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.without_contexts(), Error::Client { .. }));
assert!(matches!(&err, Error::Client { msg, .. } if msg == "value was missing"));
}
#[test]
fn test_error_display_formats() {
let client_err = Error::client("bad input");
assert_eq!(client_err.to_string(), "Invalid Request: bad input");
let internal_err = Error::internal_msg("db connection failed");
assert_eq!(internal_err.to_string(), "db connection failed");
let host_err = Error::host(MockHostError("py error".to_string()));
assert_eq!(host_err.to_string(), "MockHostError: py error");
}
#[test]
fn test_error_source_chain() {
let inner = Error::internal_msg("root cause");
let wrapped: Result<()> = Err(inner);
let outer = ContextExt::context(wrapped, "outer context").unwrap_err();
let source = outer.source();
assert!(source.is_some());
}
}