1use std::error::Error as StdError;
2use std::fmt;
3
4type Source = Box<dyn StdError + Send + Sync + 'static>;
5
6pub struct Error {
7 inner: ErrorImpl,
8}
9
10struct ErrorImpl {
11 kind: Kind,
12 source: Option<Source>,
13}
14
15#[derive(Debug)]
16enum Kind {
17 Connect,
18 NotConnected,
19 Request,
20 Conversion,
21 EventStreamDisconnect,
22 EventStream,
23 ServerInfoChanged,
24}
25
26impl Error {
27 fn new(kind: Kind) -> Self {
28 Self {
29 inner: ErrorImpl { kind, source: None },
30 }
31 }
32
33 pub(crate) fn with(mut self, source: impl Into<Source>) -> Self {
34 self.inner.source = Some(source.into());
35 self
36 }
37
38 pub(crate) fn connect(source: impl Into<Source>) -> Self {
39 Error::new(Kind::Connect).with(source)
40 }
41
42 pub(crate) fn not_connected() -> Self {
43 Error::new(Kind::NotConnected)
44 }
45
46 pub(crate) fn request(source: impl Into<Source>) -> Self {
47 Error::new(Kind::Request).with(source)
48 }
49
50 pub(crate) fn conversion(source: impl Into<Source>) -> Self {
51 Error::new(Kind::Conversion).with(source)
52 }
53
54 pub(crate) fn event_stream_disconnect() -> Self {
55 Error::new(Kind::EventStreamDisconnect)
56 }
57
58 pub(crate) fn event_stream(source: impl Into<Source>) -> Self {
59 Error::new(Kind::EventStream).with(source)
60 }
61
62 pub(crate) fn server_info_changed(source: impl Into<Source>) -> Self {
63 Error::new(Kind::ServerInfoChanged).with(source)
64 }
65
66 pub fn is_server_info_changed(&self) -> bool {
67 matches!(self.inner.kind, Kind::ServerInfoChanged)
68 }
69
70 pub fn is_version_mismatch(&self) -> bool {
73 if let Some(source) = &self.inner.source {
74 if let Some(status) = source.downcast_ref::<tonic::Status>() {
75 return status.code() == tonic::Code::FailedPrecondition
76 && status.message().contains("BUILD_VERSION_TOO_OLD");
77 }
78 }
79 false
80 }
81
82 pub(crate) fn is_digest_mismatch(&self) -> bool {
85 if let Some(source) = &self.inner.source {
86 if let Some(status) = source.downcast_ref::<tonic::Status>() {
87 return status.code() == tonic::Code::FailedPrecondition
88 && (status.message().contains("DIGEST_MISMATCH")
89 || status.message().contains("invalid digest header"));
90 }
91 }
92 false
93 }
94
95 fn description(&self) -> &str {
96 match &self.inner.kind {
97 Kind::Connect => "failed to connect to Ark server",
98 Kind::NotConnected => "no connection to Ark server",
99 Kind::Request => "request failed",
100 Kind::Conversion => "failed to convert between types",
101 Kind::EventStreamDisconnect => "got disconnected from event stream",
102 Kind::EventStream => "error via event stream",
103 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",
104 }
105 }
106}
107
108impl fmt::Debug for Error {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 let mut f = f.debug_tuple("ark_grpc::Error");
111
112 f.field(&self.inner.kind);
113
114 if let Some(source) = &self.inner.source {
115 f.field(source);
116 }
117
118 f.finish()
119 }
120}
121
122impl fmt::Display for Error {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.write_str(self.description())?;
125 if let Some(source) = self.source() {
126 f.write_str(&source.to_string())?;
127 }
128
129 Ok(())
130 }
131}
132
133impl StdError for Error {
134 fn source(&self) -> Option<&(dyn StdError + 'static)> {
135 self.inner
136 .source
137 .as_ref()
138 .map(|source| &**source as &(dyn StdError + 'static))
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn is_version_mismatch_true_for_matching_status() {
148 let status = tonic::Status::failed_precondition("BUILD_VERSION_TOO_OLD");
149 let err = Error::request(status);
150 assert!(err.is_version_mismatch());
151 }
152
153 #[test]
154 fn is_version_mismatch_false_for_other_failed_precondition() {
155 let status = tonic::Status::failed_precondition("something else");
156 let err = Error::request(status);
157 assert!(!err.is_version_mismatch());
158 }
159
160 #[test]
161 fn is_version_mismatch_false_for_other_code() {
162 let status = tonic::Status::internal("BUILD_VERSION_TOO_OLD");
163 let err = Error::request(status);
164 assert!(!err.is_version_mismatch());
165 }
166
167 #[test]
168 fn is_version_mismatch_false_for_non_tonic_error() {
169 let err = Error::request("some string error");
170 assert!(!err.is_version_mismatch());
171 }
172
173 #[test]
174 fn is_version_mismatch_false_when_no_source() {
175 let err = Error::not_connected();
176 assert!(!err.is_version_mismatch());
177 }
178
179 #[test]
180 fn is_digest_mismatch_true_for_matching_status() {
181 let status = tonic::Status::failed_precondition("invalid digest header");
182 let err = Error::request(status);
183 assert!(err.is_digest_mismatch());
184 }
185
186 #[test]
187 fn is_digest_mismatch_true_for_error_code_in_message() {
188 let status = tonic::Status::failed_precondition("DIGEST_MISMATCH");
189 let err = Error::request(status);
190 assert!(err.is_digest_mismatch());
191 }
192
193 #[test]
194 fn is_digest_mismatch_false_for_other_failed_precondition() {
195 let status = tonic::Status::failed_precondition("something else");
196 let err = Error::request(status);
197 assert!(!err.is_digest_mismatch());
198 }
199}