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::Timeout(code) => Self::Error(*code),
117            Status::Error(e) => match e {
118                ErrorKind::RejectedStatusCode(code) => Self::Error(Some(*code)),
119                ErrorKind::ReadResponseBody(e) | ErrorKind::BuildRequestClient(e) => {
120                    match e.status() {
121                        Some(code) => Self::Error(Some(code)),
122                        None => Self::Error(None),
123                    }
124                }
125                _ => Self::Error(None),
126            },
127            Status::RequestError(_) | Status::UnknownMailStatus(_) => Self::Error(None),
128        }
129    }
130}
131
132impl From<CacheStatus> for Option<StatusCode> {
133    fn from(val: CacheStatus) -> Self {
134        match val {
135            CacheStatus::Ok(status) => Some(status),
136            CacheStatus::Error(status) => status,
137            _ => None,
138        }
139    }
140}
141
142impl CacheStatus {
143    /// Returns `true` if the cache status is excluded by the given [`StatusCodeSelector`].
144    #[must_use]
145    pub fn is_excluded(&self, excluder: &StatusCodeSelector) -> bool {
146        match Option::<StatusCode>::from(*self) {
147            Some(status) => excluder.contains(status.as_u16()),
148            _ => false,
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use http::StatusCode;
156    use serde::Deserialize;
157    use serde::de::value::{BorrowedStrDeserializer, Error as DeserializerError};
158
159    use crate::CacheStatus;
160
161    fn deserialize_cache_status(s: &str) -> Result<CacheStatus, DeserializerError> {
162        let deserializer: BorrowedStrDeserializer<DeserializerError> =
163            BorrowedStrDeserializer::new(s);
164        CacheStatus::deserialize(deserializer)
165    }
166
167    #[test]
168    fn test_deserialize_cache_status_success_code() {
169        assert_eq!(
170            deserialize_cache_status("200"),
171            Ok(CacheStatus::Ok(StatusCode::OK))
172        );
173    }
174
175    #[test]
176    fn test_deserialize_cache_status_error_code() {
177        assert_eq!(
178            deserialize_cache_status("404"),
179            Ok(CacheStatus::Error(Some(StatusCode::NOT_FOUND)))
180        );
181    }
182
183    #[test]
184    fn test_deserialize_cache_status_excluded() {
185        assert_eq!(
186            deserialize_cache_status("Excluded"),
187            Ok(CacheStatus::Excluded)
188        );
189    }
190
191    #[test]
192    fn test_deserialize_cache_status_unsupported() {
193        assert_eq!(
194            deserialize_cache_status("Unsupported"),
195            Ok(CacheStatus::Unsupported)
196        );
197    }
198
199    #[test]
200    fn test_deserialize_cache_status_blank() {
201        assert_eq!(deserialize_cache_status(""), Ok(CacheStatus::Error(None)));
202    }
203}