1use thiserror::Error;
7
8#[derive(Error, Debug)]
10pub enum Error {
11 #[error("HTTP error: {0}")]
16 Http(String),
17
18 #[error("Network error: {0}")]
20 Network(String),
21
22 #[error("Request timeout")]
23 Timeout,
24
25 #[error("Unauthorized: {0}")]
30 Unauthorized(String),
31
32 #[error("Forbidden: {0}")]
34 Forbidden(String),
35
36 #[error("API error ({status}): {message}")]
41 Api {
42 status: u16,
44 message: String,
46 },
47
48 #[error("Not found: {0}")]
50 NotFound(String),
51
52 #[error("Rate limit exceeded: retry after {retry_after:?}s")]
54 RateLimited {
55 retry_after: Option<u64>,
57 },
58
59 #[error("Server error ({status}): {message}")]
61 ServerError {
62 status: u16,
64 message: String,
66 },
67
68 #[error("Serialization error: {0}")]
73 Serialization(#[from] serde_json::Error),
74
75 #[error("Invalid data: {0}")]
77 InvalidData(String),
78
79 #[error("Configuration error: {0}")]
84 Config(String),
85
86 #[error("Missing configuration: {0}")]
88 MissingConfig(String),
89
90 #[error("Storage error: {0}")]
95 Storage(String),
96
97 #[error("Credential not found: {provider}/{key}")]
99 CredentialNotFound { provider: String, key: String },
100
101 #[error("Provider not found: {0}")]
106 ProviderNotFound(String),
107
108 #[error("Provider '{provider}' does not support: {operation}")]
110 ProviderUnsupported {
111 provider: String,
112 operation: String,
114 },
115
116 #[error("IO error: {0}")]
121 Io(#[from] std::io::Error),
122
123 #[error("{0}")]
125 Other(#[from] anyhow::Error),
126}
127
128impl Error {
129 pub fn from_status(status: u16, message: impl Into<String>) -> Self {
131 let message = message.into();
132 match status {
133 401 => Error::Unauthorized(message),
134 403 => Error::Forbidden(message),
135 404 => Error::NotFound(message),
136 429 => Error::RateLimited { retry_after: None },
137 500..=599 => Error::ServerError { status, message },
138 _ => Error::Api { status, message },
139 }
140 }
141
142 pub fn is_retryable(&self) -> bool {
144 matches!(
145 self,
146 Error::Timeout
147 | Error::Network(_)
148 | Error::RateLimited { .. }
149 | Error::ServerError { .. }
150 )
151 }
152
153 pub fn is_auth_error(&self) -> bool {
155 matches!(self, Error::Unauthorized(_) | Error::Forbidden(_))
156 }
157}
158
159pub type Result<T> = std::result::Result<T, Error>;
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_from_status() {
168 assert!(matches!(
169 Error::from_status(401, "test"),
170 Error::Unauthorized(_)
171 ));
172 assert!(matches!(
173 Error::from_status(403, "test"),
174 Error::Forbidden(_)
175 ));
176 assert!(matches!(
177 Error::from_status(404, "test"),
178 Error::NotFound(_)
179 ));
180 assert!(matches!(
181 Error::from_status(429, "test"),
182 Error::RateLimited { .. }
183 ));
184 assert!(matches!(
185 Error::from_status(500, "test"),
186 Error::ServerError { .. }
187 ));
188 assert!(matches!(Error::from_status(400, "test"), Error::Api { .. }));
189 }
190
191 #[test]
192 fn test_is_retryable() {
193 assert!(Error::Timeout.is_retryable());
194 assert!(Error::Network("test".into()).is_retryable());
195 assert!(Error::RateLimited { retry_after: None }.is_retryable());
196 assert!(
197 Error::ServerError {
198 status: 500,
199 message: "test".into()
200 }
201 .is_retryable()
202 );
203 assert!(!Error::Unauthorized("test".into()).is_retryable());
204 assert!(!Error::NotFound("test".into()).is_retryable());
205 }
206
207 #[test]
208 fn test_is_auth_error() {
209 assert!(Error::Unauthorized("test".into()).is_auth_error());
210 assert!(Error::Forbidden("test".into()).is_auth_error());
211 assert!(!Error::NotFound("test".into()).is_auth_error());
212 }
213}