use std::{collections::HashSet, fmt::Display};
use http::StatusCode;
use reqwest::Response;
use serde::{Serialize, Serializer};
use crate::ErrorKind;
use super::CacheStatus;
const ICON_OK: &str = "\u{2714}"; const ICON_REDIRECTED: &str = "\u{21c4}"; const ICON_EXCLUDED: &str = "\u{003f}"; const ICON_UNSUPPORTED: &str = "\u{003f}"; const ICON_UNKNOWN: &str = "\u{003f}"; const ICON_ERROR: &str = "\u{2717}"; const ICON_TIMEOUT: &str = "\u{29d6}"; const ICON_CACHED: &str = "\u{21bb}";
#[allow(variant_size_differences)]
#[derive(Debug, Hash, PartialEq, Eq)]
pub enum Status {
Ok(StatusCode),
Error(ErrorKind),
Timeout(Option<StatusCode>),
Redirected(StatusCode),
UnknownStatusCode(StatusCode),
Excluded,
Unsupported(ErrorKind),
Cached(CacheStatus),
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Ok(code) => write!(f, "OK ({code})"),
Status::Redirected(code) => write!(f, "Redirect ({code})"),
Status::UnknownStatusCode(code) => write!(f, "Unknown status ({code})"),
Status::Excluded => f.write_str("Excluded"),
Status::Timeout(Some(code)) => write!(f, "Timeout ({code})"),
Status::Timeout(None) => f.write_str("Timeout"),
Status::Unsupported(e) => write!(f, "Unsupported: {e}"),
Status::Error(e) => write!(f, "Failed: {e}"),
Status::Cached(status) => write!(f, "Cached: {status}"),
}
}
}
impl Serialize for Status {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl Status {
#[must_use]
pub fn new(response: &Response, accepted: Option<HashSet<StatusCode>>) -> Self {
let code = response.status();
if let Some(true) = accepted.map(|a| a.contains(&code)) {
Self::Ok(code)
} else {
match response.error_for_status_ref() {
Ok(_) if code.is_success() => Self::Ok(code),
Ok(_) if code.is_redirection() => Self::Redirected(code),
Ok(_) => Self::UnknownStatusCode(code),
Err(e) => e.into(),
}
}
}
#[must_use]
#[allow(clippy::match_same_arms)]
pub fn details(&self) -> Option<String> {
match &self {
Status::Ok(code) => code.canonical_reason().map(String::from),
Status::Redirected(code) => code.canonical_reason().map(String::from),
Status::Error(e) => e.details(),
Status::Timeout(_) => None,
Status::UnknownStatusCode(_) => None,
Status::Unsupported(_) => None,
Status::Cached(_) => None,
Status::Excluded => None,
}
}
#[inline]
#[must_use]
pub const fn is_success(&self) -> bool {
matches!(self, Status::Ok(_) | Status::Cached(CacheStatus::Ok(_)))
}
#[inline]
#[must_use]
pub const fn is_failure(&self) -> bool {
matches!(
self,
Status::Error(_) | Status::Cached(CacheStatus::Error(_))
)
}
#[inline]
#[must_use]
pub const fn is_excluded(&self) -> bool {
matches!(
self,
Status::Excluded | Status::Cached(CacheStatus::Excluded)
)
}
#[inline]
#[must_use]
pub const fn is_timeout(&self) -> bool {
matches!(self, Status::Timeout(_))
}
#[inline]
#[must_use]
pub const fn is_unsupported(&self) -> bool {
matches!(
self,
Status::Unsupported(_) | Status::Cached(CacheStatus::Unsupported)
)
}
#[must_use]
pub const fn icon(&self) -> &str {
match self {
Status::Ok(_) => ICON_OK,
Status::Redirected(_) => ICON_REDIRECTED,
Status::UnknownStatusCode(_) => ICON_UNKNOWN,
Status::Excluded => ICON_EXCLUDED,
Status::Error(_) => ICON_ERROR,
Status::Timeout(_) => ICON_TIMEOUT,
Status::Unsupported(_) => ICON_UNSUPPORTED,
Status::Cached(_) => ICON_CACHED,
}
}
#[must_use]
pub fn code(&self) -> String {
match self {
Status::Ok(code) | Status::Redirected(code) | Status::UnknownStatusCode(code) => {
code.as_str().to_string()
}
Status::Excluded => "EXCLUDED".to_string(),
Status::Error(e) => match e {
ErrorKind::NetworkRequest(e)
| ErrorKind::ReadResponseBody(e)
| ErrorKind::BuildRequestClient(e) => match e.status() {
Some(code) => code.as_str().to_string(),
None => "ERR".to_string(),
},
_ => "ERR".to_string(),
},
Status::Timeout(code) => match code {
Some(code) => code.as_str().to_string(),
None => "TIMEOUT".to_string(),
},
Status::Unsupported(_) => "IGNORED".to_string(),
Status::Cached(cache_status) => match cache_status {
CacheStatus::Ok(code) => code.to_string(),
CacheStatus::Error(code) => match code {
Some(code) => code.to_string(),
None => "ERR".to_string(),
},
CacheStatus::Excluded => "EXCLUDED".to_string(),
CacheStatus::Unsupported => "IGNORED".to_string(),
},
}
}
}
impl From<ErrorKind> for Status {
fn from(e: ErrorKind) -> Self {
Self::Error(e)
}
}
impl From<reqwest::Error> for Status {
fn from(e: reqwest::Error) -> Self {
if e.is_timeout() {
Self::Timeout(e.status())
} else if e.is_builder() {
Self::Unsupported(ErrorKind::BuildRequestClient(e))
} else if e.is_body() || e.is_decode() {
Self::Unsupported(ErrorKind::ReadResponseBody(e))
} else {
Self::Error(ErrorKind::NetworkRequest(e))
}
}
}
impl From<CacheStatus> for Status {
fn from(s: CacheStatus) -> Self {
Self::Cached(s)
}
}