Skip to main content

google_cloud_gax/error/
rpc.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::error::Error;
16use google_cloud_rpc::model::ErrorInfo;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20
21/// The [Status] type defines a logical error model that is suitable for
22/// different programming environments, including REST APIs and RPC APIs. Each
23/// [Status] message contains three pieces of data: error code, error message,
24/// and error details.
25///
26/// You can find out more about this error model and how to work with it in the
27/// [API Design Guide](https://cloud.google.com/apis/design/errors).
28#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
29#[serde(default, rename_all = "camelCase")]
30#[non_exhaustive]
31pub struct Status {
32    /// The status code.
33    pub code: Code,
34
35    /// A developer-facing error message, which should be in English. Any
36    /// user-facing error message should be localized and sent in the
37    /// [Status] `details` field.
38    pub message: String,
39
40    /// A list of messages that carry the error details. There is a common set
41    /// of message types for APIs to use.
42    pub details: Vec<StatusDetails>,
43}
44
45impl Status {
46    /// Sets the value for [code][Status::code].
47    pub fn set_code<T: Into<Code>>(mut self, v: T) -> Self {
48        self.code = v.into();
49        self
50    }
51
52    /// Sets the value for [message][Status::message].
53    pub fn set_message<T: Into<String>>(mut self, v: T) -> Self {
54        self.message = v.into();
55        self
56    }
57
58    /// Sets the value for [details][Status::details].
59    pub fn set_details<T, I>(mut self, v: T) -> Self
60    where
61        T: IntoIterator<Item = I>,
62        I: Into<StatusDetails>,
63    {
64        self.details = v.into_iter().map(|v| v.into()).collect();
65        self
66    }
67}
68
69/// The canonical error codes for APIs.
70//
71/// Sometimes multiple error codes may apply.  Services should return
72/// the most specific error code that applies.  For example, prefer
73/// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply.
74/// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`.
75#[derive(Clone, Copy, Debug, Default, PartialEq)]
76#[non_exhaustive]
77pub enum Code {
78    /// Not an error; returned on success.
79    ///
80    /// HTTP Mapping: 200 OK
81    Ok = 0,
82
83    /// The operation was cancelled, typically by the caller.
84    ///
85    /// HTTP Mapping: 499 Client Closed Request
86    Cancelled = 1,
87
88    /// Unknown error.  For example, this error may be returned when
89    /// a `Status` value received from another address space belongs to
90    /// an error space that is not known in this address space.  Also
91    /// errors raised by APIs that do not return enough error information
92    /// may be converted to this error.
93    ///
94    /// HTTP Mapping: 500 Internal Server Error
95    #[default]
96    Unknown = 2,
97
98    /// The client specified an invalid argument.  Note that this differs
99    /// from `FAILED_PRECONDITION`.  `INVALID_ARGUMENT` indicates arguments
100    /// that are problematic regardless of the state of the system
101    /// (e.g., a malformed file name).
102    ///
103    /// HTTP Mapping: 400 Bad Request
104    InvalidArgument = 3,
105
106    /// The deadline expired before the operation could complete. For operations
107    /// that change the state of the system, this error may be returned
108    /// even if the operation has completed successfully.  For example, a
109    /// successful response from a server could have been delayed long
110    /// enough for the deadline to expire.
111    ///
112    /// HTTP Mapping: 504 Gateway Timeout
113    DeadlineExceeded = 4,
114
115    /// Some requested entity (e.g., file or directory) was not found.
116    ///
117    /// Note to server developers: if a request is denied for an entire class
118    /// of users, such as gradual feature rollout or undocumented allowlist,
119    /// `NOT_FOUND` may be used. If a request is denied for some users within
120    /// a class of users, such as user-based access control, `PERMISSION_DENIED`
121    /// must be used.
122    ///
123    /// HTTP Mapping: 404 Not Found
124    NotFound = 5,
125
126    /// The entity that a client attempted to create (e.g., file or directory)
127    /// already exists.
128    ///
129    /// HTTP Mapping: 409 Conflict
130    AlreadyExists = 6,
131
132    /// The caller does not have permission to execute the specified
133    /// operation. `PERMISSION_DENIED` must not be used for rejections
134    /// caused by exhausting some resource (use `RESOURCE_EXHAUSTED`
135    /// instead for those errors). `PERMISSION_DENIED` must not be
136    /// used if the caller can not be identified (use `UNAUTHENTICATED`
137    /// instead for those errors). This error code does not imply the
138    /// request is valid or the requested entity exists or satisfies
139    /// other pre-conditions.
140    ///
141    /// HTTP Mapping: 403 Forbidden
142    PermissionDenied = 7,
143
144    /// Some resource has been exhausted, perhaps a per-user quota, or
145    /// perhaps the entire file system is out of space.
146    ///
147    /// HTTP Mapping: 429 Too Many Requests
148    ResourceExhausted = 8,
149
150    /// The operation was rejected because the system is not in a state
151    /// required for the operation's execution.  For example, the directory
152    /// to be deleted is non-empty, an rmdir operation is applied to
153    /// a non-directory, etc.
154    ///
155    /// Service implementors can use the following guidelines to decide
156    /// between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`:
157    /// 1. Use `UNAVAILABLE` if the client can retry just the failing call.
158    /// 1. Use `ABORTED` if the client should retry at a higher level. For
159    ///    example, when a client-specified test-and-set fails, indicating the
160    ///    client should restart a read-modify-write sequence.
161    /// 1. Use `FAILED_PRECONDITION` if the client should not retry until
162    ///    the system state has been explicitly fixed. For example, if an "rmdir"
163    ///    fails because the directory is non-empty, `FAILED_PRECONDITION`
164    ///    should be returned since the client should not retry unless
165    ///    the files are deleted from the directory.
166    ///
167    /// HTTP Mapping: 400 Bad Request
168    FailedPrecondition = 9,
169
170    /// The operation was aborted, typically due to a concurrency issue such as
171    /// a sequencer check failure or transaction abort.
172    ///
173    /// See the guidelines above for deciding between `FAILED_PRECONDITION`,
174    /// `ABORTED`, and `UNAVAILABLE`.
175    ///
176    /// HTTP Mapping: 409 Conflict
177    ///
178    /// HTTP Mapping: 400 Bad Request
179    Aborted = 10,
180
181    /// The operation was attempted past the valid range.  E.g., seeking or
182    /// reading past end-of-file.
183    ///
184    /// Unlike `INVALID_ARGUMENT`, this error indicates a problem that may
185    /// be fixed if the system state changes. For example, a 32-bit file
186    /// system will generate `INVALID_ARGUMENT` if asked to read at an
187    /// offset that is not in the range [0,2^32-1], but it will generate
188    /// `OUT_OF_RANGE` if asked to read from an offset past the current
189    /// file size.
190    ///
191    /// There is a fair bit of overlap between `FAILED_PRECONDITION` and
192    /// `OUT_OF_RANGE`.  We recommend using `OUT_OF_RANGE` (the more specific
193    /// error) when it applies so that callers who are iterating through
194    /// a space can easily look for an `OUT_OF_RANGE` error to detect when
195    /// they are done.
196    ///
197    /// HTTP Mapping: 400 Bad Request
198    OutOfRange = 11,
199
200    /// The operation is not implemented or is not supported/enabled in this
201    /// service.
202    ///
203    /// HTTP Mapping: 501 Not Implemented
204    Unimplemented = 12,
205
206    /// Internal errors.  This means that some invariants expected by the
207    /// underlying system have been broken.  This error code is reserved
208    /// for serious errors.
209    ///
210    /// HTTP Mapping: 500 Internal Server Error
211    Internal = 13,
212
213    /// The service is currently unavailable.  This is most likely a
214    /// transient condition, which can be corrected by retrying with
215    /// a backoff. Note that it is not always safe to retry
216    /// non-idempotent operations.
217    ///
218    /// See the guidelines above for deciding between `FAILED_PRECONDITION`,
219    /// `ABORTED`, and `UNAVAILABLE`.
220    ///
221    /// HTTP Mapping: 503 Service Unavailable
222    Unavailable = 14,
223
224    /// Unrecoverable data loss or corruption.
225    ///
226    /// HTTP Mapping: 500 Internal Server Error
227    DataLoss = 15,
228
229    /// The request does not have valid authentication credentials for the
230    /// operation.
231    ///
232    /// HTTP Mapping: 401 Unauthorized
233    Unauthenticated = 16,
234}
235
236impl Code {
237    /// Returns the string representation of the error code.
238    pub fn name(&self) -> &'static str {
239        match self {
240            Code::Ok => "OK",
241            Code::Cancelled => "CANCELLED",
242            Code::Unknown => "UNKNOWN",
243            Code::InvalidArgument => "INVALID_ARGUMENT",
244            Code::DeadlineExceeded => "DEADLINE_EXCEEDED",
245            Code::NotFound => "NOT_FOUND",
246            Code::AlreadyExists => "ALREADY_EXISTS",
247            Code::PermissionDenied => "PERMISSION_DENIED",
248            Code::ResourceExhausted => "RESOURCE_EXHAUSTED",
249            Code::FailedPrecondition => "FAILED_PRECONDITION",
250            Code::Aborted => "ABORTED",
251            Code::OutOfRange => "OUT_OF_RANGE",
252            Code::Unimplemented => "UNIMPLEMENTED",
253            Code::Internal => "INTERNAL",
254            Code::Unavailable => "UNAVAILABLE",
255            Code::DataLoss => "DATA_LOSS",
256            Code::Unauthenticated => "UNAUTHENTICATED",
257        }
258    }
259}
260
261impl std::convert::From<i32> for Code {
262    fn from(value: i32) -> Self {
263        match value {
264            0 => Code::Ok,
265            1 => Code::Cancelled,
266            2 => Code::Unknown,
267            3 => Code::InvalidArgument,
268            4 => Code::DeadlineExceeded,
269            5 => Code::NotFound,
270            6 => Code::AlreadyExists,
271            7 => Code::PermissionDenied,
272            8 => Code::ResourceExhausted,
273            9 => Code::FailedPrecondition,
274            10 => Code::Aborted,
275            11 => Code::OutOfRange,
276            12 => Code::Unimplemented,
277            13 => Code::Internal,
278            14 => Code::Unavailable,
279            15 => Code::DataLoss,
280            16 => Code::Unauthenticated,
281            _ => Code::default(),
282        }
283    }
284}
285
286impl std::convert::From<Code> for String {
287    fn from(value: Code) -> String {
288        value.name().to_string()
289    }
290}
291
292impl std::fmt::Display for Code {
293    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294        f.write_str(self.name())
295    }
296}
297
298impl std::convert::TryFrom<&str> for Code {
299    type Error = String;
300    fn try_from(value: &str) -> std::result::Result<Code, Self::Error> {
301        match value {
302            "OK" => Ok(Code::Ok),
303            "CANCELLED" => Ok(Code::Cancelled),
304            "UNKNOWN" => Ok(Code::Unknown),
305            "INVALID_ARGUMENT" => Ok(Code::InvalidArgument),
306            "DEADLINE_EXCEEDED" => Ok(Code::DeadlineExceeded),
307            "NOT_FOUND" => Ok(Code::NotFound),
308            "ALREADY_EXISTS" => Ok(Code::AlreadyExists),
309            "PERMISSION_DENIED" => Ok(Code::PermissionDenied),
310            "RESOURCE_EXHAUSTED" => Ok(Code::ResourceExhausted),
311            "FAILED_PRECONDITION" => Ok(Code::FailedPrecondition),
312            "ABORTED" => Ok(Code::Aborted),
313            "OUT_OF_RANGE" => Ok(Code::OutOfRange),
314            "UNIMPLEMENTED" => Ok(Code::Unimplemented),
315            "INTERNAL" => Ok(Code::Internal),
316            "UNAVAILABLE" => Ok(Code::Unavailable),
317            "DATA_LOSS" => Ok(Code::DataLoss),
318            "UNAUTHENTICATED" => Ok(Code::Unauthenticated),
319            _ => Err(format!("unknown status code value {value}")),
320        }
321    }
322}
323
324impl Serialize for Code {
325    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
326    where
327        S: serde::Serializer,
328    {
329        serializer.serialize_i32(*self as i32)
330    }
331}
332
333impl<'de> Deserialize<'de> for Code {
334    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
335    where
336        D: serde::Deserializer<'de>,
337    {
338        i32::deserialize(deserializer).map(Code::from)
339    }
340}
341
342/// A helper class to deserialized wrapped Status messages.
343#[derive(Clone, Debug, Deserialize)]
344struct ErrorWrapper {
345    error: WrapperStatus,
346}
347
348/// Some older Google Cloud APIs return errors with an `error.errors` array containing
349/// structured details (`reason`, `domain`, `message`). Examples include:
350/// - [BigQuery REST API error messages](https://cloud.google.com/bigquery/docs/error-messages)
351/// - [Cloud Storage JSON API status codes](https://cloud.google.com/storage/docs/json_api/v1/status-codes)
352#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
353#[serde(default)]
354struct ErrorItem {
355    pub reason: Option<String>,
356    pub domain: Option<String>,
357    #[serde(flatten)]
358    pub extra: HashMap<String, Value>,
359}
360
361impl From<ErrorItem> for StatusDetails {
362    fn from(item: ErrorItem) -> Self {
363        let mut info = ErrorInfo::new();
364        if let Some(reason) = item.reason {
365            info.reason = reason;
366        }
367        if let Some(domain) = item.domain {
368            info.domain = domain;
369        }
370        info.metadata = item
371            .extra
372            .into_iter()
373            .map(|(k, v)| {
374                let v = match v {
375                    Value::String(s) => s,
376                    other => other.to_string(),
377                };
378                (k, v)
379            })
380            .collect();
381        StatusDetails::ErrorInfo(info)
382    }
383}
384
385#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
386#[serde(default)]
387#[non_exhaustive]
388struct WrapperStatus {
389    pub code: i32,
390    pub message: String,
391    pub status: Option<String>,
392    pub details: Vec<StatusDetails>,
393    pub errors: Vec<ErrorItem>,
394}
395
396impl TryFrom<&bytes::Bytes> for Status {
397    type Error = Error;
398
399    fn try_from(value: &bytes::Bytes) -> Result<Self, Self::Error> {
400        let wrapper = serde_json::from_slice::<ErrorWrapper>(value)
401            .map(|w| w.error)
402            .map_err(Error::deser)?;
403        let code = match wrapper.status.as_deref().map(Code::try_from) {
404            Some(Ok(code)) => code,
405            Some(Err(_)) | None => Code::Unknown,
406        };
407        let details = Some(wrapper.details)
408            .filter(|d| !d.is_empty())
409            .unwrap_or_else(|| {
410                wrapper
411                    .errors
412                    .into_iter()
413                    .map(StatusDetails::from)
414                    .collect()
415            });
416        Ok(Status {
417            code,
418            message: wrapper.message,
419            details,
420        })
421    }
422}
423
424impl From<google_cloud_rpc::model::Status> for Status {
425    fn from(value: google_cloud_rpc::model::Status) -> Self {
426        Self {
427            code: value.code.into(),
428            message: value.message,
429            details: value.details.into_iter().map(StatusDetails::from).collect(),
430        }
431    }
432}
433
434impl From<&google_cloud_rpc::model::Status> for Status {
435    fn from(value: &google_cloud_rpc::model::Status) -> Self {
436        Self {
437            code: value.code.into(),
438            message: value.message.clone(),
439            details: value.details.iter().map(StatusDetails::from).collect(),
440        }
441    }
442}
443
444/// The type of details associated with [Status].
445///
446/// Google cloud RPCs often return a detailed error description. This details
447/// can be used to better understand the root cause of the problem.
448#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
449#[serde(rename_all = "camelCase")]
450#[non_exhaustive]
451#[serde(tag = "@type")]
452/// Detailed information about the error.
453pub enum StatusDetails {
454    /// Describes violations in a client request.
455    ///
456    /// See [BadRequest][google_cloud_rpc::model::BadRequest] for more information.
457    #[serde(rename = "type.googleapis.com/google.rpc.BadRequest")]
458    BadRequest(google_cloud_rpc::model::BadRequest),
459
460    /// Describes additional debugging info.
461    ///
462    /// See [DebugInfo][google_cloud_rpc::model::DebugInfo] for more information.
463    #[serde(rename = "type.googleapis.com/google.rpc.DebugInfo")]
464    DebugInfo(google_cloud_rpc::model::DebugInfo),
465
466    /// Describes the cause of the error with structured details.
467    ///
468    /// See [ErrorInfo] for more information.
469    #[serde(rename = "type.googleapis.com/google.rpc.ErrorInfo")]
470    ErrorInfo(google_cloud_rpc::model::ErrorInfo),
471
472    /// Provides links to documentation or for performing an out of band action.
473    ///
474    /// See [Help][google_cloud_rpc::model::Help] for more information.
475    #[serde(rename = "type.googleapis.com/google.rpc.Help")]
476    Help(google_cloud_rpc::model::Help),
477
478    /// Provides a localized error message that is safe to return to the user.
479    ///
480    /// See [LocalizedMessage][google_cloud_rpc::model::LocalizedMessage] for more information.
481    #[serde(rename = "type.googleapis.com/google.rpc.LocalizedMessage")]
482    LocalizedMessage(google_cloud_rpc::model::LocalizedMessage),
483
484    /// Describes what preconditions have failed.
485    ///
486    /// See [PreconditionFailure][google_cloud_rpc::model::PreconditionFailure] for more information.
487    #[serde(rename = "type.googleapis.com/google.rpc.PreconditionFailure")]
488    PreconditionFailure(google_cloud_rpc::model::PreconditionFailure),
489
490    /// Describes a single quota violation.
491    ///
492    /// See [QuotaFailure][google_cloud_rpc::model::QuotaFailure] for more information.
493    #[serde(rename = "type.googleapis.com/google.rpc.QuotaFailure")]
494    QuotaFailure(google_cloud_rpc::model::QuotaFailure),
495
496    /// Contains metadata about the request that clients can attach when filing a bug.
497    ///
498    /// See [RequestInfo][google_cloud_rpc::model::RequestInfo] for more information.
499    #[serde(rename = "type.googleapis.com/google.rpc.RequestInfo")]
500    RequestInfo(google_cloud_rpc::model::RequestInfo),
501
502    /// Describes the resource that is being accessed.
503    ///
504    /// See [ResourceInfo][google_cloud_rpc::model::ResourceInfo] for more information.
505    #[serde(rename = "type.googleapis.com/google.rpc.ResourceInfo")]
506    ResourceInfo(google_cloud_rpc::model::ResourceInfo),
507
508    /// Describes when the clients can retry a failed request.
509    ///
510    /// See [RetryInfo][google_cloud_rpc::model::RetryInfo] for more information.
511    #[serde(rename = "type.googleapis.com/google.rpc.RetryInfo")]
512    RetryInfo(google_cloud_rpc::model::RetryInfo),
513
514    /// Other details (represented as Any).
515    #[serde(untagged)]
516    Other(wkt::Any),
517}
518
519impl From<wkt::Any> for StatusDetails {
520    fn from(value: wkt::Any) -> Self {
521        macro_rules! try_convert {
522            ($($variant:ident),*) => {
523                $(
524                    if let Ok(v) = value.to_msg::<google_cloud_rpc::model::$variant>() {
525                        return StatusDetails::$variant(v);
526                    }
527                )*
528            };
529        }
530
531        try_convert!(
532            BadRequest,
533            DebugInfo,
534            ErrorInfo,
535            Help,
536            LocalizedMessage,
537            PreconditionFailure,
538            QuotaFailure,
539            RequestInfo,
540            ResourceInfo,
541            RetryInfo
542        );
543
544        StatusDetails::Other(value)
545    }
546}
547
548impl From<&wkt::Any> for StatusDetails {
549    fn from(value: &wkt::Any) -> Self {
550        macro_rules! try_convert {
551            ($($variant:ident),*) => {
552                $(
553                    if let Ok(v) = value.to_msg::<google_cloud_rpc::model::$variant>() {
554                        return StatusDetails::$variant(v);
555                    }
556                )*
557            };
558        }
559
560        try_convert!(
561            BadRequest,
562            DebugInfo,
563            ErrorInfo,
564            Help,
565            LocalizedMessage,
566            PreconditionFailure,
567            QuotaFailure,
568            RequestInfo,
569            ResourceInfo,
570            RetryInfo
571        );
572
573        StatusDetails::Other(value.clone())
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use anyhow::Result;
581    use google_cloud_rpc::model::DebugInfo;
582    use google_cloud_rpc::model::ErrorInfo;
583    use google_cloud_rpc::model::LocalizedMessage;
584    use google_cloud_rpc::model::RequestInfo;
585    use google_cloud_rpc::model::ResourceInfo;
586    use google_cloud_rpc::model::RetryInfo;
587    use google_cloud_rpc::model::{BadRequest, bad_request};
588    use google_cloud_rpc::model::{Help, help};
589    use google_cloud_rpc::model::{PreconditionFailure, precondition_failure};
590    use google_cloud_rpc::model::{QuotaFailure, quota_failure};
591    use serde_json::json;
592    use test_case::test_case;
593
594    #[test]
595    fn status_basic_setters() {
596        let got = Status::default()
597            .set_code(Code::Unimplemented)
598            .set_message("test-message");
599        let want = Status {
600            code: Code::Unimplemented,
601            message: "test-message".into(),
602            ..Default::default()
603        };
604        assert_eq!(got, want);
605
606        let got = Status::default()
607            .set_code(Code::Unimplemented as i32)
608            .set_message("test-message");
609        let want = Status {
610            code: Code::Unimplemented,
611            message: "test-message".into(),
612            ..Default::default()
613        };
614        assert_eq!(got, want);
615    }
616
617    #[test]
618    fn status_detail_setter() -> Result<()> {
619        let d0 = StatusDetails::ErrorInfo(ErrorInfo::new().set_reason("test-reason"));
620        let d1 =
621            StatusDetails::Help(Help::new().set_links([help::Link::new().set_url("test-url")]));
622        let want = Status {
623            details: vec![d0.clone(), d1.clone()],
624            ..Default::default()
625        };
626
627        let got = Status::default().set_details([d0, d1]);
628        assert_eq!(got, want);
629
630        let a0 = wkt::Any::from_msg(&ErrorInfo::new().set_reason("test-reason"))?;
631        let a1 =
632            wkt::Any::from_msg(&Help::new().set_links([help::Link::new().set_url("test-url")]))?;
633        let got = Status::default().set_details(&[a0, a1]);
634        assert_eq!(got, want);
635
636        Ok(())
637    }
638
639    #[test]
640    fn serialization_all_variants() {
641        let status = Status {
642            code: Code::Unimplemented,
643            message: "test".to_string(),
644
645            details: vec![
646                StatusDetails::BadRequest(BadRequest::default().set_field_violations(vec![
647                        bad_request::FieldViolation::default()
648                            .set_field("field")
649                            .set_description("desc"),
650                    ])),
651                StatusDetails::DebugInfo(
652                    DebugInfo::default()
653                        .set_stack_entries(vec!["stack".to_string()])
654                        .set_detail("detail"),
655                ),
656                StatusDetails::ErrorInfo(
657                    ErrorInfo::default()
658                        .set_reason("reason")
659                        .set_domain("domain")
660                        .set_metadata([("", "")].into_iter().take(0)),
661                ),
662                StatusDetails::Help(Help::default().set_links(vec![
663                    help::Link::default().set_description("desc").set_url("url"),
664                ])),
665                StatusDetails::LocalizedMessage(
666                    LocalizedMessage::default()
667                        .set_locale("locale")
668                        .set_message("message"),
669                ),
670                StatusDetails::PreconditionFailure(PreconditionFailure::default().set_violations(
671                    vec![
672                            precondition_failure::Violation::default()
673                                .set_type("type")
674                                .set_subject("subject")
675                                .set_description("desc"),
676                        ],
677                )),
678                StatusDetails::QuotaFailure(QuotaFailure::default().set_violations(
679                    vec![quota_failure::Violation::default()
680                        .set_subject( "subject")
681                        .set_description( "desc")
682                    ],
683                )),
684                StatusDetails::RequestInfo(
685                    RequestInfo::default()
686                        .set_request_id("id")
687                        .set_serving_data("data"),
688                ),
689                StatusDetails::ResourceInfo(
690                    ResourceInfo::default()
691                        .set_resource_type("type")
692                        .set_resource_name("name")
693                        .set_owner("owner")
694                        .set_description("desc"),
695                ),
696                StatusDetails::RetryInfo(
697                    RetryInfo::default().set_retry_delay(wkt::Duration::clamp(1, 0)),
698                ),
699            ],
700        };
701        let got = serde_json::to_value(&status).unwrap();
702        let want = json!({
703            "code": Code::Unimplemented,
704            "message": "test",
705            "details": [
706                {"@type": "type.googleapis.com/google.rpc.BadRequest", "fieldViolations": [{"field": "field", "description": "desc"}]},
707                {"@type": "type.googleapis.com/google.rpc.DebugInfo", "stackEntries": ["stack"], "detail": "detail"},
708                {"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "reason", "domain": "domain"},
709                {"@type": "type.googleapis.com/google.rpc.Help", "links": [{"description": "desc", "url": "url"}]},
710                {"@type": "type.googleapis.com/google.rpc.LocalizedMessage", "locale": "locale", "message": "message"},
711                {"@type": "type.googleapis.com/google.rpc.PreconditionFailure", "violations": [{"type": "type", "subject": "subject", "description": "desc"}]},
712                {"@type": "type.googleapis.com/google.rpc.QuotaFailure", "violations": [{"subject": "subject", "description": "desc"}]},
713                {"@type": "type.googleapis.com/google.rpc.RequestInfo", "requestId": "id", "servingData": "data"},
714                {"@type": "type.googleapis.com/google.rpc.ResourceInfo", "resourceType": "type", "resourceName": "name", "owner": "owner", "description": "desc"},
715                {"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "1s"},
716            ]
717        });
718        assert_eq!(got, want);
719    }
720
721    #[test]
722    fn deserialization_all_variants() {
723        let json = json!({
724            "code": Code::Unknown as i32,
725            "message": "test",
726            "details": [
727                {"@type": "type.googleapis.com/google.rpc.BadRequest", "fieldViolations": [{"field": "field", "description": "desc"}]},
728                {"@type": "type.googleapis.com/google.rpc.DebugInfo", "stackEntries": ["stack"], "detail": "detail"},
729                {"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "reason", "domain": "domain", "metadata": {}},
730                {"@type": "type.googleapis.com/google.rpc.Help", "links": [{"description": "desc", "url": "url"}]},
731                {"@type": "type.googleapis.com/google.rpc.LocalizedMessage", "locale": "locale", "message": "message"},
732                {"@type": "type.googleapis.com/google.rpc.PreconditionFailure", "violations": [{"type": "type", "subject": "subject", "description": "desc"}]},
733                {"@type": "type.googleapis.com/google.rpc.QuotaFailure", "violations": [{"subject": "subject", "description": "desc"}]},
734                {"@type": "type.googleapis.com/google.rpc.RequestInfo", "requestId": "id", "servingData": "data"},
735                {"@type": "type.googleapis.com/google.rpc.ResourceInfo", "resourceType": "type", "resourceName": "name", "owner": "owner", "description": "desc"},
736                {"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "1s"},
737            ]
738        });
739        let got: Status = serde_json::from_value(json).unwrap();
740        let want = Status {
741            code: Code::Unknown,
742            message: "test".to_string(),
743            details: vec![
744                StatusDetails::BadRequest(BadRequest::default().set_field_violations(
745                    vec![bad_request::FieldViolation::default()
746                        .set_field( "field" )
747                        .set_description( "desc" )
748                    ],
749                )),
750                StatusDetails::DebugInfo(
751                    DebugInfo::default()
752                        .set_stack_entries(vec!["stack".to_string()])
753                        .set_detail("detail"),
754                ),
755                StatusDetails::ErrorInfo(
756                    ErrorInfo::default()
757                        .set_reason("reason")
758                        .set_domain("domain"),
759                ),
760                StatusDetails::Help(Help::default().set_links(vec![
761                    help::Link::default().set_description("desc").set_url("url"),
762                ])),
763                StatusDetails::LocalizedMessage(
764                    LocalizedMessage::default()
765                        .set_locale("locale")
766                        .set_message("message"),
767                ),
768                StatusDetails::PreconditionFailure(PreconditionFailure::default().set_violations(
769                    vec![precondition_failure::Violation::default()
770                        .set_type( "type" )
771                        .set_subject( "subject" )
772                        .set_description( "desc" )
773                    ],
774                )),
775                StatusDetails::QuotaFailure(QuotaFailure::default().set_violations(
776                    vec![quota_failure::Violation::default()
777                        .set_subject( "subject")
778                        .set_description( "desc")
779                    ],
780                )),
781                StatusDetails::RequestInfo(
782                    RequestInfo::default()
783                        .set_request_id("id")
784                        .set_serving_data("data"),
785                ),
786                StatusDetails::ResourceInfo(
787                    ResourceInfo::default()
788                        .set_resource_type("type")
789                        .set_resource_name("name")
790                        .set_owner("owner")
791                        .set_description("desc"),
792                ),
793                StatusDetails::RetryInfo(
794                    RetryInfo::default().set_retry_delay(wkt::Duration::clamp(1, 0)),
795                ),
796            ],
797        };
798        assert_eq!(got, want);
799    }
800
801    #[test]
802    fn serialization_other() -> Result<()> {
803        const TIME: &str = "2025-05-27T10:00:00Z";
804        let timestamp = wkt::Timestamp::try_from(TIME)?;
805        let any = wkt::Any::from_msg(&timestamp)?;
806        let input = Status {
807            code: Code::Unknown,
808            message: "test".to_string(),
809            details: vec![StatusDetails::Other(any)],
810        };
811        let got = serde_json::to_value(&input)?;
812        let want = json!({
813            "code": Code::Unknown as i32,
814            "message": "test",
815            "details": [
816                {"@type": "type.googleapis.com/google.protobuf.Timestamp", "value": TIME},
817            ]
818        });
819        assert_eq!(got, want);
820        Ok(())
821    }
822
823    #[test]
824    fn deserialization_other() -> Result<()> {
825        const TIME: &str = "2025-05-27T10:00:00Z";
826        let json = json!({
827            "code": Code::Unknown as i32,
828            "message": "test",
829            "details": [
830                {"@type": "type.googleapis.com/google.protobuf.Timestamp", "value": TIME},
831            ]
832        });
833        let timestamp = wkt::Timestamp::try_from(TIME)?;
834        let any = wkt::Any::from_msg(&timestamp)?;
835        let got: Status = serde_json::from_value(json)?;
836        let want = Status {
837            code: Code::Unknown,
838            message: "test".to_string(),
839            details: vec![StatusDetails::Other(any)],
840        };
841        assert_eq!(got, want);
842        Ok(())
843    }
844
845    #[test]
846    fn status_from_rpc_no_details() {
847        let input = google_cloud_rpc::model::Status::default()
848            .set_code(Code::Unavailable as i32)
849            .set_message("try-again");
850        let got = Status::from(&input);
851        assert_eq!(got.code, Code::Unavailable);
852        assert_eq!(got.message, "try-again");
853    }
854
855    #[test_case(
856        BadRequest::default(),
857        StatusDetails::BadRequest(BadRequest::default())
858    )]
859    #[test_case(DebugInfo::default(), StatusDetails::DebugInfo(DebugInfo::default()))]
860    #[test_case(ErrorInfo::default(), StatusDetails::ErrorInfo(ErrorInfo::default()))]
861    #[test_case(Help::default(), StatusDetails::Help(Help::default()))]
862    #[test_case(
863        LocalizedMessage::default(),
864        StatusDetails::LocalizedMessage(LocalizedMessage::default())
865    )]
866    #[test_case(
867        PreconditionFailure::default(),
868        StatusDetails::PreconditionFailure(PreconditionFailure::default())
869    )]
870    #[test_case(
871        QuotaFailure::default(),
872        StatusDetails::QuotaFailure(QuotaFailure::default())
873    )]
874    #[test_case(
875        RequestInfo::default(),
876        StatusDetails::RequestInfo(RequestInfo::default())
877    )]
878    #[test_case(
879        ResourceInfo::default(),
880        StatusDetails::ResourceInfo(ResourceInfo::default())
881    )]
882    #[test_case(RetryInfo::default(), StatusDetails::RetryInfo(RetryInfo::default()))]
883    fn status_from_rpc_status_known_detail_type<T>(detail: T, want: StatusDetails)
884    where
885        T: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
886    {
887        let input = google_cloud_rpc::model::Status::default()
888            .set_code(Code::Unavailable as i32)
889            .set_message("try-again")
890            .set_details(vec![wkt::Any::from_msg(&detail).unwrap()]);
891
892        let from_ref = Status::from(&input);
893        let status = Status::from(input);
894        assert_eq!(from_ref, status);
895        assert_eq!(status.code, Code::Unavailable);
896        assert_eq!(status.message, "try-again");
897
898        let got = status.details.first();
899        assert_eq!(got, Some(&want));
900    }
901
902    #[test]
903    fn status_from_rpc_unknown_details() {
904        let any = wkt::Any::from_msg(&wkt::Duration::clamp(123, 0)).unwrap();
905        let input = google_cloud_rpc::model::Status::default()
906            .set_code(Code::Unavailable as i32)
907            .set_message("try-again")
908            .set_details(vec![any.clone()]);
909        let from_ref = Status::from(&input);
910        let got = Status::from(input);
911        assert_eq!(from_ref, got);
912        assert_eq!(got.code, Code::Unavailable);
913        assert_eq!(got.message, "try-again");
914
915        let got = got.details.first();
916        let want = StatusDetails::Other(any);
917        assert_eq!(got, Some(&want));
918    }
919
920    // This is a sample string received from production. It is useful to
921    // validate the serialization helpers.
922    const SAMPLE_PAYLOAD: &[u8] = b"{\n  \"error\": {\n    \"code\": 400,\n    \"message\": \"The provided Secret ID [] does not match the expected format [[a-zA-Z_0-9]+]\",\n    \"status\": \"INVALID_ARGUMENT\"\n  }\n}\n";
923    const INVALID_CODE_PAYLOAD: &[u8] = b"{\n  \"error\": {\n    \"code\": 400,\n    \"message\": \"The provided Secret ID [] does not match the expected format [[a-zA-Z_0-9]+]\",\n    \"status\": \"NOT-A-VALID-CODE\"\n  }\n}\n";
924
925    // The corresponding status message.
926    fn sample_status() -> Status {
927        Status {
928            code: Code::InvalidArgument,
929            message: "The provided Secret ID [] does not match the expected format [[a-zA-Z_0-9]+]"
930                .into(),
931            details: [].into(),
932        }
933    }
934
935    #[test]
936    fn deserialize_status() {
937        let got = serde_json::from_slice::<ErrorWrapper>(SAMPLE_PAYLOAD).unwrap();
938        let want = ErrorWrapper {
939            error: WrapperStatus {
940                code: 400,
941                status: Some("INVALID_ARGUMENT".to_string()),
942                message:
943                    "The provided Secret ID [] does not match the expected format [[a-zA-Z_0-9]+]"
944                        .into(),
945                details: [].into(),
946                errors: [].into(),
947            },
948        };
949        assert_eq!(got.error, want.error);
950    }
951
952    #[test]
953    fn try_from_bytes() -> Result<()> {
954        let got = Status::try_from(&bytes::Bytes::from_static(SAMPLE_PAYLOAD))?;
955        let want = sample_status();
956        assert_eq!(got, want);
957
958        let got = Status::try_from(&bytes::Bytes::from_static(b"\"error\": 1234"));
959        let err = got.unwrap_err();
960        assert!(err.is_deserialization(), "{err:?}");
961
962        let got = Status::try_from(&bytes::Bytes::from_static(b"\"missing-error\": 1234"));
963        let err = got.unwrap_err();
964        assert!(err.is_deserialization(), "{err:?}");
965
966        let got = Status::try_from(&bytes::Bytes::from_static(INVALID_CODE_PAYLOAD))?;
967        assert_eq!(got.code, Code::Unknown);
968        Ok(())
969    }
970
971    #[test]
972    fn try_from_bytes_rest_errors() -> Result<()> {
973        const BIGQUERY_ERR_PAYLOAD: &[u8] = br#"{
974  "error": {
975    "code": 400,
976    "message": "The job encountered an error during execution. Retrying the job may solve the problem.",
977    "errors": [
978      {
979        "message": "The job encountered an error during execution. Retrying the job may solve the problem.",
980        "domain": "global",
981        "reason": "backendError"
982      }
983    ],
984    "status": "INVALID_ARGUMENT"
985  }
986}"#;
987        let got = Status::try_from(&bytes::Bytes::from_static(BIGQUERY_ERR_PAYLOAD))?;
988        assert_eq!(got.code, Code::InvalidArgument);
989        assert_eq!(
990            got.message,
991            "The job encountered an error during execution. Retrying the job may solve the problem."
992        );
993        assert_eq!(got.details.len(), 1);
994        match &got.details[0] {
995            StatusDetails::ErrorInfo(info) => {
996                assert_eq!(info.reason, "backendError");
997                assert_eq!(info.domain, "global");
998                assert_eq!(
999                    info.metadata.get("message").map(String::as_str),
1000                    Some(
1001                        "The job encountered an error during execution. Retrying the job may solve the problem."
1002                    )
1003                );
1004            }
1005            other => panic!("expected ErrorInfo, got {other:?}"),
1006        }
1007        Ok(())
1008    }
1009
1010    #[test]
1011    fn try_from_bytes_rest_errors_with_location_and_debug_info() -> Result<()> {
1012        const PAYLOAD: &[u8] = br#"{
1013  "error": {
1014    "code": 401,
1015    "message": "Invalid Credentials",
1016    "errors": [
1017      {
1018        "message": "Invalid Credentials",
1019        "domain": "global",
1020        "reason": "authError",
1021        "locationType": "header",
1022        "location": "Authorization",
1023        "debugInfo": "token expired"
1024      }
1025    ],
1026    "status": "UNAUTHENTICATED"
1027  }
1028}"#;
1029        let got = Status::try_from(&bytes::Bytes::from_static(PAYLOAD))?;
1030        assert_eq!(got.code, Code::Unauthenticated);
1031        assert_eq!(got.message, "Invalid Credentials");
1032        assert_eq!(got.details.len(), 1);
1033        match &got.details[0] {
1034            StatusDetails::ErrorInfo(info) => {
1035                assert_eq!(info.reason, "authError");
1036                assert_eq!(info.domain, "global");
1037                assert_eq!(
1038                    info.metadata.get("message").map(String::as_str),
1039                    Some("Invalid Credentials")
1040                );
1041                assert_eq!(
1042                    info.metadata.get("locationType").map(String::as_str),
1043                    Some("header")
1044                );
1045                assert_eq!(
1046                    info.metadata.get("location").map(String::as_str),
1047                    Some("Authorization")
1048                );
1049                assert_eq!(
1050                    info.metadata.get("debugInfo").map(String::as_str),
1051                    Some("token expired")
1052                );
1053            }
1054            other => panic!("expected ErrorInfo, got {other:?}"),
1055        }
1056        Ok(())
1057    }
1058
1059    #[test]
1060    fn code_to_string() {
1061        let got = String::from(Code::AlreadyExists);
1062        let want = "ALREADY_EXISTS";
1063        assert_eq!(got, want);
1064    }
1065
1066    #[test_case("OK")]
1067    #[test_case("CANCELLED")]
1068    #[test_case("UNKNOWN")]
1069    #[test_case("INVALID_ARGUMENT")]
1070    #[test_case("DEADLINE_EXCEEDED")]
1071    #[test_case("NOT_FOUND")]
1072    #[test_case("ALREADY_EXISTS")]
1073    #[test_case("PERMISSION_DENIED")]
1074    #[test_case("RESOURCE_EXHAUSTED")]
1075    #[test_case("FAILED_PRECONDITION")]
1076    #[test_case("ABORTED")]
1077    #[test_case("OUT_OF_RANGE")]
1078    #[test_case("UNIMPLEMENTED")]
1079    #[test_case("INTERNAL")]
1080    #[test_case("UNAVAILABLE")]
1081    #[test_case("DATA_LOSS")]
1082    #[test_case("UNAUTHENTICATED")]
1083    fn code_roundtrip(input: &str) -> Result<()> {
1084        let code = Code::try_from(input).unwrap();
1085        let output = String::from(code);
1086        assert_eq!(output.as_str(), input.to_string());
1087        assert_eq!(&format!("{code}"), input);
1088        assert_eq!(code.name(), input);
1089        Ok(())
1090    }
1091
1092    #[test_case("OK")]
1093    #[test_case("CANCELLED")]
1094    #[test_case("UNKNOWN")]
1095    #[test_case("INVALID_ARGUMENT")]
1096    #[test_case("DEADLINE_EXCEEDED")]
1097    #[test_case("NOT_FOUND")]
1098    #[test_case("ALREADY_EXISTS")]
1099    #[test_case("PERMISSION_DENIED")]
1100    #[test_case("RESOURCE_EXHAUSTED")]
1101    #[test_case("FAILED_PRECONDITION")]
1102    #[test_case("ABORTED")]
1103    #[test_case("OUT_OF_RANGE")]
1104    #[test_case("UNIMPLEMENTED")]
1105    #[test_case("INTERNAL")]
1106    #[test_case("UNAVAILABLE")]
1107    #[test_case("DATA_LOSS")]
1108    #[test_case("UNAUTHENTICATED")]
1109    fn code_serialize_roundtrip(input: &str) -> Result<()> {
1110        let want = Code::try_from(input).unwrap();
1111        let serialized = serde_json::to_value(want)?;
1112        let got = serde_json::from_value::<Code>(serialized)?;
1113        assert_eq!(got, want);
1114        Ok(())
1115    }
1116
1117    #[test]
1118    fn code_try_from_string_error() {
1119        let err = Code::try_from("INVALID-NOT-A-CODE");
1120        assert!(
1121            matches!(&err, Err(s) if s.contains("INVALID-NOT-A-CODE")),
1122            "expected error in try_from, got {err:?}"
1123        );
1124    }
1125
1126    #[test]
1127    fn code_deserialize_invalid_type() {
1128        let input = json!({"k": "v"});
1129        let err = serde_json::from_value::<Code>(input);
1130        assert!(err.is_err(), "expected an error, got {err:?}");
1131    }
1132
1133    #[test]
1134    fn code_deserialize_unknown() -> Result<()> {
1135        let input = json!(-17);
1136        let code = serde_json::from_value::<Code>(input)?;
1137        assert_eq!(code, Code::Unknown);
1138        Ok(())
1139    }
1140}