From 0bfe1c83e11762d09b97268cad3930611091ee45 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Fri, 24 Feb 2023 11:06:34 -0300 Subject: [PATCH 1/2] types: add support for `LocalizedMessage` error message type Following implementation at flemosr/tonic-richer-error. --- tonic-types/src/lib.rs | 4 +- .../src/richer_error/error_details/mod.rs | 54 ++++++++- .../src/richer_error/error_details/vec.rs | 13 +- tonic-types/src/richer_error/mod.rs | 62 +++++++++- .../richer_error/std_messages/loc_message.rs | 113 ++++++++++++++++++ .../src/richer_error/std_messages/mod.rs | 4 + 6 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 tonic-types/src/richer_error/std_messages/loc_message.rs diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index c7ab9a989..49758f96c 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -47,8 +47,8 @@ mod richer_error; pub use richer_error::{ BadRequest, DebugInfo, ErrorDetail, ErrorDetails, ErrorInfo, FieldViolation, Help, HelpLink, - PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, - ResourceInfo, RetryInfo, StatusExt, + LocalizedMessage, PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, + RequestInfo, 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 9301bc483..744b4e2d3 100644 --- a/tonic-types/src/richer_error/error_details/mod.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -1,8 +1,9 @@ use std::{collections::HashMap, time}; use super::std_messages::{ - BadRequest, DebugInfo, ErrorInfo, FieldViolation, Help, HelpLink, PreconditionFailure, - PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, ResourceInfo, RetryInfo, + BadRequest, DebugInfo, ErrorInfo, FieldViolation, Help, HelpLink, LocalizedMessage, + PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, + ResourceInfo, RetryInfo, }; pub(crate) mod vec; @@ -40,6 +41,9 @@ pub struct ErrorDetails { /// This field stores [`Help`] data, if any. pub(crate) help: Option, + + /// This field stores [`LocalizedMessage`] data, if any. + pub(crate) localized_message: Option, } impl ErrorDetails { @@ -337,6 +341,26 @@ impl ErrorDetails { } } + /// Generates an [`ErrorDetails`] struct with [`LocalizedMessage`] details + /// and remaining fields set to `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::ErrorDetails; + /// + /// let err_details = ErrorDetails::with_localized_message( + /// "en-US", + /// "message for the user" + /// ); + /// ``` + pub fn with_localized_message(locale: impl Into, message: impl Into) -> Self { + ErrorDetails { + localized_message: Some(LocalizedMessage::new(locale, message)), + ..ErrorDetails::new() + } + } + /// Get [`RetryInfo`] details, if any. pub fn retry_info(&self) -> Option { self.retry_info.clone() @@ -382,6 +406,11 @@ impl ErrorDetails { self.help.clone() } + /// Get [`LocalizedMessage`] details, if any. + pub fn localized_message(&self) -> Option { + self.localized_message.clone() + } + /// Set [`RetryInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// @@ -809,4 +838,25 @@ impl ErrorDetails { } false } + + /// Set [`LocalizedMessage`] 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_localized_message("en-US", "message for the user"); + /// ``` + pub fn set_localized_message( + &mut self, + locale: impl Into, + message: impl Into, + ) -> &mut Self { + self.localized_message = Some(LocalizedMessage::new(locale, message)); + self + } } diff --git a/tonic-types/src/richer_error/error_details/vec.rs b/tonic-types/src/richer_error/error_details/vec.rs index 76f35173d..9a09285e8 100644 --- a/tonic-types/src/richer_error/error_details/vec.rs +++ b/tonic-types/src/richer_error/error_details/vec.rs @@ -1,6 +1,6 @@ use super::super::std_messages::{ - BadRequest, DebugInfo, ErrorInfo, Help, PreconditionFailure, QuotaFailure, RequestInfo, - ResourceInfo, RetryInfo, + BadRequest, DebugInfo, ErrorInfo, Help, LocalizedMessage, PreconditionFailure, QuotaFailure, + RequestInfo, ResourceInfo, RetryInfo, }; /// Wraps the structs corresponding to the standard error messages, allowing @@ -34,6 +34,9 @@ pub enum ErrorDetail { /// Wraps the [`Help`] struct. Help(Help), + + /// Wraps the [`LocalizedMessage`] struct. + LocalizedMessage(LocalizedMessage), } impl From for ErrorDetail { @@ -89,3 +92,9 @@ impl From for ErrorDetail { ErrorDetail::Help(err_detail) } } + +impl From for ErrorDetail { + fn from(err_detail: LocalizedMessage) -> Self { + ErrorDetail::LocalizedMessage(err_detail) + } +} diff --git a/tonic-types/src/richer_error/mod.rs b/tonic-types/src/richer_error/mod.rs index 77a59def4..50fa03e46 100644 --- a/tonic-types/src/richer_error/mod.rs +++ b/tonic-types/src/richer_error/mod.rs @@ -12,8 +12,9 @@ use super::pb; pub use error_details::{vec::ErrorDetail, ErrorDetails}; pub use std_messages::{ - BadRequest, DebugInfo, ErrorInfo, FieldViolation, Help, HelpLink, PreconditionFailure, - PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, ResourceInfo, RetryInfo, + BadRequest, DebugInfo, ErrorInfo, FieldViolation, Help, HelpLink, LocalizedMessage, + PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, + ResourceInfo, RetryInfo, }; trait IntoAny { @@ -446,6 +447,28 @@ pub trait StatusExt: crate::sealed::Sealed { /// } /// ``` fn get_details_help(&self) -> Option; + + /// Get first [`LocalizedMessage`] 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(localized_message) = status.get_details_localized_message() { + /// // Handle localized_message details + /// } + /// } + /// }; + /// } + /// ``` + fn get_details_localized_message(&self) -> Option; } impl crate::sealed::Sealed for tonic::Status {} @@ -497,6 +520,10 @@ impl StatusExt for tonic::Status { conv_details.push(help.into_any()); } + if let Some(localized_message) = details.localized_message { + conv_details.push(localized_message.into_any()); + } + let details = gen_details_bytes(code, &message, conv_details); tonic::Status::with_details_and_metadata(code, message, details, metadata) @@ -545,6 +572,9 @@ impl StatusExt for tonic::Status { ErrorDetail::Help(help) => { conv_details.push(help.into_any()); } + ErrorDetail::LocalizedMessage(loc_message) => { + conv_details.push(loc_message.into_any()); + } } } @@ -600,6 +630,9 @@ impl StatusExt for tonic::Status { Help::TYPE_URL => { details.help = Some(Help::from_any(any)?); } + LocalizedMessage::TYPE_URL => { + details.localized_message = Some(LocalizedMessage::from_any(any)?); + } _ => {} } } @@ -645,6 +678,9 @@ impl StatusExt for tonic::Status { Help::TYPE_URL => { details.push(Help::from_any(any)?.into()); } + LocalizedMessage::TYPE_URL => { + details.push(LocalizedMessage::from_any(any)?.into()); + } _ => {} } } @@ -781,6 +817,20 @@ impl StatusExt for tonic::Status { None } + + fn get_details_localized_message(&self) -> Option { + let status = pb::Status::decode(self.details()).ok()?; + + for any in status.details.into_iter() { + if any.type_url.as_str() == LocalizedMessage::TYPE_URL { + if let Ok(detail) = LocalizedMessage::from_any(any) { + return Some(detail); + } + } + } + + None + } } #[cfg(test)] @@ -789,8 +839,8 @@ mod tests { use tonic::{Code, Status}; use super::{ - BadRequest, DebugInfo, ErrorDetails, ErrorInfo, Help, PreconditionFailure, QuotaFailure, - RequestInfo, ResourceInfo, RetryInfo, StatusExt, + BadRequest, DebugInfo, ErrorDetails, ErrorInfo, Help, LocalizedMessage, + PreconditionFailure, QuotaFailure, RequestInfo, ResourceInfo, RetryInfo, StatusExt, }; #[test] @@ -812,7 +862,8 @@ mod tests { .add_bad_request_violation("field", "description") .set_request_info("request-id", "some-request-data") .set_resource_info("resource-type", "resource-name", "owner", "description") - .add_help_link("link to resource", "resource.example.local"); + .add_help_link("link to resource", "resource.example.local") + .set_localized_message("en-US", "message for the user"); let fmt_details = format!("{:?}", err_details); @@ -830,6 +881,7 @@ mod tests { RequestInfo::new("request-id", "some-request-data").into(), ResourceInfo::new("resource-type", "resource-name", "owner", "description").into(), Help::with_link("link to resource", "resource.example.local").into(), + LocalizedMessage::new("en-US", "message for the user").into(), ]; let fmt_details_vec = format!("{:?}", err_details_vec); diff --git a/tonic-types/src/richer_error/std_messages/loc_message.rs b/tonic-types/src/richer_error/std_messages/loc_message.rs new file mode 100644 index 000000000..75469ff96 --- /dev/null +++ b/tonic-types/src/richer_error/std_messages/loc_message.rs @@ -0,0 +1,113 @@ +use prost::{DecodeError, Message}; +use prost_types::Any; + +use super::super::{pb, FromAny, IntoAny}; + +/// Used to encode/decode the `LocalizedMessage` standard error message +/// described in [error_details.proto]. Provides a localized error message +/// that is safe to return to the user. +/// +/// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto +#[derive(Clone, Debug)] +pub struct LocalizedMessage { + /// Locale used, following the specification defined in [BCP 47]. For + /// example: "en-US", "fr-CH" or "es-MX". + /// + /// [BCP 47]: http://www.rfc-editor.org/rfc/bcp/bcp47.txt + pub locale: String, + + /// Message corresponding to the locale. + pub message: String, +} + +impl LocalizedMessage { + /// Type URL of the `LocalizedMessage` standard error message type. + pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.LocalizedMessage"; + + /// Creates a new [`LocalizedMessage`] struct. + pub fn new(locale: impl Into, message: impl Into) -> Self { + LocalizedMessage { + locale: locale.into(), + message: message.into(), + } + } + + /// Returns `true` if [`LocalizedMessage`] fields are empty, and `false` if + /// they are not. + pub fn is_empty(&self) -> bool { + self.locale.is_empty() && self.message.is_empty() + } +} + +impl IntoAny for LocalizedMessage { + fn into_any(self) -> Any { + let detail_data = pb::LocalizedMessage { + locale: self.locale, + message: self.message, + }; + + Any { + type_url: LocalizedMessage::TYPE_URL.to_string(), + value: detail_data.encode_to_vec(), + } + } +} + +impl FromAny for LocalizedMessage { + fn from_any(any: Any) -> Result { + let buf: &[u8] = &any.value; + let loc_message = pb::LocalizedMessage::decode(buf)?; + + let loc_message = LocalizedMessage { + locale: loc_message.locale, + message: loc_message.message, + }; + + Ok(loc_message) + } +} + +#[cfg(test)] +mod tests { + use super::super::super::{FromAny, IntoAny}; + use super::LocalizedMessage; + + #[test] + fn gen_localized_message() { + let loc_message = LocalizedMessage::new("en-US", "message for the user"); + + let formatted = format!("{:?}", loc_message); + + let expected_filled = + "LocalizedMessage { locale: \"en-US\", message: \"message for the user\" }"; + + assert!( + formatted.eq(expected_filled), + "filled LocalizedMessage differs from expected result" + ); + + let gen_any = loc_message.into_any(); + + let formatted = format!("{:?}", gen_any); + + let expected = + "Any { type_url: \"type.googleapis.com/google.rpc.LocalizedMessage\", value: [10, 5, 101, 110, 45, 85, 83, 18, 20, 109, 101, 115, 115, 97, 103, 101, 32, 102, 111, 114, 32, 116, 104, 101, 32, 117, 115, 101, 114] }"; + + assert!( + formatted.eq(expected), + "Any from filled LocalizedMessage differs from expected result" + ); + + let br_details = match LocalizedMessage::from_any(gen_any) { + Err(error) => panic!("Error generating LocalizedMessage from Any: {:?}", error), + Ok(from_any) => from_any, + }; + + let formatted = format!("{:?}", br_details); + + assert!( + formatted.eq(expected_filled), + "LocalizedMessage 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 e4d59587d..25b773b0b 100644 --- a/tonic-types/src/richer_error/std_messages/mod.rs +++ b/tonic-types/src/richer_error/std_messages/mod.rs @@ -33,3 +33,7 @@ pub use resource_info::ResourceInfo; mod help; pub use help::{Help, HelpLink}; + +mod loc_message; + +pub use loc_message::LocalizedMessage; From 4dca9944513a480adfc650147090ab50f0516ae7 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Fri, 24 Feb 2023 11:37:45 -0300 Subject: [PATCH 2/2] types: add `ErrorDetails::with_help_link` --- .../src/richer_error/error_details/mod.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tonic-types/src/richer_error/error_details/mod.rs b/tonic-types/src/richer_error/error_details/mod.rs index 744b4e2d3..1f8f2c398 100644 --- a/tonic-types/src/richer_error/error_details/mod.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -341,6 +341,26 @@ impl ErrorDetails { } } + /// Generates an [`ErrorDetails`] struct with [`Help`] details (one + /// [`HelpLink`] set) and remaining fields set to `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::ErrorDetails; + /// + /// let err_details = ErrorDetails::with_help_link( + /// "description of link a", + /// "resource-a.example.local" + /// ); + /// ``` + pub fn with_help_link(description: impl Into, url: impl Into) -> Self { + ErrorDetails { + help: Some(Help::with_link(description, url)), + ..ErrorDetails::new() + } + } + /// Generates an [`ErrorDetails`] struct with [`LocalizedMessage`] details /// and remaining fields set to `None`. ///