use chat_engine_sdk::error::{BoxError, PluginError};
use thiserror::Error;
use toolkit_macros::domain_model;
#[domain_model]
#[derive(Debug, Error)]
pub enum ChatEngineError {
#[error("{resource} not found: {id}")]
NotFound {
resource: &'static str,
id: String,
},
#[error("forbidden: {reason}")]
Forbidden {
reason: String,
},
#[error("conflict: {reason}")]
Conflict {
reason: String,
},
#[error("bad request: {reason}")]
BadRequest {
reason: String,
},
#[error("backend unavailable: {reason}")]
BackendUnavailable {
reason: String,
retry_after: Option<std::time::Duration>,
#[source]
source: Option<BoxError>,
},
#[error("internal error: {reason}")]
Internal {
reason: String,
#[source]
source: Option<BoxError>,
},
#[error("not implemented: {reason}")]
NotImplemented {
reason: String,
},
}
impl ChatEngineError {
#[must_use]
pub fn invalid_transition(
from: chat_engine_sdk::models::LifecycleState,
to: chat_engine_sdk::models::LifecycleState,
) -> Self {
Self::Conflict {
reason: format!("invalid lifecycle transition: {from} -> {to}"),
}
}
pub fn not_found(resource: &'static str, id: impl std::fmt::Display) -> Self {
Self::NotFound {
resource,
id: id.to_string(),
}
}
pub fn bad_request(reason: impl Into<String>) -> Self {
Self::BadRequest {
reason: reason.into(),
}
}
pub fn forbidden(reason: impl Into<String>) -> Self {
Self::Forbidden {
reason: reason.into(),
}
}
pub fn conflict(reason: impl Into<String>) -> Self {
Self::Conflict {
reason: reason.into(),
}
}
pub fn internal(reason: impl Into<String>) -> Self {
Self::Internal {
reason: reason.into(),
source: None,
}
}
pub fn not_implemented(reason: impl Into<String>) -> Self {
Self::NotImplemented {
reason: reason.into(),
}
}
}
impl From<PluginError> for ChatEngineError {
fn from(err: PluginError) -> Self {
match err {
PluginError::InvalidInput { message, source } => Self::BadRequest {
reason: source
.as_ref()
.map_or_else(|| message.clone(), |s| format!("{message}: {s}")),
},
PluginError::Unauthorized { message, source } => Self::Forbidden {
reason: source
.as_ref()
.map_or_else(|| message.clone(), |s| format!("{message}: {s}")),
},
PluginError::NotFound { message, .. } => Self::NotFound {
resource: "plugin_resource",
id: message,
},
PluginError::RateLimited {
retry_after,
source,
} => Self::BackendUnavailable {
reason: "backend rate limited".to_string(),
retry_after,
source,
},
PluginError::Transient { message, source } => Self::BackendUnavailable {
reason: message,
retry_after: None,
source,
},
PluginError::Timeout { source } => Self::BackendUnavailable {
reason: "backend timeout".to_string(),
retry_after: None,
source,
},
PluginError::Internal { message, source } => Self::Internal {
reason: message,
source,
},
}
}
}
impl From<anyhow::Error> for ChatEngineError {
fn from(err: anyhow::Error) -> Self {
Self::Internal {
reason: err.to_string(),
source: Some(err.into()),
}
}
}
pub type Result<T> = std::result::Result<T, ChatEngineError>;
#[cfg(test)]
#[path = "error_tests.rs"]
mod error_tests;