From 6cdb3d4685966b71f051e4cd67c50e1d2db402f5 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Thu, 1 Dec 2022 13:48:06 -0300 Subject: [PATCH] feat(types): Add gRPC Richer Error Model support (RetryInfo) (#1095) Add the code related to richer error model support to a new `richer_error` module. This improves readability and hopefully will make it easier to add new features to `tonic-types` in the future. --- tonic-types/src/lib.rs | 445 +-------------- .../error_details/mod.rs} | 53 +- .../{ => richer_error}/error_details/vec.rs | 11 +- tonic-types/src/richer_error/mod.rs | 508 ++++++++++++++++++ .../std_messages/bad_request.rs | 3 +- .../std_messages/mod.rs} | 4 + .../richer_error/std_messages/retry_info.rs | 150 ++++++ 7 files changed, 731 insertions(+), 443 deletions(-) rename tonic-types/src/{error_details.rs => richer_error/error_details/mod.rs} (74%) rename tonic-types/src/{ => richer_error}/error_details/vec.rs (61%) create mode 100644 tonic-types/src/richer_error/mod.rs rename tonic-types/src/{ => richer_error}/std_messages/bad_request.rs (98%) rename tonic-types/src/{std_messages.rs => richer_error/std_messages/mod.rs} (58%) create mode 100644 tonic-types/src/richer_error/std_messages/retry_info.rs diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index 94abb3a4d..cfb41900f 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -21,10 +21,6 @@ #![doc(html_root_url = "https://docs.rs/tonic-types/0.6.1")] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] -use prost::{DecodeError, Message}; -use prost_types::Any; -use tonic::{codegen::Bytes, metadata::MetadataMap, Code}; - /// Useful protobuf types pub mod pb { include!("generated/google.rpc.rs"); @@ -32,440 +28,13 @@ pub mod pb { pub use pb::Status; -mod error_details; -mod std_messages; - -pub use error_details::{vec::ErrorDetail, ErrorDetails}; -pub use std_messages::{BadRequest, FieldViolation}; - -trait IntoAny { - fn into_any(self) -> Any; -} - -trait FromAny { - fn from_any(any: Any) -> Result - where - Self: Sized; -} - -fn gen_details_bytes(code: Code, message: &String, details: Vec) -> Bytes { - let status = pb::Status { - code: code as i32, - message: message.clone(), - details, - }; - - Bytes::from(status.encode_to_vec()) -} - -/// Used to implement associated functions and methods on `tonic::Status`, that -/// allow the addition and extraction of standard error details. -pub trait StatusExt { - /// Generates a `tonic::Status` with error details obtained from an - /// [`ErrorDetails`] struct, and custom metadata. - /// - /// # Examples - /// - /// ``` - /// use tonic::{metadata::MetadataMap, Code, Status}; - /// use tonic_types::{ErrorDetails, StatusExt}; - /// - /// let status = Status::with_error_details_and_metadata( - /// Code::InvalidArgument, - /// "bad request", - /// ErrorDetails::with_bad_request_violation("field", "description"), - /// MetadataMap::new() - /// ); - /// ``` - fn with_error_details_and_metadata( - code: Code, - message: impl Into, - details: ErrorDetails, - metadata: MetadataMap, - ) -> tonic::Status; - - /// Generates a `tonic::Status` with error details obtained from an - /// [`ErrorDetails`] struct. - /// - /// # Examples - /// - /// ``` - /// use tonic::{Code, Status}; - /// use tonic_types::{ErrorDetails, StatusExt}; - /// - /// let status = Status::with_error_details( - /// Code::InvalidArgument, - /// "bad request", - /// ErrorDetails::with_bad_request_violation("field", "description"), - /// ); - /// ``` - fn with_error_details( - code: Code, - message: impl Into, - details: ErrorDetails, - ) -> tonic::Status; - - /// Generates a `tonic::Status` with error details provided in a vector of - /// [`ErrorDetail`] enums, and custom metadata. - /// - /// # Examples - /// - /// ``` - /// use tonic::{metadata::MetadataMap, Code, Status}; - /// use tonic_types::{BadRequest, StatusExt}; - /// - /// let status = Status::with_error_details_vec_and_metadata( - /// Code::InvalidArgument, - /// "bad request", - /// vec![ - /// BadRequest::with_violation("field", "description").into(), - /// ], - /// MetadataMap::new() - /// ); - /// ``` - fn with_error_details_vec_and_metadata( - code: Code, - message: impl Into, - details: impl IntoIterator, - metadata: MetadataMap, - ) -> tonic::Status; - - /// Generates a `tonic::Status` with error details provided in a vector of - /// [`ErrorDetail`] enums. - /// - /// # Examples - /// - /// ``` - /// use tonic::{Code, Status}; - /// use tonic_types::{BadRequest, StatusExt}; - /// - /// let status = Status::with_error_details_vec( - /// Code::InvalidArgument, - /// "bad request", - /// vec![ - /// BadRequest::with_violation("field", "description").into(), - /// ] - /// ); - /// ``` - fn with_error_details_vec( - code: Code, - message: impl Into, - details: impl IntoIterator, - ) -> tonic::Status; - - /// Can be used to check if the error details contained in `tonic::Status` - /// are malformed or not. Tries to get an [`ErrorDetails`] struct from a - /// `tonic::Status`. If some `prost::DecodeError` occurs, it will be - /// returned. If not debugging, consider using - /// [`StatusExt::get_error_details`] or - /// [`StatusExt::get_error_details_vec`]. - /// - /// # Examples - /// - /// ``` - /// use tonic::{Status, Response}; - /// use tonic_types::{StatusExt}; - /// - /// 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 - /// } - /// // ... - /// } - /// }; - /// } - /// ``` - fn check_error_details(&self) -> Result; - - /// Get an [`ErrorDetails`] struct from `tonic::Status`. If some - /// `prost::DecodeError` occurs, an empty [`ErrorDetails`] struct will be - /// returned. - /// - /// # Examples - /// - /// ``` - /// use tonic::{Status, Response}; - /// use tonic_types::{StatusExt}; - /// - /// 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 - /// } - /// // ... - /// } - /// }; - /// } - /// ``` - fn get_error_details(&self) -> ErrorDetails; - - /// Can be used to check if the error details contained in `tonic::Status` - /// are malformed or not. Tries to get a vector of [`ErrorDetail`] enums - /// from a `tonic::Status`. If some `prost::DecodeError` occurs, it will be - /// returned. If not debugging, consider using - /// [`StatusExt::get_error_details_vec`] or - /// [`StatusExt::get_error_details`]. - /// - /// # Examples - /// - /// ``` - /// use tonic::{Status, Response}; - /// use tonic_types::{ErrorDetail, 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 - /// } - /// } - /// } - /// }; - /// } - /// ``` - fn check_error_details_vec(&self) -> Result, DecodeError>; - - /// Get a vector of [`ErrorDetail`] enums from `tonic::Status`. If some - /// `prost::DecodeError` occurs, an empty vector will be returned. - /// - /// # Examples - /// - /// ``` - /// use tonic::{Status, Response}; - /// use tonic_types::{ErrorDetail, StatusExt}; - /// - /// fn handle_request_result(req_result: Result, Status>) { - /// match req_result { - /// Ok(_) => {}, - /// Err(status) => { - /// let err_details = status.get_error_details_vec(); - /// for err_detail in err_details.iter() { - /// match err_detail { - /// ErrorDetail::BadRequest(bad_request) => { - /// // Handle bad_request details - /// } - /// // ... - /// _ => {} - /// } - /// } - /// } - /// }; - /// } - /// ``` - fn get_error_details_vec(&self) -> Vec; - - /// Get first [`BadRequest`] 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(bad_request) = status.get_details_bad_request() { - /// // Handle bad_request details - /// } - /// } - /// }; - /// } - /// ``` - fn get_details_bad_request(&self) -> Option; -} - -impl StatusExt for tonic::Status { - fn with_error_details_and_metadata( - code: Code, - message: impl Into, - details: ErrorDetails, - metadata: MetadataMap, - ) -> Self { - let message: String = message.into(); - - let mut conv_details: Vec = Vec::with_capacity(10); - - if let Some(bad_request) = details.bad_request { - conv_details.push(bad_request.into_any()); - } - - let details = gen_details_bytes(code, &message, conv_details); - - tonic::Status::with_details_and_metadata(code, message, details, metadata) - } - - fn with_error_details(code: Code, message: impl Into, details: ErrorDetails) -> Self { - tonic::Status::with_error_details_and_metadata(code, message, details, MetadataMap::new()) - } - - fn with_error_details_vec_and_metadata( - code: Code, - message: impl Into, - details: impl IntoIterator, - metadata: MetadataMap, - ) -> Self { - let message: String = message.into(); - - let mut conv_details: Vec = Vec::new(); - - for error_detail in details.into_iter() { - match error_detail { - ErrorDetail::BadRequest(bad_req) => { - conv_details.push(bad_req.into_any()); - } - } - } - - let details = gen_details_bytes(code, &message, conv_details); - - tonic::Status::with_details_and_metadata(code, message, details, metadata) - } - - fn with_error_details_vec( - code: Code, - message: impl Into, - details: impl IntoIterator, - ) -> Self { - tonic::Status::with_error_details_vec_and_metadata( - code, - message, - details, - MetadataMap::new(), - ) - } - - fn check_error_details(&self) -> Result { - let status = pb::Status::decode(self.details())?; - - let mut details = ErrorDetails::new(); - - for any in status.details.into_iter() { - match any.type_url.as_str() { - BadRequest::TYPE_URL => { - details.bad_request = Some(BadRequest::from_any(any)?); - } - _ => {} - } - } - - Ok(details) - } - - fn get_error_details(&self) -> ErrorDetails { - self.check_error_details().unwrap_or(ErrorDetails::new()) - } - - fn check_error_details_vec(&self) -> Result, DecodeError> { - let status = pb::Status::decode(self.details())?; - - let mut details: Vec = Vec::with_capacity(status.details.len()); - - for any in status.details.into_iter() { - match any.type_url.as_str() { - BadRequest::TYPE_URL => { - details.push(BadRequest::from_any(any)?.into()); - } - _ => {} - } - } - - Ok(details) - } - - fn get_error_details_vec(&self) -> Vec { - self.check_error_details_vec().unwrap_or(Vec::new()) - } - - fn get_details_bad_request(&self) -> Option { - let status = pb::Status::decode(self.details()).ok()?; - - for any in status.details.into_iter() { - match any.type_url.as_str() { - BadRequest::TYPE_URL => match BadRequest::from_any(any) { - Ok(detail) => return Some(detail), - Err(_) => {} - }, - _ => {} - } - } - - None - } -} - -#[cfg(test)] -mod tests { - use tonic::{Code, Status}; - - use super::{BadRequest, ErrorDetails, StatusExt}; - - #[test] - fn gen_status_with_details() { - let mut err_details = ErrorDetails::new(); - - err_details.add_bad_request_violation("field", "description"); - - let fmt_details = format!("{:?}", err_details); - - let err_details_vec = vec![BadRequest::with_violation("field", "description").into()]; - - let fmt_details_vec = format!("{:?}", err_details_vec); - - let status_from_struct = Status::with_error_details( - Code::InvalidArgument, - "error with bad request details", - err_details, - ); - - let status_from_vec = Status::with_error_details_vec( - Code::InvalidArgument, - "error with bad request details", - err_details_vec, - ); - - let ext_details = match status_from_vec.check_error_details() { - Ok(ext_details) => ext_details, - Err(err) => panic!( - "Error extracting details struct from status_from_vec: {:?}", - err - ), - }; - - let fmt_ext_details = format!("{:?}", ext_details); - - assert!( - fmt_ext_details.eq(&fmt_details), - "Extracted details struct differs from original details struct" - ); - - let ext_details_vec = match status_from_struct.check_error_details_vec() { - Ok(ext_details) => ext_details, - Err(err) => panic!( - "Error extracting details_vec from status_from_struct: {:?}", - err - ), - }; +mod richer_error; - let fmt_ext_details_vec = format!("{:?}", ext_details_vec); +pub use richer_error::{ + BadRequest, ErrorDetail, ErrorDetails, FieldViolation, RetryInfo, StatusExt, +}; - assert!( - fmt_ext_details_vec.eq(&fmt_details_vec), - "Extracted details vec differs from original details vec" - ); - } +mod sealed { + #[allow(unreachable_pub)] + pub trait Sealed {} } diff --git a/tonic-types/src/error_details.rs b/tonic-types/src/richer_error/error_details/mod.rs similarity index 74% rename from tonic-types/src/error_details.rs rename to tonic-types/src/richer_error/error_details/mod.rs index 5ff6e0444..0067a0fd8 100644 --- a/tonic-types/src/error_details.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -1,4 +1,6 @@ -use super::std_messages::{BadRequest, FieldViolation}; +use std::time; + +use super::std_messages::{BadRequest, FieldViolation, RetryInfo}; pub(crate) mod vec; @@ -9,6 +11,9 @@ pub(crate) mod vec; #[non_exhaustive] #[derive(Clone, Debug)] pub struct ErrorDetails { + /// This field stores [`RetryInfo`] data, if any. + pub(crate) retry_info: Option, + /// This field stores [`BadRequest`] data, if any. pub(crate) bad_request: Option, } @@ -24,7 +29,28 @@ impl ErrorDetails { /// let err_details = ErrorDetails::new(); /// ``` pub fn new() -> Self { - ErrorDetails { bad_request: None } + ErrorDetails { + retry_info: None, + bad_request: None, + } + } + + /// Generates an [`ErrorDetails`] struct with [`RetryInfo`] details and + /// remaining fields set to `None`. + /// + /// # Examples + /// + /// ``` + /// use std::time::Duration; + /// use tonic_types::{ErrorDetails}; + /// + /// let err_details = ErrorDetails::with_retry_info(Some(Duration::from_secs(5))); + /// ``` + pub fn with_retry_info(retry_delay: Option) -> Self { + ErrorDetails { + retry_info: Some(RetryInfo::new(retry_delay)), + ..ErrorDetails::new() + } } /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details and @@ -70,11 +96,34 @@ impl ErrorDetails { } } + /// Get [`RetryInfo`] details, if any + pub fn retry_info(&self) -> Option { + self.retry_info.clone() + } + /// Get [`BadRequest`] details, if any pub fn bad_request(&self) -> Option { self.bad_request.clone() } + /// Set [`RetryInfo`] details. Can be chained with other `.set_` and + /// `.add_` [`ErrorDetails`] methods. + /// + /// # Examples + /// + /// ``` + /// use std::time::Duration; + /// use tonic_types::{ErrorDetails}; + /// + /// let mut err_details = ErrorDetails::new(); + /// + /// err_details.set_retry_info(Some(Duration::from_secs(5))); + /// ``` + pub fn set_retry_info(&mut self, retry_delay: Option) -> &mut Self { + self.retry_info = Some(RetryInfo::new(retry_delay)); + self + } + /// Set [`BadRequest`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// diff --git a/tonic-types/src/error_details/vec.rs b/tonic-types/src/richer_error/error_details/vec.rs similarity index 61% rename from tonic-types/src/error_details/vec.rs rename to tonic-types/src/richer_error/error_details/vec.rs index 64128b9a1..350f2453c 100644 --- a/tonic-types/src/error_details/vec.rs +++ b/tonic-types/src/richer_error/error_details/vec.rs @@ -1,14 +1,23 @@ -use super::super::std_messages::BadRequest; +use super::super::std_messages::{BadRequest, RetryInfo}; /// Wraps the structs corresponding to the standard error messages, allowing /// the implementation and handling of vectors containing any of them. #[non_exhaustive] #[derive(Clone, Debug)] pub enum ErrorDetail { + /// Wraps the [`RetryInfo`] struct. + RetryInfo(RetryInfo), + /// Wraps the [`BadRequest`] struct. BadRequest(BadRequest), } +impl From for ErrorDetail { + fn from(err_detail: RetryInfo) -> Self { + ErrorDetail::RetryInfo(err_detail) + } +} + impl From for ErrorDetail { fn from(err_detail: BadRequest) -> Self { ErrorDetail::BadRequest(err_detail) diff --git a/tonic-types/src/richer_error/mod.rs b/tonic-types/src/richer_error/mod.rs new file mode 100644 index 000000000..50462e0b9 --- /dev/null +++ b/tonic-types/src/richer_error/mod.rs @@ -0,0 +1,508 @@ +use prost::{bytes::BytesMut, DecodeError, Message}; +use prost_types::Any; +use tonic::{codegen::Bytes, metadata::MetadataMap, Code}; + +mod error_details; +mod std_messages; + +use super::pb; + +pub use error_details::{vec::ErrorDetail, ErrorDetails}; +pub use std_messages::{BadRequest, FieldViolation, RetryInfo}; + +trait IntoAny { + fn into_any(self) -> Any; +} + +trait FromAny { + fn from_any(any: Any) -> Result + where + Self: Sized; +} + +fn gen_details_bytes(code: Code, message: &String, details: Vec) -> Bytes { + let status = pb::Status { + code: code as i32, + message: message.clone(), + details, + }; + + let mut buf = BytesMut::with_capacity(status.encoded_len()); + + // Should never panic since `buf` is initialized with sufficient capacity + status.encode(&mut buf).unwrap(); + + buf.freeze() +} + +/// Used to implement associated functions and methods on `tonic::Status`, that +/// allow the addition and extraction of standard error details. This trait is +/// sealed and not meant to be implemented outside of `tonic-types`. +pub trait StatusExt: crate::sealed::Sealed { + /// Generates a `tonic::Status` with error details obtained from an + /// [`ErrorDetails`] struct, and custom metadata. + /// + /// # Examples + /// + /// ``` + /// use tonic::{metadata::MetadataMap, Code, Status}; + /// use tonic_types::{ErrorDetails, StatusExt}; + /// + /// let status = Status::with_error_details_and_metadata( + /// Code::InvalidArgument, + /// "bad request", + /// ErrorDetails::with_bad_request_violation("field", "description"), + /// MetadataMap::new() + /// ); + /// ``` + fn with_error_details_and_metadata( + code: Code, + message: impl Into, + details: ErrorDetails, + metadata: MetadataMap, + ) -> tonic::Status; + + /// Generates a `tonic::Status` with error details obtained from an + /// [`ErrorDetails`] struct. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Code, Status}; + /// use tonic_types::{ErrorDetails, StatusExt}; + /// + /// let status = Status::with_error_details( + /// Code::InvalidArgument, + /// "bad request", + /// ErrorDetails::with_bad_request_violation("field", "description"), + /// ); + /// ``` + fn with_error_details( + code: Code, + message: impl Into, + details: ErrorDetails, + ) -> tonic::Status; + + /// Generates a `tonic::Status` with error details provided in a vector of + /// [`ErrorDetail`] enums, and custom metadata. + /// + /// # Examples + /// + /// ``` + /// use tonic::{metadata::MetadataMap, Code, Status}; + /// use tonic_types::{BadRequest, StatusExt}; + /// + /// let status = Status::with_error_details_vec_and_metadata( + /// Code::InvalidArgument, + /// "bad request", + /// vec![ + /// BadRequest::with_violation("field", "description").into(), + /// ], + /// MetadataMap::new() + /// ); + /// ``` + fn with_error_details_vec_and_metadata( + code: Code, + message: impl Into, + details: impl IntoIterator, + metadata: MetadataMap, + ) -> tonic::Status; + + /// Generates a `tonic::Status` with error details provided in a vector of + /// [`ErrorDetail`] enums. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Code, Status}; + /// use tonic_types::{BadRequest, StatusExt}; + /// + /// let status = Status::with_error_details_vec( + /// Code::InvalidArgument, + /// "bad request", + /// vec![ + /// BadRequest::with_violation("field", "description").into(), + /// ] + /// ); + /// ``` + fn with_error_details_vec( + code: Code, + message: impl Into, + details: impl IntoIterator, + ) -> tonic::Status; + + /// Can be used to check if the error details contained in `tonic::Status` + /// are malformed or not. Tries to get an [`ErrorDetails`] struct from a + /// `tonic::Status`. If some `prost::DecodeError` occurs, it will be + /// returned. If not debugging, consider using + /// [`StatusExt::get_error_details`] or + /// [`StatusExt::get_error_details_vec`]. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::{StatusExt}; + /// + /// 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 + /// } + /// // ... + /// } + /// }; + /// } + /// ``` + fn check_error_details(&self) -> Result; + + /// Get an [`ErrorDetails`] struct from `tonic::Status`. If some + /// `prost::DecodeError` occurs, an empty [`ErrorDetails`] struct will be + /// returned. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::{StatusExt}; + /// + /// 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 + /// } + /// // ... + /// } + /// }; + /// } + /// ``` + fn get_error_details(&self) -> ErrorDetails; + + /// Can be used to check if the error details contained in `tonic::Status` + /// are malformed or not. Tries to get a vector of [`ErrorDetail`] enums + /// from a `tonic::Status`. If some `prost::DecodeError` occurs, it will be + /// returned. If not debugging, consider using + /// [`StatusExt::get_error_details_vec`] or + /// [`StatusExt::get_error_details`]. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::{ErrorDetail, 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 + /// } + /// } + /// } + /// }; + /// } + /// ``` + fn check_error_details_vec(&self) -> Result, DecodeError>; + + /// Get a vector of [`ErrorDetail`] enums from `tonic::Status`. If some + /// `prost::DecodeError` occurs, an empty vector will be returned. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::{ErrorDetail, StatusExt}; + /// + /// fn handle_request_result(req_result: Result, Status>) { + /// match req_result { + /// Ok(_) => {}, + /// Err(status) => { + /// let err_details = status.get_error_details_vec(); + /// for err_detail in err_details.iter() { + /// match err_detail { + /// ErrorDetail::BadRequest(bad_request) => { + /// // Handle bad_request details + /// } + /// // ... + /// _ => {} + /// } + /// } + /// } + /// }; + /// } + /// ``` + fn get_error_details_vec(&self) -> Vec; + + /// Get first [`RetryInfo`] 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(retry_info) = status.get_details_retry_info() { + /// // Handle retry_info details + /// } + /// } + /// }; + /// } + /// ``` + fn get_details_retry_info(&self) -> Option; + + /// Get first [`BadRequest`] 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(bad_request) = status.get_details_bad_request() { + /// // Handle bad_request details + /// } + /// } + /// }; + /// } + /// ``` + fn get_details_bad_request(&self) -> Option; +} + +impl crate::sealed::Sealed for tonic::Status {} + +impl StatusExt for tonic::Status { + fn with_error_details_and_metadata( + code: Code, + message: impl Into, + details: ErrorDetails, + metadata: MetadataMap, + ) -> Self { + let message: String = message.into(); + + let mut conv_details: Vec = Vec::with_capacity(10); + + if let Some(retry_info) = details.retry_info { + conv_details.push(retry_info.into_any()); + } + + if let Some(bad_request) = details.bad_request { + conv_details.push(bad_request.into_any()); + } + + let details = gen_details_bytes(code, &message, conv_details); + + tonic::Status::with_details_and_metadata(code, message, details, metadata) + } + + fn with_error_details(code: Code, message: impl Into, details: ErrorDetails) -> Self { + tonic::Status::with_error_details_and_metadata(code, message, details, MetadataMap::new()) + } + + fn with_error_details_vec_and_metadata( + code: Code, + message: impl Into, + details: impl IntoIterator, + metadata: MetadataMap, + ) -> Self { + let message: String = message.into(); + + let mut conv_details: Vec = Vec::new(); + + for error_detail in details.into_iter() { + match error_detail { + ErrorDetail::RetryInfo(retry_info) => { + conv_details.push(retry_info.into_any()); + } + ErrorDetail::BadRequest(bad_req) => { + conv_details.push(bad_req.into_any()); + } + } + } + + let details = gen_details_bytes(code, &message, conv_details); + + tonic::Status::with_details_and_metadata(code, message, details, metadata) + } + + fn with_error_details_vec( + code: Code, + message: impl Into, + details: impl IntoIterator, + ) -> Self { + tonic::Status::with_error_details_vec_and_metadata( + code, + message, + details, + MetadataMap::new(), + ) + } + + fn check_error_details(&self) -> Result { + let status = pb::Status::decode(self.details())?; + + let mut details = ErrorDetails::new(); + + for any in status.details.into_iter() { + match any.type_url.as_str() { + RetryInfo::TYPE_URL => { + details.retry_info = Some(RetryInfo::from_any(any)?); + } + BadRequest::TYPE_URL => { + details.bad_request = Some(BadRequest::from_any(any)?); + } + _ => {} + } + } + + Ok(details) + } + + fn get_error_details(&self) -> ErrorDetails { + self.check_error_details().unwrap_or(ErrorDetails::new()) + } + + fn check_error_details_vec(&self) -> Result, DecodeError> { + let status = pb::Status::decode(self.details())?; + + let mut details: Vec = Vec::with_capacity(status.details.len()); + + for any in status.details.into_iter() { + match any.type_url.as_str() { + RetryInfo::TYPE_URL => { + details.push(RetryInfo::from_any(any)?.into()); + } + BadRequest::TYPE_URL => { + details.push(BadRequest::from_any(any)?.into()); + } + _ => {} + } + } + + Ok(details) + } + + fn get_error_details_vec(&self) -> Vec { + self.check_error_details_vec().unwrap_or(Vec::new()) + } + + fn get_details_retry_info(&self) -> Option { + let status = pb::Status::decode(self.details()).ok()?; + + for any in status.details.into_iter() { + match any.type_url.as_str() { + RetryInfo::TYPE_URL => match RetryInfo::from_any(any) { + Ok(detail) => return Some(detail), + Err(_) => {} + }, + _ => {} + } + } + + None + } + + fn get_details_bad_request(&self) -> Option { + let status = pb::Status::decode(self.details()).ok()?; + + for any in status.details.into_iter() { + match any.type_url.as_str() { + BadRequest::TYPE_URL => match BadRequest::from_any(any) { + Ok(detail) => return Some(detail), + Err(_) => {} + }, + _ => {} + } + } + + None + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + use tonic::{Code, Status}; + + use super::{BadRequest, ErrorDetails, RetryInfo, StatusExt}; + + #[test] + fn gen_status_with_details() { + let mut err_details = ErrorDetails::new(); + + err_details + .set_retry_info(Some(Duration::from_secs(5))) + .add_bad_request_violation("field", "description"); + + let fmt_details = format!("{:?}", err_details); + + let err_details_vec = vec![ + RetryInfo::new(Some(Duration::from_secs(5))).into(), + BadRequest::with_violation("field", "description").into(), + ]; + + let fmt_details_vec = format!("{:?}", err_details_vec); + + let status_from_struct = Status::with_error_details( + Code::InvalidArgument, + "error with bad request details", + err_details, + ); + + let status_from_vec = Status::with_error_details_vec( + Code::InvalidArgument, + "error with bad request details", + err_details_vec, + ); + + let ext_details = match status_from_vec.check_error_details() { + Ok(ext_details) => ext_details, + Err(err) => panic!( + "Error extracting details struct from status_from_vec: {:?}", + err + ), + }; + + let fmt_ext_details = format!("{:?}", ext_details); + + assert!( + fmt_ext_details.eq(&fmt_details), + "Extracted details struct differs from original details struct" + ); + + let ext_details_vec = match status_from_struct.check_error_details_vec() { + Ok(ext_details) => ext_details, + Err(err) => panic!( + "Error extracting details_vec from status_from_struct: {:?}", + err + ), + }; + + let fmt_ext_details_vec = format!("{:?}", ext_details_vec); + + assert!( + fmt_ext_details_vec.eq(&fmt_details_vec), + "Extracted details vec differs from original details vec" + ); + } +} diff --git a/tonic-types/src/std_messages/bad_request.rs b/tonic-types/src/richer_error/std_messages/bad_request.rs similarity index 98% rename from tonic-types/src/std_messages/bad_request.rs rename to tonic-types/src/richer_error/std_messages/bad_request.rs index f923b02d8..26b0f0555 100644 --- a/tonic-types/src/std_messages/bad_request.rs +++ b/tonic-types/src/richer_error/std_messages/bad_request.rs @@ -1,8 +1,7 @@ use prost::{DecodeError, Message}; use prost_types::Any; -use super::super::pb; -use super::super::{FromAny, IntoAny}; +use super::super::{pb, FromAny, IntoAny}; /// Used at the `field_violations` field of the [`BadRequest`] struct. /// Describes a single bad request field. diff --git a/tonic-types/src/std_messages.rs b/tonic-types/src/richer_error/std_messages/mod.rs similarity index 58% rename from tonic-types/src/std_messages.rs rename to tonic-types/src/richer_error/std_messages/mod.rs index fad442cdd..ee8ff2138 100644 --- a/tonic-types/src/std_messages.rs +++ b/tonic-types/src/richer_error/std_messages/mod.rs @@ -1,3 +1,7 @@ +mod retry_info; + +pub use retry_info::RetryInfo; + mod bad_request; pub use bad_request::{BadRequest, FieldViolation}; diff --git a/tonic-types/src/richer_error/std_messages/retry_info.rs b/tonic-types/src/richer_error/std_messages/retry_info.rs new file mode 100644 index 000000000..36a132a7b --- /dev/null +++ b/tonic-types/src/richer_error/std_messages/retry_info.rs @@ -0,0 +1,150 @@ +use std::{convert::TryFrom, time}; + +use prost::{DecodeError, Message}; +use prost_types::Any; + +use super::super::{pb, FromAny, IntoAny}; + +/// Used to encode/decode the `RetryInfo` standard error message described in +/// [error_details.proto]. Describes when the clients can retry a failed +/// request. +/// Note: When obtained from decoding `RetryInfo` messages, negative +/// `retry_delay`'s become 0. +/// +/// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto +#[derive(Clone, Debug)] +pub struct RetryInfo { + /// Informs the amout of time that clients should wait before retrying. + pub retry_delay: Option, +} + +impl RetryInfo { + /// Type URL of the `RetryInfo` standard error message type. + pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.RetryInfo"; + + /// Should not exceed `prost_types::Duration` range. Limited to + /// approximately 10,000 years. + pub const MAX_RETRY_DELAY: time::Duration = time::Duration::new(315_576_000_000, 999_999_999); + + /// Creates a new [`RetryInfo`] struct. If `retry_delay` exceeds + /// [`RetryInfo::MAX_RETRY_DELAY`], [`RetryInfo::MAX_RETRY_DELAY`] will + /// be used instead. + pub fn new(retry_delay: Option) -> Self { + let retry_delay = match retry_delay { + Some(mut delay) => { + if delay > RetryInfo::MAX_RETRY_DELAY { + delay = RetryInfo::MAX_RETRY_DELAY + } + Some(delay) + } + None => None, + }; + + RetryInfo { retry_delay } + } +} + +impl RetryInfo { + /// Returns `true` if [`RetryInfo`]'s `retry_delay` is set as `None`, and + /// `false` if it is not. + pub fn is_empty(&self) -> bool { + self.retry_delay.is_none() + } +} + +impl IntoAny for RetryInfo { + fn into_any(self) -> Any { + let retry_delay = match self.retry_delay { + Some(duration) => { + // If duration is too large, uses max `prost_types::Duration` + let duration = match prost_types::Duration::try_from(duration) { + Ok(duration) => duration, + Err(_) => prost_types::Duration { + seconds: 315_576_000_000, + nanos: 999_999_999, + }, + }; + Some(duration) + } + None => None, + }; + + let detail_data = pb::RetryInfo { retry_delay }; + + Any { + type_url: RetryInfo::TYPE_URL.to_string(), + value: detail_data.encode_to_vec(), + } + } +} + +impl FromAny for RetryInfo { + fn from_any(any: Any) -> Result { + let buf: &[u8] = &any.value; + let retry_info = pb::RetryInfo::decode(buf)?; + + let retry_delay = match retry_info.retry_delay { + Some(duration) => { + // Negative retry_delays become 0 + let duration = time::Duration::try_from(duration).unwrap_or(time::Duration::ZERO); + Some(duration) + } + None => None, + }; + + let retry_info = RetryInfo { retry_delay }; + + Ok(retry_info) + } +} + +#[cfg(test)] +mod tests { + use core::time::Duration; + + use super::super::super::{FromAny, IntoAny}; + use super::RetryInfo; + + #[test] + fn gen_retry_info() { + let error_info = RetryInfo::new(Some(Duration::from_secs(u64::MAX))); + + let formatted = format!("{:?}", error_info); + + let expected_filled = "RetryInfo { retry_delay: Some(315576000000.999999999s) }"; + + assert!( + formatted.eq(expected_filled), + "filled RetryInfo differs from expected result" + ); + + assert!( + error_info.is_empty() == false, + "filled RetryInfo returns 'false' from .has_retry_delay()" + ); + + let gen_any = error_info.into_any(); + + let formatted = format!("{:?}", gen_any); + + let expected = + "Any { type_url: \"type.googleapis.com/google.rpc.RetryInfo\", value: [10, 13, 8, 128, 188, 174, 206, 151, 9, 16, 255, 147, 235, 220, 3] }"; + + assert!( + formatted.eq(expected), + "Any from filled RetryInfo differs from expected result" + ); + + let br_details = match RetryInfo::from_any(gen_any) { + Err(error) => panic!("Error generating RetryInfo from Any: {:?}", error), + Ok(from_any) => from_any, + }; + + let formatted = format!("{:?}", br_details); + + assert!( + formatted.eq(expected_filled), + "RetryInfo from Any differs from expected result" + ); + } +}