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}