1use crate::conversions::ConversionError;
2use std::error::Error as StdError;
3use std::fmt;
4
5type Source = Box<dyn StdError + Send + Sync + 'static>;
6
7pub struct Error {
8 inner: ErrorImpl,
9}
10
11struct ErrorImpl {
12 kind: Kind,
13 source: Option<Source>,
14}
15
16#[derive(Debug)]
17enum Kind {
18 Request,
19 Conversion,
20 ServerInfoChanged,
21}
22
23impl Error {
24 fn new(kind: Kind) -> Self {
25 Self {
26 inner: ErrorImpl { kind, source: None },
27 }
28 }
29
30 pub(crate) fn with(mut self, source: impl Into<Source>) -> Self {
31 self.inner.source = Some(source.into());
32 self
33 }
34
35 pub(crate) fn request(source: impl Into<Source>) -> Self {
36 Error::new(Kind::Request).with(source)
37 }
38
39 pub(crate) fn conversion(source: impl Into<Source>) -> Self {
40 Error::new(Kind::Conversion).with(source)
41 }
42
43 pub(crate) fn server_info_changed(source: impl Into<Source>) -> Self {
44 Error::new(Kind::ServerInfoChanged).with(source)
45 }
46
47 pub fn is_server_info_changed(&self) -> bool {
50 matches!(self.inner.kind, Kind::ServerInfoChanged)
51 }
52
53 pub fn is_version_mismatch(&self) -> bool {
56 self.source_contains_any(&["BUILD_VERSION_TOO_OLD"])
57 }
58
59 pub(crate) fn is_digest_mismatch(&self) -> bool {
62 matches!(self.inner.kind, Kind::Request)
63 && self.source_contains_any(&["DIGEST_MISMATCH", "invalid digest header"])
64 }
65
66 fn source_contains_any(&self, markers: &[&str]) -> bool {
67 if let Some(source) = &self.inner.source {
68 let display = source.to_string();
69 let debug = format!("{source:?}");
70 return markers
71 .iter()
72 .any(|marker| display.contains(marker) || debug.contains(marker));
73 }
74 false
75 }
76
77 fn description(&self) -> &str {
78 match &self.inner.kind {
79 Kind::Request => "request failed",
80 Kind::Conversion => "failed to convert between types",
81 Kind::ServerInfoChanged => "Ark server info changed while processing the request. Server info was refreshed, but the failed operation was not retried. Rebuild the request and retry if safe",
82 }
83 }
84}
85
86impl fmt::Debug for Error {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 let mut f = f.debug_tuple("ark_grpc::Error");
89
90 f.field(&self.inner.kind);
91
92 if let Some(source) = &self.inner.source {
93 f.field(source);
94 }
95
96 f.finish()
97 }
98}
99
100impl fmt::Display for Error {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(self.description())
103 }
104}
105
106impl StdError for Error {
107 fn source(&self) -> Option<&(dyn StdError + 'static)> {
108 self.inner
109 .source
110 .as_ref()
111 .map(|source| &**source as &(dyn StdError + 'static))
112 }
113}
114
115impl From<ConversionError> for Error {
116 fn from(value: ConversionError) -> Self {
117 Error::conversion(value)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn is_version_mismatch_true_when_source_contains_marker() {
127 let err = Error::request("BUILD_VERSION_TOO_OLD: upgrade your client");
128 assert!(err.is_version_mismatch());
129 }
130
131 #[test]
132 fn is_version_mismatch_false_for_other_errors() {
133 let err = Error::request("connection refused");
134 assert!(!err.is_version_mismatch());
135 }
136
137 #[test]
138 fn is_version_mismatch_false_when_no_source() {
139 let err = Error::new(Kind::Request);
140 assert!(!err.is_version_mismatch());
141 }
142
143 #[test]
144 fn is_digest_mismatch_true_when_source_contains_marker() {
145 let err = Error::request("DIGEST_MISMATCH: invalid digest header");
146 assert!(err.is_digest_mismatch());
147 }
148
149 #[test]
150 fn is_digest_mismatch_true_when_generated_error_content_contains_marker() {
151 let source = crate::apis::Error::<()>::ResponseError(crate::apis::ResponseContent {
152 status: reqwest::StatusCode::PRECONDITION_FAILED,
153 content: "DIGEST_MISMATCH: invalid digest header".to_string(),
154 entity: None,
155 });
156 let err = Error::request(source);
157 assert!(err.is_digest_mismatch());
158 }
159
160 #[test]
161 fn is_digest_mismatch_false_for_other_errors() {
162 let err = Error::request("connection refused");
163 assert!(!err.is_digest_mismatch());
164 }
165
166 #[test]
167 fn is_server_info_changed_true_for_server_info_changed_kind() {
168 let err = Error::server_info_changed("DIGEST_MISMATCH");
169 assert!(err.is_server_info_changed());
170 }
171
172 #[test]
173 fn server_info_changed_is_not_classified_as_digest_mismatch() {
174 let err = Error::server_info_changed(Error::request("DIGEST_MISMATCH"));
175 assert!(err.is_server_info_changed());
176 assert!(!err.is_digest_mismatch());
177 }
178
179 #[test]
180 fn is_server_info_changed_false_for_other_errors() {
181 let err = Error::request("DIGEST_MISMATCH");
182 assert!(!err.is_server_info_changed());
183 }
184}