1use std::convert::Infallible;
8use std::fmt;
9use std::time::Duration;
10
11use http::{StatusCode, Uri};
12
13pub type Result<T, E = Error> = std::result::Result<T, E>;
15
16pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum ErrorKind {
24 Invalid,
26 Http,
29 Rejected,
32 Timeout,
34 Backend,
36 Worker,
38 Dataset,
40 Frontier,
42 Cancelled,
44}
45
46impl ErrorKind {
47 #[must_use]
49 pub const fn as_str(self) -> &'static str {
50 match self {
51 Self::Invalid => "invalid",
52 Self::Http => "http",
53 Self::Rejected => "rejected",
54 Self::Timeout => "timeout",
55 Self::Backend => "backend",
56 Self::Worker => "worker",
57 Self::Dataset => "dataset",
58 Self::Frontier => "frontier",
59 Self::Cancelled => "cancelled",
60 }
61 }
62
63 #[must_use]
66 pub const fn is_retryable(self) -> bool {
67 matches!(self, Self::Http | Self::Timeout | Self::Backend)
68 }
69}
70
71impl fmt::Display for ErrorKind {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.write_str(self.as_str())
74 }
75}
76
77#[derive(Debug, thiserror::Error)]
80pub struct Error {
81 kind: ErrorKind,
82 message: String,
83 status: Option<StatusCode>,
84 retry_after: Option<Duration>,
85 uri: Option<Box<Uri>>,
86 #[source]
87 source: Option<BoxError>,
88}
89
90impl Error {
91 #[must_use]
93 pub fn new(kind: ErrorKind) -> Self {
94 Self {
95 kind,
96 message: String::new(),
97 status: None,
98 retry_after: None,
99 uri: None,
100 source: None,
101 }
102 }
103
104 pub fn from_boxed(kind: ErrorKind, error: impl Into<BoxError>) -> Self {
109 match error.into().downcast::<Self>() {
110 Ok(error) => *error,
111 Err(error) => Self::new(kind).with_source(error),
112 }
113 }
114
115 pub fn invalid(message: impl Into<String>) -> Self {
117 Self::new(ErrorKind::Invalid).with_message(message)
118 }
119
120 pub fn backend(message: impl Into<String>) -> Self {
122 Self::new(ErrorKind::Backend).with_message(message)
123 }
124
125 pub fn worker(message: impl Into<String>) -> Self {
127 Self::new(ErrorKind::Worker).with_message(message)
128 }
129
130 #[must_use]
132 pub fn with_message(mut self, message: impl Into<String>) -> Self {
133 self.message = message.into();
134 self
135 }
136
137 #[must_use]
139 pub fn with_source(mut self, source: impl Into<BoxError>) -> Self {
140 self.source = Some(source.into());
141 self
142 }
143
144 #[must_use]
146 pub const fn with_status(mut self, status: StatusCode) -> Self {
147 self.status = Some(status);
148 self
149 }
150
151 #[must_use]
153 pub fn with_uri(mut self, uri: Uri) -> Self {
154 self.uri = Some(Box::new(uri));
155 self
156 }
157
158 #[must_use]
161 pub const fn with_retry_after(mut self, delay: Duration) -> Self {
162 self.retry_after = Some(delay);
163 self
164 }
165
166 #[must_use]
168 pub const fn kind(&self) -> ErrorKind {
169 self.kind
170 }
171
172 #[must_use]
174 pub fn message(&self) -> &str {
175 &self.message
176 }
177
178 #[must_use]
180 pub const fn status(&self) -> Option<StatusCode> {
181 self.status
182 }
183
184 #[must_use]
186 pub fn uri(&self) -> Option<&Uri> {
187 self.uri.as_deref()
188 }
189
190 #[must_use]
192 pub const fn retry_after(&self) -> Option<Duration> {
193 self.retry_after
194 }
195
196 #[must_use]
198 pub const fn is_retryable(&self) -> bool {
199 self.kind.is_retryable()
200 }
201}
202
203impl fmt::Display for Error {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 match (self.message.is_empty(), &self.source) {
206 (false, _) => f.write_str(&self.message),
207 (true, Some(source)) => write!(f, "{}: {source}", self.kind),
208 (true, None) => write!(f, "{} error", self.kind),
209 }
210 }
211}
212
213impl From<Infallible> for Error {
214 fn from(never: Infallible) -> Self {
215 match never {}
216 }
217}
218
219impl From<http::Error> for Error {
220 fn from(error: http::Error) -> Self {
221 Self::invalid("invalid request").with_source(error)
222 }
223}
224
225impl From<http::uri::InvalidUri> for Error {
226 fn from(error: http::uri::InvalidUri) -> Self {
227 Self::invalid("invalid uri").with_source(error)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use std::error::Error as _;
234
235 use super::*;
236
237 #[test]
238 fn display_prefers_message() {
239 let error = Error::backend("pool exhausted");
240 assert_eq!(error.to_string(), "pool exhausted");
241 assert_eq!(Error::new(ErrorKind::Timeout).to_string(), "timeout error");
242 }
243
244 #[test]
245 fn from_boxed_keeps_existing_kind() {
246 let inner = Error::new(ErrorKind::Timeout);
247 let error = Error::from_boxed(ErrorKind::Backend, inner);
248 assert_eq!(error.kind(), ErrorKind::Timeout);
249
250 let error = Error::from_boxed(ErrorKind::Backend, std::io::Error::other("io"));
251 assert_eq!(error.kind(), ErrorKind::Backend);
252 assert!(error.source().is_some());
253 }
254
255 #[test]
256 fn retryable_kinds() {
257 assert!(Error::new(ErrorKind::Http).is_retryable());
258 assert!(!Error::new(ErrorKind::Rejected).is_retryable());
259 assert!(!Error::invalid("bad").is_retryable());
260 }
261}