1use crate::error::Error;
16use google_cloud_rpc::model::ErrorInfo;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20
21#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
29#[serde(default, rename_all = "camelCase")]
30#[non_exhaustive]
31pub struct Status {
32 pub code: Code,
34
35 pub message: String,
39
40 pub details: Vec<StatusDetails>,
43}
44
45impl Status {
46 pub fn set_code<T: Into<Code>>(mut self, v: T) -> Self {
48 self.code = v.into();
49 self
50 }
51
52 pub fn set_message<T: Into<String>>(mut self, v: T) -> Self {
54 self.message = v.into();
55 self
56 }
57
58 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#[derive(Clone, Copy, Debug, Default, PartialEq)]
76#[non_exhaustive]
77pub enum Code {
78 Ok = 0,
82
83 Cancelled = 1,
87
88 #[default]
96 Unknown = 2,
97
98 InvalidArgument = 3,
105
106 DeadlineExceeded = 4,
114
115 NotFound = 5,
125
126 AlreadyExists = 6,
131
132 PermissionDenied = 7,
143
144 ResourceExhausted = 8,
149
150 FailedPrecondition = 9,
169
170 Aborted = 10,
180
181 OutOfRange = 11,
199
200 Unimplemented = 12,
205
206 Internal = 13,
212
213 Unavailable = 14,
223
224 DataLoss = 15,
228
229 Unauthenticated = 16,
234}
235
236impl Code {
237 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#[derive(Clone, Debug, Deserialize)]
344struct ErrorWrapper {
345 error: WrapperStatus,
346}
347
348#[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#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
449#[serde(rename_all = "camelCase")]
450#[non_exhaustive]
451#[serde(tag = "@type")]
452pub enum StatusDetails {
454 #[serde(rename = "type.googleapis.com/google.rpc.BadRequest")]
458 BadRequest(google_cloud_rpc::model::BadRequest),
459
460 #[serde(rename = "type.googleapis.com/google.rpc.DebugInfo")]
464 DebugInfo(google_cloud_rpc::model::DebugInfo),
465
466 #[serde(rename = "type.googleapis.com/google.rpc.ErrorInfo")]
470 ErrorInfo(google_cloud_rpc::model::ErrorInfo),
471
472 #[serde(rename = "type.googleapis.com/google.rpc.Help")]
476 Help(google_cloud_rpc::model::Help),
477
478 #[serde(rename = "type.googleapis.com/google.rpc.LocalizedMessage")]
482 LocalizedMessage(google_cloud_rpc::model::LocalizedMessage),
483
484 #[serde(rename = "type.googleapis.com/google.rpc.PreconditionFailure")]
488 PreconditionFailure(google_cloud_rpc::model::PreconditionFailure),
489
490 #[serde(rename = "type.googleapis.com/google.rpc.QuotaFailure")]
494 QuotaFailure(google_cloud_rpc::model::QuotaFailure),
495
496 #[serde(rename = "type.googleapis.com/google.rpc.RequestInfo")]
500 RequestInfo(google_cloud_rpc::model::RequestInfo),
501
502 #[serde(rename = "type.googleapis.com/google.rpc.ResourceInfo")]
506 ResourceInfo(google_cloud_rpc::model::ResourceInfo),
507
508 #[serde(rename = "type.googleapis.com/google.rpc.RetryInfo")]
512 RetryInfo(google_cloud_rpc::model::RetryInfo),
513
514 #[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(×tamp)?;
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(×tamp)?;
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 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 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}