diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index ea8903982..e2b5208a6 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -46,8 +46,8 @@ pub use pb::Status; mod richer_error; pub use richer_error::{ - BadRequest, DebugInfo, ErrorDetail, ErrorDetails, FieldViolation, QuotaFailure, QuotaViolation, - RetryInfo, StatusExt, + BadRequest, DebugInfo, ErrorDetail, ErrorDetails, ErrorInfo, FieldViolation, QuotaFailure, + QuotaViolation, 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 4615ca766..91d119387 100644 --- a/tonic-types/src/richer_error/error_details/mod.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -1,7 +1,7 @@ -use std::time; +use std::{collections::HashMap, time}; use super::std_messages::{ - BadRequest, DebugInfo, FieldViolation, QuotaFailure, QuotaViolation, RetryInfo, + BadRequest, DebugInfo, ErrorInfo, FieldViolation, QuotaFailure, QuotaViolation, RetryInfo, }; pub(crate) mod vec; @@ -22,6 +22,9 @@ pub struct ErrorDetails { /// This field stores [`QuotaFailure`] data, if any. pub(crate) quota_failure: Option, + /// This field stores [`ErrorInfo`] data, if any. + pub(crate) error_info: Option, + /// This field stores [`BadRequest`] data, if any. pub(crate) bad_request: Option, } @@ -117,6 +120,31 @@ impl ErrorDetails { } } + /// Generates an [`ErrorDetails`] struct with [`ErrorInfo`] details and + /// remaining fields set to `None`. + /// + /// # Examples + /// + /// ``` + /// use std::collections::HashMap; + /// use tonic_types::{ErrorDetails}; + /// + /// let mut metadata: HashMap = HashMap::new(); + /// metadata.insert("instanceLimitPerRequest".into(), "100".into()); + /// + /// let err_details = ErrorDetails::with_error_info("reason", "domain", metadata); + /// ``` + pub fn with_error_info( + reason: impl Into, + domain: impl Into, + metadata: impl Into>, + ) -> Self { + ErrorDetails { + error_info: Some(ErrorInfo::new(reason, domain, metadata)), + ..ErrorDetails::new() + } + } + /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details and /// remaining fields set to `None`. /// @@ -175,6 +203,11 @@ impl ErrorDetails { self.quota_failure.clone() } + /// Get [`ErrorInfo`] details, if any + pub fn error_info(&self) -> Option { + self.error_info.clone() + } + /// Get [`BadRequest`] details, if any pub fn bad_request(&self) -> Option { self.bad_request.clone() @@ -293,6 +326,32 @@ impl ErrorDetails { false } + /// Set [`ErrorInfo`] details. Can be chained with other `.set_` and + /// `.add_` [`ErrorDetails`] methods. + /// + /// # Examples + /// + /// ``` + /// use std::collections::HashMap; + /// use tonic_types::{ErrorDetails}; + /// + /// let mut err_details = ErrorDetails::new(); + /// + /// let mut metadata: HashMap = HashMap::new(); + /// metadata.insert("instanceLimitPerRequest".into(), "100".into()); + /// + /// err_details.set_error_info("reason", "example.local", metadata); + /// ``` + pub fn set_error_info( + &mut self, + reason: impl Into, + domain: impl Into, + metadata: impl Into>, + ) -> &mut Self { + self.error_info = Some(ErrorInfo::new(reason, domain, metadata)); + self + } + /// Set [`BadRequest`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// diff --git a/tonic-types/src/richer_error/error_details/vec.rs b/tonic-types/src/richer_error/error_details/vec.rs index 7d024d713..900e134a0 100644 --- a/tonic-types/src/richer_error/error_details/vec.rs +++ b/tonic-types/src/richer_error/error_details/vec.rs @@ -1,4 +1,4 @@ -use super::super::std_messages::{BadRequest, DebugInfo, QuotaFailure, RetryInfo}; +use super::super::std_messages::{BadRequest, DebugInfo, ErrorInfo, QuotaFailure, RetryInfo}; /// Wraps the structs corresponding to the standard error messages, allowing /// the implementation and handling of vectors containing any of them. @@ -14,6 +14,9 @@ pub enum ErrorDetail { /// Wraps the [`QuotaFailure`] struct. QuotaFailure(QuotaFailure), + /// Wraps the [`ErrorInfo`] struct. + ErrorInfo(ErrorInfo), + /// Wraps the [`BadRequest`] struct. BadRequest(BadRequest), } @@ -36,6 +39,12 @@ impl From for ErrorDetail { } } +impl From for ErrorDetail { + fn from(err_detail: ErrorInfo) -> Self { + ErrorDetail::ErrorInfo(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 index 9fd801b28..2b2720dd8 100644 --- a/tonic-types/src/richer_error/mod.rs +++ b/tonic-types/src/richer_error/mod.rs @@ -12,7 +12,7 @@ use super::pb; pub use error_details::{vec::ErrorDetail, ErrorDetails}; pub use std_messages::{ - BadRequest, DebugInfo, FieldViolation, QuotaFailure, QuotaViolation, RetryInfo, + BadRequest, DebugInfo, ErrorInfo, FieldViolation, QuotaFailure, QuotaViolation, RetryInfo, }; trait IntoAny { @@ -315,6 +315,28 @@ pub trait StatusExt: crate::sealed::Sealed { /// ``` fn get_details_quota_failure(&self) -> Option; + /// Get first [`ErrorInfo`] 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(error_info) = status.get_details_error_info() { + /// // Handle error_info details + /// } + /// } + /// }; + /// } + /// ``` + fn get_details_error_info(&self) -> Option; + /// Get first [`BadRequest`] details found on `tonic::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. /// @@ -363,6 +385,10 @@ impl StatusExt for tonic::Status { conv_details.push(quota_failure.into_any()); } + if let Some(error_info) = details.error_info { + conv_details.push(error_info.into_any()); + } + if let Some(bad_request) = details.bad_request { conv_details.push(bad_request.into_any()); } @@ -397,6 +423,9 @@ impl StatusExt for tonic::Status { ErrorDetail::QuotaFailure(quota_failure) => { conv_details.push(quota_failure.into_any()); } + ErrorDetail::ErrorInfo(error_info) => { + conv_details.push(error_info.into_any()); + } ErrorDetail::BadRequest(bad_req) => { conv_details.push(bad_req.into_any()); } @@ -437,6 +466,9 @@ impl StatusExt for tonic::Status { QuotaFailure::TYPE_URL => { details.quota_failure = Some(QuotaFailure::from_any(any)?); } + ErrorInfo::TYPE_URL => { + details.error_info = Some(ErrorInfo::from_any(any)?); + } BadRequest::TYPE_URL => { details.bad_request = Some(BadRequest::from_any(any)?); } @@ -467,6 +499,9 @@ impl StatusExt for tonic::Status { QuotaFailure::TYPE_URL => { details.push(QuotaFailure::from_any(any)?.into()); } + ErrorInfo::TYPE_URL => { + details.push(ErrorInfo::from_any(any)?.into()); + } BadRequest::TYPE_URL => { details.push(BadRequest::from_any(any)?.into()); } @@ -523,6 +558,20 @@ impl StatusExt for tonic::Status { None } + fn get_details_error_info(&self) -> Option { + let status = pb::Status::decode(self.details()).ok()?; + + for any in status.details.into_iter() { + if any.type_url.as_str() == ErrorInfo::TYPE_URL { + if let Ok(detail) = ErrorInfo::from_any(any) { + return Some(detail); + } + } + } + + None + } + fn get_details_bad_request(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; @@ -540,13 +589,18 @@ impl StatusExt for tonic::Status { #[cfg(test)] mod tests { - use std::time::Duration; + use std::{collections::HashMap, time::Duration}; use tonic::{Code, Status}; - use super::{BadRequest, DebugInfo, ErrorDetails, QuotaFailure, RetryInfo, StatusExt}; + use super::{ + BadRequest, DebugInfo, ErrorDetails, ErrorInfo, QuotaFailure, RetryInfo, StatusExt, + }; #[test] fn gen_status_with_details() { + let mut metadata = HashMap::new(); + metadata.insert("limitPerRequest".into(), "100".into()); + let mut err_details = ErrorDetails::new(); err_details @@ -556,6 +610,7 @@ mod tests { "details", ) .add_quota_failure_violation("clientip:", "description") + .set_error_info("SOME_INFO", "example.local", metadata.clone()) .add_bad_request_violation("field", "description"); let fmt_details = format!("{:?}", err_details); @@ -568,6 +623,7 @@ mod tests { ) .into(), QuotaFailure::with_violation("clientip:", "description").into(), + ErrorInfo::new("SOME_INFO", "example.local", metadata).into(), BadRequest::with_violation("field", "description").into(), ]; diff --git a/tonic-types/src/richer_error/std_messages/error_info.rs b/tonic-types/src/richer_error/std_messages/error_info.rs new file mode 100644 index 000000000..072d8f9ec --- /dev/null +++ b/tonic-types/src/richer_error/std_messages/error_info.rs @@ -0,0 +1,133 @@ +use std::collections::HashMap; + +use prost::{DecodeError, Message}; +use prost_types::Any; + +use super::super::{pb, FromAny, IntoAny}; + +/// Used to encode/decode the `ErrorInfo` standard error message described in +/// [error_details.proto]. Describes the cause of the error with structured +/// details. +/// +/// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto +#[derive(Clone, Debug)] +pub struct ErrorInfo { + /// Reason of the error. Should be a constant value that identifies the + /// proximate cause of the error. Error reasons should be unique within a + /// particular domain of errors. This should be at most 63 characters and + /// match `/[A-Z0-9_]+/`. + pub reason: String, + + /// Logical grouping to which the "reason" belongs. Normally is the + /// registered name of the service that generates the error. + pub domain: String, + + /// Additional structured details about this error. Keys should match + /// `/[a-zA-Z0-9-_]/` and be limited to 64 characters in length. + pub metadata: HashMap, +} + +impl ErrorInfo { + /// Type URL of the `ErrorInfo` standard error message type. + pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.ErrorInfo"; + + /// Creates a new [`ErrorInfo`] struct. + pub fn new( + reason: impl Into, + domain: impl Into, + metadata: impl Into>, + ) -> Self { + ErrorInfo { + reason: reason.into(), + domain: domain.into(), + metadata: metadata.into(), + } + } +} + +impl ErrorInfo { + /// Returns `true` if [`ErrorInfo`] fields are empty, and `false` if they + /// are not. + pub fn is_empty(&self) -> bool { + self.reason.is_empty() && self.domain.is_empty() && self.metadata.is_empty() + } +} + +impl IntoAny for ErrorInfo { + fn into_any(self) -> Any { + let detail_data = pb::ErrorInfo { + reason: self.reason, + domain: self.domain, + metadata: self.metadata, + }; + + Any { + type_url: ErrorInfo::TYPE_URL.to_string(), + value: detail_data.encode_to_vec(), + } + } +} + +impl FromAny for ErrorInfo { + fn from_any(any: Any) -> Result { + let buf: &[u8] = &any.value; + let debug_info = pb::ErrorInfo::decode(buf)?; + + let debug_info = ErrorInfo { + reason: debug_info.reason, + domain: debug_info.domain, + metadata: debug_info.metadata, + }; + + Ok(debug_info) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::super::super::{FromAny, IntoAny}; + use super::ErrorInfo; + + #[test] + fn gen_error_info() { + let mut metadata = HashMap::new(); + metadata.insert("instanceLimitPerRequest".into(), "100".into()); + + let error_info = ErrorInfo::new("SOME_INFO", "mydomain.com", metadata); + + let formatted = format!("{:?}", error_info); + + let expected_filled = "ErrorInfo { reason: \"SOME_INFO\", domain: \"mydomain.com\", metadata: {\"instanceLimitPerRequest\": \"100\"} }"; + + assert!( + formatted.eq(expected_filled), + "filled ErrorInfo 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.ErrorInfo\", value: [10, 9, 83, 79, 77, 69, 95, 73, 78, 70, 79, 18, 12, 109, 121, 100, 111, 109, 97, 105, 110, 46, 99, 111, 109, 26, 30, 10, 23, 105, 110, 115, 116, 97, 110, 99, 101, 76, 105, 109, 105, 116, 80, 101, 114, 82, 101, 113, 117, 101, 115, 116, 18, 3, 49, 48, 48] }"; + + assert!( + formatted.eq(expected), + "Any from filled ErrorInfo differs from expected result" + ); + + let br_details = match ErrorInfo::from_any(gen_any) { + Err(error) => panic!("Error generating ErrorInfo from Any: {:?}", error), + Ok(from_any) => from_any, + }; + + let formatted = format!("{:?}", br_details); + + assert!( + formatted.eq(expected_filled), + "ErrorInfo from Any differs from expected result" + ); + } +} diff --git a/tonic-types/src/richer_error/std_messages/mod.rs b/tonic-types/src/richer_error/std_messages/mod.rs index a7e00f115..b2cf4782d 100644 --- a/tonic-types/src/richer_error/std_messages/mod.rs +++ b/tonic-types/src/richer_error/std_messages/mod.rs @@ -10,6 +10,10 @@ mod quota_failure; pub use quota_failure::{QuotaFailure, QuotaViolation}; +mod error_info; + +pub use error_info::ErrorInfo; + mod bad_request; pub use bad_request::{BadRequest, FieldViolation};