From 1b09b89f4c25526ea81b8ec5545af7f9e0d5e850 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sat, 13 Dec 2025 23:37:54 +0530 Subject: [PATCH 01/15] `chore`: update include `thiserror` version 1.0.69 --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 8edefd48b..72fc7198a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,6 +1317,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "sqlx", + "thiserror 1.0.69", "tokio", "tokio-util", "tracing", From 033de3c11d586c9bd2110b03e25e43147f61dd52 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sat, 13 Dec 2025 23:38:38 +0530 Subject: [PATCH 02/15] `chore`: add `thiserror` dep --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 32d95dbf3..8814c41da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ numpy = "0.27.0" anyhow = { version = "1.0.100", features = ["std"] } async-trait = "0.1.89" +thiserror = "1.0" axum = "0.8.7" axum-extra = { version = "0.10.3", features = ["query"] } base64 = "0.22.1" From f9d40a25aca368a8a7481c61e90967ea34654cfc Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sat, 13 Dec 2025 23:39:42 +0530 Subject: [PATCH 03/15] chore: enhance imports with additional error handling utils --- rust/cocoindex/src/prelude.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rust/cocoindex/src/prelude.rs b/rust/cocoindex/src/prelude.rs index 023118471..5dea63819 100644 --- a/rust/cocoindex/src/prelude.rs +++ b/rust/cocoindex/src/prelude.rs @@ -28,6 +28,10 @@ pub(crate) use crate::setup::AuthRegistry; pub(crate) use cocoindex_utils as utils; pub(crate) use cocoindex_utils::error::{ApiError, invariance_violation}; pub(crate) use cocoindex_utils::{api_bail, api_error}; +pub(crate) use cocoindex_utils::error::{ + CError, CResult, ContextExt, HostError, IntoInternal, ResultExt, +}; +pub(crate) use cocoindex_utils::{client_bail, client_error, internal_bail, internal_error}; pub(crate) use cocoindex_utils::{batching, concur_control, http, retryable}; pub(crate) use anyhow::{anyhow, bail}; @@ -37,3 +41,4 @@ pub(crate) use tracing::{Span, debug, error, info, info_span, instrument, trace, pub(crate) use derivative::Derivative; pub(crate) use cocoindex_py_utils as py_utils; +pub(crate) use cocoindex_py_utils::{CResultIntoPyResult, ToCResult, cerror_to_pyerr}; From 8a01a6cd564c40a95129e6b63075e33782243711 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sat, 13 Dec 2025 23:40:01 +0530 Subject: [PATCH 04/15] chore: add `thiserror` dep --- rust/utils/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/utils/Cargo.toml b/rust/utils/Cargo.toml index 90307133d..3c8f028d3 100644 --- a/rust/utils/Cargo.toml +++ b/rust/utils/Cargo.toml @@ -8,6 +8,7 @@ license = { workspace = true } [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +thiserror = { workspace = true } tracing = { workspace = true } axum = { workspace = true } serde = { workspace = true } From 17209d939bc8c2b8b661b24a0ad2474702cf0358 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 00:11:28 +0530 Subject: [PATCH 05/15] feat): introduce `CError` type for adv error handling --- rust/utils/src/error.rs | 415 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 406 insertions(+), 9 deletions(-) diff --git a/rust/utils/src/error.rs b/rust/utils/src/error.rs index 8f6a545aa..aa73d5eb5 100644 --- a/rust/utils/src/error.rs +++ b/rust/utils/src/error.rs @@ -6,12 +6,234 @@ use axum::{ }; use serde::Serialize; use std::{ - error::Error, + any::Any, + backtrace::Backtrace, + error::Error as StdError, fmt::{Debug, Display}, sync::{Arc, Mutex}, }; -pub struct ResidualErrorData { +pub trait HostError: Debug + Display + Send + Sync + 'static { + fn as_any(&self) -> &dyn Any; +} + +#[derive(Debug)] +pub enum CError { + Context { msg: String, source: Box }, + HostLang(Box), + Client { msg: String, bt: Backtrace }, + Internal { + source: Box, + bt: Backtrace, + }, +} + +impl Display for CError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CError::Context { msg, .. } => write!(f, "{}", msg), + CError::HostLang(e) => write!(f, "{}", e), + CError::Client { msg, .. } => write!(f, "Invalid Request: {}", msg), + CError::Internal { source, .. } => write!(f, "{}", source), + } + } +} + +impl StdError for CError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + CError::Context { source, .. } => Some(source.as_ref()), + CError::Internal { source, .. } => Some(source.as_ref()), + _ => None, + } + } +} + +pub type CResult = std::result::Result; + +impl CError { + pub fn host(e: impl HostError) -> Self { + Self::HostLang(Box::new(e)) + } + + pub fn client(msg: impl Into) -> Self { + Self::Client { + msg: msg.into(), + bt: Backtrace::capture(), + } + } + + pub fn internal(e: impl StdError + Send + Sync + 'static) -> Self { + Self::Internal { + source: Box::new(e), + bt: Backtrace::capture(), + } + } + + pub fn internal_msg(msg: impl Into) -> Self { + Self::Internal { + source: Box::new(StringError(msg.into())), + bt: Backtrace::capture(), + } + } + + pub fn backtrace(&self) -> Option<&Backtrace> { + match self { + CError::Client { bt, .. } => Some(bt), + CError::Internal { bt, .. } => Some(bt), + CError::Context { source, .. } => source.backtrace(), + CError::HostLang(_) => None, + } + } + + pub fn find_host_error(&self) -> Option<&dyn HostError> { + match self { + CError::HostLang(host_err) => Some(host_err.as_ref()), + CError::Context { source, .. } => source.find_host_error(), + _ => None, + } + } + + pub fn is_host_error(&self) -> bool { + self.find_host_error().is_some() + } + + pub fn is_client_error(&self) -> bool { + matches!(self, CError::Client { .. }) + } +} + +#[derive(Debug)] +struct StringError(String); + +impl Display for StringError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl StdError for StringError {} + +pub trait IntoInternal { + fn internal(self) -> CResult; +} + +impl IntoInternal for std::result::Result { + fn internal(self) -> CResult { + self.map_err(|e| CError::Internal { + source: Box::new(e), + bt: Backtrace::capture(), + }) + } +} + +pub trait ContextExt { + fn context>(self, context: C) -> CResult; + fn with_context, F: FnOnce() -> C>(self, f: F) -> CResult; +} + +impl ContextExt for CResult { + fn context>(self, context: C) -> CResult { + self.map_err(|e| CError::Context { + msg: context.into(), + source: Box::new(e), + }) + } + + fn with_context, F: FnOnce() -> C>(self, f: F) -> CResult { + self.map_err(|e| CError::Context { + msg: f().into(), + source: Box::new(e), + }) + } +} + +// Uses cerror_context to avoid name conflicts with anyhow::Context during migration +pub trait ResultExt { + fn cerror_context>(self, context: C) -> CResult; + fn cerror_with_context, F: FnOnce() -> C>(self, f: F) -> CResult; +} + +impl ResultExt for std::result::Result { + fn cerror_context>(self, context: C) -> CResult { + self.map_err(|e| CError::Context { + msg: context.into(), + source: Box::new(CError::Internal { + source: Box::new(e), + bt: Backtrace::capture(), + }), + }) + } + + fn cerror_with_context, F: FnOnce() -> C>(self, f: F) -> CResult { + self.map_err(|e| CError::Context { + msg: f().into(), + source: Box::new(CError::Internal { + source: Box::new(e), + bt: Backtrace::capture(), + }), + }) + } +} + +impl ContextExt for Option { + fn context>(self, context: C) -> CResult { + self.ok_or_else(|| CError::client(context)) + } + + fn with_context, F: FnOnce() -> C>(self, f: F) -> CResult { + self.ok_or_else(|| CError::client(f())) + } +} + +impl IntoResponse for CError { + fn into_response(self) -> Response { + tracing::debug!("Error response:\n{:?}", self); + + let (status_code, error_msg) = match &self { + CError::Client { msg, .. } => (StatusCode::BAD_REQUEST, msg.clone()), + CError::HostLang(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + CError::Context { .. } | CError::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::CError::client(format!($fmt $(, $($arg)*)?))) + }; +} + +#[macro_export] +macro_rules! client_error { + ( $fmt:literal $(, $($arg:tt)*)?) => { + $crate::error::CError::client(format!($fmt $(, $($arg)*)?)) + }; +} + +#[macro_export] +macro_rules! internal_bail { + ( $fmt:literal $(, $($arg:tt)*)?) => { + return Err($crate::error::CError::internal_msg(format!($fmt $(, $($arg)*)?))) + }; +} + +#[macro_export] +macro_rules! internal_error { + ( $fmt:literal $(, $($arg:tt)*)?) => { + $crate::error::CError::internal_msg(format!($fmt $(, $($arg)*)?)) + }; +} + +// Legacy types below - kept for backwards compatibility during migration + +struct ResidualErrorData { message: String, debug: String, } @@ -40,15 +262,13 @@ impl Debug for ResidualError { } } -impl Error for ResidualError {} +impl StdError for ResidualError {} enum SharedErrorState { Anyhow(anyhow::Error), ResidualErrorMessage(ResidualError), } -/// SharedError allows to be cloned. -/// The original `anyhow::Error` can be extracted once, and later it decays to `ResidualError` which preserves the message and debug information. #[derive(Clone)] pub struct SharedError(Arc>); @@ -77,6 +297,7 @@ impl SharedError { err } } + impl Debug for SharedError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let state = self.0.lock().unwrap(); @@ -141,8 +362,6 @@ pub fn invariance_violation() -> anyhow::Error { anyhow::anyhow!("Invariance violation") } -// API Error types for HTTP responses - #[derive(Debug)] pub struct ApiError { pub err: anyhow::Error, @@ -164,8 +383,8 @@ impl Display for ApiError { } } -impl Error for ApiError { - fn source(&self) -> Option<&(dyn Error + 'static)> { +impl StdError for ApiError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { self.err.source() } } @@ -210,3 +429,181 @@ macro_rules! api_error { $crate::error::ApiError::new(&format!($fmt $(, $($arg)*)?), axum::http::StatusCode::BAD_REQUEST) }; } + +#[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 HostError for MockHostError { + fn as_any(&self) -> &dyn Any { + self + } + } + + #[test] + fn test_client_error_creation() { + let err = CError::client("invalid input"); + assert!(matches!(&err, CError::Client { msg, .. } if msg == "invalid input")); + assert!(err.is_client_error()); + assert!(!err.is_host_error()); + } + + #[test] + fn test_internal_error_creation() { + let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found"); + let err = CError::internal(io_err); + assert!(matches!(err, CError::Internal { .. })); + assert!(!err.is_client_error()); + assert!(!err.is_host_error()); + } + + #[test] + fn test_internal_msg_error_creation() { + let err = CError::internal_msg("something went wrong"); + assert!(matches!(err, CError::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 = CError::host(mock); + assert!(err.is_host_error()); + assert!(!err.is_client_error()); + + let host_err = err.find_host_error().unwrap(); + let downcasted = host_err.as_any().downcast_ref::(); + assert!(downcasted.is_some()); + assert_eq!(downcasted.unwrap().0, "test error"); + } + + #[test] + fn test_context_chaining() { + let inner = CError::client("base error"); + let with_context: CResult<()> = Err(inner); + let wrapped = with_context + .context("layer 1") + .context("layer 2") + .context("layer 3"); + + let err = wrapped.unwrap_err(); + assert!(matches!(&err, CError::Context { msg, .. } if msg == "layer 3")); + + if let CError::Context { source, .. } = &err { + assert!(matches!(source.as_ref(), CError::Context { msg, .. } if msg == "layer 2")); + } + assert_eq!(err.to_string(), "layer 3"); + } + + #[test] + fn test_context_preserves_host_error() { + let mock = MockHostError("original python error".to_string()); + let err = CError::host(mock); + let wrapped: CResult<()> = Err(err); + let with_context = wrapped.context("while processing request"); + + let final_err = with_context.unwrap_err(); + assert!(final_err.is_host_error()); + let host_err = final_err.find_host_error().unwrap(); + let downcasted = host_err.as_any().downcast_ref::(); + assert!(downcasted.is_some()); + assert_eq!(downcasted.unwrap().0, "original python error"); + } + + #[test] + fn test_backtrace_captured_for_client_error() { + let err = CError::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 = CError::internal_msg("test internal"); + let bt = err.backtrace(); + assert!(bt.is_some()); + } + + #[test] + fn test_backtrace_traverses_context() { + let inner = CError::internal_msg("base"); + let wrapped: CResult<()> = Err(inner); + let with_context = wrapped.context("context"); + + let err = with_context.unwrap_err(); + let bt = err.backtrace(); + assert!(bt.is_some()); + } + + #[test] + fn test_into_internal_trait() { + let io_result: Result<(), io::Error> = + Err(io::Error::new(io::ErrorKind::Other, "io error")); + let cresult = io_result.internal(); + + assert!(cresult.is_err()); + let err = cresult.unwrap_err(); + assert!(matches!(err, CError::Internal { .. })); + } + + #[test] + fn test_result_ext_cerror_context() { + let io_result: Result<(), io::Error> = + Err(io::Error::new(io::ErrorKind::Other, "io error")); + let cresult = io_result.cerror_context("while reading file"); + + assert!(cresult.is_err()); + let err = cresult.unwrap_err(); + assert!(matches!(&err, CError::Context { msg, .. } if msg == "while reading file")); + } + + #[test] + fn test_option_context_ext() { + let opt: Option = None; + let result = opt.context("value was missing"); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.is_client_error()); + assert!(matches!(&err, CError::Client { msg, .. } if msg == "value was missing")); + } + + #[test] + fn test_error_display_formats() { + let client_err = CError::client("bad input"); + assert_eq!(client_err.to_string(), "Invalid Request: bad input"); + + let internal_err = CError::internal_msg("db connection failed"); + assert_eq!(internal_err.to_string(), "db connection failed"); + + let host_err = CError::host(MockHostError("py error".to_string())); + assert_eq!(host_err.to_string(), "MockHostError: py error"); + } + + #[test] + fn test_error_source_chain() { + let inner = CError::internal_msg("root cause"); + let wrapped: CResult<()> = Err(inner); + let outer = wrapped.context("outer context").unwrap_err(); + + let source = outer.source(); + assert!(source.is_some()); + } +} From 41d508c59fc323d3fdcf23131fde31f18db051a9 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 00:12:02 +0530 Subject: [PATCH 06/15] feat(error): implement `PyErrWrapper` and relevant conversion traits --- rust/py_utils/src/error.rs | 74 +++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/rust/py_utils/src/error.rs b/rust/py_utils/src/error.rs index a50dd0a0e..d607939ba 100644 --- a/rust/py_utils/src/error.rs +++ b/rust/py_utils/src/error.rs @@ -1,6 +1,10 @@ +use cocoindex_utils::error::{CError, CResult, HostError}; +use pyo3::exceptions::{PyException, PyRuntimeError, PyValueError}; +use pyo3::prelude::*; use pyo3::types::{PyDict, PyModule, PyString}; -use pyo3::{exceptions::PyException, prelude::*}; -use std::fmt::Write; +use std::any::Any; +use std::fmt::{self, Display, Write}; + pub struct PythonExecutionContext { pub event_loop: Py, } @@ -11,6 +15,59 @@ impl PythonExecutionContext { } } +#[derive(Debug)] +pub struct PyErrWrapper(PyErr); + +impl PyErrWrapper { + pub fn new(err: PyErr) -> Self { + Self(err) + } + + pub fn into_inner(self) -> PyErr { + self.0 + } +} + +impl Display for PyErrWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl HostError for PyErrWrapper { + fn as_any(&self) -> &dyn Any { + self + } +} + +pub fn cerror_to_pyerr(err: CError) -> PyErr { + if let Some(host_err) = err.find_host_error() { + if let Some(wrapper) = host_err.as_any().downcast_ref::() { + return Python::attach(|py| wrapper.0.clone_ref(py)); + } + } + + match &err { + CError::Client { msg, .. } => PyValueError::new_err(msg.clone()), + CError::HostLang(e) => PyRuntimeError::new_err(e.to_string()), + CError::Context { .. } | CError::Internal { .. } => { + PyRuntimeError::new_err(format!("{:?}", err)) + } + } +} + +pub trait ToCResult { + fn to_cresult(self) -> CResult; +} + +impl ToCResult for PyResult { + fn to_cresult(self) -> CResult { + self.map_err(|err| CError::host(PyErrWrapper::new(err))) + } +} + +// Legacy traits down below - kept for backwards compatibility during migration + pub trait ToResultWithPyTrace { fn to_result_with_py_trace(self, py: Python<'_>) -> anyhow::Result; } @@ -20,7 +77,6 @@ impl ToResultWithPyTrace for Result { match self { Ok(value) => Ok(value), Err(err) => { - // Attempt to render a full Python-style traceback including cause/context chain let full_trace: PyResult = (|| { let exc = err.value(py); let traceback = PyModule::import(py, "traceback")?; @@ -36,7 +92,6 @@ impl ToResultWithPyTrace for Result { let err_str = match full_trace { Ok(trace) => format!("Error calling Python function:\n{trace}"), Err(_) => { - // Fallback: include the PyErr display and available traceback formatting let mut s = format!("Error calling Python function: {err}"); if let Some(tb) = err.traceback(py) { write!(&mut s, "\n{}", tb.format()?).ok(); @@ -50,6 +105,7 @@ impl ToResultWithPyTrace for Result { } } } + pub trait IntoPyResult { fn into_py_result(self) -> PyResult; } @@ -75,3 +131,13 @@ impl AnyhowIntoPyResult for anyhow::Result { } } } + +pub trait CResultIntoPyResult { + fn into_py_result(self) -> PyResult; +} + +impl CResultIntoPyResult for CResult { + fn into_py_result(self) -> PyResult { + self.map_err(cerror_to_pyerr) + } +} From 381090a5857b066fb8fdedebfdedf55d64d69b36 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 08:54:52 +0530 Subject: [PATCH 07/15] format: for ci Signed-off-by: Srihari Thyagarajan --- rust/cocoindex/src/prelude.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/cocoindex/src/prelude.rs b/rust/cocoindex/src/prelude.rs index 5dea63819..02bf1a4ee 100644 --- a/rust/cocoindex/src/prelude.rs +++ b/rust/cocoindex/src/prelude.rs @@ -27,12 +27,12 @@ pub(crate) use crate::setup; pub(crate) use crate::setup::AuthRegistry; pub(crate) use cocoindex_utils as utils; pub(crate) use cocoindex_utils::error::{ApiError, invariance_violation}; -pub(crate) use cocoindex_utils::{api_bail, api_error}; pub(crate) use cocoindex_utils::error::{ CError, CResult, ContextExt, HostError, IntoInternal, ResultExt, }; -pub(crate) use cocoindex_utils::{client_bail, client_error, internal_bail, internal_error}; +pub(crate) use cocoindex_utils::{api_bail, api_error}; pub(crate) use cocoindex_utils::{batching, concur_control, http, retryable}; +pub(crate) use cocoindex_utils::{client_bail, client_error, internal_bail, internal_error}; pub(crate) use anyhow::{anyhow, bail}; pub(crate) use async_stream::{stream, try_stream}; From f45924edf35c3164442fba41459cf406814dea49 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 08:58:46 +0530 Subject: [PATCH 08/15] refactor(error): `CError` --- rust/utils/src/error.rs | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/rust/utils/src/error.rs b/rust/utils/src/error.rs index aa73d5eb5..b3e6d1647 100644 --- a/rust/utils/src/error.rs +++ b/rust/utils/src/error.rs @@ -1,4 +1,3 @@ -use anyhow; use axum::{ Json, http::StatusCode, @@ -12,16 +11,22 @@ use std::{ fmt::{Debug, Display}, sync::{Arc, Mutex}, }; +use thiserror::Error; -pub trait HostError: Debug + Display + Send + Sync + 'static { - fn as_any(&self) -> &dyn Any; -} +pub trait HostError: Any + Debug + Display + Send + Sync + 'static {} +impl HostError for T {} #[derive(Debug)] pub enum CError { - Context { msg: String, source: Box }, + Context { + msg: String, + source: Box, + }, HostLang(Box), - Client { msg: String, bt: Backtrace }, + Client { + msg: String, + bt: Backtrace, + }, Internal { source: Box, bt: Backtrace, @@ -103,17 +108,10 @@ impl CError { } } -#[derive(Debug)] +#[derive(Debug, Error)] +#[error("{0}")] struct StringError(String); -impl Display for StringError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl StdError for StringError {} - pub trait IntoInternal { fn internal(self) -> CResult; } @@ -445,12 +443,6 @@ mod tests { } } - impl HostError for MockHostError { - fn as_any(&self) -> &dyn Any { - self - } - } - #[test] fn test_client_error_creation() { let err = CError::client("invalid input"); @@ -483,7 +475,8 @@ mod tests { assert!(!err.is_client_error()); let host_err = err.find_host_error().unwrap(); - let downcasted = host_err.as_any().downcast_ref::(); + let any: &dyn Any = host_err; // trait upcasting + let downcasted = any.downcast_ref::(); assert!(downcasted.is_some()); assert_eq!(downcasted.unwrap().0, "test error"); } @@ -516,7 +509,8 @@ mod tests { let final_err = with_context.unwrap_err(); assert!(final_err.is_host_error()); let host_err = final_err.find_host_error().unwrap(); - let downcasted = host_err.as_any().downcast_ref::(); + let any: &dyn Any = host_err; + let downcasted = any.downcast_ref::(); assert!(downcasted.is_some()); assert_eq!(downcasted.unwrap().0, "original python error"); } From 3059d0575a40e47fc3d829706c8c815299b7d7a4 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 09:01:14 +0530 Subject: [PATCH 09/15] refactor(error) --- rust/py_utils/src/error.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/rust/py_utils/src/error.rs b/rust/py_utils/src/error.rs index d607939ba..738c8343a 100644 --- a/rust/py_utils/src/error.rs +++ b/rust/py_utils/src/error.rs @@ -1,4 +1,4 @@ -use cocoindex_utils::error::{CError, CResult, HostError}; +use cocoindex_utils::error::{CError, CResult}; use pyo3::exceptions::{PyException, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyDict, PyModule, PyString}; @@ -34,15 +34,10 @@ impl Display for PyErrWrapper { } } -impl HostError for PyErrWrapper { - fn as_any(&self) -> &dyn Any { - self - } -} - pub fn cerror_to_pyerr(err: CError) -> PyErr { if let Some(host_err) = err.find_host_error() { - if let Some(wrapper) = host_err.as_any().downcast_ref::() { + let any: &dyn Any = host_err; // trait upcasting + if let Some(wrapper) = any.downcast_ref::() { return Python::attach(|py| wrapper.0.clone_ref(py)); } } From eb8204cd7211057918abb9a252c5b6a6e636b19b Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 12:52:54 +0530 Subject: [PATCH 10/15] chore: remove dep --- Cargo.lock | 1 - Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72fc7198a..8edefd48b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,7 +1317,6 @@ dependencies = [ "serde_json", "serde_path_to_error", "sqlx", - "thiserror 1.0.69", "tokio", "tokio-util", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 8814c41da..32d95dbf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ numpy = "0.27.0" anyhow = { version = "1.0.100", features = ["std"] } async-trait = "0.1.89" -thiserror = "1.0" axum = "0.8.7" axum-extra = { version = "0.10.3", features = ["query"] } base64 = "0.22.1" From a266841f380b5fbeead7f97edeff1f938446b2db Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 13:06:25 +0530 Subject: [PATCH 11/15] chore: remove `thiserror` dpe --- rust/utils/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/utils/Cargo.toml b/rust/utils/Cargo.toml index 3c8f028d3..90307133d 100644 --- a/rust/utils/Cargo.toml +++ b/rust/utils/Cargo.toml @@ -8,7 +8,6 @@ license = { workspace = true } [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } axum = { workspace = true } serde = { workspace = true } From 77491734e72bc975d821a423d089ca08dfbf703e Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 13:07:24 +0530 Subject: [PATCH 12/15] feat(error): error handling in `cerror_to_pyerr` & add fn format_error_chain` --- rust/py_utils/src/error.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/rust/py_utils/src/error.rs b/rust/py_utils/src/error.rs index 738c8343a..18e447623 100644 --- a/rust/py_utils/src/error.rs +++ b/rust/py_utils/src/error.rs @@ -34,21 +34,33 @@ impl Display for PyErrWrapper { } } +impl std::error::Error for PyErrWrapper {} + pub fn cerror_to_pyerr(err: CError) -> PyErr { if let Some(host_err) = err.find_host_error() { - let any: &dyn Any = host_err; // trait upcasting + let any: &dyn Any = host_err; if let Some(wrapper) = any.downcast_ref::() { return Python::attach(|py| wrapper.0.clone_ref(py)); } } - match &err { + match err.without_contexts() { CError::Client { msg, .. } => PyValueError::new_err(msg.clone()), - CError::HostLang(e) => PyRuntimeError::new_err(e.to_string()), - CError::Context { .. } | CError::Internal { .. } => { - PyRuntimeError::new_err(format!("{:?}", err)) - } + _ => PyRuntimeError::new_err(format_error_chain(&err)), + } +} + +fn format_error_chain(err: &CError) -> String { + let mut s = err.to_string(); + let mut current = err; + while let CError::Context { source, .. } = current { + write!(&mut s, "\nCaused by: {}", source).ok(); + current = source; + } + if let Some(bt) = err.backtrace() { + write!(&mut s, "\n\n{}", bt).ok(); } + s } pub trait ToCResult { From 4cbc7b411edb001232ee8e343de5413ea970817d Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 13:07:58 +0530 Subject: [PATCH 13/15] fix --- rust/utils/src/error.rs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/rust/utils/src/error.rs b/rust/utils/src/error.rs index b3e6d1647..cc7441706 100644 --- a/rust/utils/src/error.rs +++ b/rust/utils/src/error.rs @@ -11,10 +11,9 @@ use std::{ fmt::{Debug, Display}, sync::{Arc, Mutex}, }; -use thiserror::Error; -pub trait HostError: Any + Debug + Display + Send + Sync + 'static {} -impl HostError for T {} +pub trait HostError: Any + StdError + Send + Sync + 'static {} +impl HostError for T {} #[derive(Debug)] pub enum CError { @@ -48,8 +47,9 @@ impl StdError for CError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { CError::Context { source, .. } => Some(source.as_ref()), + CError::HostLang(e) => Some(e.as_ref()), CError::Internal { source, .. } => Some(source.as_ref()), - _ => None, + CError::Client { .. } => None, } } } @@ -103,15 +103,29 @@ impl CError { self.find_host_error().is_some() } + pub fn without_contexts(&self) -> &CError { + match self { + CError::Context { source, .. } => source.without_contexts(), + other => other, + } + } + pub fn is_client_error(&self) -> bool { - matches!(self, CError::Client { .. }) + matches!(self.without_contexts(), CError::Client { .. }) } } -#[derive(Debug, Error)] -#[error("{0}")] +#[derive(Debug)] struct StringError(String); +impl Display for StringError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl StdError for StringError {} + pub trait IntoInternal { fn internal(self) -> CResult; } @@ -443,6 +457,8 @@ mod tests { } } + impl StdError for MockHostError {} + #[test] fn test_client_error_creation() { let err = CError::client("invalid input"); From 387436029fcfcbeb994a6e3ae138be8ec526cc40 Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Sun, 14 Dec 2025 23:52:33 +0530 Subject: [PATCH 14/15] fix --- rust/cocoindex/src/ops/registration.rs | 9 +++++---- rust/cocoindex/src/ops/registry.rs | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/rust/cocoindex/src/ops/registration.rs b/rust/cocoindex/src/ops/registration.rs index 1f1e93b35..3d9dc210f 100644 --- a/rust/cocoindex/src/ops/registration.rs +++ b/rust/cocoindex/src/ops/registration.rs @@ -3,6 +3,7 @@ use super::{ targets, }; use anyhow::Result; +use cocoindex_utils::client_error; use std::sync::{LazyLock, RwLock}; fn register_executor_factories(registry: &mut ExecutorFactoryRegistry) -> Result<()> { @@ -68,28 +69,28 @@ pub fn get_source_factory( kind: &str, ) -> Result> { get_optional_source_factory(kind) - .ok_or_else(|| anyhow::anyhow!("Source factory not found for op kind: {}", kind)) + .ok_or_else(|| client_error!("Source factory not found for op kind: {}", kind)) } pub fn get_function_factory( kind: &str, ) -> Result> { get_optional_function_factory(kind) - .ok_or_else(|| anyhow::anyhow!("Function factory not found for op kind: {}", kind)) + .ok_or_else(|| client_error!("Function factory not found for op kind: {}", kind)) } pub fn get_target_factory( kind: &str, ) -> Result> { get_optional_target_factory(kind) - .ok_or_else(|| anyhow::anyhow!("Target factory not found for op kind: {}", kind)) + .ok_or_else(|| client_error!("Target factory not found for op kind: {}", kind)) } pub fn get_attachment_factory( kind: &str, ) -> Result> { get_optional_attachment_factory(kind) - .ok_or_else(|| anyhow::anyhow!("Attachment factory not found for op kind: {}", kind)) + .ok_or_else(|| client_error!("Attachment factory not found for op kind: {}", kind)) } pub fn register_factory(name: String, factory: ExecutorFactory) -> Result<()> { diff --git a/rust/cocoindex/src/ops/registry.rs b/rust/cocoindex/src/ops/registry.rs index 4e66daa53..51ccab79f 100644 --- a/rust/cocoindex/src/ops/registry.rs +++ b/rust/cocoindex/src/ops/registry.rs @@ -1,5 +1,6 @@ use super::interface::ExecutorFactory; use anyhow::Result; +use cocoindex_utils::internal_error; use std::collections::HashMap; use std::sync::Arc; @@ -31,7 +32,7 @@ impl ExecutorFactoryRegistry { pub fn register(&mut self, name: String, factory: ExecutorFactory) -> Result<()> { match factory { ExecutorFactory::Source(source_factory) => match self.source_factories.entry(name) { - std::collections::hash_map::Entry::Occupied(entry) => Err(anyhow::anyhow!( + std::collections::hash_map::Entry::Occupied(entry) => Err(internal_error!( "Source factory with name already exists: {}", entry.key() )), @@ -42,7 +43,7 @@ impl ExecutorFactoryRegistry { }, ExecutorFactory::SimpleFunction(function_factory) => { match self.function_factories.entry(name) { - std::collections::hash_map::Entry::Occupied(entry) => Err(anyhow::anyhow!( + std::collections::hash_map::Entry::Occupied(entry) => Err(internal_error!( "Function factory with name already exists: {}", entry.key() )), @@ -54,7 +55,7 @@ impl ExecutorFactoryRegistry { } ExecutorFactory::ExportTarget(target_factory) => { match self.target_factories.entry(name) { - std::collections::hash_map::Entry::Occupied(entry) => Err(anyhow::anyhow!( + std::collections::hash_map::Entry::Occupied(entry) => Err(internal_error!( "Target factory with name already exists: {}", entry.key() )), @@ -66,7 +67,7 @@ impl ExecutorFactoryRegistry { } ExecutorFactory::TargetAttachment(target_attachment_factory) => { match self.target_attachment_factories.entry(name) { - std::collections::hash_map::Entry::Occupied(entry) => Err(anyhow::anyhow!( + std::collections::hash_map::Entry::Occupied(entry) => Err(internal_error!( "Target attachment factory with name already exists: {}", entry.key() )), From 44bc99cc06785750d252ab7beb2e5895affffe0e Mon Sep 17 00:00:00 2001 From: Srihari Thyagarajan Date: Mon, 15 Dec 2025 00:05:13 +0530 Subject: [PATCH 15/15] `refactor(error/s)`: replace `anyhow` with `client_error` and `client_bail` for _consistent_ error handling throuhgout --- rust/cocoindex/src/base/duration.rs | 28 +++--- rust/cocoindex/src/base/json_schema.rs | 2 +- rust/cocoindex/src/base/value.rs | 63 ++++++------ rust/cocoindex/src/builder/analyzer.rs | 8 +- rust/cocoindex/src/execution/db_tracking.rs | 4 +- rust/cocoindex/src/execution/dumper.rs | 2 +- rust/cocoindex/src/execution/evaluator.rs | 35 +++---- .../src/execution/indexing_status.rs | 2 +- rust/cocoindex/src/execution/memoization.rs | 6 +- rust/cocoindex/src/execution/row_indexer.rs | 2 +- .../cocoindex/src/execution/source_indexer.rs | 14 +-- rust/cocoindex/src/lib_context.rs | 4 +- rust/cocoindex/src/llm/anthropic.rs | 13 ++- rust/cocoindex/src/llm/bedrock.rs | 4 +- rust/cocoindex/src/llm/gemini.rs | 10 +- rust/cocoindex/src/llm/mod.rs | 2 +- rust/cocoindex/src/llm/openai.rs | 6 +- rust/cocoindex/src/llm/voyage.rs | 2 +- .../cocoindex/src/ops/functions/embed_text.rs | 2 +- .../src/ops/functions/split_recursively.rs | 8 +- rust/cocoindex/src/ops/py_factory.rs | 12 +-- .../cocoindex/src/ops/sources/google_drive.rs | 2 +- rust/cocoindex/src/ops/sources/postgres.rs | 20 ++-- rust/cocoindex/src/ops/targets/kuzu.rs | 2 +- rust/cocoindex/src/ops/targets/neo4j.rs | 16 ++-- rust/cocoindex/src/ops/targets/postgres.rs | 8 +- rust/cocoindex/src/ops/targets/qdrant.rs | 12 +-- .../src/ops/targets/shared/property_graph.rs | 12 +-- rust/cocoindex/src/prelude.rs | 8 +- rust/cocoindex/src/py/convert.rs | 2 +- rust/cocoindex/src/py/mod.rs | 2 +- rust/cocoindex/src/setup/driver.rs | 12 +-- rust/utils/src/error.rs | 96 ++++++++++++++++++- 33 files changed, 254 insertions(+), 167 deletions(-) diff --git a/rust/cocoindex/src/base/duration.rs b/rust/cocoindex/src/base/duration.rs index 61e8d8338..06072f468 100644 --- a/rust/cocoindex/src/base/duration.rs +++ b/rust/cocoindex/src/base/duration.rs @@ -1,6 +1,6 @@ use std::f64; -use anyhow::{Result, anyhow, bail}; +use crate::prelude::*; use chrono::Duration; /// Parses a string of number-unit pairs into a vector of (number, unit), @@ -28,20 +28,20 @@ fn parse_components( } } if num_str.is_empty() { - bail!("Expected number in: {}", original_input); + client_bail!("Expected number in: {}", original_input); } let num = num_str .parse::() - .map_err(|_| anyhow!("Invalid number '{}' in: {}", num_str, original_input))?; + .map_err(|_| client_error!("Invalid number '{}' in: {}", num_str, original_input))?; if let Some(&unit) = iter.peek() { if allowed_units.contains(&unit) { result.push((num, unit)); iter.next(); } else { - bail!("Invalid unit '{}' in: {}", unit, original_input); + client_bail!("Invalid unit '{}' in: {}", unit, original_input); } } else { - bail!( + client_bail!( "Missing unit after number '{}' in: {}", num_str, original_input @@ -60,7 +60,7 @@ fn parse_iso8601_duration(s: &str, original_input: &str) -> Result { }; if !s_after_sign.starts_with('P') { - bail!("Duration must start with 'P' in: {}", original_input); + client_bail!("Duration must start with 'P' in: {}", original_input); } let s_after_p = &s_after_sign[1..]; @@ -77,7 +77,7 @@ fn parse_iso8601_duration(s: &str, original_input: &str) -> Result { let time_components = if let Some(time_str) = time_part { let comps = parse_components(time_str, &['H', 'M', 'S'], original_input)?; if comps.is_empty() { - bail!( + client_bail!( "Time part present but no time components in: {}", original_input ); @@ -88,7 +88,7 @@ fn parse_iso8601_duration(s: &str, original_input: &str) -> Result { }; if date_components.is_empty() && time_components.is_empty() { - bail!("No components in duration: {}", original_input); + client_bail!("No components in duration: {}", original_input); } // Accumulate date duration @@ -138,7 +138,7 @@ fn parse_iso8601_duration(s: &str, original_input: &str) -> Result { fn parse_human_readable_duration(s: &str, original_input: &str) -> Result { let parts: Vec<&str> = s.split_whitespace().collect(); if parts.is_empty() || parts.len() % 2 != 0 { - bail!( + client_bail!( "Invalid human-readable duration format in: {}", original_input ); @@ -147,9 +147,9 @@ fn parse_human_readable_duration(s: &str, original_input: &str) -> Result> = parts .chunks(2) .map(|chunk| { - let num: i64 = chunk[0] - .parse() - .map_err(|_| anyhow!("Invalid number '{}' in: {}", chunk[0], original_input))?; + let num: i64 = chunk[0].parse().map_err(|_| { + client_error!("Invalid number '{}' in: {}", chunk[0], original_input) + })?; match chunk[1].to_lowercase().as_str() { "day" | "days" => Ok(Duration::days(num)), @@ -158,7 +158,7 @@ fn parse_human_readable_duration(s: &str, original_input: &str) -> Result Ok(Duration::seconds(num)), "millisecond" | "milliseconds" => Ok(Duration::milliseconds(num)), "microsecond" | "microseconds" => Ok(Duration::microseconds(num)), - _ => bail!("Invalid unit '{}' in: {}", chunk[1], original_input), + _ => client_bail!("Invalid unit '{}' in: {}", chunk[1], original_input), } }) .collect(); @@ -171,7 +171,7 @@ pub fn parse_duration(s: &str) -> Result { let original_input = s; let s = s.trim(); if s.is_empty() { - bail!("Empty duration string"); + client_bail!("Empty duration string"); } let is_likely_iso8601 = match s.as_bytes() { diff --git a/rust/cocoindex/src/base/json_schema.rs b/rust/cocoindex/src/base/json_schema.rs index 1a663337e..28c1f0486 100644 --- a/rust/cocoindex/src/base/json_schema.rs +++ b/rust/cocoindex/src/base/json_schema.rs @@ -346,7 +346,7 @@ impl ValueExtractor { .remove(object_wrapper_field_name) .unwrap_or(serde_json::Value::Null), _ => { - bail!("Field `{}` not found", object_wrapper_field_name) + client_bail!("Field `{}` not found", object_wrapper_field_name) } } } else { diff --git a/rust/cocoindex/src/base/value.rs b/rust/cocoindex/src/base/value.rs index bb191eb09..9f6f2907d 100644 --- a/rust/cocoindex/src/base/value.rs +++ b/rust/cocoindex/src/base/value.rs @@ -279,56 +279,56 @@ impl KeyPart { pub fn bytes_value(&self) -> Result<&Bytes> { match self { KeyPart::Bytes(v) => Ok(v), - _ => anyhow::bail!("expected bytes value, but got {}", self.kind_str()), + _ => client_bail!("expected bytes value, but got {}", self.kind_str()), } } pub fn str_value(&self) -> Result<&Arc> { match self { KeyPart::Str(v) => Ok(v), - _ => anyhow::bail!("expected str value, but got {}", self.kind_str()), + _ => client_bail!("expected str value, but got {}", self.kind_str()), } } pub fn bool_value(&self) -> Result { match self { KeyPart::Bool(v) => Ok(*v), - _ => anyhow::bail!("expected bool value, but got {}", self.kind_str()), + _ => client_bail!("expected bool value, but got {}", self.kind_str()), } } pub fn int64_value(&self) -> Result { match self { KeyPart::Int64(v) => Ok(*v), - _ => anyhow::bail!("expected int64 value, but got {}", self.kind_str()), + _ => client_bail!("expected int64 value, but got {}", self.kind_str()), } } pub fn range_value(&self) -> Result { match self { KeyPart::Range(v) => Ok(*v), - _ => anyhow::bail!("expected range value, but got {}", self.kind_str()), + _ => client_bail!("expected range value, but got {}", self.kind_str()), } } pub fn uuid_value(&self) -> Result { match self { KeyPart::Uuid(v) => Ok(*v), - _ => anyhow::bail!("expected uuid value, but got {}", self.kind_str()), + _ => client_bail!("expected uuid value, but got {}", self.kind_str()), } } pub fn date_value(&self) -> Result { match self { KeyPart::Date(v) => Ok(*v), - _ => anyhow::bail!("expected date value, but got {}", self.kind_str()), + _ => client_bail!("expected date value, but got {}", self.kind_str()), } } pub fn struct_value(&self) -> Result<&Vec> { match self { KeyPart::Struct(v) => Ok(v), - _ => anyhow::bail!("expected struct value, but got {}", self.kind_str()), + _ => client_bail!("expected struct value, but got {}", self.kind_str()), } } @@ -438,7 +438,7 @@ impl KeyValue { serde_json::Value::Array(arr) => std::iter::zip(arr.into_iter(), schema) .map(|(v, s)| Value::::from_json(v, &s.value_type.typ)?.into_key()) .collect::>>()?, - _ => anyhow::bail!("expected array value, but got {}", value), + _ => client_bail!("expected array value, but got {}", value), } }; Ok(Self(field_values)) @@ -848,7 +848,7 @@ impl Value { .collect::>>()?, ), Value::Null | Value::UTable(_) | Value::KTable(_) | Value::LTable(_) => { - anyhow::bail!("invalid key value type") + client_bail!("invalid key value type") } }; Ok(result) @@ -864,7 +864,7 @@ impl Value { .collect::>>()?, ), Value::Null | Value::UTable(_) | Value::KTable(_) | Value::LTable(_) => { - anyhow::bail!("invalid key value type") + client_bail!("invalid key value type") } }; Ok(result) @@ -891,70 +891,70 @@ impl Value { pub fn as_bytes(&self) -> Result<&Bytes> { match self { Value::Basic(BasicValue::Bytes(v)) => Ok(v), - _ => anyhow::bail!("expected bytes value, but got {}", self.kind()), + _ => client_bail!("expected bytes value, but got {}", self.kind()), } } pub fn as_str(&self) -> Result<&Arc> { match self { Value::Basic(BasicValue::Str(v)) => Ok(v), - _ => anyhow::bail!("expected str value, but got {}", self.kind()), + _ => client_bail!("expected str value, but got {}", self.kind()), } } pub fn as_bool(&self) -> Result { match self { Value::Basic(BasicValue::Bool(v)) => Ok(*v), - _ => anyhow::bail!("expected bool value, but got {}", self.kind()), + _ => client_bail!("expected bool value, but got {}", self.kind()), } } pub fn as_int64(&self) -> Result { match self { Value::Basic(BasicValue::Int64(v)) => Ok(*v), - _ => anyhow::bail!("expected int64 value, but got {}", self.kind()), + _ => client_bail!("expected int64 value, but got {}", self.kind()), } } pub fn as_float32(&self) -> Result { match self { Value::Basic(BasicValue::Float32(v)) => Ok(*v), - _ => anyhow::bail!("expected float32 value, but got {}", self.kind()), + _ => client_bail!("expected float32 value, but got {}", self.kind()), } } pub fn as_float64(&self) -> Result { match self { Value::Basic(BasicValue::Float64(v)) => Ok(*v), - _ => anyhow::bail!("expected float64 value, but got {}", self.kind()), + _ => client_bail!("expected float64 value, but got {}", self.kind()), } } pub fn as_range(&self) -> Result { match self { Value::Basic(BasicValue::Range(v)) => Ok(*v), - _ => anyhow::bail!("expected range value, but got {}", self.kind()), + _ => client_bail!("expected range value, but got {}", self.kind()), } } pub fn as_json(&self) -> Result<&Arc> { match self { Value::Basic(BasicValue::Json(v)) => Ok(v), - _ => anyhow::bail!("expected json value, but got {}", self.kind()), + _ => client_bail!("expected json value, but got {}", self.kind()), } } pub fn as_vector(&self) -> Result<&Arc<[BasicValue]>> { match self { Value::Basic(BasicValue::Vector(v)) => Ok(v), - _ => anyhow::bail!("expected vector value, but got {}", self.kind()), + _ => client_bail!("expected vector value, but got {}", self.kind()), } } pub fn as_struct(&self) -> Result<&FieldValues> { match self { Value::Struct(v) => Ok(v), - _ => anyhow::bail!("expected struct value, but got {}", self.kind()), + _ => client_bail!("expected struct value, but got {}", self.kind()), } } } @@ -1138,16 +1138,15 @@ impl BasicValue { (serde_json::Value::Bool(v), BasicValueType::Bool) => BasicValue::Bool(v), (serde_json::Value::Number(v), BasicValueType::Int64) => BasicValue::Int64( v.as_i64() - .ok_or_else(|| anyhow::anyhow!("invalid int64 value {v}"))?, + .ok_or_else(|| client_error!("invalid int64 value {v}"))?, ), (serde_json::Value::Number(v), BasicValueType::Float32) => BasicValue::Float32( v.as_f64() - .ok_or_else(|| anyhow::anyhow!("invalid fp32 value {v}"))? - as f32, + .ok_or_else(|| client_error!("invalid fp32 value {v}"))? as f32, ), (serde_json::Value::Number(v), BasicValueType::Float64) => BasicValue::Float64( v.as_f64() - .ok_or_else(|| anyhow::anyhow!("invalid fp64 value {v}"))?, + .ok_or_else(|| client_error!("invalid fp64 value {v}"))?, ), (v, BasicValueType::Range) => BasicValue::Range(utils::deser::from_json_value(v)?), (serde_json::Value::String(v), BasicValueType::Uuid) => BasicValue::Uuid(v.parse()?), @@ -1193,11 +1192,11 @@ impl BasicValue { (v, BasicValueType::Union(typ)) => { let arr = match v { serde_json::Value::Array(arr) => arr, - _ => anyhow::bail!("Invalid JSON value for union, expect array"), + _ => client_bail!("Invalid JSON value for union, expect array"), }; if arr.len() != 2 { - anyhow::bail!( + client_bail!( "Invalid union tuple: expect 2 values, received {}", arr.len() ); @@ -1217,7 +1216,7 @@ impl BasicValue { let cur_type = typ .types .get(tag_id) - .ok_or_else(|| anyhow::anyhow!("No type in `tag_id` \"{tag_id}\" found"))?; + .ok_or_else(|| client_error!("No type in `tag_id` \"{tag_id}\" found"))?; BasicValue::UnionVariant { tag_id, @@ -1225,7 +1224,7 @@ impl BasicValue { } } (v, t) => { - anyhow::bail!("Value and type not matched.\nTarget type {t:?}\nJSON value: {v}\n") + client_bail!("Value and type not matched.\nTarget type {t:?}\nJSON value: {v}\n") } }; Ok(result) @@ -1297,13 +1296,13 @@ where v.into_iter() .map(|v| { if s.row.fields.len() < num_key_parts { - anyhow::bail!("Invalid KTable schema: expect at least {} fields, got {}", num_key_parts, s.row.fields.len()); + client_bail!("Invalid KTable schema: expect at least {} fields, got {}", num_key_parts, s.row.fields.len()); } let mut fields_iter = s.row.fields.iter(); match v { serde_json::Value::Array(v) => { if v.len() != fields_iter.len() { - anyhow::bail!("Invalid KTable value: expect {} values, received {}", fields_iter.len(), v.len()); + client_bail!("Invalid KTable value: expect {} values, received {}", fields_iter.len(), v.len()); } let mut field_vals_iter = v.into_iter(); @@ -1365,7 +1364,7 @@ where } } (v, t) => { - anyhow::bail!("Value and type not matched.\nTarget type {t:?}\nJSON value: {v}\n") + client_bail!("Value and type not matched.\nTarget type {t:?}\nJSON value: {v}\n") } }; Ok(result) diff --git a/rust/cocoindex/src/builder/analyzer.rs b/rust/cocoindex/src/builder/analyzer.rs index 712e2e1ca..6b25e6336 100644 --- a/rust/cocoindex/src/builder/analyzer.rs +++ b/rust/cocoindex/src/builder/analyzer.rs @@ -60,7 +60,7 @@ impl StructSchemaBuilder { let field_idx = self.fields.len() as u32; match self.field_name_idx.entry(field.name.clone()) { std::collections::hash_map::Entry::Occupied(_) => { - bail!("Field name already exists: {}", field.name); + client_bail!("Field name already exists: {}", field.name); } std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(field_idx); @@ -465,7 +465,7 @@ impl DataScopeBuilder { let mut def_fp = base_def_fp; if field_path.is_empty() { - bail!("Field path is empty"); + client_bail!("Field path is empty"); } let mut i = 0; @@ -1096,7 +1096,7 @@ impl AnalyzerContext { .fields .iter() .position(|field| &field.name == f) - .ok_or_else(|| anyhow!("field not found: {}", f)) + .ok_or_else(|| client_error!("field not found: {}", f)) }) .collect::>>()?; @@ -1396,7 +1396,7 @@ pub async fn analyze_flow( targets: targets_analyzed_ss .into_iter() .enumerate() - .map(|(idx, v)| v.ok_or_else(|| anyhow!("target op `{}` not found", idx))) + .map(|(idx, v)| v.ok_or_else(|| internal_error!("target op `{}` not found", idx))) .collect::>>()?, declarations: declarations_analyzed_ss, }; diff --git a/rust/cocoindex/src/execution/db_tracking.rs b/rust/cocoindex/src/execution/db_tracking.rs index d68280eee..2020bc2e2 100644 --- a/rust/cocoindex/src/execution/db_tracking.rs +++ b/rust/cocoindex/src/execution/db_tracking.rs @@ -411,7 +411,7 @@ pub async fn read_source_state( db_executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>, ) -> Result> { let Some(table_name) = db_setup.source_state_table_name.as_ref() else { - bail!("Source state table not enabled for this flow"); + client_bail!("Source state table not enabled for this flow"); }; let query_str = format!( @@ -435,7 +435,7 @@ pub async fn upsert_source_state( db_executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>, ) -> Result<()> { let Some(table_name) = db_setup.source_state_table_name.as_ref() else { - bail!("Source state table not enabled for this flow"); + client_bail!("Source state table not enabled for this flow"); }; let query_str = format!( diff --git a/rust/cocoindex/src/execution/dumper.rs b/rust/cocoindex/src/execution/dumper.rs index a1607d0ee..cab8f902a 100644 --- a/rust/cocoindex/src/execution/dumper.rs +++ b/rust/cocoindex/src/execution/dumper.rs @@ -282,7 +282,7 @@ pub async fn evaluate_and_dump( let output_dir = Path::new(&options.output_dir); if output_dir.exists() { if !output_dir.is_dir() { - return Err(anyhow::anyhow!("The path exists and is not a directory")); + return Err(client_error!("The path exists and is not a directory")); } } else { tokio::fs::create_dir(output_dir).await?; diff --git a/rust/cocoindex/src/execution/evaluator.rs b/rust/cocoindex/src/execution/evaluator.rs index 877a99e70..a74829492 100644 --- a/rust/cocoindex/src/execution/evaluator.rs +++ b/rust/cocoindex/src/execution/evaluator.rs @@ -76,7 +76,7 @@ impl ScopeValueBuilder { .zip(&mut builder.fields) { r.set(augmented_value(v, &t.value_type.typ)?) - .map_err(|_| anyhow!("Value of field `{}` is already set", t.name)) + .map_err(|_| internal_error!("Value of field `{}` is already set", t.name)) .into_py_result()?; } Ok(builder) @@ -115,7 +115,7 @@ fn augmented_value( .map(|v| ScopeValueBuilder::augmented_from(v, t)) .collect::>>()?, ), - (val, _) => bail!("Value kind doesn't match the type {val_type}: {val:?}"), + (val, _) => internal_bail!("Value kind doesn't match the type {val_type}: {val:?}"), }; Ok(value) } @@ -195,7 +195,7 @@ impl<'a> ScopeEntry<'a> { } else { let struct_field_schema = match &field_schema.value_type.typ { schema::ValueType::Struct(s) => s, - _ => bail!("Expect struct field"), + _ => internal_bail!("Expect struct field"), }; Self::get_local_field_schema(struct_field_schema, &indices[1..])? }; @@ -211,7 +211,7 @@ impl<'a> ScopeEntry<'a> { } else if let value::KeyPart::Struct(fields) = key_val { Self::get_local_key_field(&fields[indices[0] as usize], &indices[1..])? } else { - bail!("Only struct can be accessed by sub field"); + internal_bail!("Only struct can be accessed by sub field"); }; Ok(result) } @@ -227,7 +227,7 @@ impl<'a> ScopeEntry<'a> { } else if let value::Value::Struct(fields) = val { Self::get_local_field(&fields.fields[indices[0] as usize], &indices[1..])? } else { - bail!("Only struct can be accessed by sub field"); + internal_bail!("Only struct can be accessed by sub field"); }; Ok(result) } @@ -240,7 +240,7 @@ impl<'a> ScopeEntry<'a> { let index_base = self.key.value_field_index_base(); let val = self.value.fields[(first_index - index_base) as usize] .get() - .ok_or_else(|| anyhow!("Field {} is not set", first_index))?; + .ok_or_else(|| internal_error!("Field {} is not set", first_index))?; Self::get_local_field(val, &field_ref.fields_idx[1..]) } @@ -248,14 +248,17 @@ impl<'a> ScopeEntry<'a> { let first_index = field_ref.fields_idx[0] as usize; let index_base = self.key.value_field_index_base(); let result = if first_index < index_base { - let key_val = self.key.key().ok_or_else(|| anyhow!("Key is not set"))?; + let key_val = self + .key + .key() + .ok_or_else(|| internal_error!("Key is not set"))?; let key_part = Self::get_local_key_field(&key_val[first_index], &field_ref.fields_idx[1..])?; key_part.clone().into() } else { let val = self.value.fields[(first_index - index_base) as usize] .get() - .ok_or_else(|| anyhow!("Field {} is not set", first_index))?; + .ok_or_else(|| internal_error!("Field {} is not set", first_index))?; let val_part = Self::get_local_field(val, &field_ref.fields_idx[1..])?; value::Value::from_alternative_ref(val_part) }; @@ -280,7 +283,7 @@ impl<'a> ScopeEntry<'a> { let field_index = output_field.field_idx as usize; let index_base = self.key.value_field_index_base() as usize; self.value.fields[field_index - index_base].set(val).map_err(|_| { - anyhow!("Field {field_index} for scope is already set, violating single-definition rule.") + internal_error!("Field {field_index} for scope is already set, violating single-definition rule.") })?; Ok(()) } @@ -302,7 +305,7 @@ fn assemble_value( AnalyzedValueMapping::Constant { value } => value.clone(), AnalyzedValueMapping::Field(field_ref) => scoped_entries .headn(field_ref.scope_up_level as usize) - .ok_or_else(|| anyhow!("Invalid scope_up_level: {}", field_ref.scope_up_level))? + .ok_or_else(|| internal_error!("Invalid scope_up_level: {}", field_ref.scope_up_level))? .get_field(&field_ref.local)?, AnalyzedValueMapping::Struct(mapping) => { let fields = mapping @@ -382,7 +385,7 @@ where return res; } _ = &mut timeout_future => { - return Err(anyhow!( + return Err(internal_error!( "Function '{}' ({}) timed out after {} seconds", op_kind, op_name, timeout_duration.as_secs() )); @@ -481,7 +484,7 @@ async fn evaluate_op_scope( let target_field_schema = head_scope.get_field_schema(&op.local_field_ref)?; let table_schema = match &target_field_schema.value_type.typ { schema::ValueType::Table(cs) => cs, - _ => bail!("Expect target field to be a table"), + _ => internal_bail!("Expect target field to be a table"), }; let target_field = head_scope.get_value_field_builder(&op.local_field_ref)?; @@ -543,7 +546,7 @@ async fn evaluate_op_scope( }) .collect::>(), _ => { - bail!("Target field type is expected to be a table"); + internal_bail!("Target field type is expected to be a table"); } }; try_join_all(task_futs) @@ -575,7 +578,7 @@ async fn evaluate_op_scope( }; let collector_entry = scoped_entries .headn(op.collector_ref.scope_up_level as usize) - .ok_or_else(|| anyhow::anyhow!("Collector level out of bound"))?; + .ok_or_else(|| internal_error!("Collector level out of bound"))?; // Assemble input values let input_values: Vec = @@ -670,7 +673,7 @@ pub async fn evaluate_source_entry( { schema::ValueType::Table(cs) => cs, _ => { - bail!("Expect source output to be a table") + internal_bail!("Expect source output to be a table") } }; @@ -725,7 +728,7 @@ pub async fn evaluate_transient_flow( ); if input_values.len() != flow.execution_plan.input_fields.len() { - bail!( + client_bail!( "Input values length mismatch: expect {}, got {}", flow.execution_plan.input_fields.len(), input_values.len() diff --git a/rust/cocoindex/src/execution/indexing_status.rs b/rust/cocoindex/src/execution/indexing_status.rs index fa2994f53..cb039959b 100644 --- a/rust/cocoindex/src/execution/indexing_status.rs +++ b/rust/cocoindex/src/execution/indexing_status.rs @@ -20,7 +20,7 @@ impl SourceLogicFingerprint { let import_op = &exec_plan.import_ops[source_idx]; let mut fp = Fingerprinter::default(); if exec_plan.export_ops.len() != export_exec_ctx.len() { - bail!("`export_ops` count does not match `export_exec_ctx` count"); + internal_bail!("`export_ops` count does not match `export_exec_ctx` count"); } for (export_op, export_op_exec_ctx) in std::iter::zip(exec_plan.export_ops.iter(), export_exec_ctx.iter()) diff --git a/rust/cocoindex/src/execution/memoization.rs b/rust/cocoindex/src/execution/memoization.rs index a5422aff2..7e89ccae2 100644 --- a/rust/cocoindex/src/execution/memoization.rs +++ b/rust/cocoindex/src/execution/memoization.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, bail}; +use crate::prelude::*; use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, @@ -127,7 +127,7 @@ impl EvaluationMemory { pub fn into_stored(self) -> Result { if self.evaluation_only { - bail!("For evaluation only, cannot convert to stored MemoizationInfo"); + internal_bail!("For evaluation only, cannot convert to stored MemoizationInfo"); } let cache = if let Some(cache) = self.cache { cache @@ -150,7 +150,7 @@ impl EvaluationMemory { }) .collect::>()? } else { - bail!("Cache is disabled, cannot convert to stored MemoizationInfo"); + internal_bail!("Cache is disabled, cannot convert to stored MemoizationInfo"); }; let uuids = self .uuids diff --git a/rust/cocoindex/src/execution/row_indexer.rs b/rust/cocoindex/src/execution/row_indexer.rs index d2395d002..c3e1d1bad 100644 --- a/rust/cocoindex/src/execution/row_indexer.rs +++ b/rust/cocoindex/src/execution/row_indexer.rs @@ -899,7 +899,7 @@ pub async fn evaluate_source_entry_with_memory( ) .await? .value - .ok_or_else(|| anyhow::anyhow!("value not returned"))?; + .ok_or_else(|| internal_error!("value not returned"))?; let output = match source_value { interface::SourceValue::Existence(source_value) => { Some(evaluate_source_entry(src_eval_ctx, source_value, &memory, None).await?) diff --git a/rust/cocoindex/src/execution/source_indexer.rs b/rust/cocoindex/src/execution/source_indexer.rs index b47d53066..a5f15d775 100644 --- a/rust/cocoindex/src/execution/source_indexer.rs +++ b/rust/cocoindex/src/execution/source_indexer.rs @@ -439,10 +439,10 @@ impl SourceIndexingContext { if let Some(ref op_stats) = operation_in_process_stats_for_async { op_stats.start_processing(&operation_name_for_async, 1); } - let row_input = row_input - .key_aux_info - .as_ref() - .ok_or_else(|| anyhow!("`key_aux_info` must be provided"))?; + let row_input = + row_input.key_aux_info.as_ref().ok_or_else(|| { + internal_error!("`key_aux_info` must be provided") + })?; let read_options = interface::SourceExecutorReadOptions { include_value: true, include_ordinal: true, @@ -461,7 +461,7 @@ impl SourceIndexingContext { .unwrap_or(interface::Ordinal::unavailable()), data.content_version_fp, data.value - .ok_or_else(|| anyhow::anyhow!("value is not available"))?, + .ok_or_else(|| internal_error!("value is not available"))?, ) } }; @@ -591,7 +591,7 @@ impl SourceIndexingContext { let source_version = SourceVersion::from_current_with_ordinal( row.data .ordinal - .ok_or_else(|| anyhow::anyhow!("ordinal is not available"))?, + .ok_or_else(|| internal_error!("ordinal is not available"))?, ); { let mut state = self.state.lock().unwrap(); @@ -704,7 +704,7 @@ impl batching::Runner for UpdateOnceRunner { let input = inputs .into_iter() .next() - .ok_or_else(|| anyhow::anyhow!("no input"))?; + .ok_or_else(|| internal_error!("no input"))?; input .context .update_once(&input.stats, &update_options) diff --git a/rust/cocoindex/src/lib_context.rs b/rust/cocoindex/src/lib_context.rs index 5857c89cf..a04ac66e8 100644 --- a/rust/cocoindex/src/lib_context.rs +++ b/rust/cocoindex/src/lib_context.rs @@ -268,7 +268,7 @@ impl LibContext { pub fn require_persistence_ctx(&self) -> Result<&PersistenceContext> { self.persistence_ctx.as_ref().ok_or_else(|| { - anyhow!( + client_error!( "Database is required for this operation. \ The easiest way is to set COCOINDEX_DATABASE_URL environment variable. \ Please see https://cocoindex.io/docs/core/settings for more details." @@ -333,7 +333,7 @@ fn get_settings() -> Result { let settings = if let Some(get_settings_fn) = &*get_settings_fn { get_settings_fn()? } else { - bail!("CocoIndex setting function is not provided"); + client_bail!("CocoIndex setting function is not provided"); }; Ok(settings) } diff --git a/rust/cocoindex/src/llm/anthropic.rs b/rust/cocoindex/src/llm/anthropic.rs index bf12b753c..4ef06372f 100644 --- a/rust/cocoindex/src/llm/anthropic.rs +++ b/rust/cocoindex/src/llm/anthropic.rs @@ -22,9 +22,8 @@ impl Client { let api_key = if let Some(key) = api_key { key } else { - std::env::var("ANTHROPIC_API_KEY").map_err(|_| { - anyhow::anyhow!("ANTHROPIC_API_KEY environment variable must be set") - })? + std::env::var("ANTHROPIC_API_KEY") + .map_err(|_| client_error!("ANTHROPIC_API_KEY environment variable must be set"))? }; Ok(Self { @@ -105,7 +104,7 @@ impl LlmGenerationClient for Client { let mut resp_json: serde_json::Value = resp.json().await.context("Invalid JSON")?; if let Some(error) = resp_json.get("error") { - bail!("Anthropic API error: {:?}", error); + client_bail!("Anthropic API error: {:?}", error); } // Debug print full response @@ -144,16 +143,16 @@ impl LlmGenerationClient for Client { serde_json::to_string(&value)? } Err(e2) => { - return Err(anyhow::anyhow!(format!( + return Err(client_error!( "No structured tool output or text found in response, and permissive JSON5 parsing also failed: {e}; {e2}" - ))); + )); } } } } } _ => { - return Err(anyhow::anyhow!( + return Err(client_error!( "No structured tool output or text found in response" )); } diff --git a/rust/cocoindex/src/llm/bedrock.rs b/rust/cocoindex/src/llm/bedrock.rs index 00baccd45..5ac295ed3 100644 --- a/rust/cocoindex/src/llm/bedrock.rs +++ b/rust/cocoindex/src/llm/bedrock.rs @@ -123,7 +123,7 @@ impl LlmGenerationClient for Client { // Check for errors in the response if let Some(error) = resp_json.get("error") { - bail!("Bedrock API error: {:?}", error); + client_bail!("Bedrock API error: {:?}", error); } // Debug print full response (uncomment for debugging) @@ -162,7 +162,7 @@ impl LlmGenerationClient for Client { text_parts.join("") } } else { - return Err(anyhow::anyhow!("No content found in Bedrock response")); + return Err(client_error!("No content found in Bedrock response")); }; Ok(LlmGenerateResponse { text }) diff --git a/rust/cocoindex/src/llm/gemini.rs b/rust/cocoindex/src/llm/gemini.rs index b475c2c18..bf9d12c8c 100644 --- a/rust/cocoindex/src/llm/gemini.rs +++ b/rust/cocoindex/src/llm/gemini.rs @@ -43,7 +43,7 @@ impl AiStudioClient { key } else { std::env::var("GEMINI_API_KEY") - .map_err(|_| anyhow::anyhow!("GEMINI_API_KEY environment variable must be set"))? + .map_err(|_| client_error!("GEMINI_API_KEY environment variable must be set"))? }; Ok(Self { @@ -154,12 +154,12 @@ impl LlmGenerationClient for AiStudioClient { let resp_json: Value = resp.json().await.context("Invalid JSON")?; if let Some(error) = resp_json.get("error") { - bail!("Gemini API error: {:?}", error); + client_bail!("Gemini API error: {:?}", error); } let mut resp_json = resp_json; let text = match &mut resp_json["candidates"][0]["content"]["parts"][0]["text"] { Value::String(s) => std::mem::take(s), - _ => bail!("No text in response"), + _ => client_bail!("No text in response"), }; Ok(LlmGenerateResponse { text }) @@ -365,7 +365,7 @@ impl LlmGenerationClient for VertexAiClient { .and_then(|content| content.parts.into_iter().next()) .and_then(|part| part.data) else { - bail!("No text in response"); + client_bail!("No text in response"); }; Ok(super::LlmGenerateResponse { text }) } @@ -428,7 +428,7 @@ impl LlmEmbeddingClient for VertexAiClient { let embeddings = prediction .get_mut("embeddings") .map(|v| v.take()) - .ok_or_else(|| anyhow::anyhow!("No embeddings in prediction"))?; + .ok_or_else(|| client_error!("No embeddings in prediction"))?; let embedding: ContentEmbedding = utils::deser::from_json_value(embeddings)?; Ok(embedding.values) }) diff --git a/rust/cocoindex/src/llm/mod.rs b/rust/cocoindex/src/llm/mod.rs index a7a1cb5b9..39fd7e6f9 100644 --- a/rust/cocoindex/src/llm/mod.rs +++ b/rust/cocoindex/src/llm/mod.rs @@ -209,6 +209,6 @@ pub fn detect_image_mime_type(bytes: &[u8]) -> Result<&'static str> { let infer = &*INFER; match infer.get(bytes) { Some(info) if info.mime_type().starts_with("image/") => Ok(info.mime_type()), - _ => bail!("Unknown or unsupported image format"), + _ => client_bail!("Unknown or unsupported image format"), } } diff --git a/rust/cocoindex/src/llm/openai.rs b/rust/cocoindex/src/llm/openai.rs index 20faf5312..dbfe45705 100644 --- a/rust/cocoindex/src/llm/openai.rs +++ b/rust/cocoindex/src/llm/openai.rs @@ -82,7 +82,7 @@ impl Client { }; let api_base = - address.ok_or_else(|| anyhow::anyhow!("address is required for Azure OpenAI"))?; + address.ok_or_else(|| client_error!("address is required for Azure OpenAI"))?; // Default to API version that supports structured outputs (json_schema). let api_version = config @@ -91,7 +91,7 @@ impl Client { let api_key = api_key .or_else(|| std::env::var("AZURE_OPENAI_API_KEY").ok()) - .ok_or_else(|| anyhow::anyhow!( + .ok_or_else(|| client_error!( "AZURE_OPENAI_API_KEY must be set either via api_key parameter or environment variable" ))?; @@ -201,7 +201,7 @@ where .into_iter() .next() .and_then(|choice| choice.message.content) - .ok_or_else(|| anyhow::anyhow!("No response from OpenAI"))?; + .ok_or_else(|| client_error!("No response from OpenAI"))?; Ok(super::LlmGenerateResponse { text }) } diff --git a/rust/cocoindex/src/llm/voyage.rs b/rust/cocoindex/src/llm/voyage.rs index a2acad904..5ed1b31cb 100644 --- a/rust/cocoindex/src/llm/voyage.rs +++ b/rust/cocoindex/src/llm/voyage.rs @@ -42,7 +42,7 @@ impl Client { key } else { std::env::var("VOYAGE_API_KEY") - .map_err(|_| anyhow::anyhow!("VOYAGE_API_KEY environment variable must be set"))? + .map_err(|_| client_error!("VOYAGE_API_KEY environment variable must be set"))? }; Ok(Self { diff --git a/rust/cocoindex/src/ops/functions/embed_text.rs b/rust/cocoindex/src/ops/functions/embed_text.rs index 2d83783ac..bd2a689cf 100644 --- a/rust/cocoindex/src/ops/functions/embed_text.rs +++ b/rust/cocoindex/src/ops/functions/embed_text.rs @@ -81,7 +81,7 @@ impl BatchedFunctionExecutor for Executor { actual = embedding.len(), ); } else { - bail!( + client_bail!( "Expected output dimension {expected} but got {actual} from the embedding API. \ Consider setting `output_dimension` to {actual} as a workaround.", expected = self.args.expected_output_dimension, diff --git a/rust/cocoindex/src/ops/functions/split_recursively.rs b/rust/cocoindex/src/ops/functions/split_recursively.rs index 381218a89..334854e3f 100644 --- a/rust/cocoindex/src/ops/functions/split_recursively.rs +++ b/rust/cocoindex/src/ops/functions/split_recursively.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, anyhow}; +use anyhow::Context; use regex::{Matches, Regex}; use std::sync::LazyLock; use std::{collections::HashMap, sync::Arc}; @@ -675,9 +675,9 @@ impl SimpleFunctionExecutor for Executor { { let mut parser = tree_sitter::Parser::new(); parser.set_language(&tree_sitter_info.tree_sitter_lang)?; - let tree = parser - .parse(full_text.as_ref(), None) - .ok_or_else(|| anyhow!("failed in parsing text in language: {}", lang_info.name))?; + let tree = parser.parse(full_text.as_ref(), None).ok_or_else(|| { + internal_error!("failed in parsing text in language: {}", lang_info.name) + })?; recursive_chunker.split_root_chunk(ChunkKind::TreeSitterNode { tree_sitter_info, node: tree.root_node(), diff --git a/rust/cocoindex/src/ops/py_factory.rs b/rust/cocoindex/src/ops/py_factory.rs index 1ae4fa4b6..d9ea3cdf5 100644 --- a/rust/cocoindex/src/ops/py_factory.rs +++ b/rust/cocoindex/src/ops/py_factory.rs @@ -12,7 +12,7 @@ use crate::{ ops::sdk::SetupStateCompatibility, py::{self, ToResultWithPyTrace}, }; -use anyhow::{Result, anyhow}; +use anyhow::Result; use py_utils::from_py_future; #[pyclass(name = "OpArgSchema")] @@ -243,7 +243,7 @@ impl interface::SimpleFunctionFactory for PyFunctionFactory { let py_exec_ctx = context .py_exec_ctx .as_ref() - .ok_or_else(|| anyhow!("Python execution context is missing"))? + .ok_or_else(|| internal_error!("Python execution context is missing"))? .clone(); let (prepare_fut, enable_cache, timeout, batching_options) = Python::attach(|py| -> anyhow::Result<_> { @@ -473,7 +473,7 @@ impl PySourceExecutor { // Each item should be a tuple of (key, data) let tuple = bound_item .cast::() - .map_err(|e| anyhow!("Failed to downcast to PyTuple: {}", e))?; + .map_err(|e| client_error!("Failed to downcast to PyTuple: {}", e))?; if tuple.len() != 2 { api_bail!("Expected tuple of length 2 from Python source iterator"); } @@ -579,7 +579,7 @@ impl interface::SourceFactory for PySourceConnectorFactory { let py_exec_ctx = context .py_exec_ctx .as_ref() - .ok_or_else(|| anyhow!("Python execution context is missing"))? + .ok_or_else(|| internal_error!("Python execution context is missing"))? .clone(); // First get the table type (this doesn't require executor) @@ -745,7 +745,7 @@ impl interface::TargetFactory for PyExportTargetFactory { let py_exec_ctx = context .py_exec_ctx .as_ref() - .ok_or_else(|| anyhow!("Python execution context is missing"))? + .ok_or_else(|| internal_error!("Python execution context is missing"))? .clone(); for data_collection in data_collections.into_iter() { let (py_export_ctx, persistent_key, setup_state) = Python::attach(|py| { @@ -948,7 +948,7 @@ impl interface::TargetFactory for PyExportTargetFactory { let py_exec_ctx = context .py_exec_ctx .as_ref() - .ok_or_else(|| anyhow!("Python execution context is missing"))? + .ok_or_else(|| internal_error!("Python execution context is missing"))? .clone(); let py_result = Python::attach(move |py| -> Result<_> { let result_coro = self diff --git a/rust/cocoindex/src/ops/sources/google_drive.rs b/rust/cocoindex/src/ops/sources/google_drive.rs index d6c5a0b13..4f9098c15 100644 --- a/rust/cocoindex/src/ops/sources/google_drive.rs +++ b/rust/cocoindex/src/ops/sources/google_drive.rs @@ -219,7 +219,7 @@ impl Executor { if modified_time <= *cutoff_time { break 'paginate; } - let file_id = file.id.ok_or_else(|| anyhow!("File has no id"))?; + let file_id = file.id.ok_or_else(|| internal_error!("File has no id"))?; if self.is_file_covered(&file_id).await? { changes.push(SourceChange { key: KeyValue::from_single_part(file_id), diff --git a/rust/cocoindex/src/ops/sources/postgres.rs b/rust/cocoindex/src/ops/sources/postgres.rs index e45b83335..30f527205 100644 --- a/rust/cocoindex/src/ops/sources/postgres.rs +++ b/rust/cocoindex/src/ops/sources/postgres.rs @@ -345,7 +345,7 @@ async fn fetch_table_schema( Some(ord) => { let schema = ordinal_field_schema .as_ref() - .ok_or_else(|| anyhow::anyhow!("`ordinal_column` `{}` not found in table", ord))?; + .ok_or_else(|| client_error!("`ordinal_column` `{}` not found in table", ord))?; if !is_supported_ordinal_type(&schema.schema.value_type.typ) { api_bail!( "Unsupported `ordinal_column` type for `{}`. Supported types: Int64, LocalDateTime, OffsetDateTime", @@ -471,7 +471,7 @@ impl SourceExecutor for PostgresSourceExecutor { qb.push("\" WHERE "); if key.len() != self.table_schema.primary_key_columns.len() { - bail!( + internal_bail!( "Composite key has {} values but table has {} primary key columns", key.len(), self.table_schema.primary_key_columns.len() @@ -648,10 +648,10 @@ impl PostgresSourceExecutor { let mut payload: serde_json::Value = utils::deser::from_json_str(notification.payload())?; let payload = payload .as_object_mut() - .ok_or_else(|| anyhow::anyhow!("'fields' field is not an object"))?; + .ok_or_else(|| client_error!("'fields' field is not an object"))?; let Some(serde_json::Value::String(op)) = payload.get_mut("op") else { - return Err(anyhow::anyhow!( + return Err(client_error!( "Missing or invalid 'op' field in notification" )); }; @@ -660,16 +660,16 @@ impl PostgresSourceExecutor { let mut fields = std::mem::take( payload .get_mut("fields") - .ok_or_else(|| anyhow::anyhow!("Missing 'fields' field in notification"))? + .ok_or_else(|| client_error!("Missing 'fields' field in notification"))? .as_object_mut() - .ok_or_else(|| anyhow::anyhow!("'fields' field is not an object"))?, + .ok_or_else(|| client_error!("'fields' field is not an object"))?, ); // Extract primary key values to construct the key let mut key_parts = Vec::with_capacity(self.table_schema.primary_key_columns.len()); for pk_col in &self.table_schema.primary_key_columns { let field_value = fields.get_mut(&pk_col.schema.name).ok_or_else(|| { - anyhow::anyhow!("Missing primary key field: {}", pk_col.schema.name) + client_error!("Missing primary key field: {}", pk_col.schema.name) })?; let key_part = Self::decode_key_ordinal_value_in_json( @@ -712,7 +712,7 @@ impl PostgresSourceExecutor { content_version_fp: None, } } - _ => return Err(anyhow::anyhow!("Unknown operation: {}", op)), + _ => return Err(client_error!("Unknown operation: {}", op)), }; Ok(SourceChange { @@ -742,7 +742,7 @@ impl PostgresSourceExecutor { if let Some(i) = n.as_i64() { BasicValue::Int64(i).into() } else { - bail!("Invalid integer value: {}", n) + client_bail!("Invalid integer value: {}", n) } } (ValueType::Basic(BasicValueType::Uuid), serde_json::Value::String(s)) => { @@ -762,7 +762,7 @@ impl PostgresSourceExecutor { BasicValue::OffsetDateTime(dt).into() } (_, json_value) => { - bail!( + client_bail!( "Got unsupported JSON value for type {value_type}: {}", serde_json::to_string(&json_value)? ); diff --git a/rust/cocoindex/src/ops/targets/kuzu.rs b/rust/cocoindex/src/ops/targets/kuzu.rs index 48a999b6a..3fb550015 100644 --- a/rust/cocoindex/src/ops/targets/kuzu.rs +++ b/rust/cocoindex/src/ops/targets/kuzu.rs @@ -362,7 +362,7 @@ fn append_basic_value(cypher: &mut CypherBuilder, basic_value: &BasicValue) -> R write!(cypher.query_mut(), "]")?; } v @ (BasicValue::UnionVariant { .. } | BasicValue::Time(_) | BasicValue::Json(_)) => { - bail!("value types are not supported in Kuzu: {}", v.kind()); + client_bail!("value types are not supported in Kuzu: {}", v.kind()); } } Ok(()) diff --git a/rust/cocoindex/src/ops/targets/neo4j.rs b/rust/cocoindex/src/ops/targets/neo4j.rs index ad2f54b45..7c603c4c9 100644 --- a/rust/cocoindex/src/ops/targets/neo4j.rs +++ b/rust/cocoindex/src/ops/targets/neo4j.rs @@ -115,7 +115,7 @@ fn json_value_to_bolt_value(value: &serde_json::Value) -> Result { } else if let Some(f) = v.as_f64() { BoltType::Float(neo4rs::BoltFloat::new(f)) } else { - anyhow::bail!("Unsupported JSON number: {}", v) + client_bail!("Unsupported JSON number: {}", v) } } serde_json::Value::String(v) => BoltType::String(neo4rs::BoltString::new(v)), @@ -213,7 +213,7 @@ fn basic_value_to_bolt(value: &BasicValue, schema: &BasicValueType) -> Result>()?, }), - _ => anyhow::bail!("Non-vector type got vector value: {}", schema), + _ => internal_bail!("Non-vector type got vector value: {}", schema), }, BasicValue::Json(v) => json_value_to_bolt_value(v)?, BasicValue::UnionVariant { tag_id, value } => match schema { @@ -221,11 +221,11 @@ fn basic_value_to_bolt(value: &BasicValue, schema: &BasicValueType) -> Result anyhow::bail!("Non-union type got union value: {}", schema), + _ => internal_bail!("Non-union type got union value: {}", schema), }, }; Ok(bolt_value) @@ -236,11 +236,11 @@ fn value_to_bolt(value: &Value, schema: &schema::ValueType) -> Result Value::Null => BoltType::Null(neo4rs::BoltNull), Value::Basic(v) => match schema { ValueType::Basic(t) => basic_value_to_bolt(v, t)?, - _ => anyhow::bail!("Non-basic type got basic value: {}", schema), + _ => internal_bail!("Non-basic type got basic value: {}", schema), }, Value::Struct(v) => match schema { ValueType::Struct(t) => field_values_to_bolt(v.fields.iter(), t.fields.iter())?, - _ => anyhow::bail!("Non-struct type got struct value: {}", schema), + _ => internal_bail!("Non-struct type got struct value: {}", schema), }, Value::UTable(v) | Value::LTable(v) => match schema { ValueType::Table(t) => BoltType::List(neo4rs::BoltList { @@ -249,7 +249,7 @@ fn value_to_bolt(value: &Value, schema: &schema::ValueType) -> Result .map(|v| field_values_to_bolt(v.0.fields.iter(), t.row.fields.iter())) .collect::>()?, }), - _ => anyhow::bail!("Non-table type got table value: {}", schema), + _ => internal_bail!("Non-table type got table value: {}", schema), }, Value::KTable(v) => match schema { ValueType::Table(t) => BoltType::List(neo4rs::BoltList { @@ -263,7 +263,7 @@ fn value_to_bolt(value: &Value, schema: &schema::ValueType) -> Result }) .collect::>()?, }), - _ => anyhow::bail!("Non-table type got table value: {}", schema), + _ => internal_bail!("Non-table type got table value: {}", schema), }, }; Ok(bolt_value) diff --git a/rust/cocoindex/src/ops/targets/postgres.rs b/rust/cocoindex/src/ops/targets/postgres.rs index f37198b98..36636f445 100644 --- a/rust/cocoindex/src/ops/targets/postgres.rs +++ b/rust/cocoindex/src/ops/targets/postgres.rs @@ -125,7 +125,7 @@ fn bind_value_field<'arg>( BasicValue::Float32(v) => *v, BasicValue::Float64(v) => *v as f32, BasicValue::Int64(v) => *v as f32, - v => bail!("unexpected vector element type: {}", v.kind()), + v => client_bail!("unexpected vector element type: {}", v.kind()), }) }) .collect::>>()?; @@ -248,7 +248,7 @@ impl ExportContext { bind_key_field(&mut query_builder, key_value)?; } if self.value_fields_schema.len() != upsert.value.fields.len() { - bail!( + internal_bail!( "unmatched value length: {} vs {}", self.value_fields_schema.len(), upsert.value.fields.len() @@ -825,7 +825,7 @@ impl TargetFactoryBase for TargetFactory { .map(|d| { // Validate: if schema is specified, table_name must be explicit if d.spec.schema.is_some() && d.spec.table_name.is_none() { - bail!( + client_bail!( "Postgres target '{}': when 'schema' is specified, 'table_name' must also be explicitly provided. \ Auto-generated table names are not supported with custom schemas", d.name @@ -913,7 +913,7 @@ impl TargetFactoryBase for TargetFactory { for mut_groups in mut_groups_by_db_ref.values() { let db_pool = &mut_groups .first() - .ok_or_else(|| anyhow!("empty group"))? + .ok_or_else(|| internal_error!("empty group"))? .export_context .db_pool; let mut txn = db_pool.begin().await?; diff --git a/rust/cocoindex/src/ops/targets/qdrant.rs b/rust/cocoindex/src/ops/targets/qdrant.rs index fb62d2221..3030bf086 100644 --- a/rust/cocoindex/src/ops/targets/qdrant.rs +++ b/rust/cocoindex/src/ops/targets/qdrant.rs @@ -95,11 +95,11 @@ fn encode_dense_vector(v: &BasicValue) -> Result { BasicValue::Float32(f) => *f, BasicValue::Float64(f) => *f as f32, BasicValue::Int64(i) => *i as f32, - _ => bail!("Unsupported vector type: {:?}", elem.kind()), + _ => client_bail!("Unsupported vector type: {:?}", elem.kind()), }) }) .collect::>>()?, - _ => bail!("Expected a vector field, got {:?}", v), + _ => client_bail!("Expected a vector field, got {:?}", v), }; Ok(vec.into()) } @@ -110,7 +110,7 @@ fn encode_multi_dense_vector(v: &BasicValue) -> Result { .iter() .map(encode_dense_vector) .collect::>>()?, - _ => bail!("Expected a vector field, got {:?}", v), + _ => client_bail!("Expected a vector field, got {:?}", v), }; Ok(vecs.into()) } @@ -225,7 +225,7 @@ impl SetupChange { params = params.multivector_config(MultiVectorConfigBuilder::new( MultiVectorComparator::from_str_name(multi_vector_comparator) .ok_or_else(|| { - anyhow!( + client_error!( "unrecognized multi vector comparator: {}", multi_vector_comparator ) @@ -293,7 +293,7 @@ fn key_to_point_id(key_value: &KeyValue) -> Result { KeyPart::Str(v) => PointId::from(v.to_string()), KeyPart::Int64(v) => PointId::from(*v as u64), KeyPart::Uuid(v) => PointId::from(v.to_string()), - e => bail!("Invalid Qdrant point ID: {e}"), + e => client_bail!("Invalid Qdrant point ID: {e}"), }; Ok(point_id) @@ -322,7 +322,7 @@ fn values_to_payload( } }, _ => { - bail!("Expected a vector field, got {:?}", value); + client_bail!("Expected a vector field, got {:?}", value); } }; vectors = vectors.add_vector(field_name.clone(), vector); diff --git a/rust/cocoindex/src/ops/targets/shared/property_graph.rs b/rust/cocoindex/src/ops/targets/shared/property_graph.rs index 92990c34b..7646b87c1 100644 --- a/rust/cocoindex/src/ops/targets/shared/property_graph.rs +++ b/rust/cocoindex/src/ops/targets/shared/property_graph.rs @@ -193,7 +193,7 @@ impl GraphElementSchemaBuilder { fields_idx } else { if existing_fields.len() != fields.len() { - bail!( + client_bail!( "{elem_type} {kind} fields number mismatch: {} vs {}", existing_fields.len(), fields.len() @@ -208,13 +208,13 @@ impl GraphElementSchemaBuilder { .iter() .map(|existing_field| { let (idx, typ) = fields_map.remove(&existing_field.name).ok_or_else(|| { - anyhow!( + client_error!( "{elem_type} {kind} field `{}` not found in some collector", existing_field.name ) })?; if typ != existing_field.value_type { - bail!( + client_bail!( "{elem_type} {kind} field `{}` type mismatch: {} vs {}", existing_field.name, typ, @@ -249,7 +249,7 @@ impl GraphElementSchemaBuilder { fn build_schema(self) -> Result { if self.key_fields.is_empty() { - bail!( + client_bail!( "No key fields specified for Node label `{}`", self.elem_type ); @@ -310,7 +310,7 @@ impl<'a, AuthEntry> DependentNodeLabelAnalyzer<'a, AuthEntry> { schema_builders: &mut HashMap, GraphElementSchemaBuilder>, ) -> Result<(GraphElementType, GraphElementInputFieldsIdx)> { if !self.remaining_fields.is_empty() { - anyhow::bail!( + client_bail!( "Fields not mapped for {}: {}", self.graph_elem_type, self.remaining_fields.keys().join(", ") @@ -323,7 +323,7 @@ impl<'a, AuthEntry> DependentNodeLabelAnalyzer<'a, AuthEntry> { .map(|(field_name, (idx, typ))| (idx, FieldSchema::new(field_name, typ))) .partition(|(_, f)| self.primary_key_fields.contains(&f.name)); if key_fields.len() != self.primary_key_fields.len() { - bail!( + client_bail!( "Primary key fields number mismatch: {} vs {}", key_fields.iter().map(|(_, f)| &f.name).join(", "), self.primary_key_fields.iter().join(", ") diff --git a/rust/cocoindex/src/prelude.rs b/rust/cocoindex/src/prelude.rs index 02bf1a4ee..91b9e25e4 100644 --- a/rust/cocoindex/src/prelude.rs +++ b/rust/cocoindex/src/prelude.rs @@ -1,6 +1,5 @@ #![allow(unused_imports)] -pub(crate) use anyhow::{Context, Result}; pub(crate) use async_trait::async_trait; pub(crate) use chrono::{DateTime, Utc}; pub(crate) use futures::{FutureExt, StreamExt}; @@ -27,14 +26,13 @@ pub(crate) use crate::setup; pub(crate) use crate::setup::AuthRegistry; pub(crate) use cocoindex_utils as utils; pub(crate) use cocoindex_utils::error::{ApiError, invariance_violation}; -pub(crate) use cocoindex_utils::error::{ - CError, CResult, ContextExt, HostError, IntoInternal, ResultExt, -}; +pub(crate) use cocoindex_utils::error::{CError, CResult, ContextExt, IntoInternal, ResultExt}; pub(crate) use cocoindex_utils::{api_bail, api_error}; pub(crate) use cocoindex_utils::{batching, concur_control, http, retryable}; pub(crate) use cocoindex_utils::{client_bail, client_error, internal_bail, internal_error}; -pub(crate) use anyhow::{anyhow, bail}; +pub(crate) use anyhow::{Context, Result}; + pub(crate) use async_stream::{stream, try_stream}; pub(crate) use tracing::{Span, debug, error, info, info_span, instrument, trace, warn}; diff --git a/rust/cocoindex/src/py/convert.rs b/rust/cocoindex/src/py/convert.rs index 122a38da1..394aa8e2e 100644 --- a/rust/cocoindex/src/py/convert.rs +++ b/rust/cocoindex/src/py/convert.rs @@ -321,7 +321,7 @@ pub fn value_from_py_object<'py>( .map(|v| { let mut iter = v.fields.into_iter(); if iter.len() < num_key_parts { - anyhow::bail!( + client_bail!( "Invalid KTable value: expect at least {} fields, got {}", num_key_parts, iter.len() diff --git a/rust/cocoindex/src/py/mod.rs b/rust/cocoindex/src/py/mod.rs index d4653fe4f..ba6f7e191 100644 --- a/rust/cocoindex/src/py/mod.rs +++ b/rust/cocoindex/src/py/mod.rs @@ -400,7 +400,7 @@ impl Flow { let py_exec_ctx = flow_ctx .py_exec_ctx .as_ref() - .ok_or_else(|| anyhow!("Python execution context is missing"))?; + .ok_or_else(|| internal_error!("Python execution context is missing"))?; let task_locals = pyo3_async_runtimes::TaskLocals::new( py_exec_ctx.event_loop.bind(py).clone(), ); diff --git a/rust/cocoindex/src/setup/driver.rs b/rust/cocoindex/src/setup/driver.rs index 7bb5fac54..7b77c92f0 100644 --- a/rust/cocoindex/src/setup/driver.rs +++ b/rust/cocoindex/src/setup/driver.rs @@ -54,7 +54,7 @@ impl std::str::FromStr for MetadataRecordType { } else if let Some(target_id) = s.strip_prefix("Target:") { Ok(Self::Target(target_id.to_string())) } else { - anyhow::bail!("Invalid MetadataRecordType string: {}", s) + internal_bail!("Invalid MetadataRecordType string: {}", s) } } } @@ -222,7 +222,7 @@ fn group_states "Creating", ObjectStatus::Deleted => "Deleting", ObjectStatus::Existing => "Updating resources for ", - _ => bail!("invalid flow status"), + _ => internal_bail!("invalid flow status"), }; write!(write, "\n{verb} flow {}:\n", flow_ctx.flow_name())?; @@ -647,7 +647,7 @@ async fn apply_changes_for_flow( resources.into_iter(), |targets_change| async move { let factory = get_export_target_factory(target_kind).ok_or_else(|| { - anyhow::anyhow!("No factory found for target kind: {}", target_kind) + internal_error!("No factory found for target kind: {}", target_kind) })?; for target_change in targets_change.iter() { for delete in target_change.setup_change.attachments_change.deletes.iter() { @@ -793,7 +793,7 @@ impl SetupChangeBundle { let flows = lib_context.flows.lock().unwrap(); flows .get(flow_name) - .ok_or_else(|| anyhow::anyhow!("Flow instance not found: {flow_name}"))? + .ok_or_else(|| client_error!("Flow instance not found: {flow_name}"))? .clone() }; let flow_exec_ctx = flow_ctx.get_execution_ctx_for_setup().read().await; @@ -845,7 +845,7 @@ impl SetupChangeBundle { let flows = lib_context.flows.lock().unwrap(); flows .get(flow_name) - .ok_or_else(|| anyhow::anyhow!("Flow instance not found: {flow_name}"))? + .ok_or_else(|| client_error!("Flow instance not found: {flow_name}"))? .clone() }; let mut flow_exec_ctx = flow_ctx.get_execution_ctx_for_setup().write().await; diff --git a/rust/utils/src/error.rs b/rust/utils/src/error.rs index cc7441706..2066d9f3a 100644 --- a/rust/utils/src/error.rs +++ b/rust/utils/src/error.rs @@ -115,6 +115,94 @@ impl CError { } } +impl From for CError { + fn from(e: std::fmt::Error) -> Self { + CError::internal(e) + } +} + +impl From for CError { + fn from(e: std::io::Error) -> Self { + CError::internal(e) + } +} + +impl From for CError { + fn from(e: serde_json::Error) -> Self { + CError::internal(e) + } +} + +// Wrapper for anyhow::Error to implement StdError +#[derive(Debug)] +pub struct AnyhowWrapper(anyhow::Error); + +impl Display for AnyhowWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl StdError for AnyhowWrapper { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.0.source() + } +} + +impl From for CError { + fn from(e: anyhow::Error) -> Self { + CError::Internal { + source: Box::new(AnyhowWrapper(e)), + bt: Backtrace::capture(), + } + } +} + +impl From for CError { + fn from(e: ApiError) -> Self { + if e.status_code == StatusCode::BAD_REQUEST { + CError::Client { + msg: e.err.to_string(), + bt: Backtrace::capture(), + } + } else { + CError::Internal { + source: Box::new(AnyhowWrapper(e.err)), + bt: Backtrace::capture(), + } + } + } +} + +impl From for CError { + fn from(e: crate::retryable::Error) -> Self { + CError::Internal { + source: Box::new(AnyhowWrapper(e.error)), + bt: Backtrace::capture(), + } + } +} + +impl From for CError { + fn from(e: crate::fingerprint::FingerprinterError) -> Self { + CError::internal(e) + } +} + +#[cfg(feature = "sqlx")] +impl From for CError { + fn from(e: sqlx::Error) -> Self { + CError::internal(e) + } +} + +#[cfg(feature = "neo4rs")] +impl From for CError { + fn from(e: neo4rs::Error) -> Self { + CError::internal(e) + } +} + #[derive(Debug)] struct StringError(String); @@ -218,28 +306,28 @@ impl IntoResponse for CError { #[macro_export] macro_rules! client_bail { ( $fmt:literal $(, $($arg:tt)*)?) => { - return Err($crate::error::CError::client(format!($fmt $(, $($arg)*)?))) + return Err(anyhow::Error::from($crate::error::CError::client(format!($fmt $(, $($arg)*)?)))) }; } #[macro_export] macro_rules! client_error { ( $fmt:literal $(, $($arg:tt)*)?) => { - $crate::error::CError::client(format!($fmt $(, $($arg)*)?)) + anyhow::Error::from($crate::error::CError::client(format!($fmt $(, $($arg)*)?))) }; } #[macro_export] macro_rules! internal_bail { ( $fmt:literal $(, $($arg:tt)*)?) => { - return Err($crate::error::CError::internal_msg(format!($fmt $(, $($arg)*)?))) + return Err(anyhow::Error::from($crate::error::CError::internal_msg(format!($fmt $(, $($arg)*)?)))) }; } #[macro_export] macro_rules! internal_error { ( $fmt:literal $(, $($arg:tt)*)?) => { - $crate::error::CError::internal_msg(format!($fmt $(, $($arg)*)?)) + anyhow::Error::from($crate::error::CError::internal_msg(format!($fmt $(, $($arg)*)?))) }; }