use loonfs::RuntimeError;
use loonfs_api::{ErrorCode, ErrorDetails, NamespaceId};
use loonfs_client::ClientError;
use loonfs_grep::GrepError;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{code}: {message}")]
pub(crate) struct BackendError {
pub code: String,
pub message: String,
pub request_id: Option<String>,
pub details: Option<Box<ErrorDetails>>,
}
impl BackendError {
pub(crate) fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
request_id: None,
details: None,
}
}
pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
Self::new("invalid_config", message)
}
pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
Self::new("invalid_input", message)
}
pub(crate) fn client_error(message: impl Into<String>) -> Self {
Self::new("client_error", message)
}
pub(crate) fn io_error(message: impl Into<String>) -> Self {
Self::new("io_error", message)
}
pub(crate) fn runtime_error(message: impl Into<String>) -> Self {
Self::new("runtime_error", message)
}
}
impl From<ClientError> for BackendError {
fn from(error: ClientError) -> Self {
match error {
ClientError::ConfigIo(message) | ClientError::ConfigDecode(message) => {
Self::invalid_config(message)
}
ClientError::MissingConfigField { field } => {
Self::invalid_config(format!("missing `{field}`"))
}
ClientError::ConfigValidation { field, reason } => {
Self::invalid_config(format!("invalid `{field}`: {reason}"))
}
ClientError::InvalidNamespacePath(message) => Self::invalid_input(message),
ClientError::InvalidCommitId(message) | ClientError::InvalidCheckpointId(message) => {
Self::new(ErrorCode::InvalidRequest.as_str(), message)
}
ClientError::Http(message) | ClientError::Json(message) => Self::client_error(message),
ClientError::Api {
code,
message,
request_id,
details,
..
} => Self {
code,
message,
request_id,
details,
},
ClientError::Io(message) => Self::io_error(format!("i/o error: {message}")),
other => Self::client_error(other.to_string()),
}
}
}
pub(crate) fn map_runtime_error(error: RuntimeError) -> BackendError {
match error {
RuntimeError::Config(message) => BackendError::invalid_config(message),
RuntimeError::RuntimeTask(message) => BackendError::runtime_error(message),
error => BackendError {
code: error.code().as_str().to_owned(),
message: error.to_string(),
request_id: None,
details: error.details().map(Box::new),
},
}
}
pub(crate) fn map_namespace_scoped_runtime_error(
namespace_id: &NamespaceId,
error: RuntimeError,
) -> BackendError {
if error.code() == ErrorCode::NamespaceNotFound {
return BackendError::new(
ErrorCode::NamespaceNotFound.as_str(),
format!("namespace `{namespace_id}` does not exist"),
);
}
map_runtime_error(error)
}
pub(crate) fn map_namespace_scoped_grep_error(
namespace_id: &NamespaceId,
error: GrepError,
) -> BackendError {
match error {
GrepError::Runtime(error) => map_namespace_scoped_runtime_error(namespace_id, error),
error => BackendError::new(error.code().as_str(), error.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::{map_runtime_error, BackendError, ClientError};
use loonfs::RuntimeError;
use loonfs_api::ChangeSeq;
#[test]
fn embedded_runtime_errors_carry_their_structured_details() {
let error = map_runtime_error(RuntimeError::Core(
loonfs::CoreError::StaleHeadPrecondition {
expected: ChangeSeq(41),
actual: ChangeSeq(45),
},
));
assert_eq!(error.code, "stale_head");
let details = error.details.expect("core details survive the seam");
assert_eq!(details.expected_head_seq, Some(ChangeSeq(41)));
assert_eq!(details.actual_head_seq, Some(ChangeSeq(45)));
}
#[test]
fn api_errors_pass_their_code_and_message_through_verbatim() {
let error = BackendError::from(ClientError::Api {
status: 404,
code: "namespace_not_found".to_owned(),
feature: None,
message: "namespace `demo` does not exist".to_owned(),
request_id: None,
details: None,
});
assert_eq!(error.code, "namespace_not_found");
assert_eq!(error.message, "namespace `demo` does not exist");
}
#[test]
fn config_and_transport_errors_map_to_backend_local_codes() {
let error = BackendError::from(ClientError::MissingConfigField {
field: "server_url",
});
assert_eq!(error.code, "invalid_config");
assert_eq!(error.message, "missing `server_url`");
let error = BackendError::from(ClientError::Http("connection refused".to_owned()));
assert_eq!(error.code, "client_error");
let error = BackendError::from(ClientError::Io("read failed".to_owned()));
assert_eq!(error.code, "io_error");
assert_eq!(error.message, "i/o error: read failed");
}
}