Skip to content

Commit 97a7c82

Browse files
authored
Merge pull request #220 from http-rs/expires
Expires
2 parents dc87e9d + 5ed9dfb commit 97a7c82

File tree

6 files changed

+621
-0
lines changed

6 files changed

+621
-0
lines changed

LICENSE-APACHE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@
176176
END OF TERMS AND CONDITIONS
177177

178178
Copyright 2019 Yoshua Wuyts
179+
Copyright 2016-2018 Michael Tilli (Pyfisch) & `httpdate` contributors
179180

180181
Licensed under the Apache License, Version 2.0 (the "License");
181182
you may not use this file except in compliance with the License.

LICENSE-MIT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ The MIT License (MIT)
33
Copyright (c) 2019 Yoshua Wuyts
44
Copyright (c) 2017 http-rs authors
55
Copyright (c) 2020 Jacob Brown
6+
Copyright (c) 2016-2018 Michael Tilli (Pyfisch) & `httpdate` contributors
67

78
Permission is hereby granted, free of charge, to any person obtaining a copy
89
of this software and associated documentation files (the "Software"), to deal

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, section 5.3: Expires](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.expiration())?;
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 expiration(&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.expiration())?;
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;

0 commit comments

Comments
 (0)