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}
24
25impl Error {
26 fn new(kind: Kind) -> Self {
27 Self {
28 inner: ErrorImpl { kind, source: None },
29 }
30 }
31
32 pub(crate) fn with(mut self, source: impl Into<Source>) -> Self {
33 self.inner.source = Some(source.into());
34 self
35 }
36
37 pub(crate) fn connect(source: impl Into<Source>) -> Self {
38 Error::new(Kind::Connect).with(source)
39 }
40
41 pub(crate) fn not_connected() -> Self {
42 Error::new(Kind::NotConnected)
43 }
44
45 pub(crate) fn request(source: impl Into<Source>) -> Self {
46 Error::new(Kind::Request).with(source)
47 }
48
49 pub(crate) fn conversion(source: impl Into<Source>) -> Self {
50 Error::new(Kind::Conversion).with(source)
51 }
52
53 pub(crate) fn event_stream_disconnect() -> Self {
54 Error::new(Kind::EventStreamDisconnect)
55 }
56
57 pub(crate) fn event_stream(source: impl Into<Source>) -> Self {
58 Error::new(Kind::EventStream).with(source)
59 }
60
61 pub fn is_version_mismatch(&self) -> bool {
64 if let Some(source) = &self.inner.source {
65 if let Some(status) = source.downcast_ref::<tonic::Status>() {
66 return status.code() == tonic::Code::FailedPrecondition
67 && status.message().contains("BUILD_VERSION_TOO_OLD");
68 }
69 }
70 false
71 }
72
73 fn description(&self) -> &str {
74 match &self.inner.kind {
75 Kind::Connect => "failed to connect to Ark server",
76 Kind::NotConnected => "no connection to Ark server",
77 Kind::Request => "request failed",
78 Kind::Conversion => "failed to convert between types",
79 Kind::EventStreamDisconnect => "got disconnected from event stream",
80 Kind::EventStream => "error via event stream",
81 }
82 }
83}
84
85impl fmt::Debug for Error {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 let mut f = f.debug_tuple("ark_grpc::Error");
88
89 f.field(&self.inner.kind);
90
91 if let Some(source) = &self.inner.source {
92 f.field(source);
93 }
94
95 f.finish()
96 }
97}
98
99impl fmt::Display for Error {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str(self.description())?;
102 if let Some(source) = self.source() {
103 f.write_str(&source.to_string())?;
104 }
105
106 Ok(())
107 }
108}
109
110impl StdError for Error {
111 fn source(&self) -> Option<&(dyn StdError + 'static)> {
112 self.inner
113 .source
114 .as_ref()
115 .map(|source| &**source as &(dyn StdError + 'static))
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn is_version_mismatch_true_for_matching_status() {
125 let status = tonic::Status::failed_precondition("BUILD_VERSION_TOO_OLD");
126 let err = Error::request(status);
127 assert!(err.is_version_mismatch());
128 }
129
130 #[test]
131 fn is_version_mismatch_false_for_other_failed_precondition() {
132 let status = tonic::Status::failed_precondition("something else");
133 let err = Error::request(status);
134 assert!(!err.is_version_mismatch());
135 }
136
137 #[test]
138 fn is_version_mismatch_false_for_other_code() {
139 let status = tonic::Status::internal("BUILD_VERSION_TOO_OLD");
140 let err = Error::request(status);
141 assert!(!err.is_version_mismatch());
142 }
143
144 #[test]
145 fn is_version_mismatch_false_for_non_tonic_error() {
146 let err = Error::request("some string error");
147 assert!(!err.is_version_mismatch());
148 }
149
150 #[test]
151 fn is_version_mismatch_false_when_no_source() {
152 let err = Error::not_connected();
153 assert!(!err.is_version_mismatch());
154 }
155}