From f1233968fb545f445ebe6448f41a30b3ace805ae Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Fri, 7 Aug 2020 17:55:07 +0200 Subject: [PATCH] add a structured cache::Age header --- src/cache/age.rs | 117 +++++++++++++++++++++++++++++++++++++++++++++++ src/cache/mod.rs | 2 + 2 files changed, 119 insertions(+) create mode 100644 src/cache/age.rs diff --git a/src/cache/age.rs b/src/cache/age.rs new file mode 100644 index 00000000..16d5a3f3 --- /dev/null +++ b/src/cache/age.rs @@ -0,0 +1,117 @@ +use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, AGE}; +use crate::Status; + +use std::fmt::Debug; +use std::option; +use std::time::Duration; + +/// HTTP `Age` header +/// +/// # Specifications +/// +/// - [RFC 7234 Hypertext Transfer Protocol (HTTP/1.1): Caching](https://tools.ietf.org/html/rfc7234#section-5.1) +/// +/// # Examples +/// +/// ``` +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::Response; +/// use http_types::cache::Age; +/// use std::time::Duration; +/// +/// let age = Age::new(Duration::from_secs(12)); +/// +/// let mut res = Response::new(200); +/// age.apply(&mut res); +/// +/// let age = Age::from_headers(res)?.unwrap(); +/// assert_eq!(age, Age::new(Duration::from_secs(12))); +/// # +/// # Ok(()) } +/// ``` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct Age { + dur: Duration, +} + +impl Age { + /// Create a new instance of `Age`. + pub fn new(dur: Duration) -> Self { + Self { dur } + } + + /// Create an instance of `Age` from a `Headers` instance. + /// + /// # Implementation note + /// + /// A header value of `"null"` is treated the same as if no header was sent. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let headers = match headers.as_ref().get(AGE) { + Some(headers) => headers, + None => return Ok(None), + }; + + // If we successfully parsed the header then there's always at least one + // entry. We want the last entry. + let header = headers.iter().last().unwrap(); + + let num: u64 = header.as_str().parse().status(400)?; + let dur = Duration::from_secs_f64(num as f64); + + Ok(Some(Self { dur })) + } + + /// Insert a `HeaderName` + `HeaderValue` pair into a `Headers` instance. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(AGE, self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + AGE + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + let output = self.dur.as_secs().to_string(); + + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } +} + +impl ToHeaderValues for Age { + type Iter = option::IntoIter; + fn to_header_values(&self) -> crate::Result { + // A HeaderValue will always convert into itself. + Ok(self.value().to_header_values().unwrap()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::headers::Headers; + + #[test] + fn smoke() -> crate::Result<()> { + let age = Age::new(Duration::from_secs(12)); + + let mut headers = Headers::new(); + age.apply(&mut headers); + + let age = Age::from_headers(headers)?.unwrap(); + assert_eq!(age, Age::new(Duration::from_secs(12))); + Ok(()) + } + + #[test] + fn bad_request_on_parse_error() -> crate::Result<()> { + let mut headers = Headers::new(); + headers.insert(AGE, ""); + let err = Age::from_headers(headers).unwrap_err(); + assert_eq!(err.status(), 400); + Ok(()) + } +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index e954c58c..1019be27 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -10,6 +10,8 @@ //! - [MDN: HTTP Conditional Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) mod cache_control; +mod age; pub use cache_control::CacheControl; pub use cache_control::CacheDirective; +pub use age::Age;