diff --git a/src/conditional/if_modified_since.rs b/src/conditional/if_modified_since.rs new file mode 100644 index 00000000..60a1b373 --- /dev/null +++ b/src/conditional/if_modified_since.rs @@ -0,0 +1,125 @@ +use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, IF_MODIFIED_SINCE}; +use crate::utils::{fmt_http_date, parse_http_date}; + +use std::fmt::Debug; +use std::option; +use std::time::SystemTime; + +/// HTTP `IfModifiedSince` header +/// +/// # Specifications +/// +/// - [RFC 7232, section 3.3: If-Modified-Since](https://tools.ietf.org/html/rfc7232#section-3.3) +/// +/// # Examples +/// +/// ``` +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::Response; +/// use http_types::conditional::IfModifiedSince; +/// use std::time::{SystemTime, Duration}; +/// +/// let time = SystemTime::now() + Duration::from_secs(5 * 60); +/// let expires = IfModifiedSince::new(time); +/// +/// let mut res = Response::new(200); +/// expires.apply(&mut res); +/// +/// let expires = IfModifiedSince::from_headers(res)?.unwrap(); +/// +/// // HTTP dates only have second-precision. +/// let elapsed = time.duration_since(expires.modified())?; +/// assert_eq!(elapsed.as_secs(), 0); +/// # +/// # Ok(()) } +/// ``` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct IfModifiedSince { + instant: SystemTime, +} + +impl IfModifiedSince { + /// Create a new instance of `IfModifiedSince`. + pub fn new(instant: SystemTime) -> Self { + Self { instant } + } + + /// Returns the last modification time listed. + pub fn modified(&self) -> SystemTime { + self.instant + } + + /// Create an instance of `IfModifiedSince` from a `Headers` instance. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let headers = match headers.as_ref().get(IF_MODIFIED_SINCE) { + 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 instant = parse_http_date(header.as_str())?; + Ok(Some(Self { instant })) + } + + /// Insert a `HeaderName` + `HeaderValue` pair into a `Headers` instance. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(IF_MODIFIED_SINCE, self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + IF_MODIFIED_SINCE + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + let output = fmt_http_date(self.instant); + + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } +} + +impl ToHeaderValues for IfModifiedSince { + 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; + use std::time::Duration; + + #[test] + fn smoke() -> crate::Result<()> { + let time = SystemTime::now() + Duration::from_secs(5 * 60); + let expires = IfModifiedSince::new(time); + + let mut headers = Headers::new(); + expires.apply(&mut headers); + + let expires = IfModifiedSince::from_headers(headers)?.unwrap(); + + // HTTP dates only have second-precision + let elapsed = time.duration_since(expires.modified())?; + assert_eq!(elapsed.as_secs(), 0); + Ok(()) + } + + #[test] + fn bad_request_on_parse_error() -> crate::Result<()> { + let mut headers = Headers::new(); + headers.insert(IF_MODIFIED_SINCE, ""); + let err = IfModifiedSince::from_headers(headers).unwrap_err(); + assert_eq!(err.status(), 400); + Ok(()) + } +} diff --git a/src/conditional/if_unmodified_since.rs b/src/conditional/if_unmodified_since.rs new file mode 100644 index 00000000..8b9dbd7b --- /dev/null +++ b/src/conditional/if_unmodified_since.rs @@ -0,0 +1,125 @@ +use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, IF_UNMODIFIED_SINCE}; +use crate::utils::{fmt_http_date, parse_http_date}; + +use std::fmt::Debug; +use std::option; +use std::time::SystemTime; + +/// HTTP `IfUnmodifiedSince` header +/// +/// # Specifications +/// +/// - [RFC 7232, section 3.4: If-Unmodified-Since](https://tools.ietf.org/html/rfc7232#section-3.4) +/// +/// # Examples +/// +/// ``` +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::Response; +/// use http_types::conditional::IfUnmodifiedSince; +/// use std::time::{SystemTime, Duration}; +/// +/// let time = SystemTime::now() + Duration::from_secs(5 * 60); +/// let expires = IfUnmodifiedSince::new(time); +/// +/// let mut res = Response::new(200); +/// expires.apply(&mut res); +/// +/// let expires = IfUnmodifiedSince::from_headers(res)?.unwrap(); +/// +/// // HTTP dates only have second-precision. +/// let elapsed = time.duration_since(expires.modified())?; +/// assert_eq!(elapsed.as_secs(), 0); +/// # +/// # Ok(()) } +/// ``` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct IfUnmodifiedSince { + instant: SystemTime, +} + +impl IfUnmodifiedSince { + /// Create a new instance of `IfUnmodifiedSince`. + pub fn new(instant: SystemTime) -> Self { + Self { instant } + } + + /// Returns the last modification time listed. + pub fn modified(&self) -> SystemTime { + self.instant + } + + /// Create an instance of `IfUnmodifiedSince` from a `Headers` instance. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let headers = match headers.as_ref().get(IF_UNMODIFIED_SINCE) { + 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 instant = parse_http_date(header.as_str())?; + Ok(Some(Self { instant })) + } + + /// Insert a `HeaderName` + `HeaderValue` pair into a `Headers` instance. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(IF_UNMODIFIED_SINCE, self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + IF_UNMODIFIED_SINCE + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + let output = fmt_http_date(self.instant); + + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } +} + +impl ToHeaderValues for IfUnmodifiedSince { + 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; + use std::time::Duration; + + #[test] + fn smoke() -> crate::Result<()> { + let time = SystemTime::now() + Duration::from_secs(5 * 60); + let expires = IfUnmodifiedSince::new(time); + + let mut headers = Headers::new(); + expires.apply(&mut headers); + + let expires = IfUnmodifiedSince::from_headers(headers)?.unwrap(); + + // HTTP dates only have second-precision + let elapsed = time.duration_since(expires.modified())?; + assert_eq!(elapsed.as_secs(), 0); + Ok(()) + } + + #[test] + fn bad_request_on_parse_error() -> crate::Result<()> { + let mut headers = Headers::new(); + headers.insert(IF_UNMODIFIED_SINCE, ""); + let err = IfUnmodifiedSince::from_headers(headers).unwrap_err(); + assert_eq!(err.status(), 400); + Ok(()) + } +} diff --git a/src/conditional/last_modified.rs b/src/conditional/last_modified.rs new file mode 100644 index 00000000..110b7fa3 --- /dev/null +++ b/src/conditional/last_modified.rs @@ -0,0 +1,125 @@ +use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, LAST_MODIFIED}; +use crate::utils::{fmt_http_date, parse_http_date}; + +use std::fmt::Debug; +use std::option; +use std::time::SystemTime; + +/// HTTP `LastModified` header +/// +/// # Specifications +/// +/// - [RFC 7232, section 2.2: Last-Modified](https://tools.ietf.org/html/rfc7232#section-2.2) +/// +/// # Examples +/// +/// ``` +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::Response; +/// use http_types::conditional::LastModified; +/// use std::time::{SystemTime, Duration}; +/// +/// let time = SystemTime::now() + Duration::from_secs(5 * 60); +/// let last_modified = LastModified::new(time); +/// +/// let mut res = Response::new(200); +/// last_modified.apply(&mut res); +/// +/// let last_modified = LastModified::from_headers(res)?.unwrap(); +/// +/// // HTTP dates only have second-precision. +/// let elapsed = time.duration_since(last_modified.modified())?; +/// assert_eq!(elapsed.as_secs(), 0); +/// # +/// # Ok(()) } +/// ``` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct LastModified { + instant: SystemTime, +} + +impl LastModified { + /// Create a new instance of `LastModified`. + pub fn new(instant: SystemTime) -> Self { + Self { instant } + } + + /// Returns the last modification time listed. + pub fn modified(&self) -> SystemTime { + self.instant + } + + /// Create an instance of `LastModified` from a `Headers` instance. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let headers = match headers.as_ref().get(LAST_MODIFIED) { + 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 instant = parse_http_date(header.as_str())?; + Ok(Some(Self { instant })) + } + + /// Insert a `HeaderName` + `HeaderValue` pair into a `Headers` instance. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(LAST_MODIFIED, self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + LAST_MODIFIED + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + let output = fmt_http_date(self.instant); + + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } +} + +impl ToHeaderValues for LastModified { + 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; + use std::time::Duration; + + #[test] + fn smoke() -> crate::Result<()> { + let time = SystemTime::now() + Duration::from_secs(5 * 60); + let last_modified = LastModified::new(time); + + let mut headers = Headers::new(); + last_modified.apply(&mut headers); + + let last_modified = LastModified::from_headers(headers)?.unwrap(); + + // HTTP dates only have second-precision + let elapsed = time.duration_since(last_modified.modified())?; + assert_eq!(elapsed.as_secs(), 0); + Ok(()) + } + + #[test] + fn bad_request_on_parse_error() -> crate::Result<()> { + let mut headers = Headers::new(); + headers.insert(LAST_MODIFIED, ""); + let err = LastModified::from_headers(headers).unwrap_err(); + assert_eq!(err.status(), 400); + Ok(()) + } +} diff --git a/src/conditional/mod.rs b/src/conditional/mod.rs index ca3db5ab..348e307e 100644 --- a/src/conditional/mod.rs +++ b/src/conditional/mod.rs @@ -9,5 +9,11 @@ //! - [MDN: HTTP Conditional Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) mod etag; +mod if_modified_since; +mod if_unmodified_since; +mod last_modified; pub use etag::ETag; +pub use if_modified_since::IfModifiedSince; +pub use if_unmodified_since::IfUnmodifiedSince; +pub use last_modified::LastModified;