use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct CrpcError {
pub code: CrpcErrorCode,
pub message: String,
pub detail: Option<Box<serde_json::value::Value>>,
}
impl CrpcError {
pub fn new(code: CrpcErrorCode, message: String) -> Self {
Self {
code,
message,
detail: None,
}
}
pub fn new_with_detail(
code: CrpcErrorCode,
message: String,
detail: serde_json::value::Value,
) -> Self {
Self {
code,
message,
detail: Some(Box::new(detail)),
}
}
pub fn new_from_status(status: StatusCode) -> Self {
Self {
code: status.into(),
message: match status {
StatusCode::BAD_REQUEST => "Invalid request".to_string(),
StatusCode::UNAUTHORIZED => "Unauthorized".to_string(),
StatusCode::FORBIDDEN => "Forbidden".to_string(),
StatusCode::NOT_FOUND => "Not found".to_string(),
StatusCode::TOO_MANY_REQUESTS => "Too many requests".to_string(),
StatusCode::INTERNAL_SERVER_ERROR => "Internal server error".to_string(),
StatusCode::BAD_GATEWAY => "Bad gateway".to_string(),
StatusCode::SERVICE_UNAVAILABLE => "Service unavailable".to_string(),
StatusCode::GATEWAY_TIMEOUT => "Gateway timeout".to_string(),
_ => "Unknown error".to_string(),
},
detail: None,
}
}
}
impl std::error::Error for CrpcError {}
impl std::fmt::Display for CrpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"connect RPC error (code: {}): {}",
self.code, self.message
)
}
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum CrpcErrorCode {
Canceled,
Unknown,
InvalidArgument,
DeadlineExceeded,
NotFound,
AlreadyExists,
PermissionDenied,
ResourceExhausted,
FailedPrecondition,
Aborted,
OutOfRange,
Unimplemented,
Internal,
Unavailable,
DataLoss,
Unauthenticated,
}
impl std::fmt::Display for CrpcErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Canceled => write!(f, "canceled"),
Self::Unknown => write!(f, "unknown"),
Self::InvalidArgument => write!(f, "invalid_argument"),
Self::DeadlineExceeded => write!(f, "deadline_exceeded"),
Self::NotFound => write!(f, "not_found"),
Self::AlreadyExists => write!(f, "already_exists"),
Self::PermissionDenied => write!(f, "permission_denied"),
Self::ResourceExhausted => write!(f, "resource_exhausted"),
Self::FailedPrecondition => write!(f, "failed_precondition"),
Self::Aborted => write!(f, "aborted"),
Self::OutOfRange => write!(f, "out_of_range"),
Self::Unimplemented => write!(f, "unimplemented"),
Self::Internal => write!(f, "internal"),
Self::Unavailable => write!(f, "unavailable"),
Self::DataLoss => write!(f, "data_loss"),
Self::Unauthenticated => write!(f, "unauthenticated"),
}
}
}
impl From<StatusCode> for CrpcErrorCode {
fn from(status: StatusCode) -> Self {
match status {
StatusCode::BAD_REQUEST => Self::InvalidArgument,
StatusCode::UNAUTHORIZED => Self::Unauthenticated,
StatusCode::FORBIDDEN => Self::PermissionDenied,
StatusCode::NOT_FOUND => Self::NotFound,
StatusCode::INTERNAL_SERVER_ERROR => Self::Internal,
StatusCode::TOO_MANY_REQUESTS => Self::Unavailable,
StatusCode::BAD_GATEWAY => Self::Unavailable,
StatusCode::SERVICE_UNAVAILABLE => Self::Unavailable,
StatusCode::GATEWAY_TIMEOUT => Self::Unavailable,
_ => Self::Unknown,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crpc_error_serializes_and_deserializes() {
let err = CrpcError {
code: CrpcErrorCode::InvalidArgument,
message: "invalid argument".to_string(),
detail: Some(serde_json::json!({"foo": "bar"}).into()),
};
let serialized = serde_json::to_string(&err).expect("failed to serialize");
assert_eq!(
r#"{"code":"invalid_argument","message":"invalid argument","detail":{"foo":"bar"}}"#,
serialized
);
let deserialized: CrpcError =
serde_json::from_str(&serialized).expect("failed to deserialize");
assert_eq!(
err.code as u8, deserialized.code as u8,
"error code should match after serialization round-trip"
);
assert_eq!(
err.message, deserialized.message,
"error message should match after serialization round-trip"
);
}
}