diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index e59b521e4..f86d61d8a 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -48,7 +48,7 @@ mod richer_error; pub use richer_error::{ BadRequest, DebugInfo, ErrorDetail, ErrorDetails, ErrorInfo, FieldViolation, PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, - RetryInfo, StatusExt, + ResourceInfo, RetryInfo, StatusExt, }; mod sealed { diff --git a/tonic-types/src/richer_error/error_details/mod.rs b/tonic-types/src/richer_error/error_details/mod.rs index f559820ae..00e7dbb3b 100644 --- a/tonic-types/src/richer_error/error_details/mod.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, time}; use super::std_messages::{ BadRequest, DebugInfo, ErrorInfo, FieldViolation, PreconditionFailure, PreconditionViolation, - QuotaFailure, QuotaViolation, RequestInfo, RetryInfo, + QuotaFailure, QuotaViolation, RequestInfo, ResourceInfo, RetryInfo, }; pub(crate) mod vec; @@ -34,6 +34,9 @@ pub struct ErrorDetails { /// This field stores [`RequestInfo`] data, if any. pub(crate) request_info: Option, + + /// This field stores [`ResourceInfo`] data, if any. + pub(crate) resource_info: Option, } impl ErrorDetails { @@ -276,6 +279,38 @@ impl ErrorDetails { } } + /// Generates an [`ErrorDetails`] struct with [`ResourceInfo`] details and + /// remaining fields set to `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::ErrorDetails; + /// + /// let err_details = ErrorDetails::with_resource_info( + /// "res_type", + /// "res_name", + /// "owner", + /// "description", + /// ); + /// ``` + pub fn with_resource_info( + resource_type: impl Into, + resource_name: impl Into, + owner: impl Into, + description: impl Into, + ) -> Self { + ErrorDetails { + resource_info: Some(ResourceInfo::new( + resource_type, + resource_name, + owner, + description, + )), + ..ErrorDetails::new() + } + } + /// Get [`RetryInfo`] details, if any. pub fn retry_info(&self) -> Option { self.retry_info.clone() @@ -311,6 +346,11 @@ impl ErrorDetails { self.request_info.clone() } + /// Get [`ResourceInfo`] details, if any. + pub fn resource_info(&self) -> Option { + self.resource_info.clone() + } + /// Set [`RetryInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// @@ -638,4 +678,32 @@ impl ErrorDetails { self.request_info = Some(RequestInfo::new(request_id, serving_data)); self } + + /// Set [`ResourceInfo`] details. Can be chained with other `.set_` and + /// `.add_` [`ErrorDetails`] methods. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::ErrorDetails; + /// + /// let mut err_details = ErrorDetails::new(); + /// + /// err_details.set_resource_info("res_type", "res_name", "owner", "description"); + /// ``` + pub fn set_resource_info( + &mut self, + resource_type: impl Into, + resource_name: impl Into, + owner: impl Into, + description: impl Into, + ) -> &mut Self { + self.resource_info = Some(ResourceInfo::new( + resource_type, + resource_name, + owner, + description, + )); + self + } } diff --git a/tonic-types/src/richer_error/error_details/vec.rs b/tonic-types/src/richer_error/error_details/vec.rs index c7e516141..a22db091c 100644 --- a/tonic-types/src/richer_error/error_details/vec.rs +++ b/tonic-types/src/richer_error/error_details/vec.rs @@ -1,5 +1,6 @@ use super::super::std_messages::{ - BadRequest, DebugInfo, ErrorInfo, PreconditionFailure, QuotaFailure, RequestInfo, RetryInfo, + BadRequest, DebugInfo, ErrorInfo, PreconditionFailure, QuotaFailure, RequestInfo, ResourceInfo, + RetryInfo, }; /// Wraps the structs corresponding to the standard error messages, allowing @@ -27,6 +28,9 @@ pub enum ErrorDetail { /// Wraps the [`RequestInfo`] struct. RequestInfo(RequestInfo), + + /// Wraps the [`ResourceInfo`] struct. + ResourceInfo(ResourceInfo), } impl From for ErrorDetail { @@ -70,3 +74,9 @@ impl From for ErrorDetail { ErrorDetail::RequestInfo(err_detail) } } + +impl From for ErrorDetail { + fn from(err_detail: ResourceInfo) -> Self { + ErrorDetail::ResourceInfo(err_detail) + } +} diff --git a/tonic-types/src/richer_error/mod.rs b/tonic-types/src/richer_error/mod.rs index 7b6b75e7c..87bd9bc64 100644 --- a/tonic-types/src/richer_error/mod.rs +++ b/tonic-types/src/richer_error/mod.rs @@ -13,7 +13,7 @@ use super::pb; pub use error_details::{vec::ErrorDetail, ErrorDetails}; pub use std_messages::{ BadRequest, DebugInfo, ErrorInfo, FieldViolation, PreconditionFailure, PreconditionViolation, - QuotaFailure, QuotaViolation, RequestInfo, RetryInfo, + QuotaFailure, QuotaViolation, RequestInfo, ResourceInfo, RetryInfo, }; trait IntoAny { @@ -153,12 +153,13 @@ pub trait StatusExt: crate::sealed::Sealed { /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, - /// Err(status) => { - /// let err_details = status.get_error_details(); - /// if let Some(bad_request) = err_details.bad_request() { - /// // Handle bad_request details + /// Err(status) => match status.check_error_details() { + /// Ok(err_details) => { + /// // Handle extracted details + /// } + /// Err(decode_error) => { + /// // Handle decode_error /// } - /// // ... /// } /// }; /// } @@ -201,19 +202,17 @@ pub trait StatusExt: crate::sealed::Sealed { /// /// ``` /// use tonic::{Status, Response}; - /// use tonic_types::{ErrorDetail, StatusExt}; + /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, - /// Err(status) => { - /// match status.check_error_details_vec() { - /// Ok(err_details) => { - /// // Handle extracted details - /// } - /// Err(decode_error) => { - /// // Handle decode_error - /// } + /// Err(status) => match status.check_error_details_vec() { + /// Ok(err_details) => { + /// // Handle extracted details + /// } + /// Err(decode_error) => { + /// // Handle decode_error /// } /// } /// }; @@ -403,6 +402,28 @@ pub trait StatusExt: crate::sealed::Sealed { /// } /// ``` fn get_details_request_info(&self) -> Option; + + /// Get first [`ResourceInfo`] details found on `tonic::Status`, if any. + /// If some `prost::DecodeError` occurs, returns `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::StatusExt; + /// + /// fn handle_request_result(req_result: Result, Status>) { + /// match req_result { + /// Ok(_) => {}, + /// Err(status) => { + /// if let Some(resource_info) = status.get_details_resource_info() { + /// // Handle resource_info details + /// } + /// } + /// }; + /// } + /// ``` + fn get_details_resource_info(&self) -> Option; } impl crate::sealed::Sealed for tonic::Status {} @@ -446,6 +467,10 @@ impl StatusExt for tonic::Status { conv_details.push(request_info.into_any()); } + if let Some(resource_info) = details.resource_info { + conv_details.push(resource_info.into_any()); + } + let details = gen_details_bytes(code, &message, conv_details); tonic::Status::with_details_and_metadata(code, message, details, metadata) @@ -488,6 +513,9 @@ impl StatusExt for tonic::Status { ErrorDetail::RequestInfo(req_info) => { conv_details.push(req_info.into_any()); } + ErrorDetail::ResourceInfo(res_info) => { + conv_details.push(res_info.into_any()); + } } } @@ -537,6 +565,9 @@ impl StatusExt for tonic::Status { RequestInfo::TYPE_URL => { details.request_info = Some(RequestInfo::from_any(any)?); } + ResourceInfo::TYPE_URL => { + details.resource_info = Some(ResourceInfo::from_any(any)?); + } _ => {} } } @@ -576,6 +607,9 @@ impl StatusExt for tonic::Status { RequestInfo::TYPE_URL => { details.push(RequestInfo::from_any(any)?.into()); } + ResourceInfo::TYPE_URL => { + details.push(ResourceInfo::from_any(any)?.into()); + } _ => {} } } @@ -684,6 +718,20 @@ impl StatusExt for tonic::Status { None } + + fn get_details_resource_info(&self) -> Option { + let status = pb::Status::decode(self.details()).ok()?; + + for any in status.details.into_iter() { + if any.type_url.as_str() == ResourceInfo::TYPE_URL { + if let Ok(detail) = ResourceInfo::from_any(any) { + return Some(detail); + } + } + } + + None + } } #[cfg(test)] diff --git a/tonic-types/src/richer_error/std_messages/mod.rs b/tonic-types/src/richer_error/std_messages/mod.rs index 3ff6c11b3..21d8950d3 100644 --- a/tonic-types/src/richer_error/std_messages/mod.rs +++ b/tonic-types/src/richer_error/std_messages/mod.rs @@ -25,3 +25,7 @@ pub use bad_request::{BadRequest, FieldViolation}; mod request_info; pub use request_info::RequestInfo; + +mod resource_info; + +pub use resource_info::ResourceInfo; diff --git a/tonic-types/src/richer_error/std_messages/resource_info.rs b/tonic-types/src/richer_error/std_messages/resource_info.rs new file mode 100644 index 000000000..c166982cb --- /dev/null +++ b/tonic-types/src/richer_error/std_messages/resource_info.rs @@ -0,0 +1,129 @@ +use prost::{DecodeError, Message}; +use prost_types::Any; + +use super::super::{pb, FromAny, IntoAny}; + +/// Used to encode/decode the `ResourceInfo` standard error message described +/// in [error_details.proto]. Describes the resource that is being accessed. +/// +/// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto +#[derive(Clone, Debug)] +pub struct ResourceInfo { + /// Type of resource being accessed. + pub resource_type: String, + + /// Name of the resource being accessed. + pub resource_name: String, + + /// The owner of the resource (optional). + pub owner: String, + + /// Describes the error encountered when accessing the resource. + pub description: String, +} + +impl ResourceInfo { + /// Type URL of the `ResourceInfo` standard error message type. + pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.ResourceInfo"; + + /// Creates a new [`ResourceInfo`] struct. + pub fn new( + resource_type: impl Into, + resource_name: impl Into, + owner: impl Into, + description: impl Into, + ) -> Self { + ResourceInfo { + resource_type: resource_type.into(), + resource_name: resource_name.into(), + owner: owner.into(), + description: description.into(), + } + } + + /// Returns `true` if [`ResourceInfo`] fields are empty, and `false` if + /// they are not. + pub fn is_empty(&self) -> bool { + self.resource_type.is_empty() + && self.resource_name.is_empty() + && self.owner.is_empty() + && self.description.is_empty() + } +} + +impl IntoAny for ResourceInfo { + fn into_any(self) -> Any { + let detail_data = pb::ResourceInfo { + resource_type: self.resource_type, + resource_name: self.resource_name, + owner: self.owner, + description: self.description, + }; + + Any { + type_url: ResourceInfo::TYPE_URL.to_string(), + value: detail_data.encode_to_vec(), + } + } +} + +impl FromAny for ResourceInfo { + fn from_any(any: Any) -> Result { + let buf: &[u8] = &any.value; + let res_info = pb::ResourceInfo::decode(buf)?; + + let debug_info = ResourceInfo { + resource_type: res_info.resource_type, + resource_name: res_info.resource_name, + owner: res_info.owner, + description: res_info.description, + }; + + Ok(debug_info) + } +} + +#[cfg(test)] +mod tests { + use super::super::super::{FromAny, IntoAny}; + use super::ResourceInfo; + + #[test] + fn gen_error_info() { + let error_info = + ResourceInfo::new("resource-type", "resource-name", "owner", "description"); + + let formatted = format!("{:?}", error_info); + + let expected_filled = "ResourceInfo { resource_type: \"resource-type\", resource_name: \"resource-name\", owner: \"owner\", description: \"description\" }"; + + assert!( + formatted.eq(expected_filled), + "filled ResourceInfo differs from expected result" + ); + + let gen_any = error_info.into_any(); + + let formatted = format!("{:?}", gen_any); + + let expected = + "Any { type_url: \"type.googleapis.com/google.rpc.ResourceInfo\", value: [10, 13, 114, 101, 115, 111, 117, 114, 99, 101, 45, 116, 121, 112, 101, 18, 13, 114, 101, 115, 111, 117, 114, 99, 101, 45, 110, 97, 109, 101, 26, 5, 111, 119, 110, 101, 114, 34, 11, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110] }"; + + assert!( + formatted.eq(expected), + "Any from filled ResourceInfo differs from expected result" + ); + + let br_details = match ResourceInfo::from_any(gen_any) { + Err(error) => panic!("Error generating ResourceInfo from Any: {:?}", error), + Ok(from_any) => from_any, + }; + + let formatted = format!("{:?}", br_details); + + assert!( + formatted.eq(expected_filled), + "ResourceInfo from Any differs from expected result" + ); + } +}