http_types_rs/cache/
expires.rs

1use crate::headers::{Header, HeaderName, HeaderValue, Headers, EXPIRES};
2use crate::utils::{fmt_http_date, parse_http_date};
3
4use std::fmt::Debug;
5use std::time::{Duration, SystemTime};
6
7/// HTTP `Expires` header
8///
9/// # Specifications
10///
11/// - [RFC 7234, section 5.3: Expires](https://tools.ietf.org/html/rfc7234#section-5.3)
12///
13/// # Examples
14///
15/// ```
16/// # fn main() -> http_types_rs::Result<()> {
17/// #
18/// use http_types_rs::Response;
19/// use http_types_rs::cache::Expires;
20/// use std::time::{SystemTime, Duration};
21///
22/// let time = SystemTime::now() + Duration::from_secs(5 * 60);
23/// let expires = Expires::new_at(time);
24///
25/// let mut res = Response::new(200);
26/// res.insert_header(&expires, &expires);
27///
28/// let expires = Expires::from_headers(res)?.unwrap();
29///
30/// // HTTP dates only have second-precision.
31/// let elapsed = time.duration_since(expires.expiration())?;
32/// assert_eq!(elapsed.as_secs(), 0);
33/// #
34/// # Ok(()) }
35/// ```
36#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
37pub struct Expires {
38    instant: SystemTime,
39}
40
41impl Expires {
42    /// Create a new instance of `Expires`.
43    pub fn new(dur: Duration) -> Self {
44        let instant = SystemTime::now() + dur;
45        Self { instant }
46    }
47
48    /// Create a new instance of `Expires` from secs.
49    pub fn new_at(instant: SystemTime) -> Self {
50        Self { instant }
51    }
52
53    /// Get the expiration time.
54    pub fn expiration(&self) -> SystemTime {
55        self.instant
56    }
57
58    /// Create an instance of `Expires` from a `Headers` instance.
59    pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
60        let headers = match headers.as_ref().get(EXPIRES) {
61            Some(headers) => headers,
62            None => return Ok(None),
63        };
64
65        // If we successfully parsed the header then there's always at least one
66        // entry. We want the last entry.
67        let header = headers.iter().last().unwrap();
68
69        let instant = parse_http_date(header.as_str())?;
70        Ok(Some(Self { instant }))
71    }
72}
73
74impl Header for Expires {
75    fn header_name(&self) -> HeaderName {
76        EXPIRES
77    }
78    fn header_value(&self) -> HeaderValue {
79        let output = fmt_http_date(self.instant);
80
81        // SAFETY: the internal string is validated to be ASCII.
82        unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
83    }
84}
85
86#[cfg(test)]
87mod test {
88    use super::*;
89    use crate::headers::Headers;
90
91    #[test]
92    fn smoke() -> crate::Result<()> {
93        let time = SystemTime::now() + Duration::from_secs(5 * 60);
94        let expires = Expires::new_at(time);
95
96        let mut headers = Headers::new();
97        expires.apply_header(&mut headers);
98
99        let expires = Expires::from_headers(headers)?.unwrap();
100
101        // HTTP dates only have second-precision
102        let elapsed = time.duration_since(expires.expiration())?;
103        assert_eq!(elapsed.as_secs(), 0);
104        Ok(())
105    }
106
107    #[test]
108    fn bad_request_on_parse_error() {
109        let mut headers = Headers::new();
110        headers.insert(EXPIRES, "<nori ate the tag. yum.>").unwrap();
111        let err = Expires::from_headers(headers).unwrap_err();
112        assert_eq!(err.status(), 400);
113    }
114}