From 47465a6ab2678cb422c02c67028194976a46ceff Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Fri, 26 Aug 2022 09:53:12 -0300 Subject: [PATCH 1/6] types: add tonic as dependency, add error_details.proto --- tonic-types/Cargo.toml | 1 + tonic-types/build.rs | 6 +- tonic-types/proto/error_details.proto | 250 ++++++++++++++++++++++++++ tonic-types/src/lib.rs | 3 +- 4 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 tonic-types/proto/error_details.proto diff --git a/tonic-types/Cargo.toml b/tonic-types/Cargo.toml index b38dee781..237db3d18 100644 --- a/tonic-types/Cargo.toml +++ b/tonic-types/Cargo.toml @@ -17,6 +17,7 @@ version = "0.6.0" [dependencies] prost = "0.11" prost-types = "0.11" +tonic = {version = "0.8", path = "../tonic"} [build-dependencies] prost-build = "0.11" diff --git a/tonic-types/build.rs b/tonic-types/build.rs index 88b28da40..1c601479d 100644 --- a/tonic-types/build.rs +++ b/tonic-types/build.rs @@ -1,3 +1,7 @@ fn main() { - prost_build::compile_protos(&["proto/status.proto"], &["proto/"]).unwrap(); + prost_build::compile_protos( + &["proto/status.proto", "proto/error_details.proto"], + &["proto/"], + ) + .unwrap(); } diff --git a/tonic-types/proto/error_details.proto b/tonic-types/proto/error_details.proto new file mode 100644 index 000000000..7bdf0dd39 --- /dev/null +++ b/tonic-types/proto/error_details.proto @@ -0,0 +1,250 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/duration.proto"; + +option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails"; +option java_multiple_files = true; +option java_outer_classname = "ErrorDetailsProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// Describes when the clients can retry a failed request. Clients could ignore +// the recommendation here or retry when this information is missing from error +// responses. +// +// It's always recommended that clients should use exponential backoff when +// retrying. +// +// Clients should wait until `retry_delay` amount of time has passed since +// receiving the error response before retrying. If retrying requests also +// fail, clients should use an exponential backoff scheme to gradually increase +// the delay between retries based on `retry_delay`, until either a maximum +// number of retries have been reached or a maximum retry delay cap has been +// reached. +message RetryInfo { + // Clients should wait at least this long between retrying the same request. + google.protobuf.Duration retry_delay = 1; +} + +// Describes additional debugging info. +message DebugInfo { + // The stack trace entries indicating where the error occurred. + repeated string stack_entries = 1; + + // Additional debugging information provided by the server. + string detail = 2; +} + +// Describes how a quota check failed. +// +// For example if a daily limit was exceeded for the calling project, +// a service could respond with a QuotaFailure detail containing the project +// id and the description of the quota limit that was exceeded. If the +// calling project hasn't enabled the service in the developer console, then +// a service could respond with the project id and set `service_disabled` +// to true. +// +// Also see RetryInfo and Help types for other details about handling a +// quota failure. +message QuotaFailure { + // A message type used to describe a single quota violation. For example, a + // daily quota or a custom quota that was exceeded. + message Violation { + // The subject on which the quota check failed. + // For example, "clientip:" or "project:". + string subject = 1; + + // A description of how the quota check failed. Clients can use this + // description to find more about the quota configuration in the service's + // public documentation, or find the relevant quota limit to adjust through + // developer console. + // + // For example: "Service disabled" or "Daily Limit for read operations + // exceeded". + string description = 2; + } + + // Describes all quota violations. + repeated Violation violations = 1; +} + +// Describes the cause of the error with structured details. +// +// Example of an error when contacting the "pubsub.googleapis.com" API when it +// is not enabled: +// ```json +// { "reason": "API_DISABLED" +// "domain": "googleapis.com" +// "metadata": { +// "resource": "projects/123", +// "service": "pubsub.googleapis.com" +// } +// } +// ``` +// This response indicates that the pubsub.googleapis.com API is not enabled. +// +// Example of an error that is returned when attempting to create a Spanner +// instance in a region that is out of stock: +// ```json +// { "reason": "STOCKOUT" +// "domain": "spanner.googleapis.com", +// "metadata": { +// "availableRegions": "us-central1,us-east2" +// } +// } +// ``` +message ErrorInfo { + // The reason of the error. This is a constant value that identifies the + // proximate cause of the error. Error reasons are unique within a particular + // domain of errors. This should be at most 63 characters and match + // /[A-Z0-9_]+/. + string reason = 1; + + // The logical grouping to which the "reason" belongs. The error domain + // is typically the registered service name of the tool or product that + // generates the error. Example: "pubsub.googleapis.com". If the error is + // generated by some common infrastructure, the error domain must be a + // globally unique value that identifies the infrastructure. For Google API + // infrastructure, the error domain is "googleapis.com". + string domain = 2; + + // Additional structured details about this error. + // + // Keys should match /[a-zA-Z0-9-_]/ and be limited to 64 characters in + // length. When identifying the current value of an exceeded limit, the units + // should be contained in the key, not the value. For example, rather than + // {"instanceLimit": "100/request"}, should be returned as, + // {"instanceLimitPerRequest": "100"}, if the client exceeds the number of + // instances that can be created in a single (batch) request. + map metadata = 3; +} + +// Describes what preconditions have failed. +// +// For example, if an RPC failed because it required the Terms of Service to be +// acknowledged, it could list the terms of service violation in the +// PreconditionFailure message. +message PreconditionFailure { + // A message type used to describe a single precondition failure. + message Violation { + // The type of PreconditionFailure. We recommend using a service-specific + // enum type to define the supported precondition violation subjects. For + // example, "TOS" for "Terms of Service violation". + string type = 1; + + // The subject, relative to the type, that failed. + // For example, "google.com/cloud" relative to the "TOS" type would indicate + // which terms of service is being referenced. + string subject = 2; + + // A description of how the precondition failed. Developers can use this + // description to understand how to fix the failure. + // + // For example: "Terms of service not accepted". + string description = 3; + } + + // Describes all precondition violations. + repeated Violation violations = 1; +} + +// Describes violations in a client request. This error type focuses on the +// syntactic aspects of the request. +message BadRequest { + // A message type used to describe a single bad request field. + message FieldViolation { + // A path leading to a field in the request body. The value will be a + // sequence of dot-separated identifiers that identify a protocol buffer + // field. E.g., "field_violations.field" would identify this field. + string field = 1; + + // A description of why the request element is bad. + string description = 2; + } + + // Describes all violations in a client request. + repeated FieldViolation field_violations = 1; +} + +// Contains metadata about the request that clients can attach when filing a bug +// or providing other forms of feedback. +message RequestInfo { + // An opaque string that should only be interpreted by the service generating + // it. For example, it can be used to identify requests in the service's logs. + string request_id = 1; + + // Any data that was used to serve this request. For example, an encrypted + // stack trace that can be sent back to the service provider for debugging. + string serving_data = 2; +} + +// Describes the resource that is being accessed. +message ResourceInfo { + // A name for the type of resource being accessed, e.g. "sql table", + // "cloud storage bucket", "file", "Google calendar"; or the type URL + // of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". + string resource_type = 1; + + // The name of the resource being accessed. For example, a shared calendar + // name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current + // error is [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED]. + string resource_name = 2; + + // The owner of the resource (optional). + // For example, "user:" or "project:". + string owner = 3; + + // Describes what error is encountered when accessing this resource. + // For example, updating a cloud project may require the `writer` permission + // on the developer console project. + string description = 4; +} + +// Provides links to documentation or for performing an out of band action. +// +// For example, if a quota check failed with an error indicating the calling +// project hasn't enabled the accessed service, this can contain a URL pointing +// directly to the right place in the developer console to flip the bit. +message Help { + // Describes a URL link. + message Link { + // Describes what the link offers. + string description = 1; + + // The URL of the link. + string url = 2; + } + + // URL(s) pointing to additional information on handling the current error. + repeated Link links = 1; +} + +// Provides a localized error message that is safe to return to the user +// which can be attached to an RPC error. +message LocalizedMessage { + // The locale used following the specification defined at + // http://www.rfc-editor.org/rfc/bcp/bcp47.txt. + // Examples are: "en-US", "fr-CH", "es-MX" + string locale = 1; + + // The localized error message in the above locale. + string message = 2; +} \ No newline at end of file diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index f59ec6ba7..6fdb9b831 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -13,7 +13,8 @@ #![doc(html_root_url = "https://docs.rs/tonic-types/0.6.0")] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] -mod pb { +/// Useful protobuf types +pub mod pb { include!(concat!(env!("OUT_DIR"), "/google.rpc.rs")); } From dffa044efdb74b7645f7695dc97ac31dc5192684 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Fri, 26 Aug 2022 10:40:04 -0300 Subject: [PATCH 2/6] types: add BadRequest support from flemosr/tonic-richer-error --- tonic-types/src/error_details.rs | 137 +++++++ tonic-types/src/error_details_vec.rs | 15 + tonic-types/src/lib.rs | 394 +++++++++++++++++++- tonic-types/src/std_messages.rs | 3 + tonic-types/src/std_messages/bad_request.rs | 194 ++++++++++ 5 files changed, 742 insertions(+), 1 deletion(-) create mode 100644 tonic-types/src/error_details.rs create mode 100644 tonic-types/src/error_details_vec.rs create mode 100644 tonic-types/src/std_messages.rs create mode 100644 tonic-types/src/std_messages/bad_request.rs diff --git a/tonic-types/src/error_details.rs b/tonic-types/src/error_details.rs new file mode 100644 index 000000000..44ff55b54 --- /dev/null +++ b/tonic-types/src/error_details.rs @@ -0,0 +1,137 @@ +use super::std_messages::*; + +/// Groups the standard error messages structs. Provides associated +/// functions and methods to setup and edit each error message independently. +/// Used when extracting error details from `tonic::Status`, and when +/// creating a `tonic::Status` with error details. +#[derive(Clone, Debug)] +pub struct ErrorDetails { + /// This field stores [`BadRequest`] data, if any. + pub bad_request: Option, +} + +impl ErrorDetails { + /// Generates an [`ErrorDetails`] struct with all fields set to `None`. + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails}; + /// + /// let err_details = ErrorDetails::new(); + /// ``` + pub fn new() -> Self { + ErrorDetails { bad_request: None } + } + + /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details and + /// remaining fields set to `None`. + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails, FieldViolation}; + /// + /// let err_details = ErrorDetails::with_bad_request(vec![ + /// FieldViolation::new("field_1", "description 1"), + /// FieldViolation::new("field_2", "description 2"), + /// ]); + /// ``` + pub fn with_bad_request(field_violations: Vec) -> Self { + ErrorDetails { + bad_request: Some(BadRequest::new(field_violations)), + ..ErrorDetails::new() + } + } + + /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details (one + /// [`FieldViolation`] set) and remaining fields set to `None`. + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails}; + /// + /// let err_details = ErrorDetails::with_bad_request_violation( + /// "field", + /// "description", + /// ); + /// ``` + pub fn with_bad_request_violation( + field: impl Into, + description: impl Into, + ) -> Self { + ErrorDetails { + bad_request: Some(BadRequest::with_violation(field, description)), + ..ErrorDetails::new() + } + } +} + +impl ErrorDetails { + /// Set [`BadRequest`] details. Can be chained with other `.set_` and + /// `.add_` [`ErrorDetails`] methods. + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails, FieldViolation}; + /// + /// let mut err_details = ErrorDetails::new(); + /// + /// err_details.set_bad_request(vec![ + /// FieldViolation::new("field_1", "description 1"), + /// FieldViolation::new("field_2", "description 2"), + /// ]); + /// ``` + pub fn set_bad_request(&mut self, violations: Vec) -> &mut Self { + self.bad_request = Some(BadRequest::new(violations)); + self + } + + /// Adds a [`FieldViolation`] to [`BadRequest`] details. Sets + /// [`BadRequest`] details if it is not set yet. Can be chained with other + /// `.set_` and `.add_` [`ErrorDetails`] methods. + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails}; + /// + /// let mut err_details = ErrorDetails::new(); + /// + /// err_details.add_bad_request_violation("field", "description"); + /// ``` + pub fn add_bad_request_violation( + &mut self, + field: impl Into, + description: impl Into, + ) -> &mut Self { + match &mut self.bad_request { + Some(bad_request) => { + bad_request.add_violation(field, description); + } + None => { + self.bad_request = Some(BadRequest::with_violation(field, description)); + } + }; + self + } + + /// Returns `true` if [`BadRequest`] is set and its `field_violations` + /// vector is not empty, otherwise returns `false`. + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails}; + /// + /// let mut err_details = ErrorDetails::with_bad_request(vec![]); + /// + /// assert_eq!(err_details.has_bad_request_violations(), false); + /// + /// err_details.add_bad_request_violation("field", "description"); + /// + /// assert_eq!(err_details.has_bad_request_violations(), true); + /// ``` + pub fn has_bad_request_violations(&self) -> bool { + if let Some(bad_request) = &self.bad_request { + return !bad_request.field_violations.is_empty(); + } + false + } +} diff --git a/tonic-types/src/error_details_vec.rs b/tonic-types/src/error_details_vec.rs new file mode 100644 index 000000000..b9809a1bb --- /dev/null +++ b/tonic-types/src/error_details_vec.rs @@ -0,0 +1,15 @@ +use super::std_messages::*; + +/// Wraps the structs corresponding to the standard error messages, allowing +/// the implementation and handling of vectors containing any of them. +#[derive(Clone, Debug)] +pub enum ErrorDetail { + /// Wraps the [`BadRequest`] struct. + BadRequest(BadRequest), +} + +impl From for ErrorDetail { + fn from(err_detail: BadRequest) -> Self { + ErrorDetail::BadRequest(err_detail) + } +} diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index 6fdb9b831..4ffe0cdb4 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -1,4 +1,14 @@ -//! A collection of useful protobuf types that can be used with `tonic`. +/*! +A collection of useful protobuf types that can be used with `tonic`. + +This crate also introduces the [`WithErrorDetails`] trait and implements it in +[`tonic::Status`], allowing the implementation of the [gRPC Richer Error Model] +with [`tonic`] in a convenient way. + +[`tonic::Status`]: https://docs.rs/tonic/0.8.0/tonic/struct.Status.html +[`tonic`]: https://docs.rs/tonic/0.8.0/tonic/ +[gRPC Richer Error Model]: https://www.grpc.io/docs/guides/error/ +*/ #![warn( missing_debug_implementations, @@ -13,9 +23,391 @@ #![doc(html_root_url = "https://docs.rs/tonic-types/0.6.0")] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] +use prost::{DecodeError, Message}; +use prost_types::Any; +use tonic::{codegen::Bytes, Code}; + /// Useful protobuf types pub mod pb { include!(concat!(env!("OUT_DIR"), "/google.rpc.rs")); } pub use pb::Status; + +mod error_details; +mod error_details_vec; +mod std_messages; + +pub use std_messages::*; + +pub use error_details::ErrorDetails; + +pub use error_details_vec::ErrorDetail; + +trait IntoAny { + fn into_any(self) -> Any; +} + +trait FromAny { + fn from_any(any: Any) -> Result + where + Self: Sized; +} + +/// Used to implement associated functions and methods on `tonic::Status`, that +/// allow the addition and extraction of standard error details. +pub trait WithErrorDetails { + /// Generates a `tonic::Status` with error details obtained from an + /// [`ErrorDetails`] struct. + /// # Examples + /// + /// ``` + /// use tonic::{Code, Status}; + /// use tonic_types::{ErrorDetails, WithErrorDetails}; + /// + /// let status = Status::with_error_details( + /// Code::InvalidArgument, + /// "bad request", + /// ErrorDetails::with_bad_request_violation("field", "description"), + /// ); + /// ``` + fn with_error_details( + code: tonic::Code, + message: impl Into, + details: ErrorDetails, + ) -> 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, WithErrorDetails}; + /// + /// let status = Status::with_error_details_vec( + /// Code::InvalidArgument, + /// "bad request", + /// vec![ + /// BadRequest::with_violation("field", "description").into(), + /// ] + /// ); + /// ``` + fn with_error_details_vec( + code: tonic::Code, + message: impl Into, + details: Vec, + ) -> 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 + /// [`WithErrorDetails::get_error_details`] or + /// [`WithErrorDetails::get_error_details_vec`]. + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::{WithErrorDetails}; + /// + /// 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::{WithErrorDetails}; + /// + /// 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 + /// [`WithErrorDetails::get_error_details_vec`] or + /// [`WithErrorDetails::get_error_details`]. + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::{ErrorDetail, WithErrorDetails}; + /// + /// 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, WithErrorDetails}; + /// + /// 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::{WithErrorDetails}; + /// + /// 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 WithErrorDetails for tonic::Status { + fn with_error_details(code: Code, message: impl Into, details: ErrorDetails) -> 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 status = pb::Status { + code: code as i32, + message: message.clone(), + details: conv_details, + }; + + tonic::Status::with_details(code, message, Bytes::from(status.encode_to_vec())) + } + + fn with_error_details_vec( + code: Code, + message: impl Into, + details: Vec, + ) -> Self { + let message: String = message.into(); + + let mut conv_details: Vec = Vec::with_capacity(details.len()); + + for error_detail in details.into_iter() { + match error_detail { + ErrorDetail::BadRequest(bad_req) => { + conv_details.push(bad_req.into_any()); + } + } + } + + let status = pb::Status { + code: code as i32, + message: message.clone(), + details: conv_details, + }; + + tonic::Status::with_details(code, message, Bytes::from(status.encode_to_vec())) + } + + 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, WithErrorDetails}; + + #[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); + + println!("{fmt_details}\n"); + + let err_details_vec = vec![BadRequest::with_violation("field", "description").into()]; + + let fmt_details_vec = format!("{:?}", err_details_vec); + + println!("{fmt_details_vec}\n"); + + let status_from_struct = Status::with_error_details( + Code::InvalidArgument, + "error with bad request details", + err_details, + ); + + let fmt_status_with_details = format!("{:?}", status_from_struct); + + println!("{:?}\n", fmt_status_with_details); + + let status_from_vec = Status::with_error_details_vec( + Code::InvalidArgument, + "error with bad request details", + err_details_vec, + ); + + let fmt_status_with_details_vec = format!("{:?}", status_from_vec); + + println!("{:?}\n", fmt_status_with_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); + + println!("{fmt_ext_details}\n"); + + 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); + + println!("fmt_ext_details_vec: {:?}\n", fmt_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.rs b/tonic-types/src/std_messages.rs new file mode 100644 index 000000000..fad442cdd --- /dev/null +++ b/tonic-types/src/std_messages.rs @@ -0,0 +1,3 @@ +mod bad_request; + +pub use bad_request::{BadRequest, FieldViolation}; diff --git a/tonic-types/src/std_messages/bad_request.rs b/tonic-types/src/std_messages/bad_request.rs new file mode 100644 index 000000000..864995896 --- /dev/null +++ b/tonic-types/src/std_messages/bad_request.rs @@ -0,0 +1,194 @@ +use prost::{DecodeError, Message}; +use prost_types::Any; + +use super::super::pb; +use super::super::{FromAny, IntoAny}; + +/// Used at the `field_violations` field of the [`BadRequest`] struct. +/// Describes a single bad request field. +#[derive(Clone, Debug)] +pub struct FieldViolation { + /// Path leading to a field in the request body. Value should be a + /// sequence of dot-separated identifiers that identify a protocol buffer + /// field. + pub field: String, + + /// Description of why the field is bad. + pub description: String, +} + +impl FieldViolation { + /// Creates a new [`FieldViolation`] struct. + pub fn new(field: impl Into, description: impl Into) -> Self { + FieldViolation { + field: field.into(), + description: description.into(), + } + } +} + +/// Used to encode/decode the `BadRequest` standard error message described in +/// [error_details.proto]. Describes violations in a client request. Focuses +/// on the syntactic aspects of the request. +/// +/// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto +#[derive(Clone, Debug)] +pub struct BadRequest { + /// Describes all field violations of the request. + pub field_violations: Vec, +} + +impl BadRequest { + /// Type URL of the `BadRequest` standard error message type. + pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.BadRequest"; + + /// Creates a new [`BadRequest`] struct. + pub fn new(field_violations: Vec) -> Self { + BadRequest { field_violations } + } + + /// Creates a new [`BadRequest`] struct with a single [`FieldViolation`] in + /// `field_violations`. + pub fn with_violation(field: impl Into, description: impl Into) -> Self { + BadRequest { + field_violations: vec![FieldViolation { + field: field.into(), + description: description.into(), + }], + } + } +} + +impl BadRequest { + /// Adds a [`FieldViolation`] to [`BadRequest`]'s `field_violations`. + pub fn add_violation( + &mut self, + field: impl Into, + description: impl Into, + ) -> &mut Self { + self.field_violations.append(&mut vec![FieldViolation { + field: field.into(), + description: description.into(), + }]); + self + } + + /// Returns `true` if [`BadRequest`]'s `field_violations` vector is empty, + /// and `false` if it is not. + pub fn is_empty(&self) -> bool { + self.field_violations.is_empty() + } +} + +impl IntoAny for BadRequest { + fn into_any(self) -> Any { + let detail_data = pb::BadRequest { + field_violations: self + .field_violations + .into_iter() + .map(|v| pb::bad_request::FieldViolation { + field: v.field, + description: v.description, + }) + .collect(), + }; + + Any { + type_url: BadRequest::TYPE_URL.to_string(), + value: detail_data.encode_to_vec(), + } + } +} + +impl FromAny for BadRequest { + fn from_any(any: Any) -> Result { + let buf: &[u8] = &any.value; + let bad_req = pb::BadRequest::decode(buf)?; + + let bad_req = BadRequest { + field_violations: bad_req + .field_violations + .into_iter() + .map(|v| FieldViolation { + field: v.field, + description: v.description, + }) + .collect(), + }; + + Ok(bad_req) + } +} + +#[cfg(test)] +mod tests { + + use super::super::super::{FromAny, IntoAny}; + use super::BadRequest; + + #[test] + fn gen_bad_request() { + let mut br_details = BadRequest::new(Vec::new()); + let formatted = format!("{:?}", br_details); + + println!("empty BadRequest -> {formatted}"); + + let expected = "BadRequest { field_violations: [] }"; + + assert!( + formatted.eq(expected), + "empty BadRequest differs from expected result" + ); + + assert!( + br_details.is_empty(), + "empty BadRequest returns 'false' from .is_empty()" + ); + + br_details + .add_violation("field_a", "description_a") + .add_violation("field_b", "description_b"); + + let formatted = format!("{:?}", br_details); + + println!("filled BadRequest -> {formatted}"); + + let expected_filled = "BadRequest { field_violations: [FieldViolation { field: \"field_a\", description: \"description_a\" }, FieldViolation { field: \"field_b\", description: \"description_b\" }] }"; + + assert!( + formatted.eq(expected_filled), + "filled BadRequest differs from expected result" + ); + + assert!( + br_details.is_empty() == false, + "filled BadRequest returns 'true' from .is_empty()" + ); + + let gen_any = br_details.into_any(); + let formatted = format!("{:?}", gen_any); + + println!("Any generated from BadRequest -> {formatted}"); + + let expected = "Any { type_url: \"type.googleapis.com/google.rpc.BadRequest\", value: [10, 24, 10, 7, 102, 105, 101, 108, 100, 95, 97, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 95, 97, 10, 24, 10, 7, 102, 105, 101, 108, 100, 95, 98, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 95, 98] }"; + + assert!( + formatted.eq(expected), + "Any from filled BadRequest differs from expected result" + ); + + let br_details = match BadRequest::from_any(gen_any) { + Err(error) => panic!("Error generating BadRequest from Any: {:?}", error), + Ok(from_any) => from_any, + }; + + let formatted = format!("{:?}", br_details); + + println!("BadRequest generated from Any -> {formatted}"); + + assert!( + formatted.eq(expected_filled), + "BadRequest from Any differs from expected result" + ); + } +} From 6091bb2c235110a308d320f08782f2e49b755273 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Sun, 28 Aug 2022 19:53:48 -0300 Subject: [PATCH 3/6] types: adjust code following suggestions Adjustments following suggestions by @LucioFranco in https://github.com/hyperium/tonic/pull/1068. Adjust style, remove unecessary prints, avoid glob imports, apply `non_exhaustive` to `ErrorDetails` and `ErrorDetail`, avoid pub fields in `ErrorDetails`, adjust `WithErrorDetails::with_error_details_vec` args, add `gen_details_bytes`. --- tonic-types/src/error_details.rs | 12 ++-- tonic-types/src/error_details_vec.rs | 3 +- tonic-types/src/lib.rs | 80 ++++++++------------- tonic-types/src/std_messages/bad_request.rs | 10 --- 4 files changed, 41 insertions(+), 64 deletions(-) diff --git a/tonic-types/src/error_details.rs b/tonic-types/src/error_details.rs index 44ff55b54..8ea2599d2 100644 --- a/tonic-types/src/error_details.rs +++ b/tonic-types/src/error_details.rs @@ -1,13 +1,14 @@ -use super::std_messages::*; +use super::std_messages::{BadRequest, FieldViolation}; /// Groups the standard error messages structs. Provides associated /// functions and methods to setup and edit each error message independently. /// Used when extracting error details from `tonic::Status`, and when /// creating a `tonic::Status` with error details. +#[non_exhaustive] #[derive(Clone, Debug)] pub struct ErrorDetails { /// This field stores [`BadRequest`] data, if any. - pub bad_request: Option, + pub(crate) bad_request: Option, } impl ErrorDetails { @@ -63,9 +64,12 @@ impl ErrorDetails { ..ErrorDetails::new() } } -} -impl ErrorDetails { + /// Get [`BadRequest`] details, if any + pub fn bad_request(&self) -> Option { + self.bad_request.clone() + } + /// Set [`BadRequest`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// # Examples diff --git a/tonic-types/src/error_details_vec.rs b/tonic-types/src/error_details_vec.rs index b9809a1bb..85ce3b44f 100644 --- a/tonic-types/src/error_details_vec.rs +++ b/tonic-types/src/error_details_vec.rs @@ -1,7 +1,8 @@ -use super::std_messages::*; +use super::std_messages::BadRequest; /// 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 [`BadRequest`] struct. diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index 4ffe0cdb4..55464e967 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -1,14 +1,12 @@ -/*! -A collection of useful protobuf types that can be used with `tonic`. - -This crate also introduces the [`WithErrorDetails`] trait and implements it in -[`tonic::Status`], allowing the implementation of the [gRPC Richer Error Model] -with [`tonic`] in a convenient way. - -[`tonic::Status`]: https://docs.rs/tonic/0.8.0/tonic/struct.Status.html -[`tonic`]: https://docs.rs/tonic/0.8.0/tonic/ -[gRPC Richer Error Model]: https://www.grpc.io/docs/guides/error/ -*/ +//! A collection of useful protobuf types that can be used with `tonic`. +//! +//! This crate also introduces the [`WithErrorDetails`] trait and implements it +//! in [`tonic::Status`], allowing the implementation of the +//! [gRPC Richer Error Model] with [`tonic`] in a convenient way. +//! +//! [`tonic::Status`]: https://docs.rs/tonic/0.8.0/tonic/struct.Status.html +//! [`tonic`]: https://docs.rs/tonic/0.8.0/tonic/ +//! [gRPC Richer Error Model]: https://www.grpc.io/docs/guides/error/ #![warn( missing_debug_implementations, @@ -38,11 +36,9 @@ mod error_details; mod error_details_vec; mod std_messages; -pub use std_messages::*; - pub use error_details::ErrorDetails; - pub use error_details_vec::ErrorDetail; +pub use std_messages::{BadRequest, FieldViolation}; trait IntoAny { fn into_any(self) -> Any; @@ -54,6 +50,16 @@ trait FromAny { 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 WithErrorDetails { @@ -72,7 +78,7 @@ pub trait WithErrorDetails { /// ); /// ``` fn with_error_details( - code: tonic::Code, + code: Code, message: impl Into, details: ErrorDetails, ) -> tonic::Status; @@ -94,9 +100,9 @@ pub trait WithErrorDetails { /// ); /// ``` fn with_error_details_vec( - code: tonic::Code, + code: Code, message: impl Into, - details: Vec, + details: impl IntoIterator, ) -> tonic::Status; /// Can be used to check if the error details contained in `tonic::Status` @@ -116,7 +122,7 @@ pub trait WithErrorDetails { /// Ok(_) => {}, /// Err(status) => { /// let err_details = status.get_error_details(); - /// if let Some(bad_request) = err_details.bad_request { + /// if let Some(bad_request) = err_details.bad_request() { /// // Handle bad_request details /// } /// // ... @@ -140,7 +146,7 @@ pub trait WithErrorDetails { /// Ok(_) => {}, /// Err(status) => { /// let err_details = status.get_error_details(); - /// if let Some(bad_request) = err_details.bad_request { + /// if let Some(bad_request) = err_details.bad_request() { /// // Handle bad_request details /// } /// // ... @@ -240,23 +246,19 @@ impl WithErrorDetails for tonic::Status { conv_details.push(bad_request.into_any()); } - let status = pb::Status { - code: code as i32, - message: message.clone(), - details: conv_details, - }; + let details = gen_details_bytes(code, &message, conv_details); - tonic::Status::with_details(code, message, Bytes::from(status.encode_to_vec())) + tonic::Status::with_details(code, message, details) } fn with_error_details_vec( code: Code, message: impl Into, - details: Vec, + details: impl IntoIterator, ) -> Self { let message: String = message.into(); - let mut conv_details: Vec = Vec::with_capacity(details.len()); + let mut conv_details: Vec = Vec::new(); for error_detail in details.into_iter() { match error_detail { @@ -266,13 +268,9 @@ impl WithErrorDetails for tonic::Status { } } - let status = pb::Status { - code: code as i32, - message: message.clone(), - details: conv_details, - }; + let details = gen_details_bytes(code, &message, conv_details); - tonic::Status::with_details(code, message, Bytes::from(status.encode_to_vec())) + tonic::Status::with_details(code, message, details) } fn check_error_details(&self) -> Result { @@ -348,34 +346,22 @@ mod tests { let fmt_details = format!("{:?}", err_details); - println!("{fmt_details}\n"); - let err_details_vec = vec![BadRequest::with_violation("field", "description").into()]; let fmt_details_vec = format!("{:?}", err_details_vec); - println!("{fmt_details_vec}\n"); - let status_from_struct = Status::with_error_details( Code::InvalidArgument, "error with bad request details", err_details, ); - let fmt_status_with_details = format!("{:?}", status_from_struct); - - println!("{:?}\n", fmt_status_with_details); - let status_from_vec = Status::with_error_details_vec( Code::InvalidArgument, "error with bad request details", err_details_vec, ); - let fmt_status_with_details_vec = format!("{:?}", status_from_vec); - - println!("{:?}\n", fmt_status_with_details_vec); - let ext_details = match status_from_vec.check_error_details() { Ok(ext_details) => ext_details, Err(err) => panic!( @@ -386,8 +372,6 @@ mod tests { let fmt_ext_details = format!("{:?}", ext_details); - println!("{fmt_ext_details}\n"); - assert!( fmt_ext_details.eq(&fmt_details), "Extracted details struct differs from original details struct" @@ -403,8 +387,6 @@ mod tests { let fmt_ext_details_vec = format!("{:?}", ext_details_vec); - println!("fmt_ext_details_vec: {:?}\n", fmt_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/std_messages/bad_request.rs index 864995896..aac7a4d56 100644 --- a/tonic-types/src/std_messages/bad_request.rs +++ b/tonic-types/src/std_messages/bad_request.rs @@ -57,9 +57,7 @@ impl BadRequest { }], } } -} -impl BadRequest { /// Adds a [`FieldViolation`] to [`BadRequest`]'s `field_violations`. pub fn add_violation( &mut self, @@ -131,8 +129,6 @@ mod tests { let mut br_details = BadRequest::new(Vec::new()); let formatted = format!("{:?}", br_details); - println!("empty BadRequest -> {formatted}"); - let expected = "BadRequest { field_violations: [] }"; assert!( @@ -151,8 +147,6 @@ mod tests { let formatted = format!("{:?}", br_details); - println!("filled BadRequest -> {formatted}"); - let expected_filled = "BadRequest { field_violations: [FieldViolation { field: \"field_a\", description: \"description_a\" }, FieldViolation { field: \"field_b\", description: \"description_b\" }] }"; assert!( @@ -168,8 +162,6 @@ mod tests { let gen_any = br_details.into_any(); let formatted = format!("{:?}", gen_any); - println!("Any generated from BadRequest -> {formatted}"); - let expected = "Any { type_url: \"type.googleapis.com/google.rpc.BadRequest\", value: [10, 24, 10, 7, 102, 105, 101, 108, 100, 95, 97, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 95, 97, 10, 24, 10, 7, 102, 105, 101, 108, 100, 95, 98, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 95, 98] }"; assert!( @@ -184,8 +176,6 @@ mod tests { let formatted = format!("{:?}", br_details); - println!("BadRequest generated from Any -> {formatted}"); - assert!( formatted.eq(expected_filled), "BadRequest from Any differs from expected result" From 67379ae568426e07888fe8a743d1f80f9cc576a4 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Sun, 28 Aug 2022 20:01:32 -0300 Subject: [PATCH 4/6] types: add generated protobuf code As suggested by @LucioFranco in https://github.com/hyperium/tonic/pull/1068#discussion_r956117520. This avoids the need for consumers to have `protoc` in their path. Implemented following changes in https://github.com/hyperium/tonic/pull/1065. --- tonic-types/Cargo.toml | 4 +- tonic-types/build.rs | 7 - tonic-types/src/generated/google.protobuf.rs | 0 tonic-types/src/generated/google.rpc.rs | 273 +++++++++++++++++++ tonic-types/src/lib.rs | 2 +- tonic-types/tests/bootstrap.rs | 28 ++ 6 files changed, 304 insertions(+), 10 deletions(-) delete mode 100644 tonic-types/build.rs create mode 100644 tonic-types/src/generated/google.protobuf.rs create mode 100644 tonic-types/src/generated/google.rpc.rs create mode 100644 tonic-types/tests/bootstrap.rs diff --git a/tonic-types/Cargo.toml b/tonic-types/Cargo.toml index 237db3d18..a66c20fe4 100644 --- a/tonic-types/Cargo.toml +++ b/tonic-types/Cargo.toml @@ -19,5 +19,5 @@ prost = "0.11" prost-types = "0.11" tonic = {version = "0.8", path = "../tonic"} -[build-dependencies] -prost-build = "0.11" +[dev-dependencies] +tonic-build = {version = "0.8", path = "../tonic-build", features = ["prost"]} \ No newline at end of file diff --git a/tonic-types/build.rs b/tonic-types/build.rs deleted file mode 100644 index 1c601479d..000000000 --- a/tonic-types/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() { - prost_build::compile_protos( - &["proto/status.proto", "proto/error_details.proto"], - &["proto/"], - ) - .unwrap(); -} diff --git a/tonic-types/src/generated/google.protobuf.rs b/tonic-types/src/generated/google.protobuf.rs new file mode 100644 index 000000000..e69de29bb diff --git a/tonic-types/src/generated/google.rpc.rs b/tonic-types/src/generated/google.rpc.rs new file mode 100644 index 000000000..081490b06 --- /dev/null +++ b/tonic-types/src/generated/google.rpc.rs @@ -0,0 +1,273 @@ +/// The `Status` type defines a logical error model that is suitable for +/// different programming environments, including REST APIs and RPC APIs. It is +/// used by \[gRPC\](). Each `Status` message contains +/// three pieces of data: error code, error message, and error details. +/// +/// You can find out more about this error model and how to work with it in the +/// [API Design Guide](). +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Status { + /// The status code, which should be an enum value of \[google.rpc.Code][google.rpc.Code\]. + #[prost(int32, tag="1")] + pub code: i32, + /// A developer-facing error message, which should be in English. Any + /// user-facing error message should be localized and sent in the + /// \[google.rpc.Status.details][google.rpc.Status.details\] field, or localized by the client. + #[prost(string, tag="2")] + pub message: ::prost::alloc::string::String, + /// A list of messages that carry the error details. There is a common set of + /// message types for APIs to use. + #[prost(message, repeated, tag="3")] + pub details: ::prost::alloc::vec::Vec<::prost_types::Any>, +} +/// Describes when the clients can retry a failed request. Clients could ignore +/// the recommendation here or retry when this information is missing from error +/// responses. +/// +/// It's always recommended that clients should use exponential backoff when +/// retrying. +/// +/// Clients should wait until `retry_delay` amount of time has passed since +/// receiving the error response before retrying. If retrying requests also +/// fail, clients should use an exponential backoff scheme to gradually increase +/// the delay between retries based on `retry_delay`, until either a maximum +/// number of retries have been reached or a maximum retry delay cap has been +/// reached. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RetryInfo { + /// Clients should wait at least this long between retrying the same request. + #[prost(message, optional, tag="1")] + pub retry_delay: ::core::option::Option<::prost_types::Duration>, +} +/// Describes additional debugging info. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DebugInfo { + /// The stack trace entries indicating where the error occurred. + #[prost(string, repeated, tag="1")] + pub stack_entries: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Additional debugging information provided by the server. + #[prost(string, tag="2")] + pub detail: ::prost::alloc::string::String, +} +/// Describes how a quota check failed. +/// +/// For example if a daily limit was exceeded for the calling project, +/// a service could respond with a QuotaFailure detail containing the project +/// id and the description of the quota limit that was exceeded. If the +/// calling project hasn't enabled the service in the developer console, then +/// a service could respond with the project id and set `service_disabled` +/// to true. +/// +/// Also see RetryInfo and Help types for other details about handling a +/// quota failure. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QuotaFailure { + /// Describes all quota violations. + #[prost(message, repeated, tag="1")] + pub violations: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `QuotaFailure`. +pub mod quota_failure { + /// A message type used to describe a single quota violation. For example, a + /// daily quota or a custom quota that was exceeded. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Violation { + /// The subject on which the quota check failed. + /// For example, "clientip:" or "project:". + #[prost(string, tag="1")] + pub subject: ::prost::alloc::string::String, + /// A description of how the quota check failed. Clients can use this + /// description to find more about the quota configuration in the service's + /// public documentation, or find the relevant quota limit to adjust through + /// developer console. + /// + /// For example: "Service disabled" or "Daily Limit for read operations + /// exceeded". + #[prost(string, tag="2")] + pub description: ::prost::alloc::string::String, + } +} +/// Describes the cause of the error with structured details. +/// +/// Example of an error when contacting the "pubsub.googleapis.com" API when it +/// is not enabled: +/// ```json +/// { "reason": "API_DISABLED" +/// "domain": "googleapis.com" +/// "metadata": { +/// "resource": "projects/123", +/// "service": "pubsub.googleapis.com" +/// } +/// } +/// ``` +/// This response indicates that the pubsub.googleapis.com API is not enabled. +/// +/// Example of an error that is returned when attempting to create a Spanner +/// instance in a region that is out of stock: +/// ```json +/// { "reason": "STOCKOUT" +/// "domain": "spanner.googleapis.com", +/// "metadata": { +/// "availableRegions": "us-central1,us-east2" +/// } +/// } +/// ``` +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ErrorInfo { + /// The reason of the error. This is a constant value that identifies the + /// proximate cause of the error. Error reasons are unique within a particular + /// domain of errors. This should be at most 63 characters and match + /// /\[A-Z0-9_\]+/. + #[prost(string, tag="1")] + pub reason: ::prost::alloc::string::String, + /// The logical grouping to which the "reason" belongs. The error domain + /// is typically the registered service name of the tool or product that + /// generates the error. Example: "pubsub.googleapis.com". If the error is + /// generated by some common infrastructure, the error domain must be a + /// globally unique value that identifies the infrastructure. For Google API + /// infrastructure, the error domain is "googleapis.com". + #[prost(string, tag="2")] + pub domain: ::prost::alloc::string::String, + /// Additional structured details about this error. + /// + /// Keys should match /\[a-zA-Z0-9-_\]/ and be limited to 64 characters in + /// length. When identifying the current value of an exceeded limit, the units + /// should be contained in the key, not the value. For example, rather than + /// {"instanceLimit": "100/request"}, should be returned as, + /// {"instanceLimitPerRequest": "100"}, if the client exceeds the number of + /// instances that can be created in a single (batch) request. + #[prost(map="string, string", tag="3")] + pub metadata: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, +} +/// Describes what preconditions have failed. +/// +/// For example, if an RPC failed because it required the Terms of Service to be +/// acknowledged, it could list the terms of service violation in the +/// PreconditionFailure message. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PreconditionFailure { + /// Describes all precondition violations. + #[prost(message, repeated, tag="1")] + pub violations: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `PreconditionFailure`. +pub mod precondition_failure { + /// A message type used to describe a single precondition failure. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Violation { + /// The type of PreconditionFailure. We recommend using a service-specific + /// enum type to define the supported precondition violation subjects. For + /// example, "TOS" for "Terms of Service violation". + #[prost(string, tag="1")] + pub r#type: ::prost::alloc::string::String, + /// The subject, relative to the type, that failed. + /// For example, "google.com/cloud" relative to the "TOS" type would indicate + /// which terms of service is being referenced. + #[prost(string, tag="2")] + pub subject: ::prost::alloc::string::String, + /// A description of how the precondition failed. Developers can use this + /// description to understand how to fix the failure. + /// + /// For example: "Terms of service not accepted". + #[prost(string, tag="3")] + pub description: ::prost::alloc::string::String, + } +} +/// Describes violations in a client request. This error type focuses on the +/// syntactic aspects of the request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BadRequest { + /// Describes all violations in a client request. + #[prost(message, repeated, tag="1")] + pub field_violations: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `BadRequest`. +pub mod bad_request { + /// A message type used to describe a single bad request field. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct FieldViolation { + /// A path leading to a field in the request body. The value will be a + /// sequence of dot-separated identifiers that identify a protocol buffer + /// field. E.g., "field_violations.field" would identify this field. + #[prost(string, tag="1")] + pub field: ::prost::alloc::string::String, + /// A description of why the request element is bad. + #[prost(string, tag="2")] + pub description: ::prost::alloc::string::String, + } +} +/// Contains metadata about the request that clients can attach when filing a bug +/// or providing other forms of feedback. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RequestInfo { + /// An opaque string that should only be interpreted by the service generating + /// it. For example, it can be used to identify requests in the service's logs. + #[prost(string, tag="1")] + pub request_id: ::prost::alloc::string::String, + /// Any data that was used to serve this request. For example, an encrypted + /// stack trace that can be sent back to the service provider for debugging. + #[prost(string, tag="2")] + pub serving_data: ::prost::alloc::string::String, +} +/// Describes the resource that is being accessed. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResourceInfo { + /// A name for the type of resource being accessed, e.g. "sql table", + /// "cloud storage bucket", "file", "Google calendar"; or the type URL + /// of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". + #[prost(string, tag="1")] + pub resource_type: ::prost::alloc::string::String, + /// The name of the resource being accessed. For example, a shared calendar + /// name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current + /// error is \[google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED\]. + #[prost(string, tag="2")] + pub resource_name: ::prost::alloc::string::String, + /// The owner of the resource (optional). + /// For example, "user:" or "project:". + #[prost(string, tag="3")] + pub owner: ::prost::alloc::string::String, + /// Describes what error is encountered when accessing this resource. + /// For example, updating a cloud project may require the `writer` permission + /// on the developer console project. + #[prost(string, tag="4")] + pub description: ::prost::alloc::string::String, +} +/// Provides links to documentation or for performing an out of band action. +/// +/// For example, if a quota check failed with an error indicating the calling +/// project hasn't enabled the accessed service, this can contain a URL pointing +/// directly to the right place in the developer console to flip the bit. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Help { + /// URL(s) pointing to additional information on handling the current error. + #[prost(message, repeated, tag="1")] + pub links: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `Help`. +pub mod help { + /// Describes a URL link. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Link { + /// Describes what the link offers. + #[prost(string, tag="1")] + pub description: ::prost::alloc::string::String, + /// The URL of the link. + #[prost(string, tag="2")] + pub url: ::prost::alloc::string::String, + } +} +/// Provides a localized error message that is safe to return to the user +/// which can be attached to an RPC error. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LocalizedMessage { + /// The locale used following the specification defined at + /// + /// Examples are: "en-US", "fr-CH", "es-MX" + #[prost(string, tag="1")] + pub locale: ::prost::alloc::string::String, + /// The localized error message in the above locale. + #[prost(string, tag="2")] + pub message: ::prost::alloc::string::String, +} diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index 55464e967..ee23d66dd 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -27,7 +27,7 @@ use tonic::{codegen::Bytes, Code}; /// Useful protobuf types pub mod pb { - include!(concat!(env!("OUT_DIR"), "/google.rpc.rs")); + include!("generated/google.rpc.rs"); } pub use pb::Status; diff --git a/tonic-types/tests/bootstrap.rs b/tonic-types/tests/bootstrap.rs new file mode 100644 index 000000000..1acec2894 --- /dev/null +++ b/tonic-types/tests/bootstrap.rs @@ -0,0 +1,28 @@ +use std::{path::PathBuf, process::Command}; + +#[test] +fn bootstrap() { + let iface_files = &["proto/status.proto", "proto/error_details.proto"]; + let dirs = &["proto"]; + + let out_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("generated"); + + tonic_build::configure() + .out_dir(format!("{}", out_dir.display())) + .compile(iface_files, dirs) + .unwrap(); + + let status = Command::new("git") + .arg("diff") + .arg("--exit-code") + .arg("--") + .arg(format!("{}", out_dir.display())) + .status() + .unwrap(); + + if !status.success() { + panic!("You should commit the protobuf files"); + } +} From c10ded7b2c9bbf2d2a5e4d977a4466091c48128e Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Sun, 28 Aug 2022 20:14:09 -0300 Subject: [PATCH 5/6] types: add custom metadata support This allows consumers to provide custom metadata when creating a `Status` with error details. --- tonic-types/src/lib.rs | 79 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index ee23d66dd..eebae327a 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -23,7 +23,7 @@ use prost::{DecodeError, Message}; use prost_types::Any; -use tonic::{codegen::Bytes, Code}; +use tonic::{codegen::Bytes, metadata::MetadataMap, Code}; /// Useful protobuf types pub mod pb { @@ -63,6 +63,28 @@ fn gen_details_bytes(code: Code, message: &String, details: Vec) -> Bytes { /// Used to implement associated functions and methods on `tonic::Status`, that /// allow the addition and extraction of standard error details. pub trait WithErrorDetails { + /// 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, WithErrorDetails}; + /// + /// 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 @@ -83,6 +105,30 @@ pub trait WithErrorDetails { 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, WithErrorDetails}; + /// + /// 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 @@ -237,7 +283,12 @@ pub trait WithErrorDetails { } impl WithErrorDetails for tonic::Status { - fn with_error_details(code: Code, message: impl Into, details: ErrorDetails) -> Self { + 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); @@ -248,13 +299,18 @@ impl WithErrorDetails for tonic::Status { let details = gen_details_bytes(code, &message, conv_details); - tonic::Status::with_details(code, message, details) + tonic::Status::with_details_and_metadata(code, message, details, metadata) } - fn with_error_details_vec( + 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(); @@ -270,7 +326,20 @@ impl WithErrorDetails for tonic::Status { let details = gen_details_bytes(code, &message, conv_details); - tonic::Status::with_details(code, message, 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 { From 9f7ecfb152d2b48ad3d7cfaf0a286f9918c2d302 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Mon, 12 Sep 2022 16:56:20 -0300 Subject: [PATCH 6/6] types: adjust code following suggestions Adjustments following suggestions by @LucioFranco in https://github.com/hyperium/tonic/pull/1068. Move `error_details_vec` mod into `error_details` mod, adjust doc comments, rename `WithErrorDetails` trait to `StatusExt`. --- tonic-types/src/error_details.rs | 8 +++ .../vec.rs} | 2 +- tonic-types/src/lib.rs | 53 +++++++++++-------- tonic-types/src/std_messages/bad_request.rs | 1 - 4 files changed, 39 insertions(+), 25 deletions(-) rename tonic-types/src/{error_details_vec.rs => error_details/vec.rs} (90%) diff --git a/tonic-types/src/error_details.rs b/tonic-types/src/error_details.rs index 8ea2599d2..5ff6e0444 100644 --- a/tonic-types/src/error_details.rs +++ b/tonic-types/src/error_details.rs @@ -1,5 +1,7 @@ use super::std_messages::{BadRequest, FieldViolation}; +pub(crate) mod vec; + /// Groups the standard error messages structs. Provides associated /// functions and methods to setup and edit each error message independently. /// Used when extracting error details from `tonic::Status`, and when @@ -13,6 +15,7 @@ pub struct ErrorDetails { impl ErrorDetails { /// Generates an [`ErrorDetails`] struct with all fields set to `None`. + /// /// # Examples /// /// ``` @@ -26,6 +29,7 @@ impl ErrorDetails { /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details and /// remaining fields set to `None`. + /// /// # Examples /// /// ``` @@ -45,6 +49,7 @@ impl ErrorDetails { /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details (one /// [`FieldViolation`] set) and remaining fields set to `None`. + /// /// # Examples /// /// ``` @@ -72,6 +77,7 @@ impl ErrorDetails { /// Set [`BadRequest`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. + /// /// # Examples /// /// ``` @@ -92,6 +98,7 @@ impl ErrorDetails { /// Adds a [`FieldViolation`] to [`BadRequest`] details. Sets /// [`BadRequest`] details if it is not set yet. Can be chained with other /// `.set_` and `.add_` [`ErrorDetails`] methods. + /// /// # Examples /// /// ``` @@ -119,6 +126,7 @@ impl ErrorDetails { /// Returns `true` if [`BadRequest`] is set and its `field_violations` /// vector is not empty, otherwise returns `false`. + /// /// # Examples /// /// ``` diff --git a/tonic-types/src/error_details_vec.rs b/tonic-types/src/error_details/vec.rs similarity index 90% rename from tonic-types/src/error_details_vec.rs rename to tonic-types/src/error_details/vec.rs index 85ce3b44f..64128b9a1 100644 --- a/tonic-types/src/error_details_vec.rs +++ b/tonic-types/src/error_details/vec.rs @@ -1,4 +1,4 @@ -use super::std_messages::BadRequest; +use super::super::std_messages::BadRequest; /// Wraps the structs corresponding to the standard error messages, allowing /// the implementation and handling of vectors containing any of them. diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index eebae327a..fef58631b 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -1,11 +1,11 @@ //! A collection of useful protobuf types that can be used with `tonic`. //! -//! This crate also introduces the [`WithErrorDetails`] trait and implements it -//! in [`tonic::Status`], allowing the implementation of the +//! This crate also introduces the [`StatusExt`] trait and implements it in +//! [`tonic::Status`], allowing the implementation of the //! [gRPC Richer Error Model] with [`tonic`] in a convenient way. //! -//! [`tonic::Status`]: https://docs.rs/tonic/0.8.0/tonic/struct.Status.html -//! [`tonic`]: https://docs.rs/tonic/0.8.0/tonic/ +//! [`tonic::Status`]: https://docs.rs/tonic/latest/tonic/struct.Status.html +//! [`tonic`]: https://docs.rs/tonic/latest/tonic/ //! [gRPC Richer Error Model]: https://www.grpc.io/docs/guides/error/ #![warn( @@ -33,11 +33,9 @@ pub mod pb { pub use pb::Status; mod error_details; -mod error_details_vec; mod std_messages; -pub use error_details::ErrorDetails; -pub use error_details_vec::ErrorDetail; +pub use error_details::{vec::ErrorDetail, ErrorDetails}; pub use std_messages::{BadRequest, FieldViolation}; trait IntoAny { @@ -62,14 +60,15 @@ fn gen_details_bytes(code: Code, message: &String, details: Vec) -> Bytes { /// Used to implement associated functions and methods on `tonic::Status`, that /// allow the addition and extraction of standard error details. -pub trait WithErrorDetails { +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, WithErrorDetails}; + /// use tonic_types::{ErrorDetails, StatusExt}; /// /// let status = Status::with_error_details_and_metadata( /// Code::InvalidArgument, @@ -87,11 +86,12 @@ pub trait WithErrorDetails { /// Generates a `tonic::Status` with error details obtained from an /// [`ErrorDetails`] struct. + /// /// # Examples /// /// ``` /// use tonic::{Code, Status}; - /// use tonic_types::{ErrorDetails, WithErrorDetails}; + /// use tonic_types::{ErrorDetails, StatusExt}; /// /// let status = Status::with_error_details( /// Code::InvalidArgument, @@ -107,11 +107,12 @@ pub trait WithErrorDetails { /// 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, WithErrorDetails}; + /// use tonic_types::{BadRequest, StatusExt}; /// /// let status = Status::with_error_details_vec_and_metadata( /// Code::InvalidArgument, @@ -131,11 +132,12 @@ pub trait WithErrorDetails { /// Generates a `tonic::Status` with error details provided in a vector of /// [`ErrorDetail`] enums. + /// /// # Examples /// /// ``` /// use tonic::{Code, Status}; - /// use tonic_types::{BadRequest, WithErrorDetails}; + /// use tonic_types::{BadRequest, StatusExt}; /// /// let status = Status::with_error_details_vec( /// Code::InvalidArgument, @@ -155,13 +157,14 @@ pub trait WithErrorDetails { /// 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 - /// [`WithErrorDetails::get_error_details`] or - /// [`WithErrorDetails::get_error_details_vec`]. + /// [`StatusExt::get_error_details`] or + /// [`StatusExt::get_error_details_vec`]. + /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; - /// use tonic_types::{WithErrorDetails}; + /// use tonic_types::{StatusExt}; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { @@ -181,11 +184,12 @@ pub trait WithErrorDetails { /// 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::{WithErrorDetails}; + /// use tonic_types::{StatusExt}; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { @@ -206,13 +210,14 @@ pub trait WithErrorDetails { /// 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 - /// [`WithErrorDetails::get_error_details_vec`] or - /// [`WithErrorDetails::get_error_details`]. + /// [`StatusExt::get_error_details_vec`] or + /// [`StatusExt::get_error_details`]. + /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; - /// use tonic_types::{ErrorDetail, WithErrorDetails}; + /// use tonic_types::{ErrorDetail, StatusExt}; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { @@ -234,11 +239,12 @@ pub trait WithErrorDetails { /// 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, WithErrorDetails}; + /// use tonic_types::{ErrorDetail, StatusExt}; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { @@ -262,11 +268,12 @@ pub trait WithErrorDetails { /// 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::{WithErrorDetails}; + /// use tonic_types::{StatusExt}; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { @@ -282,7 +289,7 @@ pub trait WithErrorDetails { fn get_details_bad_request(&self) -> Option; } -impl WithErrorDetails for tonic::Status { +impl StatusExt for tonic::Status { fn with_error_details_and_metadata( code: Code, message: impl Into, @@ -405,7 +412,7 @@ impl WithErrorDetails for tonic::Status { mod tests { use tonic::{Code, Status}; - use super::{BadRequest, ErrorDetails, WithErrorDetails}; + use super::{BadRequest, ErrorDetails, StatusExt}; #[test] fn gen_status_with_details() { diff --git a/tonic-types/src/std_messages/bad_request.rs b/tonic-types/src/std_messages/bad_request.rs index aac7a4d56..f923b02d8 100644 --- a/tonic-types/src/std_messages/bad_request.rs +++ b/tonic-types/src/std_messages/bad_request.rs @@ -120,7 +120,6 @@ impl FromAny for BadRequest { #[cfg(test)] mod tests { - use super::super::super::{FromAny, IntoAny}; use super::BadRequest;