lychee_lib/types/
cache.rs

1use std::fmt::Display;
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::{ErrorKind, Status, StatusCodeExcluder};
6
7/// Representation of the status of a cached request. This is kept simple on
8/// purpose because the type gets serialized to a cache file and might need to
9/// be parsed by other tools or edited by humans.
10#[derive(Debug, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
11pub enum CacheStatus {
12    /// The cached request delivered a valid response
13    Ok(u16),
14    /// The cached request failed before
15    Error(Option<u16>),
16    /// The request was excluded (skipped)
17    Excluded,
18    /// The protocol is not yet supported
19    // We no longer cache unsupported files as they might be supported in future
20    // versions.
21    // Nevertheless, keep for compatibility when deserializing older cache
22    // files, even though this no longer gets serialized. Can be removed at a
23    // later point in time.
24    Unsupported,
25}
26
27impl<'de> Deserialize<'de> for CacheStatus {
28    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
29    where
30        D: Deserializer<'de>,
31    {
32        let status = <&str as Deserialize<'de>>::deserialize(deserializer)?;
33        match status {
34            "Excluded" => Ok(CacheStatus::Excluded),
35            // Keep for compatibility with older cache files, even though this
36            // no longer gets serialized. Can be removed at a later point in
37            // time.
38            "Unsupported" => Ok(CacheStatus::Unsupported),
39            other => match other.parse::<u16>() {
40                Ok(code) => match code {
41                    // classify successful status codes as cache status success
42                    // Does not account for status code overrides passed through
43                    // the 'accept' flag. Instead, this is handled at a higher level
44                    // when the cache status is converted to a status.
45                    200..=299 => Ok(CacheStatus::Ok(code)),
46                    // classify redirects, client errors, & server errors as cache status error
47                    _ => Ok(CacheStatus::Error(Some(code))),
48                },
49                Err(_) => Ok(CacheStatus::Error(None)),
50            },
51        }
52    }
53}
54
55impl Display for CacheStatus {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::Ok(_) => write!(f, "OK (cached)"),
59            Self::Error(_) => write!(f, "Error (cached)"),
60            Self::Excluded => write!(f, "Excluded (cached)"),
61            Self::Unsupported => write!(f, "Unsupported (cached)"),
62        }
63    }
64}
65
66impl From<&Status> for CacheStatus {
67    fn from(s: &Status) -> Self {
68        match s {
69            Status::Cached(s) => *s,
70            // Reqwest treats unknown status codes as Ok(StatusCode).
71            // TODO: Use accepted status codes to decide whether this is a
72            // success or failure
73            Status::Ok(code) | Status::UnknownStatusCode(code) => Self::Ok(code.as_u16()),
74            Status::Excluded => Self::Excluded,
75            Status::Unsupported(_) => Self::Unsupported,
76            Status::Redirected(code, _) => Self::Error(Some(code.as_u16())),
77            Status::Timeout(code) => Self::Error(code.map(|code| code.as_u16())),
78            Status::Error(e) => match e {
79                ErrorKind::RejectedStatusCode(code) => Self::Error(Some(code.as_u16())),
80                ErrorKind::ReadResponseBody(e) | ErrorKind::BuildRequestClient(e) => {
81                    match e.status() {
82                        Some(code) => Self::Error(Some(code.as_u16())),
83                        None => Self::Error(None),
84                    }
85                }
86                _ => Self::Error(None),
87            },
88            Status::RequestError(_) => Self::Error(None),
89        }
90    }
91}
92
93impl From<CacheStatus> for Option<u16> {
94    fn from(val: CacheStatus) -> Self {
95        match val {
96            CacheStatus::Ok(status) => Some(status),
97            CacheStatus::Error(status) => status,
98            _ => None,
99        }
100    }
101}
102
103impl CacheStatus {
104    /// Returns `true` if the cache status is excluded by the given [`StatusCodeExcluder`].
105    #[must_use]
106    pub fn is_excluded(&self, excluder: &StatusCodeExcluder) -> bool {
107        match Option::<u16>::from(*self) {
108            Some(status) => excluder.contains(status),
109            _ => false,
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use serde::Deserialize;
117    use serde::de::value::{BorrowedStrDeserializer, Error as DeserializerError};
118
119    use crate::CacheStatus;
120
121    fn deserialize_cache_status(s: &str) -> Result<CacheStatus, DeserializerError> {
122        let deserializer: BorrowedStrDeserializer<DeserializerError> =
123            BorrowedStrDeserializer::new(s);
124        CacheStatus::deserialize(deserializer)
125    }
126
127    #[test]
128    fn test_deserialize_cache_status_success_code() {
129        assert_eq!(deserialize_cache_status("200"), Ok(CacheStatus::Ok(200)));
130    }
131
132    #[test]
133    fn test_deserialize_cache_status_error_code() {
134        assert_eq!(
135            deserialize_cache_status("404"),
136            Ok(CacheStatus::Error(Some(404)))
137        );
138    }
139
140    #[test]
141    fn test_deserialize_cache_status_excluded() {
142        assert_eq!(
143            deserialize_cache_status("Excluded"),
144            Ok(CacheStatus::Excluded)
145        );
146    }
147
148    #[test]
149    fn test_deserialize_cache_status_unsupported() {
150        assert_eq!(
151            deserialize_cache_status("Unsupported"),
152            Ok(CacheStatus::Unsupported)
153        );
154    }
155
156    #[test]
157    fn test_deserialize_cache_status_blank() {
158        assert_eq!(deserialize_cache_status(""), Ok(CacheStatus::Error(None)));
159    }
160}