1use std::fmt;
19
20pub struct Error {
22 kind: ErrorKind,
24
25 message: String,
27
28 source: Option<anyhow::Error>,
30
31 context: Vec<String>,
33
34 retryable: bool,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ErrorKind {
41 CredentialInvalid,
44
45 PermissionDenied,
48
49 ConfigInvalid,
52
53 RequestInvalid,
56
57 RateLimited,
60
61 Unexpected,
64}
65
66impl Error {
67 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
69 Self {
70 kind,
71 message: message.into(),
72 source: None,
73 context: Vec::new(),
74 retryable: kind.default_retryable(),
75 }
76 }
77
78 pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
80 self.source = Some(source.into());
81 self
82 }
83
84 pub fn with_context(mut self, context: impl fmt::Display) -> Self {
86 self.context.push(context.to_string());
87 self
88 }
89
90 pub fn set_retryable(mut self, retryable: bool) -> Self {
92 self.retryable = retryable;
93 self
94 }
95
96 pub fn kind(&self) -> ErrorKind {
98 self.kind
99 }
100
101 pub fn is_retryable(&self) -> bool {
103 self.retryable
104 }
105
106 pub fn context(&self) -> &[String] {
108 &self.context
109 }
110}
111
112impl ErrorKind {
113 fn default_retryable(&self) -> bool {
115 matches!(self, ErrorKind::RateLimited)
116 }
117}
118
119impl Error {
121 pub fn credential_invalid(message: impl Into<String>) -> Self {
123 Self::new(ErrorKind::CredentialInvalid, message)
124 }
125
126 pub fn permission_denied(message: impl Into<String>) -> Self {
128 Self::new(ErrorKind::PermissionDenied, message)
129 }
130
131 pub fn config_invalid(message: impl Into<String>) -> Self {
133 Self::new(ErrorKind::ConfigInvalid, message)
134 }
135
136 pub fn request_invalid(message: impl Into<String>) -> Self {
138 Self::new(ErrorKind::RequestInvalid, message)
139 }
140
141 pub fn rate_limited(message: impl Into<String>) -> Self {
143 Self::new(ErrorKind::RateLimited, message)
144 }
145
146 pub fn unexpected(message: impl Into<String>) -> Self {
148 Self::new(ErrorKind::Unexpected, message)
149 }
150}
151
152impl fmt::Display for Error {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 write!(f, "{}", self.message)
155 }
156}
157
158impl fmt::Debug for Error {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 let mut debug = f.debug_struct("Error");
162 debug.field("kind", &self.kind);
163 debug.field("message", &self.message);
164
165 if !self.context.is_empty() {
166 debug.field("context", &self.context);
167 }
168
169 if let Some(source) = &self.source {
170 debug.field("source", source);
171 }
172
173 debug.field("retryable", &self.retryable);
174 debug.finish()
175 }
176}
177
178impl std::error::Error for Error {
179 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
180 self.source.as_ref().map(|e| e.as_ref())
181 }
182}
183
184impl fmt::Display for ErrorKind {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 match self {
187 ErrorKind::CredentialInvalid => write!(f, "invalid credentials"),
188 ErrorKind::PermissionDenied => write!(f, "permission denied"),
189 ErrorKind::ConfigInvalid => write!(f, "invalid configuration"),
190 ErrorKind::RequestInvalid => write!(f, "invalid request"),
191 ErrorKind::RateLimited => write!(f, "rate limited"),
192 ErrorKind::Unexpected => write!(f, "unexpected error"),
193 }
194 }
195}
196
197pub type Result<T> = std::result::Result<T, Error>;
199
200impl From<anyhow::Error> for Error {
202 fn from(err: anyhow::Error) -> Self {
203 Self::unexpected(err.to_string()).with_source(err)
204 }
205}
206
207impl From<fmt::Error> for Error {
208 fn from(err: fmt::Error) -> Self {
209 Self::unexpected(err.to_string()).with_source(err)
210 }
211}
212
213impl From<http::Error> for Error {
214 fn from(err: http::Error) -> Self {
215 Self::request_invalid(err.to_string()).with_source(err)
216 }
217}
218
219impl From<http::header::InvalidHeaderValue> for Error {
220 fn from(err: http::header::InvalidHeaderValue) -> Self {
221 Self::request_invalid(err.to_string()).with_source(err)
222 }
223}
224
225impl From<http::uri::InvalidUri> for Error {
226 fn from(err: http::uri::InvalidUri) -> Self {
227 Self::request_invalid(err.to_string()).with_source(err)
228 }
229}
230
231impl From<http::uri::InvalidUriParts> for Error {
232 fn from(err: http::uri::InvalidUriParts) -> Self {
233 Self::request_invalid(err.to_string()).with_source(err)
234 }
235}
236
237impl From<std::string::FromUtf8Error> for Error {
238 fn from(err: std::string::FromUtf8Error) -> Self {
239 Self::unexpected(err.to_string()).with_source(err)
240 }
241}
242
243impl From<std::io::Error> for Error {
244 fn from(err: std::io::Error) -> Self {
245 use std::io::ErrorKind;
246
247 let kind = err.kind();
248 let message = err.to_string();
249 let source = anyhow::Error::from(err);
250
251 match kind {
252 ErrorKind::NotFound => Self::config_invalid(message).with_source(source),
253 ErrorKind::PermissionDenied => Self::permission_denied(message).with_source(source),
254 _ => Self::unexpected(message)
255 .with_source(source)
256 .set_retryable(matches!(
257 kind,
258 ErrorKind::TimedOut | ErrorKind::Interrupted | ErrorKind::ConnectionRefused
259 )),
260 }
261 }
262}
263
264impl From<http::header::InvalidHeaderName> for Error {
265 fn from(err: http::header::InvalidHeaderName) -> Self {
266 Self::request_invalid(err.to_string()).with_source(err)
267 }
268}
269
270impl From<http::header::ToStrError> for Error {
271 fn from(err: http::header::ToStrError) -> Self {
272 Self::request_invalid(err.to_string()).with_source(err)
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_error_creation() {
282 let err = Error::credential_invalid("token expired");
283 assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
284 assert!(!err.is_retryable());
285 }
286
287 #[test]
288 fn test_error_with_context() {
289 let err = Error::permission_denied("access denied")
290 .with_context("role: arn:aws:iam::123456789012:role/MyRole")
291 .with_context("operation: AssumeRole");
292
293 assert_eq!(err.context().len(), 2);
294 assert_eq!(
295 err.context()[0],
296 "role: arn:aws:iam::123456789012:role/MyRole"
297 );
298 assert_eq!(err.context()[1], "operation: AssumeRole");
299 }
300
301 #[test]
302 fn test_rate_limited_default_retryable() {
303 let err = Error::rate_limited("too many requests");
304 assert!(err.is_retryable());
305 }
306
307 #[test]
308 fn test_override_retryable() {
309 let err = Error::unexpected("network timeout").set_retryable(true);
310 assert!(err.is_retryable());
311
312 let err = Error::rate_limited("quota exceeded").set_retryable(false);
313 assert!(!err.is_retryable());
314 }
315
316 #[test]
317 fn test_error_debug_format() {
318 let err = Error::config_invalid("missing region")
319 .with_context("file: ~/.aws/config")
320 .with_context("profile: default");
321
322 let debug_str = format!("{:?}", err);
323 assert!(debug_str.contains("ConfigInvalid"));
324 assert!(debug_str.contains("missing region"));
325 assert!(debug_str.contains("~/.aws/config"));
326 }
327}