use tonic::{Code, Status};
pub mod errmsg {
pub const CONNECTION_FAILED: &str = "connection failed: ";
pub const TRANSPORT_ERROR: &str = "transport error: ";
pub const GRPC_ERROR: &str = "grpc error: ";
pub const INVALID_ARGUMENT: &str = "invalid argument: ";
pub const INVALID_TIMESTAMP: &str = "invalid timestamp: ";
}
pub type Result<T> = std::result::Result<T, ClientError>;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("{}{msg}", errmsg::CONNECTION_FAILED)]
Connection { msg: String },
#[error("{}{source}", errmsg::TRANSPORT_ERROR)]
Transport {
#[from]
source: tonic::transport::Error,
},
#[error("{}{status}", errmsg::GRPC_ERROR)]
Grpc { status: Box<Status> },
#[error("{}{msg}", errmsg::INVALID_ARGUMENT)]
InvalidArgument { msg: String },
#[error("{}{msg}", errmsg::INVALID_TIMESTAMP)]
InvalidTimestamp { msg: String },
}
impl From<Status> for ClientError {
fn from(status: Status) -> Self {
ClientError::Grpc {
status: Box::new(status),
}
}
}
impl ClientError {
pub fn message(&self) -> String {
match self {
ClientError::Connection { msg } => msg.clone(),
ClientError::Transport { source } => source.to_string(),
ClientError::Grpc { status } => status.message().to_string(),
ClientError::InvalidArgument { msg } => msg.clone(),
ClientError::InvalidTimestamp { msg } => msg.clone(),
}
}
pub fn code(&self) -> Option<Code> {
match self {
ClientError::Grpc { status } => Some(status.code()),
_ => None,
}
}
pub fn status(&self) -> Option<&Status> {
match self {
ClientError::Grpc { status } => Some(status),
_ => None,
}
}
pub fn is_not_found(&self) -> bool {
matches!(self.code(), Some(Code::NotFound))
}
pub fn is_precondition_failed(&self) -> bool {
matches!(self.code(), Some(Code::FailedPrecondition))
}
pub fn is_invalid_argument(&self) -> bool {
matches!(self.code(), Some(Code::InvalidArgument))
|| matches!(self, ClientError::InvalidArgument { .. })
}
pub fn is_connection_error(&self) -> bool {
matches!(
self,
ClientError::Connection { .. } | ClientError::Transport { .. }
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_error_display() {
let err = ClientError::Connection {
msg: "refused".to_string(),
};
assert_eq!(err.to_string(), "connection failed: refused");
}
#[test]
fn test_connection_error_message() {
let err = ClientError::Connection {
msg: "timeout".to_string(),
};
assert_eq!(err.message(), "timeout");
}
#[test]
fn test_invalid_argument_error_display() {
let err = ClientError::InvalidArgument {
msg: "missing field".to_string(),
};
assert_eq!(err.to_string(), "invalid argument: missing field");
}
#[test]
fn test_invalid_argument_error_message() {
let err = ClientError::InvalidArgument {
msg: "bad value".to_string(),
};
assert_eq!(err.message(), "bad value");
}
#[test]
fn test_invalid_timestamp_error_display() {
let err = ClientError::InvalidTimestamp {
msg: "bad format".to_string(),
};
assert_eq!(err.to_string(), "invalid timestamp: bad format");
}
#[test]
fn test_invalid_timestamp_error_message() {
let err = ClientError::InvalidTimestamp {
msg: "parse failed".to_string(),
};
assert_eq!(err.message(), "parse failed");
}
#[test]
fn test_grpc_error_from_status() {
let status = Status::not_found("resource not found");
let err: ClientError = status.into();
assert!(matches!(err, ClientError::Grpc { .. }));
}
#[test]
fn test_grpc_error_message() {
let status = Status::internal("server error");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert_eq!(err.message(), "server error");
}
#[test]
fn test_grpc_error_code() {
let status = Status::not_found("missing");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert_eq!(err.code(), Some(Code::NotFound));
}
#[test]
fn test_grpc_error_status() {
let status = Status::permission_denied("access denied");
let err = ClientError::Grpc {
status: Box::new(status),
};
let s = err.status().unwrap();
assert_eq!(s.code(), Code::PermissionDenied);
assert_eq!(s.message(), "access denied");
}
#[test]
fn test_non_grpc_error_code_is_none() {
let err = ClientError::Connection {
msg: "refused".to_string(),
};
assert_eq!(err.code(), None);
}
#[test]
fn test_non_grpc_error_status_is_none() {
let err = ClientError::InvalidArgument {
msg: "bad".to_string(),
};
assert!(err.status().is_none());
}
#[test]
fn test_is_not_found_true() {
let status = Status::not_found("missing");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert!(err.is_not_found());
}
#[test]
fn test_is_not_found_false_other_code() {
let status = Status::internal("error");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert!(!err.is_not_found());
}
#[test]
fn test_is_not_found_false_non_grpc() {
let err = ClientError::Connection {
msg: "refused".to_string(),
};
assert!(!err.is_not_found());
}
#[test]
fn test_is_precondition_failed_true() {
let status = Status::failed_precondition("conflict");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert!(err.is_precondition_failed());
}
#[test]
fn test_is_precondition_failed_false() {
let status = Status::not_found("missing");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert!(!err.is_precondition_failed());
}
#[test]
fn test_is_invalid_argument_grpc_true() {
let status = Status::invalid_argument("bad input");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert!(err.is_invalid_argument());
}
#[test]
fn test_is_invalid_argument_client_error_true() {
let err = ClientError::InvalidArgument {
msg: "missing".to_string(),
};
assert!(err.is_invalid_argument());
}
#[test]
fn test_is_invalid_argument_false() {
let err = ClientError::Connection {
msg: "refused".to_string(),
};
assert!(!err.is_invalid_argument());
}
#[test]
fn test_is_connection_error_connection_true() {
let err = ClientError::Connection {
msg: "refused".to_string(),
};
assert!(err.is_connection_error());
}
#[test]
fn test_is_connection_error_grpc_false() {
let status = Status::internal("error");
let err = ClientError::Grpc {
status: Box::new(status),
};
assert!(!err.is_connection_error());
}
#[test]
fn test_is_connection_error_invalid_argument_false() {
let err = ClientError::InvalidArgument {
msg: "bad".to_string(),
};
assert!(!err.is_connection_error());
}
}