1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use std::fmt::Display;

use serde::{Deserialize, Deserializer, Serialize};

use crate::{ErrorKind, Status};

/// Representation of the status of a cached request. This is kept simple on
/// purpose because the type gets serialized to a cache file and might need to
/// be parsed by other tools or edited by humans.
#[derive(Debug, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum CacheStatus {
    /// The cached request delivered a valid response
    Ok(u16),
    /// The cached request failed before
    Error(Option<u16>),
    /// The request was excluded (skipped)
    Excluded,
    /// The protocol is not yet supported
    // We no longer cache unsupported files as they might be supported in future
    // versions.
    // Nevertheless, keep for compatibility when deserializing older cache
    // files, even though this no longer gets serialized. Can be removed at a
    // later point in time.
    Unsupported,
}

impl<'de> Deserialize<'de> for CacheStatus {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let status = <&str as Deserialize<'de>>::deserialize(deserializer)?;
        match status {
            "Excluded" => Ok(CacheStatus::Excluded),
            // Keep for compatibility with older cache files, even though this
            // no longer gets serialized. Can be removed at a later point in
            // time.
            "Unsupported" => Ok(CacheStatus::Unsupported),
            other => match other.parse::<u16>() {
                Ok(code) => match code {
                    // classify successful status codes as cache status success
                    // Does not account for status code overrides passed through
                    // the 'accept' flag. Instead, this is handled at a higher level
                    // when the cache status is converted to a status.
                    200..=299 => Ok(CacheStatus::Ok(code)),
                    // classify redirects, client errors, & server errors as cache status error
                    _ => Ok(CacheStatus::Error(Some(code))),
                },
                Err(_) => Ok(CacheStatus::Error(None)),
            },
        }
    }
}

impl Display for CacheStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Ok(_) => write!(f, "OK (cached)"),
            Self::Error(_) => write!(f, "Error (cached)"),
            Self::Excluded => write!(f, "Excluded (cached)"),
            Self::Unsupported => write!(f, "Unsupported (cached)"),
        }
    }
}

impl From<&Status> for CacheStatus {
    fn from(s: &Status) -> Self {
        match s {
            Status::Cached(s) => *s,
            // Reqwest treats unknown status codes as Ok(StatusCode).
            // TODO: Use accepted status codes to decide whether this is a
            // success or failure
            Status::Ok(code) | Status::UnknownStatusCode(code) => Self::Ok(code.as_u16()),
            Status::Excluded => Self::Excluded,
            Status::Unsupported(_) => Self::Unsupported,
            Status::Redirected(code) => Self::Error(Some(code.as_u16())),
            Status::Timeout(code) => Self::Error(code.map(|code| code.as_u16())),
            Status::Error(e) => match e {
                ErrorKind::NetworkRequest(e)
                | ErrorKind::ReadResponseBody(e)
                | ErrorKind::BuildRequestClient(e) => match e.status() {
                    Some(code) => Self::Error(Some(code.as_u16())),
                    None => Self::Error(None),
                },
                _ => Self::Error(None),
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use serde::de::value::{BorrowedStrDeserializer, Error as DeserializerError};
    use serde::Deserialize;

    use crate::CacheStatus;

    fn deserialize_cache_status(s: &str) -> Result<CacheStatus, DeserializerError> {
        let deserializer: BorrowedStrDeserializer<DeserializerError> =
            BorrowedStrDeserializer::new(s);
        CacheStatus::deserialize(deserializer)
    }

    #[test]
    fn test_deserialize_cache_status_success_code() {
        assert_eq!(deserialize_cache_status("200"), Ok(CacheStatus::Ok(200)));
    }

    #[test]
    fn test_deserialize_cache_status_error_code() {
        assert_eq!(
            deserialize_cache_status("404"),
            Ok(CacheStatus::Error(Some(404)))
        );
    }

    #[test]
    fn test_deserialize_cache_status_excluded() {
        assert_eq!(
            deserialize_cache_status("Excluded"),
            Ok(CacheStatus::Excluded)
        );
    }

    #[test]
    fn test_deserialize_cache_status_unsupported() {
        assert_eq!(
            deserialize_cache_status("Unsupported"),
            Ok(CacheStatus::Unsupported)
        );
    }

    #[test]
    fn test_deserialize_cache_status_blank() {
        assert_eq!(deserialize_cache_status(""), Ok(CacheStatus::Error(None)));
    }
}