appbiotic_code_error/
lib.rs

1//! # appbiotic-code-error
2//!
3//! Appbiotic Code Error is a set of types to make it easier assembling
4//! services and applications with similar error reporting to the end-user.
5//! It is not necessarily meant for lower level libraries such as adding
6//! derived traits for serialization, or database libraries where
7//! specifically-typed error handling is required.
8//!
9//! This component's Rust-based API is original; however, the error codes and
10//! descriptions are copied directly from the
11//! https://github.com/googleapis/googleapis project.
12
13use std::fmt;
14
15use strum_macros::IntoStaticStr;
16
17pub mod code {
18    pub const OK: i32 = 0;
19    pub const CANCELLED: i32 = 1;
20    pub const UNKNOWN: i32 = 2;
21    pub const INVALID_ARGUMENT: i32 = 3;
22    pub const DEADLINE_EXCEEDED: i32 = 4;
23    pub const NOT_FOUND: i32 = 5;
24    pub const ALREADY_EXISTS: i32 = 6;
25    pub const PERMISSION_DENIED: i32 = 7;
26    pub const UNAUTHENTICATED: i32 = 16;
27    pub const RESOURCE_EXHAUSTED: i32 = 8;
28    pub const FAILED_PRECONDITION: i32 = 9;
29    pub const ABORTED: i32 = 10;
30    pub const OUT_OF_RANGE: i32 = 11;
31    pub const UNIMPLEMENTED: i32 = 12;
32    pub const INTERNAL: i32 = 13;
33    pub const UNAVAILABLE: i32 = 14;
34    pub const DATA_LOSS: i32 = 15;
35}
36
37// TODO: Find or create library for format and flow markdown comments.
38
39pub type Result<T> = std::result::Result<T, Error>;
40
41#[derive(Clone, Debug, thiserror::Error, IntoStaticStr)]
42#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
43pub enum Error {
44    /// The operation was cancelled, typically by the caller.
45    ///
46    /// | Mapping | Code | Description                                         |
47    /// | :------ | ---: | :-------------------------------------------------- |
48    /// | HTTP    |  499 | Client Closed Request                               |
49    /// | gRPC    |    1 | Cancelled                                           |
50    Cancelled(ErrorStatus),
51
52    /// Unknown error. For example, this error may be returned when a [`ErrorStatus`]
53    /// value received from another address space belongs to an error space
54    /// that is not known in this address space. Also errors raised by APIs
55    /// that do not return enough error information may be converted to this
56    /// error.
57    ///
58    /// | Mapping | Code | Description                                         |
59    /// | :------ | ---: | :-------------------------------------------------- |
60    /// | HTTP    |  500 | Internal Server Error                               |
61    /// | gRPC    |    2 | Unknown                                             |
62    Unknown(ErrorStatus),
63
64    /// The client specified an invalid argument.  Note that this differs
65    /// from [`Error::FailedPrecondition`].  [`Error::InvalidArgument`] indicates arguments
66    /// that are problematic regardless of the state of the system
67    /// (e.g., a malformed file name).
68    ///
69    /// | Mapping | Code | Description                                         |
70    /// | :------ | ---: | :-------------------------------------------------- |
71    /// | HTTP    |  400 | Bad Request                                         |
72    /// | gRPC    |    3 | Invalid argument                                    |
73    InvalidArgument(ErrorStatus),
74
75    /// The deadline expired before the operation could complete. For operations
76    /// that change the state of the system, this error may be returned
77    /// even if the operation has completed successfully.  For example, a
78    /// successful response from a server could have been delayed long
79    /// enough for the deadline to expire.
80    ///
81    /// | Mapping | Code | Description                                         |
82    /// | :------ | ---: | :-------------------------------------------------- |
83    /// | HTTP    |  504 | Gateway Timeout                                     |
84    /// | gRPC    |    4 | Deadline exceeded                                   |
85    DeadlineExceeded(ErrorStatus),
86
87    /// Some requested entity (e.g., file or directory) was not found.
88    ///
89    /// Note to server developers: if a request is denied for an entire class
90    /// of users, such as gradual feature rollout or undocumented allowlist,
91    /// [`Error::NotFound`] may be used. If a request is denied for some users
92    /// within a class of users, such as user-based access control,
93    /// [`Error::PermissionDenied`] must be used.
94    ///
95    /// | Mapping | Code | Description                                         |
96    /// | :------ | ---: | :-------------------------------------------------- |
97    /// | HTTP    |  404 | Not Found                                           |
98    /// | gRPC    |    5 | Not found                                           |
99    NotFound(ErrorStatus),
100
101    /// The entity that a client attempted to create (e.g., file or directory)
102    /// already exists.
103    ///
104    /// | Mapping | Code | Description                                         |
105    /// | :------ | ---: | :-------------------------------------------------- |
106    /// | HTTP    |  409 | Conflict                                            |
107    /// | gRPC    |    6 | Already exists                                      |
108    AlreadyExists(ErrorStatus),
109
110    /// The caller does not have permission to execute the specified
111    /// operation. [`Error::PermissionDenied`] must not be used for rejections
112    /// caused by exhausting some resource (use [`Error::ResourceExhausted`]
113    /// instead for those errors). [`Error::PermissionDenied`] must not be
114    /// used if the caller can not be identified (use [`Error::Unauthenticated`]
115    /// instead for those errors). This error code does not imply the
116    /// request is valid or the requested entity exists or satisfies
117    /// other pre-conditions.
118    ///
119    /// | Mapping | Code | Description                                         |
120    /// | :------ | ---: | :-------------------------------------------------- |
121    /// | HTTP    |  403 | Forbidden                                           |
122    /// | gRPC    |    7 | Permission denied                                   |
123    PermissionDenied(ErrorStatus),
124
125    /// The request does not have valid authentication credentials for the
126    /// operation.
127    ///
128    /// | Mapping | Code | Description                                         |
129    /// | :------ | ---: | :-------------------------------------------------- |
130    /// | HTTP    |  401 | Unauthorized                                        |
131    /// | gRPC    |   16 | Permission denied                                   |
132    Unauthenticated(ErrorStatus),
133
134    /// Some resource has been exhausted, perhaps a per-user quota, or
135    /// perhaps the entire file system is out of space.
136    ///
137    /// | Mapping | Code | Description                                         |
138    /// | :------ | ---: | :-------------------------------------------------- |
139    /// | HTTP    |  429 | Too Many Requests                                   |
140    /// | gRPC    |    8 | Permission denied                                   |
141    ResourceExhausted(ErrorStatus),
142
143    /// The operation was rejected because the system is not in a state
144    /// required for the operation's execution.  For example, the directory
145    /// to be deleted is non-empty, an rmdir operation is applied to
146    /// a non-directory, etc.
147    ///
148    /// Service implementors can use the following guidelines to decide
149    /// between [`Error::FailedPrecondition`], [`Error::Aborted`], and
150    /// [`Error::Unavailable`]:
151    ///
152    ///  - Use [`Error::Unavailable`] if the client can retry just the failing
153    ///    call.
154    ///  - Use [`Error::Aborted`] if the client should retry at a higher level.
155    ///    For example, when a client-specified test-and-set fails, indicating
156    ///    the client should restart a read-modify-write sequence.
157    ///  - Use [`Error::FailedPrecondition`] if the client should not retry
158    ///    until the system state has been explicitly fixed. For example, if an
159    ///    "rmdir" fails because the directory is non-empty,
160    ///    [`Error::FailedPrecondition`] should be returned since the client
161    ///    should not retry unless the files are deleted from the directory.
162    ///
163    /// | Mapping | Code | Description                                         |
164    /// | :------ | ---: | :-------------------------------------------------- |
165    /// | HTTP    |  400 | Bad Request                                         |
166    /// | gRPC    |    9 | Failed precondition                                 |
167    FailedPrecondition(ErrorStatus),
168
169    /// The operation was aborted, typically due to a concurrency issue such as
170    /// a sequencer check failure or transaction abort.
171    ///
172    /// See the guidelines above for deciding between
173    /// [`Error::FailedPrecondition`], [`Error::Aborted`], and
174    /// [`Error::Unavailable`].
175    ///
176    /// | Mapping | Code | Description                                         |
177    /// | :------ | ---: | :-------------------------------------------------- |
178    /// | HTTP    |  409 | Conflict                                            |
179    /// | gRPC    |   10 | Aborted                                             |
180    Aborted(ErrorStatus),
181
182    /// The operation was attempted past the valid range.  E.g., seeking or
183    /// reading past end-of-file.
184    ///
185    /// Unlike [`Error::InvalidArgument`], this error indicates a problem that
186    /// may be fixed if the system state changes. For example, a 32-bit file
187    /// system will generate [`Error::InvalidArgument`] if asked to read at an
188    /// offset that is not in the range [0,2^32-1], but it will generate
189    /// [`Error::OutOfRange`] if asked to read from an offset past the current
190    /// file size.
191    ///
192    /// There is a fair bit of overlap between [`Error::FailedPrecondition`] and
193    /// [`Error::OutOfRange`].  We recommend using [`Error::OutOfRange`] (the
194    /// more specific error) when it applies so that callers who are iterating
195    /// through a space can easily look for an [`Error::OutOfRange`] error to
196    /// detect when they are done.
197    ///
198    /// | Mapping | Code | Description                                         |
199    /// | :------ | ---: | :-------------------------------------------------- |
200    /// | HTTP    |  400 | Bad Request                                         |
201    /// | gRPC    |   11 | Out of range                                        |
202    OutOfRange(ErrorStatus),
203
204    /// The operation is not implemented or is not supported/enabled in this
205    /// service.
206    ///
207    /// | Mapping | Code | Description                                         |
208    /// | :------ | ---: | :-------------------------------------------------- |
209    /// | HTTP    |  501 | Not implemented                                     |
210    /// | gRPC    |   12 | Unimplemented                                       |
211    Unimplemented(ErrorStatus),
212
213    /// Internal errors.  This means that some invariants expected by the
214    /// underlying system have been broken.  This error code is reserved for
215    /// serious errors.
216    ///
217    /// | Mapping | Code | Description                                         |
218    /// | :------ | ---: | :-------------------------------------------------- |
219    /// | HTTP    |  500 | Internal Server Error                               |
220    /// | gRPC    |   13 | Internal                                            |
221    Internal(ErrorStatus),
222
223    /// The service is currently unavailable.  This is most likely a transient
224    /// condition, which can be corrected by retrying with
225    /// a backoff. Note that it is not always safe to retry
226    /// non-idempotent operations.
227    ///
228    /// See the guidelines above for deciding between
229    /// [`Error::FailedPrecondition`], [`Error::Aborted`], and
230    /// [`Error::Unavailable`].
231    ///
232    /// | Mapping | Code | Description                                         |
233    /// | :------ | ---: | :-------------------------------------------------- |
234    /// | HTTP    |  503 | Service Unavailable                                 |
235    /// | gRPC    |   14 | Unavailable                                         |
236    Unavailable(ErrorStatus),
237
238    /// Unrecoverable data loss or corruption.
239    ///
240    /// | Mapping | Code | Description                                         |
241    /// | :------ | ---: | :-------------------------------------------------- |
242    /// | HTTP    |  500 | Internal Server Error                               |
243    /// | gRPC    |   15 | Data loss                                           |
244    DataLoss(ErrorStatus),
245}
246
247// TODO: Replace strum with a more detailed display implementation.
248impl fmt::Display for Error {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        f.write_str(self.into())
251    }
252}
253
254impl Error {
255    pub fn inner(&self) -> &ErrorStatus {
256        match self {
257            Error::Internal(status) => status,
258            Error::Unknown(status) => status,
259            Error::Cancelled(status) => status,
260            Error::InvalidArgument(status) => status,
261            Error::DeadlineExceeded(status) => status,
262            Error::NotFound(status) => status,
263            Error::AlreadyExists(status) => status,
264            Error::PermissionDenied(status) => status,
265            Error::Unauthenticated(status) => status,
266            Error::ResourceExhausted(status) => status,
267            Error::FailedPrecondition(status) => status,
268            Error::Aborted(status) => status,
269            Error::OutOfRange(status) => status,
270            Error::Unimplemented(status) => status,
271            Error::Unavailable(status) => status,
272            Error::DataLoss(status) => status,
273        }
274    }
275}
276
277impl From<Error> for ErrorStatus {
278    fn from(value: Error) -> Self {
279        match value {
280            Error::Internal(status) => status,
281            Error::Unknown(status) => status,
282            Error::Cancelled(status) => status,
283            Error::InvalidArgument(status) => status,
284            Error::DeadlineExceeded(status) => status,
285            Error::NotFound(status) => status,
286            Error::AlreadyExists(status) => status,
287            Error::PermissionDenied(status) => status,
288            Error::Unauthenticated(status) => status,
289            Error::ResourceExhausted(status) => status,
290            Error::FailedPrecondition(status) => status,
291            Error::Aborted(status) => status,
292            Error::OutOfRange(status) => status,
293            Error::Unimplemented(status) => status,
294            Error::Unavailable(status) => status,
295            Error::DataLoss(status) => status,
296        }
297    }
298}
299
300impl Error {
301    /// Returns the gRPC code value.
302    ///
303    /// See https://github.com/googleapis/googleapis/blob/f36c65081b19e0758ef5696feca27c7dcee5475e/google/rpc/code.proto.
304    pub fn code(&self) -> i32 {
305        match self {
306            Error::Cancelled(_) => code::CANCELLED,
307            Error::Unknown(_) => code::UNKNOWN,
308            Error::InvalidArgument(_) => code::INVALID_ARGUMENT,
309            Error::DeadlineExceeded(_) => code::DEADLINE_EXCEEDED,
310            Error::NotFound(_) => code::NOT_FOUND,
311            Error::AlreadyExists(_) => code::ALREADY_EXISTS,
312            Error::PermissionDenied(_) => code::PERMISSION_DENIED,
313            Error::Unauthenticated(_) => code::UNAUTHENTICATED,
314            Error::ResourceExhausted(_) => code::RESOURCE_EXHAUSTED,
315            Error::FailedPrecondition(_) => code::FAILED_PRECONDITION,
316            Error::Aborted(_) => code::ABORTED,
317            Error::OutOfRange(_) => code::OUT_OF_RANGE,
318            Error::Unimplemented(_) => code::UNIMPLEMENTED,
319            Error::Internal(_) => code::INTERNAL,
320            Error::Unavailable(_) => code::UNAVAILABLE,
321            Error::DataLoss(_) => code::DATA_LOSS,
322        }
323    }
324
325    // TODO: Build macros to automate building of the error helper functions.
326
327    pub fn cancelled<S: AsRef<str>>(message: S) -> Error {
328        Error::Cancelled(ErrorStatus::default().with_message(message))
329    }
330
331    pub fn unknown<S: AsRef<str>>(message: S) -> Error {
332        Error::Unknown(ErrorStatus::default().with_message(message))
333    }
334
335    pub fn invalid_argument<S: AsRef<str>>(message: S) -> Error {
336        Error::InvalidArgument(ErrorStatus::default().with_message(message))
337    }
338
339    pub fn deadline_exceeded<S: AsRef<str>>(message: S) -> Error {
340        Error::DeadlineExceeded(ErrorStatus::default().with_message(message))
341    }
342
343    pub fn not_found<S: AsRef<str>>(message: S) -> Error {
344        Error::NotFound(ErrorStatus::default().with_message(message))
345    }
346
347    pub fn already_exists<S: AsRef<str>>(message: S) -> Error {
348        Error::AlreadyExists(ErrorStatus::default().with_message(message))
349    }
350
351    pub fn permission_denied<S: AsRef<str>>(message: S) -> Error {
352        Error::PermissionDenied(ErrorStatus::default().with_message(message))
353    }
354
355    pub fn unauthenticated<S: AsRef<str>>(message: S) -> Error {
356        Error::Unauthenticated(ErrorStatus::default().with_message(message))
357    }
358
359    pub fn resource_exhausted<S: AsRef<str>>(message: S) -> Error {
360        Error::ResourceExhausted(ErrorStatus::default().with_message(message))
361    }
362
363    pub fn failed_precondition<S: AsRef<str>>(message: S) -> Error {
364        Error::FailedPrecondition(ErrorStatus::default().with_message(message))
365    }
366
367    pub fn aborted<S: AsRef<str>>(message: S) -> Error {
368        Error::Aborted(ErrorStatus::default().with_message(message))
369    }
370
371    pub fn out_of_range<S: AsRef<str>>(message: S) -> Error {
372        Error::OutOfRange(ErrorStatus::default().with_message(message))
373    }
374
375    pub fn unimplemented<S: AsRef<str>>(message: S) -> Error {
376        Error::Unimplemented(ErrorStatus::default().with_message(message))
377    }
378
379    pub fn internal<S: AsRef<str>>(message: S) -> Error {
380        Error::Internal(ErrorStatus::default().with_message(message))
381    }
382
383    pub fn unavailable<S: AsRef<str>>(message: S) -> Error {
384        Error::Unavailable(ErrorStatus::default().with_message(message))
385    }
386
387    pub fn data_loss<S: AsRef<str>>(message: S) -> Error {
388        Error::DataLoss(ErrorStatus::default().with_message(message))
389    }
390
391    /// Appends a `ErrorDetails::DebugInfo` with info from `error`.
392    pub fn with_error<E: fmt::Display>(self, error: E) -> Error {
393        match self {
394            Error::Cancelled(details) => Error::Cancelled(details.with_error(error)),
395            Error::Unknown(details) => Error::Unknown(details.with_error(error)),
396            Error::InvalidArgument(details) => Error::InvalidArgument(details.with_error(error)),
397            Error::DeadlineExceeded(details) => Error::DeadlineExceeded(details.with_error(error)),
398            Error::NotFound(details) => Error::NotFound(details.with_error(error)),
399            Error::AlreadyExists(details) => Error::AlreadyExists(details.with_error(error)),
400            Error::PermissionDenied(details) => Error::PermissionDenied(details.with_error(error)),
401            Error::Unauthenticated(details) => Error::Unauthenticated(details.with_error(error)),
402            Error::ResourceExhausted(details) => {
403                Error::ResourceExhausted(details.with_error(error))
404            }
405            Error::FailedPrecondition(details) => {
406                Error::FailedPrecondition(details.with_error(error))
407            }
408            Error::Aborted(details) => Error::Aborted(details.with_error(error)),
409            Error::OutOfRange(details) => Error::OutOfRange(details.with_error(error)),
410            Error::Unimplemented(details) => Error::Unimplemented(details.with_error(error)),
411            Error::Internal(details) => Error::Internal(details.with_error(error)),
412            Error::Unavailable(details) => Error::Unavailable(details.with_error(error)),
413            Error::DataLoss(details) => Error::DataLoss(details.with_error(error)),
414        }
415    }
416}
417
418#[cfg(feature = "with-http")]
419impl From<Error> for http::StatusCode {
420    fn from(value: Error) -> Self {
421        match value {
422            Error::Cancelled(_) => {
423                http::StatusCode::from_u16(499).unwrap_or(http::StatusCode::IM_A_TEAPOT)
424            }
425            Error::Unknown(_) => http::StatusCode::INTERNAL_SERVER_ERROR,
426            Error::InvalidArgument(_) => http::StatusCode::BAD_REQUEST,
427            Error::DeadlineExceeded(_) => http::StatusCode::GATEWAY_TIMEOUT,
428            Error::NotFound(_) => http::StatusCode::NOT_FOUND,
429            Error::AlreadyExists(_) => http::StatusCode::CONFLICT,
430            Error::PermissionDenied(_) => http::StatusCode::FORBIDDEN,
431            Error::Unauthenticated(_) => http::StatusCode::UNAUTHORIZED,
432            Error::ResourceExhausted(_) => http::StatusCode::TOO_MANY_REQUESTS,
433            Error::FailedPrecondition(_) => http::StatusCode::BAD_REQUEST,
434            Error::Aborted(_) => http::StatusCode::CONFLICT,
435            Error::OutOfRange(_) => http::StatusCode::BAD_REQUEST,
436            Error::Unimplemented(_) => http::StatusCode::NOT_IMPLEMENTED,
437            Error::Internal(_) => http::StatusCode::INTERNAL_SERVER_ERROR,
438            Error::Unavailable(_) => http::StatusCode::SERVICE_UNAVAILABLE,
439            Error::DataLoss(_) => http::StatusCode::INTERNAL_SERVER_ERROR,
440        }
441    }
442}
443
444// TODO: Properly map error details into a tonic Status.
445#[cfg(feature = "with-tonic")]
446impl Error {
447    pub fn into_tonic_status(self) -> tonic::Status {
448        match self {
449            Error::Internal(status) => {
450                tonic::Status::new(tonic::Code::Internal, status.message.unwrap_or_default())
451            }
452            Error::Cancelled(status) => {
453                tonic::Status::new(tonic::Code::Cancelled, status.message.unwrap_or_default())
454            }
455            Error::Unknown(status) => {
456                tonic::Status::new(tonic::Code::Unknown, status.message.unwrap_or_default())
457            }
458            Error::InvalidArgument(status) => tonic::Status::new(
459                tonic::Code::InvalidArgument,
460                status.message.unwrap_or_default(),
461            ),
462            Error::DeadlineExceeded(status) => tonic::Status::new(
463                tonic::Code::DeadlineExceeded,
464                status.message.unwrap_or_default(),
465            ),
466            Error::NotFound(status) => {
467                tonic::Status::new(tonic::Code::NotFound, status.message.unwrap_or_default())
468            }
469            Error::AlreadyExists(status) => tonic::Status::new(
470                tonic::Code::AlreadyExists,
471                status.message.unwrap_or_default(),
472            ),
473            Error::PermissionDenied(status) => tonic::Status::new(
474                tonic::Code::PermissionDenied,
475                status.message.unwrap_or_default(),
476            ),
477            Error::Unauthenticated(status) => tonic::Status::new(
478                tonic::Code::Unauthenticated,
479                status.message.unwrap_or_default(),
480            ),
481            Error::ResourceExhausted(status) => tonic::Status::new(
482                tonic::Code::ResourceExhausted,
483                status.message.unwrap_or_default(),
484            ),
485            Error::FailedPrecondition(status) => tonic::Status::new(
486                tonic::Code::FailedPrecondition,
487                status.message.unwrap_or_default(),
488            ),
489            Error::Aborted(status) => {
490                tonic::Status::new(tonic::Code::Aborted, status.message.unwrap_or_default())
491            }
492            Error::OutOfRange(status) => {
493                tonic::Status::new(tonic::Code::OutOfRange, status.message.unwrap_or_default())
494            }
495            Error::Unimplemented(status) => tonic::Status::new(
496                tonic::Code::Unimplemented,
497                status.message.unwrap_or_default(),
498            ),
499            Error::Unavailable(status) => {
500                tonic::Status::new(tonic::Code::Unavailable, status.message.unwrap_or_default())
501            }
502            Error::DataLoss(status) => {
503                tonic::Status::new(tonic::Code::DataLoss, status.message.unwrap_or_default())
504            }
505        }
506    }
507}
508
509#[cfg(feature = "with-tonic")]
510impl TryFrom<tonic::Status> for Error {
511    type Error = Error;
512
513    fn try_from(value: tonic::Status) -> Result<Self, Self::Error> {
514        match value.code() {
515            tonic::Code::Ok => Err(Error::invalid_argument("Cannot convert OK status to Error")),
516            tonic::Code::Cancelled => Ok(Error::cancelled(value.message())),
517            tonic::Code::Unknown => Ok(Error::unknown(value.message())),
518            tonic::Code::InvalidArgument => Ok(Error::invalid_argument(value.message())),
519            tonic::Code::DeadlineExceeded => Ok(Error::deadline_exceeded(value.message())),
520            tonic::Code::NotFound => Ok(Error::not_found(value.message())),
521            tonic::Code::AlreadyExists => Ok(Error::already_exists(value.message())),
522            tonic::Code::PermissionDenied => Ok(Error::permission_denied(value.message())),
523            tonic::Code::ResourceExhausted => Ok(Error::resource_exhausted(value.message())),
524            tonic::Code::FailedPrecondition => Ok(Error::failed_precondition(value.message())),
525            tonic::Code::Aborted => Ok(Error::aborted(value.message())),
526            tonic::Code::OutOfRange => Ok(Error::out_of_range(value.message())),
527            tonic::Code::Unimplemented => Ok(Error::unimplemented(value.message())),
528            tonic::Code::Internal => Ok(Error::internal(value.message())),
529            tonic::Code::Unavailable => Ok(Error::unavailable(value.message())),
530            tonic::Code::DataLoss => Ok(Error::data_loss(value.message())),
531            tonic::Code::Unauthenticated => Ok(Error::unauthenticated(value.message())),
532        }
533    }
534}
535
536/// The `Status` type defines a logical error model that is suitable for
537/// different programming environments, including REST APIs and RPC APIs. It is
538/// used by [gRPC](https://github.com/grpc). Each `Status` message contains
539/// three pieces of data: error code, error message, and error details.
540///
541/// You can find out more about this error model and how to work with it in the
542/// [API Design Guide](https://cloud.google.com/apis/design/errors).
543#[derive(Clone, Debug, Default)]
544pub struct ErrorStatus {
545    /// A developer-facing error message, which should be in English. Any
546    /// user-facing error message should be localized and sent in the
547    /// `details` field in a `ErrorDetails::LocalizedMessage`.
548    pub message: Option<String>,
549    /// A list of messages that carry the error details.  There is a common set
550    /// of message types for APIs to use.    
551    pub details: Option<Vec<ErrorDetails>>,
552}
553
554impl ErrorStatus {
555    pub fn with_message<M: AsRef<str>>(self, message: M) -> Self {
556        ErrorStatus {
557            message: Some(message.as_ref().to_owned()),
558            details: self.details,
559        }
560    }
561
562    pub fn with_error<E: fmt::Display>(self, error: E) -> Self {
563        let mut details = self.details.unwrap_or_default();
564        details.push(ErrorDetails::DebugInfo {
565            stack_entries: None,
566            detail: Some(error.to_string()),
567        });
568        ErrorStatus {
569            message: self.message,
570            details: Some(details),
571        }
572    }
573}
574
575/// The specific details of an error that may be optionally forwarded to an
576/// end-user.
577///
578/// These error detail kinds and documentation have been imported from
579/// https://github.com/googleapis/googleapis/blob/f36c65081b19e0758ef5696feca27c7dcee5475e/google/rpc/error_details.proto.
580#[derive(Clone, Debug, IntoStaticStr)]
581#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
582pub enum ErrorDetails {
583    /// Describes violations in a client request. This error type focuses on the
584    /// syntactic aspects of the request.    
585    BadRequest {
586        /// Describes all violations in a client request.
587        field_violations: Vec<FieldViolation>,
588    },
589    /// Describes additional debugging info.
590    DebugInfo {
591        /// The stack trace entries indicating where the error occurred.
592        stack_entries: Option<Vec<String>>,
593        /// Additional debugging information provided by the server.
594        detail: Option<String>,
595    },
596    /// Provides a localized error message that is safe to return to the user
597    /// which can be attached to an RPC error.
598    LocalizedMessage {
599        /// The locale used following the specification defined at
600        /// <https://www.rfc-editor.org/rfc/bcp/bcp47.txt>.
601        /// Examples are: "en-US", "fr-CH", "es-MX"
602        locale: String,
603        /// The localized error message in the above locale.
604        message: String,
605    },
606}
607
608impl ErrorDetails {
609    pub fn bad_request(field_violation: FieldViolation) -> Self {
610        ErrorDetails::BadRequest {
611            field_violations: vec![field_violation],
612        }
613    }
614
615    pub fn debug_info<D: AsRef<str>>(detail: D) -> Self {
616        ErrorDetails::DebugInfo {
617            stack_entries: None,
618            detail: Some(detail.as_ref().to_owned()),
619        }
620    }
621
622    pub fn localized_message<L: AsRef<str>, M: AsRef<str>>(locale: L, message: M) -> Self {
623        ErrorDetails::LocalizedMessage {
624            locale: locale.as_ref().to_owned(),
625            message: message.as_ref().to_owned(),
626        }
627    }
628}
629
630// TODO: Replace strum with a more detailed display implementation.
631impl fmt::Display for ErrorDetails {
632    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633        f.write_str(self.into())
634    }
635}
636
637/// A message type used to describe a single bad request field.
638#[derive(Clone, Debug)]
639pub struct FieldViolation {
640    /// A path that leads to a field in the request body. The value will be a
641    /// sequence of dot-separated identifiers that identify a protocol buffer
642    /// field.
643    ///
644    /// Consider the following:
645    ///
646    /// ```protobuf
647    ///     message CreateContactRequest {
648    ///       message EmailAddress {
649    ///         enum Type {
650    ///           TYPE_UNSPECIFIED = 0;
651    ///           HOME = 1;
652    ///           WORK = 2;
653    ///         }
654    ///
655    ///         optional string email = 1;
656    ///         repeated EmailType type = 2;
657    ///       }
658    ///
659    ///       string full_name = 1;
660    ///       repeated EmailAddress email_addresses = 2;
661    ///     }   
662    /// ```
663    /// In this example, in proto `field` could take one of the following values:
664    ///
665    /// * `full_name` for a violation in the `full_name` value
666    /// * `email_addresses[1].email` for a violation in the `email` field of the
667    ///   first `email_addresses` message
668    /// * `email_addresses[3].type[2]` for a violation in the second `type`
669    ///   value in the third `email_addresses` message.
670    ///
671    /// In JSON, the same values are represented as:
672    ///
673    /// * `fullName` for a violation in the `fullName` value
674    /// * `emailAddresses[1].email` for a violation in the `email` field of the
675    ///   first `emailAddresses` message
676    /// * `emailAddresses[3].type[2]` for a violation in the second `type`
677    ///   value in the third `emailAddresses` message.    
678    pub field: Field,
679    /// A description of why the request element is bad.
680    pub description: Option<String>,
681}
682
683#[derive(Clone, Debug)]
684pub struct Field {
685    path_reversed: Vec<Property>,
686}
687
688impl Field {
689    pub fn new(property: Property) -> Self {
690        Field {
691            path_reversed: vec![property],
692        }
693    }
694
695    pub fn with_context(mut self, context: Property) -> Self {
696        self.path_reversed.push(context);
697        self
698    }
699}
700
701impl fmt::Display for Field {
702    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
703        for i in (0..self.path_reversed.len()).rev() {
704            write!(f, r#"{}"#, self.path_reversed.get(i).ok_or(fmt::Error)?,)?;
705            if i > 0 {
706                write!(f, ".")?;
707            }
708        }
709        Ok(())
710    }
711}
712
713#[derive(Clone, Debug)]
714pub enum Property {
715    Member { name: String },
716    MapMember { name: String, key: String },
717    ArrayMember { name: String, index: usize },
718}
719
720impl fmt::Display for Property {
721    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
722        match self {
723            Property::Member { name } => write!(f, r#"{}"#, name),
724            Property::MapMember { name, key } => write!(f, r#"{}["{}"]"#, name, key),
725            Property::ArrayMember { name, index } => write!(f, r#"{}[{}]"#, name, index),
726        }
727    }
728}
729
730/// A request for inter-module communication.
731#[derive(Clone)]
732pub struct Request<T>
733where
734    T: Send,
735{
736    pub message: T,
737}
738
739/// A response for inter-module communication.
740#[derive(Clone)]
741pub struct Response<T>
742where
743    T: Send,
744{
745    pub message: T,
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    #[test]
753    fn error_display() {
754        let error = Error::internal("Something bad happened").with_error("Invalid operation");
755        assert_eq!(error.to_string(), "INTERNAL");
756        assert!(error
757            .inner()
758            .details
759            .as_ref()
760            .expect("some error details")
761            .iter()
762            .any(|d| &d.to_string() == "DEBUG_INFO"));
763    }
764
765    #[test]
766    fn property_member_display() {
767        let field = Property::Member {
768            name: "nickname".to_string(),
769        };
770        assert_eq!(field.to_string().as_str(), "nickname");
771    }
772
773    #[test]
774    fn property_map_member_display() {
775        let field = Property::MapMember {
776            name: "children".to_string(),
777            key: "son".to_string(),
778        };
779        assert_eq!(field.to_string().as_str(), r#"children["son"]"#);
780    }
781
782    #[test]
783    fn property_array_member_display() {
784        let field = Property::ArrayMember {
785            name: "children".to_string(),
786            index: 3,
787        };
788        assert_eq!(field.to_string().as_str(), r#"children[3]"#);
789    }
790
791    #[test]
792    fn property_display() {
793        let argument = Field::new(Property::MapMember {
794            name: "nicknames".to_string(),
795            key: "joe".to_string(),
796        })
797        .with_context(Property::ArrayMember {
798            name: "children".to_string(),
799            index: 3,
800        })
801        .with_context(Property::Member {
802            name: "family".to_string(),
803        });
804
805        assert_eq!(
806            argument.to_string().as_str(),
807            r#"family.children[3].nicknames["joe"]"#
808        );
809    }
810}