Skip to content

Commit d0bf1cb

Browse files
committed
Add cache::Expires header
1 parent 33afcbd commit d0bf1cb

File tree

4 files changed

+139
-2
lines changed

4 files changed

+139
-2
lines changed

src/cache/expires.rs

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

src/cache/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
1111
mod age;
1212
mod cache_control;
13+
mod expires;
1314

1415
pub use age::Age;
1516
pub use cache_control::CacheControl;
1617
pub use cache_control::CacheDirective;
18+
pub use expires::Expires;

src/utils/date.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::fmt::{self, Display, Formatter};
22
use std::str::{from_utf8, FromStr};
33
use std::time::{Duration, SystemTime, UNIX_EPOCH};
44

5+
use crate::StatusCode;
56
use crate::{bail, ensure, format_err};
67

78
const IMF_FIXDATE_LENGTH: usize = 29;
@@ -39,7 +40,10 @@ pub(crate) struct HttpDate {
3940
/// ascdate formats. Two digit years are mapped to dates between
4041
/// 1970 and 2069.
4142
pub(crate) fn parse_http_date(s: &str) -> crate::Result<SystemTime> {
42-
s.parse::<HttpDate>().map(|d| d.into())
43+
s.parse::<HttpDate>().map(|d| d.into()).map_err(|mut e| {
44+
e.set_status(StatusCode::BadRequest);
45+
e
46+
})
4347
}
4448

4549
/// Format a date to be used in a HTTP header field.

src/utils/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod date;
22

3-
pub(crate) use date::{fmt_http_date, parse_http_date};
3+
pub(crate) use date::fmt_http_date;
4+
pub(crate) use date::parse_http_date;
45

56
/// Declares unstable items.
67
#[doc(hidden)]

0 commit comments

Comments
 (0)