1use std::{collections::HashSet, fmt::Display};
2
3use super::CacheStatus;
4use crate::ErrorKind;
5use crate::RequestError;
6use crate::ratelimit::CacheableResponse;
7use http::StatusCode;
8use serde::ser::SerializeStruct;
9use serde::{Serialize, Serializer};
10
11const ICON_OK: &str = "✔";
12const ICON_EXCLUDED: &str = "?";
13const ICON_UNSUPPORTED: &str = "\u{003f}"; const ICON_UNKNOWN: &str = "?";
15const ICON_ERROR: &str = "✗";
16const ICON_TIMEOUT: &str = "⧖";
17const ICON_CACHED: &str = "↻";
18
19#[allow(variant_size_differences)]
21#[derive(Debug, Hash, PartialEq, Eq)]
22pub enum Status {
23 Ok(StatusCode),
25 Error(ErrorKind),
27 RequestError(RequestError),
29 Timeout(Option<StatusCode>),
31 UnknownStatusCode(StatusCode),
33 UnknownMailStatus(String),
37 Excluded,
39 Unsupported(ErrorKind),
43 Cached(CacheStatus),
45}
46
47impl Display for Status {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Status::Ok(code) => write!(f, "{code}"),
51 Status::UnknownStatusCode(code) => write!(f, "Unknown status ({code})"),
52 Status::UnknownMailStatus(_) => write!(f, "Unknown mail status"),
53 Status::Timeout(Some(code)) => write!(f, "Timeout ({code})"),
54 Status::Timeout(None) => f.write_str("Timeout"),
55 Status::Unsupported(e) => write!(f, "Unsupported: {e}"),
56 Status::Error(e) => write!(f, "{e}"),
57 Status::RequestError(e) => write!(f, "{e}"),
58 Status::Cached(status) => write!(f, "{status}"),
59 Status::Excluded => f.write_str("Excluded"),
60 }
61 }
62}
63
64impl Serialize for Status {
65 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
66 where
67 S: Serializer,
68 {
69 let s;
70
71 if let Some(code) = self.code() {
72 s = serializer.serialize_struct("Status", 2)?;
73 let mut s = s;
74 s.serialize_field("text", &self.to_string())?;
75 s.serialize_field("code", &code.as_u16())?;
76 s.end()
77 } else {
78 s = serializer.serialize_struct("Status", 2)?;
79 let mut s = s;
80 s.serialize_field("text", &self.to_string())?;
81 s.serialize_field("details", &self.details())?;
82 s.end()
83 }
84 }
85}
86
87impl Status {
88 #[must_use]
90 pub(crate) fn new(response: &CacheableResponse, accepted: &HashSet<StatusCode>) -> Self {
91 let status = response.status;
92 if accepted.contains(&status) {
93 Self::Ok(status)
94 } else {
95 Self::Error(ErrorKind::RejectedStatusCode(status))
96 }
97 }
98
99 #[must_use]
109 pub fn from_cache_status(s: CacheStatus, accepted: &HashSet<StatusCode>) -> Self {
110 match s {
111 CacheStatus::Ok(code) => {
112 if matches!(s, CacheStatus::Ok(_)) || accepted.contains(&code) {
113 return Self::Cached(CacheStatus::Ok(code));
114 }
115 Self::Cached(CacheStatus::Error(Some(code)))
116 }
117 CacheStatus::Error(code) => {
118 if let Some(code) = code
119 && accepted.contains(&code)
120 {
121 return Self::Cached(CacheStatus::Ok(code));
122 }
123 Self::Cached(CacheStatus::Error(code))
124 }
125 _ => Self::Cached(s),
126 }
127 }
128
129 #[must_use]
137 #[allow(clippy::match_same_arms)]
138 pub fn details(&self) -> String {
139 match &self {
140 Status::Ok(code) => code.to_string(),
141 Status::Error(e) => e.details(),
142 Status::RequestError(e) => e.error().details(),
143 Status::UnknownMailStatus(reason) => reason.clone(),
144 Status::Timeout(_) => "Request timed out".into(),
145 Status::Excluded => "This is due to your 'exclude' values".into(),
146 Status::Unsupported(_) | Status::Cached(_) | Status::UnknownStatusCode(_) => {
147 self.to_string()
148 }
149 }
150 }
151
152 #[inline]
154 #[must_use]
155 pub const fn is_success(&self) -> bool {
156 matches!(self, Status::Ok(_) | Status::Cached(CacheStatus::Ok(_)))
157 }
158
159 #[inline]
161 #[must_use]
162 pub const fn is_error(&self) -> bool {
163 matches!(
164 self,
165 Status::Error(_)
166 | Status::RequestError(_)
167 | Status::Cached(CacheStatus::Error(_))
168 | Status::Timeout(_)
169 )
170 }
171
172 #[inline]
174 #[must_use]
175 pub const fn is_excluded(&self) -> bool {
176 matches!(
177 self,
178 Status::Excluded | Status::Cached(CacheStatus::Excluded)
179 )
180 }
181
182 #[inline]
184 #[must_use]
185 pub const fn is_timeout(&self) -> bool {
186 matches!(self, Status::Timeout(_))
187 }
188
189 #[inline]
191 #[must_use]
192 pub const fn is_unsupported(&self) -> bool {
193 matches!(
194 self,
195 Status::Unsupported(_) | Status::Cached(CacheStatus::Unsupported)
196 )
197 }
198
199 #[inline]
205 #[must_use]
206 pub const fn is_unknown(&self) -> bool {
207 matches!(self, Status::UnknownStatusCode(_))
208 }
209
210 #[must_use]
212 pub const fn icon(&self) -> &str {
213 match self {
214 Status::Ok(_) => ICON_OK,
215 Status::UnknownStatusCode(_) | Status::UnknownMailStatus(_) => ICON_UNKNOWN,
216 Status::Excluded => ICON_EXCLUDED,
217 Status::Error(_) | Status::RequestError(_) => ICON_ERROR,
218 Status::Timeout(_) => ICON_TIMEOUT,
219 Status::Unsupported(_) => ICON_UNSUPPORTED,
220 Status::Cached(_) => ICON_CACHED,
221 }
222 }
223
224 #[must_use]
226 pub fn code(&self) -> Option<StatusCode> {
227 match self {
228 Status::Ok(code)
229 | Status::UnknownStatusCode(code)
230 | Status::Timeout(Some(code))
231 | Status::Cached(CacheStatus::Ok(code) | CacheStatus::Error(Some(code))) => Some(*code),
232 Status::Error(kind) | Status::Unsupported(kind) => match kind {
233 ErrorKind::RejectedStatusCode(status_code) => Some(*status_code),
234 _ => match kind.reqwest_error() {
235 Some(error) => error.status(),
236 None => None,
237 },
238 },
239 _ => None,
240 }
241 }
242
243 #[must_use]
245 pub fn code_as_string(&self) -> String {
246 match self {
247 Status::Ok(code) | Status::UnknownStatusCode(code) => code.as_u16().to_string(),
248 Status::UnknownMailStatus(_) => "UNKNOWN".to_string(),
249 Status::Excluded => "EXCLUDED".to_string(),
250 Status::Error(e) => match e {
251 ErrorKind::RejectedStatusCode(code) => code.as_u16().to_string(),
252 ErrorKind::ReadResponseBody(e) | ErrorKind::BuildRequestClient(e) => {
253 match e.status() {
254 Some(code) => code.as_u16().to_string(),
255 None => "ERROR".to_string(),
256 }
257 }
258 _ => "ERROR".to_string(),
259 },
260 Status::RequestError(_) => "ERROR".to_string(),
261 Status::Timeout(code) => match code {
262 Some(code) => code.as_u16().to_string(),
263 None => "TIMEOUT".to_string(),
264 },
265 Status::Unsupported(_) => "IGNORED".to_string(),
266 Status::Cached(cache_status) => match cache_status {
267 CacheStatus::Ok(code) => code.as_u16().to_string(),
268 CacheStatus::Error(code) => match code {
269 Some(code) => code.as_u16().to_string(),
270 None => "ERROR".to_string(),
271 },
272 CacheStatus::Excluded => "EXCLUDED".to_string(),
273 CacheStatus::Unsupported => "IGNORED".to_string(),
274 },
275 }
276 }
277}
278
279impl From<ErrorKind> for Status {
280 fn from(e: ErrorKind) -> Self {
281 match e {
282 ErrorKind::InvalidUrlHost => Status::Unsupported(ErrorKind::InvalidUrlHost),
283 ErrorKind::NetworkRequest(e)
284 | ErrorKind::ReadResponseBody(e)
285 | ErrorKind::BuildRequestClient(e) => {
286 if e.is_timeout() {
287 Self::Timeout(e.status())
288 } else if e.is_builder() {
289 Self::Unsupported(ErrorKind::BuildRequestClient(e))
290 } else if e.is_body() || e.is_decode() {
291 Self::Unsupported(ErrorKind::ReadResponseBody(e))
292 } else {
293 Self::Error(ErrorKind::NetworkRequest(e))
294 }
295 }
296 e => Self::Error(e),
297 }
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use crate::{CacheStatus, ErrorKind, Status};
304 use http::StatusCode;
305
306 #[test]
307 fn test_status_serialization() {
308 let status_ok = Status::Ok(StatusCode::from_u16(200).unwrap());
309 let serialized_with_code = serde_json::to_string(&status_ok).unwrap();
310 assert_eq!(r#"{"text":"200 OK","code":200}"#, serialized_with_code);
311
312 let status_error = Status::Error(ErrorKind::EmptyUrl);
313 let serialized_with_error = serde_json::to_string(&status_error).unwrap();
314 assert_eq!(
315 r#"{"text":"Empty URL found but a URL must not be empty","details":"Empty URL found but a URL must not be empty"}"#,
316 serialized_with_error
317 );
318
319 let status_timeout = Status::Timeout(None);
320 let serialized_without_code = serde_json::to_string(&status_timeout).unwrap();
321 assert_eq!(
322 r#"{"text":"Timeout","details":"Request timed out"}"#,
323 serialized_without_code
324 );
325 }
326
327 #[test]
328 fn test_get_status_code() {
329 assert_eq!(
330 Status::Ok(StatusCode::from_u16(200).unwrap())
331 .code()
332 .unwrap(),
333 200
334 );
335 assert_eq!(
336 Status::Timeout(Some(StatusCode::from_u16(408).unwrap()))
337 .code()
338 .unwrap(),
339 408
340 );
341 assert_eq!(
342 Status::UnknownStatusCode(StatusCode::from_u16(999).unwrap())
343 .code()
344 .unwrap(),
345 999
346 );
347 assert_eq!(
348 Status::Cached(CacheStatus::Ok(StatusCode::OK))
349 .code()
350 .unwrap(),
351 200
352 );
353 assert_eq!(
354 Status::Cached(CacheStatus::Error(Some(StatusCode::NOT_FOUND)))
355 .code()
356 .unwrap(),
357 404
358 );
359 assert_eq!(Status::Timeout(None).code(), None);
360 assert_eq!(Status::Cached(CacheStatus::Error(None)).code(), None);
361 assert_eq!(Status::Excluded.code(), None);
362 assert_eq!(
363 Status::Unsupported(ErrorKind::InvalidStatusCode(999)).code(),
364 None
365 );
366 }
367
368 #[test]
369 fn test_status_unknown() {
370 assert!(Status::UnknownStatusCode(StatusCode::from_u16(999).unwrap()).is_unknown());
371 assert!(!Status::Ok(StatusCode::from_u16(200).unwrap()).is_unknown());
372 }
373}