1use std::fmt;
2use thiserror::Error;
3
4#[derive(Error)]
6#[error("{message}")]
7pub struct Error {
8 kind: ErrorKind,
10
11 message: String,
13
14 #[source]
16 source: Option<anyhow::Error>,
17
18 context: Vec<String>,
20
21 retryable: bool,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ErrorKind {
28 CredentialInvalid,
31
32 PermissionDenied,
35
36 ConfigInvalid,
39
40 RequestInvalid,
43
44 RateLimited,
47
48 Unexpected,
51}
52
53impl Error {
54 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
56 Self {
57 kind,
58 message: message.into(),
59 source: None,
60 context: Vec::new(),
61 retryable: kind.default_retryable(),
62 }
63 }
64
65 pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
67 self.source = Some(source.into());
68 self
69 }
70
71 pub fn with_context(mut self, context: impl fmt::Display) -> Self {
73 self.context.push(context.to_string());
74 self
75 }
76
77 pub fn set_retryable(mut self, retryable: bool) -> Self {
79 self.retryable = retryable;
80 self
81 }
82
83 pub fn kind(&self) -> ErrorKind {
85 self.kind
86 }
87
88 pub fn is_retryable(&self) -> bool {
90 self.retryable
91 }
92
93 pub fn context(&self) -> &[String] {
95 &self.context
96 }
97}
98
99impl ErrorKind {
100 fn default_retryable(&self) -> bool {
102 matches!(self, ErrorKind::RateLimited)
103 }
104}
105
106impl Error {
108 pub fn credential_invalid(message: impl Into<String>) -> Self {
110 Self::new(ErrorKind::CredentialInvalid, message)
111 }
112
113 pub fn permission_denied(message: impl Into<String>) -> Self {
115 Self::new(ErrorKind::PermissionDenied, message)
116 }
117
118 pub fn config_invalid(message: impl Into<String>) -> Self {
120 Self::new(ErrorKind::ConfigInvalid, message)
121 }
122
123 pub fn request_invalid(message: impl Into<String>) -> Self {
125 Self::new(ErrorKind::RequestInvalid, message)
126 }
127
128 pub fn rate_limited(message: impl Into<String>) -> Self {
130 Self::new(ErrorKind::RateLimited, message)
131 }
132
133 pub fn unexpected(message: impl Into<String>) -> Self {
135 Self::new(ErrorKind::Unexpected, message)
136 }
137}
138
139impl fmt::Debug for Error {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 let mut debug = f.debug_struct("Error");
143 debug.field("kind", &self.kind);
144 debug.field("message", &self.message);
145
146 if !self.context.is_empty() {
147 debug.field("context", &self.context);
148 }
149
150 if let Some(source) = &self.source {
151 debug.field("source", source);
152 }
153
154 debug.field("retryable", &self.retryable);
155 debug.finish()
156 }
157}
158
159impl fmt::Display for ErrorKind {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 match self {
162 ErrorKind::CredentialInvalid => write!(f, "invalid credentials"),
163 ErrorKind::PermissionDenied => write!(f, "permission denied"),
164 ErrorKind::ConfigInvalid => write!(f, "invalid configuration"),
165 ErrorKind::RequestInvalid => write!(f, "invalid request"),
166 ErrorKind::RateLimited => write!(f, "rate limited"),
167 ErrorKind::Unexpected => write!(f, "unexpected error"),
168 }
169 }
170}
171
172pub type Result<T> = std::result::Result<T, Error>;
174
175impl From<anyhow::Error> for Error {
177 fn from(err: anyhow::Error) -> Self {
178 Self::unexpected(err.to_string()).with_source(err)
179 }
180}
181
182impl From<std::fmt::Error> for Error {
183 fn from(err: std::fmt::Error) -> Self {
184 Self::unexpected(err.to_string()).with_source(err)
185 }
186}
187
188impl From<http::Error> for Error {
189 fn from(err: http::Error) -> Self {
190 Self::request_invalid(err.to_string()).with_source(err)
191 }
192}
193
194impl From<http::header::InvalidHeaderValue> for Error {
195 fn from(err: http::header::InvalidHeaderValue) -> Self {
196 Self::request_invalid(err.to_string()).with_source(err)
197 }
198}
199
200impl From<http::uri::InvalidUri> for Error {
201 fn from(err: http::uri::InvalidUri) -> Self {
202 Self::request_invalid(err.to_string()).with_source(err)
203 }
204}
205
206impl From<http::uri::InvalidUriParts> for Error {
207 fn from(err: http::uri::InvalidUriParts) -> Self {
208 Self::request_invalid(err.to_string()).with_source(err)
209 }
210}
211
212impl From<std::string::FromUtf8Error> for Error {
213 fn from(err: std::string::FromUtf8Error) -> Self {
214 Self::unexpected(err.to_string()).with_source(err)
215 }
216}
217
218impl From<std::io::Error> for Error {
219 fn from(err: std::io::Error) -> Self {
220 use std::io::ErrorKind;
221
222 let kind = err.kind();
223 let message = err.to_string();
224 let source = anyhow::Error::from(err);
225
226 match kind {
227 ErrorKind::NotFound => Self::config_invalid(message).with_source(source),
228 ErrorKind::PermissionDenied => Self::permission_denied(message).with_source(source),
229 _ => Self::unexpected(message)
230 .with_source(source)
231 .set_retryable(matches!(
232 kind,
233 ErrorKind::TimedOut | ErrorKind::Interrupted | ErrorKind::ConnectionRefused
234 )),
235 }
236 }
237}
238
239impl From<http::header::InvalidHeaderName> for Error {
240 fn from(err: http::header::InvalidHeaderName) -> Self {
241 Self::request_invalid(err.to_string()).with_source(err)
242 }
243}
244
245impl From<http::header::ToStrError> for Error {
246 fn from(err: http::header::ToStrError) -> Self {
247 Self::request_invalid(err.to_string()).with_source(err)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_error_creation() {
257 let err = Error::credential_invalid("token expired");
258 assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
259 assert!(!err.is_retryable());
260 }
261
262 #[test]
263 fn test_error_with_context() {
264 let err = Error::permission_denied("access denied")
265 .with_context("role: arn:aws:iam::123456789012:role/MyRole")
266 .with_context("operation: AssumeRole");
267
268 assert_eq!(err.context().len(), 2);
269 assert_eq!(
270 err.context()[0],
271 "role: arn:aws:iam::123456789012:role/MyRole"
272 );
273 assert_eq!(err.context()[1], "operation: AssumeRole");
274 }
275
276 #[test]
277 fn test_rate_limited_default_retryable() {
278 let err = Error::rate_limited("too many requests");
279 assert!(err.is_retryable());
280 }
281
282 #[test]
283 fn test_override_retryable() {
284 let err = Error::unexpected("network timeout").set_retryable(true);
285 assert!(err.is_retryable());
286
287 let err = Error::rate_limited("quota exceeded").set_retryable(false);
288 assert!(!err.is_retryable());
289 }
290
291 #[test]
292 fn test_error_debug_format() {
293 let err = Error::config_invalid("missing region")
294 .with_context("file: ~/.aws/config")
295 .with_context("profile: default");
296
297 let debug_str = format!("{:?}", err);
298 assert!(debug_str.contains("ConfigInvalid"));
299 assert!(debug_str.contains("missing region"));
300 assert!(debug_str.contains("~/.aws/config"));
301 }
302}