1use std::fmt::{self, Display, Formatter};
2
3use crate::transport::meta::ResponseMeta;
4
5const MAX_ERROR_BODY_CHARS: usize = 256;
6
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum Error {
9 InvalidConfiguration(String),
10 MissingCredentials,
11 Transport(String),
12 Timeout(String),
13 RateLimited {
14 endpoint: &'static str,
15 retry_after: Option<u64>,
16 request_id: Option<String>,
17 attempt_count: u32,
18 body: Option<String>,
19 },
20 HttpStatus {
21 endpoint: &'static str,
22 status: u16,
23 request_id: Option<String>,
24 attempt_count: u32,
25 body: Option<String>,
26 },
27 Deserialize(String),
28 InvalidRequest(String),
29 Pagination(String),
30 NotImplemented {
31 operation: &'static str,
32 },
33}
34
35impl Display for Error {
36 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::InvalidConfiguration(message) => {
39 write!(f, "invalid configuration: {message}")
40 }
41 Self::MissingCredentials => write!(f, "missing credentials"),
42 Self::Transport(message) => write!(f, "transport error: {message}"),
43 Self::Timeout(message) => write!(f, "timeout error: {message}"),
44 Self::RateLimited {
45 endpoint,
46 retry_after,
47 request_id,
48 attempt_count,
49 body,
50 } => write_transport_error(
51 f,
52 "rate limited",
53 *endpoint,
54 Some(("retry_after", retry_after.map(|value| value.to_string()))),
55 request_id.as_deref(),
56 *attempt_count,
57 body.as_deref(),
58 ),
59 Self::HttpStatus {
60 endpoint,
61 status,
62 request_id,
63 attempt_count,
64 body,
65 } => write_transport_error(
66 f,
67 "http status error",
68 *endpoint,
69 Some(("status", Some(status.to_string()))),
70 request_id.as_deref(),
71 *attempt_count,
72 body.as_deref(),
73 ),
74 Self::Deserialize(message) => write!(f, "deserialize error: {message}"),
75 Self::InvalidRequest(message) => write!(f, "invalid request: {message}"),
76 Self::Pagination(message) => write!(f, "pagination error: {message}"),
77 Self::NotImplemented { operation } => {
78 write!(f, "operation not implemented: {operation}")
79 }
80 }
81 }
82}
83
84impl std::error::Error for Error {}
85
86impl Error {
87 pub(crate) fn from_rate_limited(meta: ResponseMeta, body: String) -> Self {
88 Self::RateLimited {
89 endpoint: meta.endpoint_name,
90 retry_after: meta.retry_after.map(|value| value.as_secs()),
91 request_id: meta.request_id,
92 attempt_count: meta.attempt_count,
93 body: snippet_body(body),
94 }
95 }
96
97 pub(crate) fn from_http_status(meta: ResponseMeta, body: String) -> Self {
98 Self::HttpStatus {
99 endpoint: meta.endpoint_name,
100 status: meta.status,
101 request_id: meta.request_id,
102 attempt_count: meta.attempt_count,
103 body: snippet_body(body),
104 }
105 }
106
107 pub(crate) fn from_reqwest(error: reqwest::Error) -> Self {
108 if error.is_timeout() {
109 Self::Timeout(error.to_string())
110 } else {
111 Self::Transport(error.to_string())
112 }
113 }
114
115 pub fn endpoint(&self) -> Option<&str> {
116 match self {
117 Self::RateLimited { endpoint, .. } | Self::HttpStatus { endpoint, .. } => {
118 Some(endpoint)
119 }
120 _ => None,
121 }
122 }
123
124 pub fn request_id(&self) -> Option<&str> {
125 match self {
126 Self::RateLimited { request_id, .. } | Self::HttpStatus { request_id, .. } => {
127 request_id.as_deref()
128 }
129 _ => None,
130 }
131 }
132}
133
134fn write_transport_error(
135 f: &mut Formatter<'_>,
136 label: &str,
137 endpoint: &'static str,
138 primary_field: Option<(&str, Option<String>)>,
139 request_id: Option<&str>,
140 attempt_count: u32,
141 body: Option<&str>,
142) -> fmt::Result {
143 write!(f, "{label}: endpoint={endpoint}")?;
144
145 if let Some((field_name, Some(field_value))) = primary_field {
146 write!(f, ", {field_name}={field_value}")?;
147 }
148
149 if let Some(request_id) = request_id {
150 write!(f, ", request_id={request_id}")?;
151 }
152
153 write!(f, ", attempt_count={attempt_count}")?;
154
155 if let Some(body) = body {
156 write!(f, ", body={body}")?;
157 }
158
159 Ok(())
160}
161
162fn snippet_body(body: String) -> Option<String> {
163 if body.is_empty() {
164 return None;
165 }
166
167 let mut snippet: String = body.chars().take(MAX_ERROR_BODY_CHARS).collect();
168 if snippet.len() < body.len() {
169 snippet.push_str("...");
170 }
171
172 Some(snippet)
173}