Skip to main content

lychee_lib/types/
cache.rs

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