1use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use crate::capability::Capability;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
38#[error("{kind}: {message}")]
39pub struct BackendError {
40 pub kind: BackendErrorKind,
42 pub message: String,
44 pub retryable: bool,
46}
47
48impl BackendError {
49 #[must_use]
51 pub fn new(kind: BackendErrorKind, message: impl Into<String>) -> Self {
52 let retryable = kind.is_retryable();
53 Self {
54 kind,
55 message: message.into(),
56 retryable,
57 }
58 }
59
60 #[must_use]
62 pub fn with_retryable(
63 kind: BackendErrorKind,
64 message: impl Into<String>,
65 retryable: bool,
66 ) -> Self {
67 Self {
68 kind,
69 message: message.into(),
70 retryable,
71 }
72 }
73
74 #[must_use]
76 pub fn is_retryable(&self) -> bool {
77 self.retryable
78 }
79
80 #[must_use]
84 pub fn auth(message: impl Into<String>) -> Self {
85 Self::new(BackendErrorKind::Authentication, message)
86 }
87
88 #[must_use]
90 pub fn rate_limit(message: impl Into<String>) -> Self {
91 Self::new(BackendErrorKind::RateLimit, message)
92 }
93
94 #[must_use]
96 pub fn invalid_request(message: impl Into<String>) -> Self {
97 Self::new(BackendErrorKind::InvalidRequest, message)
98 }
99
100 #[must_use]
102 pub fn unavailable(message: impl Into<String>) -> Self {
103 Self::new(BackendErrorKind::Unavailable, message)
104 }
105
106 #[must_use]
108 pub fn network(message: impl Into<String>) -> Self {
109 Self::new(BackendErrorKind::Network, message)
110 }
111
112 #[must_use]
114 pub fn backend(message: impl Into<String>) -> Self {
115 Self::new(BackendErrorKind::BackendError, message)
116 }
117
118 #[must_use]
120 pub fn parse(message: impl Into<String>) -> Self {
121 Self::new(BackendErrorKind::ParseError, message)
122 }
123
124 #[must_use]
126 pub fn timeout(message: impl Into<String>) -> Self {
127 Self::new(BackendErrorKind::Timeout, message)
128 }
129
130 #[must_use]
132 pub fn unsupported(capability: &Capability) -> Self {
133 Self::new(
134 BackendErrorKind::UnsupportedCapability,
135 format!("capability not supported: {capability}"),
136 )
137 }
138
139 #[must_use]
141 pub fn resource_exhausted(message: impl Into<String>) -> Self {
142 Self::new(BackendErrorKind::ResourceExhausted, message)
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub enum BackendErrorKind {
151 Authentication,
153 RateLimit,
155 InvalidRequest,
157 Unavailable,
159 Network,
161 BackendError,
163 ParseError,
165 Timeout,
167 UnsupportedCapability,
169 ResourceExhausted,
171 Configuration,
173}
174
175impl BackendErrorKind {
176 #[must_use]
178 pub fn is_retryable(self) -> bool {
179 matches!(
180 self,
181 Self::RateLimit | Self::Unavailable | Self::Network | Self::Timeout
182 )
183 }
184}
185
186impl std::fmt::Display for BackendErrorKind {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::Authentication => write!(f, "authentication"),
190 Self::RateLimit => write!(f, "rate_limit"),
191 Self::InvalidRequest => write!(f, "invalid_request"),
192 Self::Unavailable => write!(f, "unavailable"),
193 Self::Network => write!(f, "network"),
194 Self::BackendError => write!(f, "backend_error"),
195 Self::ParseError => write!(f, "parse_error"),
196 Self::Timeout => write!(f, "timeout"),
197 Self::UnsupportedCapability => write!(f, "unsupported_capability"),
198 Self::ResourceExhausted => write!(f, "resource_exhausted"),
199 Self::Configuration => write!(f, "configuration"),
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn new_sets_retryable_from_kind() {
210 let err = BackendError::new(BackendErrorKind::RateLimit, "slow down");
211 assert_eq!(err.kind, BackendErrorKind::RateLimit);
212 assert_eq!(err.message, "slow down");
213 assert!(err.is_retryable());
214
215 let err = BackendError::new(BackendErrorKind::InvalidRequest, "bad");
216 assert!(!err.is_retryable());
217 }
218
219 #[test]
220 fn with_retryable_overrides_default() {
221 let err = BackendError::with_retryable(BackendErrorKind::InvalidRequest, "retry me", true);
222 assert!(err.is_retryable());
223
224 let err = BackendError::with_retryable(BackendErrorKind::RateLimit, "no retry", false);
225 assert!(!err.is_retryable());
226 }
227
228 #[test]
229 fn convenience_auth() {
230 let err = BackendError::auth("denied");
231 assert_eq!(err.kind, BackendErrorKind::Authentication);
232 assert!(!err.is_retryable());
233 }
234
235 #[test]
236 fn convenience_rate_limit() {
237 let err = BackendError::rate_limit("quota exceeded");
238 assert_eq!(err.kind, BackendErrorKind::RateLimit);
239 assert!(err.is_retryable());
240 }
241
242 #[test]
243 fn convenience_invalid_request() {
244 let err = BackendError::invalid_request("missing field");
245 assert_eq!(err.kind, BackendErrorKind::InvalidRequest);
246 assert!(!err.is_retryable());
247 }
248
249 #[test]
250 fn convenience_unavailable() {
251 let err = BackendError::unavailable("down");
252 assert_eq!(err.kind, BackendErrorKind::Unavailable);
253 assert!(err.is_retryable());
254 }
255
256 #[test]
257 fn convenience_network() {
258 let err = BackendError::network("connection refused");
259 assert_eq!(err.kind, BackendErrorKind::Network);
260 assert!(err.is_retryable());
261 }
262
263 #[test]
264 fn convenience_backend() {
265 let err = BackendError::backend("500");
266 assert_eq!(err.kind, BackendErrorKind::BackendError);
267 assert!(!err.is_retryable());
268 }
269
270 #[test]
271 fn convenience_parse() {
272 let err = BackendError::parse("invalid json");
273 assert_eq!(err.kind, BackendErrorKind::ParseError);
274 assert!(!err.is_retryable());
275 }
276
277 #[test]
278 fn convenience_timeout() {
279 let err = BackendError::timeout("10s elapsed");
280 assert_eq!(err.kind, BackendErrorKind::Timeout);
281 assert!(err.is_retryable());
282 }
283
284 #[test]
285 fn convenience_unsupported() {
286 let err = BackendError::unsupported(&Capability::ImageUnderstanding);
287 assert_eq!(err.kind, BackendErrorKind::UnsupportedCapability);
288 assert!(!err.is_retryable());
289 assert!(err.message.contains("ImageUnderstanding"));
290 }
291
292 #[test]
293 fn convenience_resource_exhausted() {
294 let err = BackendError::resource_exhausted("out of memory");
295 assert_eq!(err.kind, BackendErrorKind::ResourceExhausted);
296 assert!(!err.is_retryable());
297 }
298
299 #[test]
300 fn kind_is_retryable() {
301 assert!(!BackendErrorKind::Authentication.is_retryable());
302 assert!(BackendErrorKind::RateLimit.is_retryable());
303 assert!(!BackendErrorKind::InvalidRequest.is_retryable());
304 assert!(BackendErrorKind::Unavailable.is_retryable());
305 assert!(BackendErrorKind::Network.is_retryable());
306 assert!(!BackendErrorKind::BackendError.is_retryable());
307 assert!(!BackendErrorKind::ParseError.is_retryable());
308 assert!(BackendErrorKind::Timeout.is_retryable());
309 assert!(!BackendErrorKind::UnsupportedCapability.is_retryable());
310 assert!(!BackendErrorKind::ResourceExhausted.is_retryable());
311 assert!(!BackendErrorKind::Configuration.is_retryable());
312 }
313
314 #[test]
315 fn kind_display() {
316 assert_eq!(
317 BackendErrorKind::Authentication.to_string(),
318 "authentication"
319 );
320 assert_eq!(BackendErrorKind::RateLimit.to_string(), "rate_limit");
321 assert_eq!(
322 BackendErrorKind::InvalidRequest.to_string(),
323 "invalid_request"
324 );
325 assert_eq!(BackendErrorKind::Unavailable.to_string(), "unavailable");
326 assert_eq!(BackendErrorKind::Network.to_string(), "network");
327 assert_eq!(BackendErrorKind::BackendError.to_string(), "backend_error");
328 assert_eq!(BackendErrorKind::ParseError.to_string(), "parse_error");
329 assert_eq!(BackendErrorKind::Timeout.to_string(), "timeout");
330 assert_eq!(
331 BackendErrorKind::UnsupportedCapability.to_string(),
332 "unsupported_capability"
333 );
334 assert_eq!(
335 BackendErrorKind::ResourceExhausted.to_string(),
336 "resource_exhausted"
337 );
338 assert_eq!(BackendErrorKind::Configuration.to_string(), "configuration");
339 }
340
341 #[test]
342 fn backend_error_display() {
343 let err = BackendError::new(BackendErrorKind::Timeout, "operation timed out");
344 assert_eq!(err.to_string(), "timeout: operation timed out");
345 }
346}