use std::sync::Arc;
use thiserror::Error;
use async_trait::async_trait;
use protobuf::MessageFull;
use crate::communication::RegistrationError;
use crate::{UAttributes, UCode, UStatus, UUri};
use super::{CallOptions, UPayload};
#[derive(Clone, Error, Debug)]
pub enum ServiceInvocationError {
#[error("entity already exists: {0}")]
AlreadyExists(String),
#[error("request timed out")]
DeadlineExceeded,
#[error("failed precondition: {0}")]
FailedPrecondition(String),
#[error("internal error: {0}")]
Internal(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("no such entity: {0}")]
NotFound(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("resource exhausted: {0}")]
ResourceExhausted(String),
#[error("unknown error: {0}")]
RpcError(UStatus),
#[error("unauthenticated")]
Unauthenticated,
#[error("resource unavailable: {0}")]
Unavailable(String),
#[error("unimplemented: {0}")]
Unimplemented(String),
}
impl From<UStatus> for ServiceInvocationError {
fn from(value: UStatus) -> Self {
match value.code.enum_value() {
Ok(UCode::ALREADY_EXISTS) => ServiceInvocationError::AlreadyExists(value.get_message()),
Ok(UCode::DEADLINE_EXCEEDED) => ServiceInvocationError::DeadlineExceeded,
Ok(UCode::FAILED_PRECONDITION) => {
ServiceInvocationError::FailedPrecondition(value.get_message())
}
Ok(UCode::INTERNAL) => ServiceInvocationError::Internal(value.get_message()),
Ok(UCode::INVALID_ARGUMENT) => {
ServiceInvocationError::InvalidArgument(value.get_message())
}
Ok(UCode::NOT_FOUND) => ServiceInvocationError::NotFound(value.get_message()),
Ok(UCode::PERMISSION_DENIED) => {
ServiceInvocationError::PermissionDenied(value.get_message())
}
Ok(UCode::RESOURCE_EXHAUSTED) => {
ServiceInvocationError::ResourceExhausted(value.get_message())
}
Ok(UCode::UNAUTHENTICATED) => ServiceInvocationError::Unauthenticated,
Ok(UCode::UNAVAILABLE) => ServiceInvocationError::Unavailable(value.get_message()),
Ok(UCode::UNIMPLEMENTED) => ServiceInvocationError::Unimplemented(value.get_message()),
_ => ServiceInvocationError::RpcError(value),
}
}
}
impl From<ServiceInvocationError> for UStatus {
fn from(value: ServiceInvocationError) -> Self {
match value {
ServiceInvocationError::AlreadyExists(msg) => {
UStatus::fail_with_code(UCode::ALREADY_EXISTS, msg)
}
ServiceInvocationError::DeadlineExceeded => {
UStatus::fail_with_code(UCode::DEADLINE_EXCEEDED, "request timed out")
}
ServiceInvocationError::FailedPrecondition(msg) => {
UStatus::fail_with_code(UCode::FAILED_PRECONDITION, msg)
}
ServiceInvocationError::Internal(msg) => UStatus::fail_with_code(UCode::INTERNAL, msg),
ServiceInvocationError::InvalidArgument(msg) => {
UStatus::fail_with_code(UCode::INVALID_ARGUMENT, msg)
}
ServiceInvocationError::NotFound(msg) => UStatus::fail_with_code(UCode::NOT_FOUND, msg),
ServiceInvocationError::PermissionDenied(msg) => {
UStatus::fail_with_code(UCode::PERMISSION_DENIED, msg)
}
ServiceInvocationError::ResourceExhausted(msg) => {
UStatus::fail_with_code(UCode::RESOURCE_EXHAUSTED, msg)
}
ServiceInvocationError::Unauthenticated => {
UStatus::fail_with_code(UCode::UNAUTHENTICATED, "client must authenticate")
}
ServiceInvocationError::Unavailable(msg) => {
UStatus::fail_with_code(UCode::UNAVAILABLE, msg)
}
ServiceInvocationError::Unimplemented(msg) => {
UStatus::fail_with_code(UCode::UNIMPLEMENTED, msg)
}
_ => UStatus::fail_with_code(UCode::UNKNOWN, "unknown"),
}
}
}
#[cfg_attr(any(test, feature = "test-util"), mockall::automock)]
#[async_trait]
pub trait RpcClient: Send + Sync {
async fn invoke_method(
&self,
method: UUri,
call_options: CallOptions,
payload: Option<UPayload>,
) -> Result<Option<UPayload>, ServiceInvocationError>;
}
impl dyn RpcClient {
pub async fn invoke_proto_method<T, R>(
&self,
method: UUri,
call_options: CallOptions,
request_message: T,
) -> Result<R, ServiceInvocationError>
where
T: MessageFull,
R: MessageFull,
{
let payload = UPayload::try_from_protobuf(request_message)
.map_err(|e| ServiceInvocationError::InvalidArgument(e.to_string()))?;
let result = self
.invoke_method(method, call_options, Some(payload))
.await?;
if let Some(result) = result {
UPayload::extract_protobuf::<R>(&result)
.map_err(|e| ServiceInvocationError::InvalidArgument(e.to_string()))
} else {
Err(ServiceInvocationError::InvalidArgument(
"No payload".to_string(),
))
}
}
}
#[cfg_attr(any(test, feature = "test-util"), mockall::automock)]
#[async_trait]
pub trait RequestHandler: Send + Sync {
async fn handle_request(
&self,
resource_id: u16,
message_attributes: &UAttributes,
request_payload: Option<UPayload>,
) -> Result<Option<UPayload>, ServiceInvocationError>;
}
#[async_trait]
pub trait RpcServer {
async fn register_endpoint(
&self,
origin_filter: Option<&UUri>,
resource_id: u16,
request_handler: Arc<dyn RequestHandler>,
) -> Result<(), RegistrationError>;
async fn unregister_endpoint(
&self,
origin_filter: Option<&UUri>,
resource_id: u16,
request_handler: Arc<dyn RequestHandler>,
) -> Result<(), RegistrationError>;
}
#[cfg(not(tarpaulin_include))]
#[cfg(any(test, feature = "test-util"))]
mockall::mock! {
pub RpcServerImpl {
pub async fn do_register_endpoint<'a>(&'a self, origin_filter: Option<&'a UUri>, resource_id: u16, request_handler: Arc<dyn RequestHandler>) -> Result<(), RegistrationError>;
pub async fn do_unregister_endpoint<'a>(&'a self, origin_filter: Option<&'a UUri>, resource_id: u16, request_handler: Arc<dyn RequestHandler>) -> Result<(), RegistrationError>;
}
}
#[cfg(not(tarpaulin_include))]
#[cfg(any(test, feature = "test-util"))]
#[async_trait]
impl RpcServer for MockRpcServerImpl {
async fn register_endpoint(
&self,
origin_filter: Option<&UUri>,
resource_id: u16,
request_handler: Arc<dyn RequestHandler>,
) -> Result<(), RegistrationError> {
self.do_register_endpoint(origin_filter, resource_id, request_handler)
.await
}
async fn unregister_endpoint(
&self,
origin_filter: Option<&UUri>,
resource_id: u16,
request_handler: Arc<dyn RequestHandler>,
) -> Result<(), RegistrationError> {
self.do_unregister_endpoint(origin_filter, resource_id, request_handler)
.await
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use protobuf::well_known_types::wrappers::StringValue;
use crate::{communication::CallOptions, UUri};
use super::*;
#[tokio::test]
async fn test_invoke_proto_method_fails_for_unexpected_return_type() {
let mut rpc_client = MockRpcClient::new();
rpc_client
.expect_invoke_method()
.once()
.returning(|_method, _options, _payload| {
let error = UStatus::fail_with_code(UCode::INTERNAL, "internal error");
let response_payload = UPayload::try_from_protobuf(error).unwrap();
Ok(Some(response_payload))
});
let client: Arc<dyn RpcClient> = Arc::new(rpc_client);
let mut request = StringValue::new();
request.value = "hello".to_string();
let result = client
.invoke_proto_method::<StringValue, StringValue>(
UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(),
CallOptions::for_rpc_request(5_000, None, None, None),
request,
)
.await;
assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_))));
}
#[tokio::test]
async fn test_invoke_proto_method_fails_for_missing_response_payload() {
let mut rpc_client = MockRpcClient::new();
rpc_client
.expect_invoke_method()
.once()
.return_const(Ok(None));
let client: Arc<dyn RpcClient> = Arc::new(rpc_client);
let mut request = StringValue::new();
request.value = "hello".to_string();
let result = client
.invoke_proto_method::<StringValue, StringValue>(
UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(),
CallOptions::for_rpc_request(5_000, None, None, None),
request,
)
.await;
assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_))));
}
}