use crate::proto;
#[derive(Debug, thiserror::Error)]
pub enum GqlError {
#[error("protocol error: {0}")]
Protocol(String),
#[error("session error: {0}")]
Session(String),
#[error("transaction error: {0}")]
Transaction(String),
#[error("backend error: {source}")]
Backend {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("GQL error {}: {}", .status.code, .status.message)]
Status {
status: proto::GqlStatus,
},
#[error("transport error: {0}")]
Transport(#[from] tonic::transport::Error),
#[error("gRPC error: {0}")]
Grpc(#[from] tonic::Status),
}
impl GqlError {
pub fn backend(err: impl std::error::Error + Send + Sync + 'static) -> Self {
Self::Backend {
source: Box::new(err),
}
}
#[must_use]
pub fn status(code: &str, message: impl Into<String>) -> Self {
Self::Status {
status: crate::status::error(code, message),
}
}
#[must_use]
pub fn to_grpc_status(&self) -> tonic::Status {
match self {
Self::Session(msg) => tonic::Status::not_found(msg.clone()),
Self::Transaction(msg) => tonic::Status::failed_precondition(msg.clone()),
Self::Protocol(msg) => tonic::Status::invalid_argument(msg.clone()),
Self::Backend { source } => tonic::Status::internal(source.to_string()),
Self::Status { status } => {
tonic::Status::internal(format!("{}: {}", status.code, status.message))
}
Self::Transport(err) => tonic::Status::unavailable(err.to_string()),
Self::Grpc(status) => status.clone(),
}
}
#[must_use]
pub fn to_optional_service_status(&self) -> tonic::Status {
match self {
Self::Session(msg) if msg.contains("not found") => {
tonic::Status::not_found(msg.clone())
}
Self::Protocol(msg) => tonic::Status::unimplemented(msg.clone()),
other => other.to_grpc_status(),
}
}
#[must_use]
pub fn gql_status(&self) -> Option<&proto::GqlStatus> {
match self {
Self::Status { status } => Some(status),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_error_wrapping() {
let err = GqlError::backend(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"connection refused",
));
assert!(matches!(err, GqlError::Backend { .. }));
assert!(err.to_string().contains("connection refused"));
}
#[test]
fn status_error() {
let err = GqlError::status(crate::status::INVALID_SYNTAX, "unexpected token 'METCH'");
assert!(matches!(err, GqlError::Status { .. }));
assert!(err.to_string().contains("42001"));
assert!(err.gql_status().is_some());
}
#[test]
fn session_to_grpc() {
let err = GqlError::Session("session abc123 not found".to_owned());
let grpc = err.to_grpc_status();
assert_eq!(grpc.code(), tonic::Code::NotFound);
}
#[test]
fn non_status_has_no_gql_status() {
let err = GqlError::Protocol("bad frame".to_owned());
assert!(err.gql_status().is_none());
}
}