use tonic::Status;
use crate::error::{Error, Result};
impl From<Error> for Status {
fn from(error: Error) -> Self {
let code = error.code().to_grpc_code();
let message = error.message().to_string();
let mut status = Status::new(code, message);
if let Ok(details_json) = serde_json::to_vec(&serde_json::json!({
"type": error.error_type(),
"data": error.data(),
}))
&& let Ok(metadata_value) = details_json.try_into()
{
status
.metadata_mut()
.insert_bin("error-details-bin", metadata_value);
}
status
}
}
pub type GrpcResult<T> = Result<T, Status>;
#[cfg(test)]
mod tests {
use super::*;
use crate::{
define_error,
error::{ErrorCode, TypedError},
};
use tonic::{Code, Response, Status};
define_error!(ErrorCode::InvalidInput, InvalidRequest);
define_error!(ErrorCode::ResourceNotFound, NotFound);
#[test]
fn test_error_to_grpc_status() {
let error = InvalidRequest::error("Invalid field: email");
let status: Status = error.into();
assert_eq!(status.code(), Code::InvalidArgument);
assert_eq!(status.message(), "Invalid field: email");
let metadata = status.metadata();
assert!(metadata.get_bin("error-details-bin").is_some());
}
#[test]
fn test_error_to_grpc_status_with_data() {
let error = InvalidRequest::error_with_data(
"Invalid fields",
serde_json::json!({
"fields": ["email", "password"]
}),
);
let status: Status = error.into();
assert_eq!(status.code(), Code::InvalidArgument);
assert_eq!(status.message(), "Invalid fields");
let metadata = status.metadata();
let details_bytes = metadata.get_bin("error-details-bin").unwrap();
let byte_data = details_bytes.to_bytes().unwrap();
let details: serde_json::Value = serde_json::from_slice(&byte_data).unwrap();
assert_eq!(details["type"], "InvalidRequest");
assert!(details["data"]["fields"].is_array());
}
#[test]
fn test_all_grpc_code_mappings() {
let test_cases = vec![
(ErrorCode::InvalidInput, Code::InvalidArgument),
(ErrorCode::Unauthorized, Code::Unauthenticated),
(ErrorCode::Forbidden, Code::PermissionDenied),
(ErrorCode::ResourceNotFound, Code::NotFound),
(ErrorCode::Conflict, Code::AlreadyExists),
(ErrorCode::RateLimited, Code::ResourceExhausted),
(ErrorCode::Internal, Code::Internal),
(ErrorCode::Unavailable, Code::Unavailable),
(ErrorCode::Timeout, Code::DeadlineExceeded),
];
for (error_code, expected_grpc_code) in test_cases {
let error = Error::new(error_code, "test_type", "Test message");
let status: Status = error.into();
assert_eq!(
status.code(),
expected_grpc_code,
"Failed for code {:?}",
error_code
);
}
}
#[tokio::test]
async fn test_grpc_result_with_error() {
async fn greet(name: String) -> GrpcResult<Response<String>> {
if name.is_empty() {
return Err(InvalidRequest::error("Name cannot be empty").into());
}
Ok(Response::new(format!("Hello, {}", name)))
}
let result = greet("".to_string()).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), Code::InvalidArgument);
assert_eq!(status.message(), "Name cannot be empty");
}
#[tokio::test]
async fn test_error_propagation() {
async fn find_user(id: u32) -> Result<String> {
if id == 0 {
return Err(NotFound::error(format!("User {} not found", id)));
}
Ok(format!("User {}", id))
}
async fn get_user(id: u32) -> GrpcResult<Response<String>> {
let user = find_user(id).await?;
Ok(Response::new(user))
}
let result = get_user(0).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), Code::NotFound);
assert_eq!(status.message(), "User 0 not found");
}
#[test]
fn test_error_metadata_contains_type() {
let error = NotFound::error("Resource not found");
let status: Status = error.into();
let metadata = status.metadata();
let details_bytes = metadata.get_bin("error-details-bin").unwrap();
let byte_data = details_bytes.to_bytes().unwrap();
let details_json: serde_json::Value = serde_json::from_slice(&byte_data).unwrap();
assert_eq!(details_json["type"], "NotFound");
}
}