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