Skip to content

Commit f93c100

Browse files
authored
Merge pull request #222 from http-rs/if-modified-since
Add `conditional::{IfModifiedSince, IfUnmodifiedSince, LastModified}`
2 parents 9259a12 + c58def9 commit f93c100

File tree

4 files changed

+381
-0
lines changed

4 files changed

+381
-0
lines changed

src/conditional/if_modified_since.rs

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

src/conditional/last_modified.rs

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

src/conditional/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,11 @@
99
//! - [MDN: HTTP Conditional Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)
1010
1111
mod etag;
12+
mod if_modified_since;
13+
mod if_unmodified_since;
14+
mod last_modified;
1215

1316
pub use etag::ETag;
17+
pub use if_modified_since::IfModifiedSince;
18+
pub use if_unmodified_since::IfUnmodifiedSince;
19+
pub use last_modified::LastModified;

0 commit comments

Comments
 (0)