1use thiserror::Error;
34
35pub type Result<T> = std::result::Result<T, Error>;
37
38#[derive(Error, Debug)]
40pub enum Error {
41 #[error("http {status}: {category} - {message} (req={request_id:?})")]
43 Http {
44 status: u16,
46 category: String,
48 message: String,
50 request_id: Option<String>,
52 },
53
54 #[error("deserialize: {0}")]
56 Deserialize(String),
57
58 #[error("network: {0}")]
60 Network(String),
61
62 #[error("timeout")]
64 Timeout,
65
66 #[error("config: {0}")]
68 Config(String),
69
70 #[error("other: {0}")]
72 Other(String),
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum ErrorKind {
78 Auth,
80 Validation,
82 NotFound,
84 RateLimit,
86 Timeout,
88 Internal,
90 ServiceUnavailable,
92 Crypto,
94 Config,
96 Other,
98}
99
100impl ErrorKind {
101 pub fn from_category(category: &str) -> Self {
103 match category {
104 "auth" => ErrorKind::Auth,
105 "validation" => ErrorKind::Validation,
106 "not_found" => ErrorKind::NotFound,
107 "rate_limit" => ErrorKind::RateLimit,
108 "timeout" => ErrorKind::Timeout,
109 "internal" => ErrorKind::Internal,
110 "service" => ErrorKind::ServiceUnavailable,
111 "crypto" => ErrorKind::Crypto,
112 "config" => ErrorKind::Config,
113 _ => ErrorKind::Other,
114 }
115 }
116}
117
118impl Error {
119 pub fn kind(&self) -> ErrorKind {
121 match self {
122 Error::Http { category, .. } => ErrorKind::from_category(category),
123 Error::Timeout => ErrorKind::Timeout,
124 Error::Config(_) => ErrorKind::Config,
125 _ => ErrorKind::Other,
126 }
127 }
128
129 pub fn is_retryable(&self) -> bool {
131 match self {
132 Error::Http { status, .. } => matches!(status, 429 | 500 | 502 | 503 | 504),
133 Error::Network(_) => true,
134 Error::Timeout => true,
135 _ => false,
136 }
137 }
138
139 pub fn status_code(&self) -> Option<u16> {
141 match self {
142 Error::Http { status, .. } => Some(*status),
143 _ => None,
144 }
145 }
146
147 pub fn request_id(&self) -> Option<&str> {
149 match self {
150 Error::Http { request_id, .. } => request_id.as_deref(),
151 _ => None,
152 }
153 }
154
155 pub(crate) fn from_response(
157 status: u16,
158 error: &str,
159 message: &str,
160 request_id: Option<String>,
161 ) -> Self {
162 Error::Http {
163 status,
164 category: error.to_string(),
165 message: message.to_string(),
166 request_id,
167 }
168 }
169}
170
171#[derive(Debug, serde::Deserialize)]
173pub(crate) struct ErrorResponse {
174 pub error: String,
175 pub message: String,
176 #[allow(dead_code)]
177 pub timestamp: String,
178 pub status: u16,
179}
180
181impl From<reqwest::Error> for Error {
182 fn from(err: reqwest::Error) -> Self {
183 if err.is_timeout() {
184 Error::Timeout
185 } else if err.is_connect() || err.is_request() {
186 Error::Network(err.to_string())
187 } else if err.is_decode() {
188 Error::Deserialize(err.to_string())
189 } else {
190 Error::Other(err.to_string())
191 }
192 }
193}
194
195impl From<serde_json::Error> for Error {
196 fn from(err: serde_json::Error) -> Self {
197 Error::Deserialize(err.to_string())
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_error_kind_from_category() {
207 assert_eq!(ErrorKind::from_category("auth"), ErrorKind::Auth);
208 assert_eq!(
209 ErrorKind::from_category("validation"),
210 ErrorKind::Validation
211 );
212 assert_eq!(ErrorKind::from_category("not_found"), ErrorKind::NotFound);
213 assert_eq!(ErrorKind::from_category("unknown"), ErrorKind::Other);
214 }
215
216 #[test]
217 fn test_error_is_retryable() {
218 let err = Error::Http {
219 status: 429,
220 category: "rate_limit".to_string(),
221 message: "Too many requests".to_string(),
222 request_id: Some("req-123".to_string()),
223 };
224 assert!(err.is_retryable());
225
226 let err = Error::Http {
227 status: 404,
228 category: "not_found".to_string(),
229 message: "Secret not found".to_string(),
230 request_id: None,
231 };
232 assert!(!err.is_retryable());
233
234 let err = Error::Network("Connection failed".to_string());
235 assert!(err.is_retryable());
236
237 let err = Error::Config("Invalid URL".to_string());
238 assert!(!err.is_retryable());
239 }
240
241 #[test]
242 fn test_error_status_code() {
243 let err = Error::Http {
244 status: 401,
245 category: "auth".to_string(),
246 message: "Unauthorized".to_string(),
247 request_id: None,
248 };
249 assert_eq!(err.status_code(), Some(401));
250
251 let err = Error::Timeout;
252 assert_eq!(err.status_code(), None);
253 }
254
255 #[test]
256 fn test_error_request_id() {
257 let err = Error::Http {
258 status: 500,
259 category: "internal".to_string(),
260 message: "Server error".to_string(),
261 request_id: Some("req-456".to_string()),
262 };
263 assert_eq!(err.request_id(), Some("req-456"));
264
265 let err = Error::Network("Failed".to_string());
266 assert_eq!(err.request_id(), None);
267 }
268}