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