Skip to content

Commit

Permalink
Add conditional::Vary
Browse files Browse the repository at this point in the history
  • Loading branch information
yoshuawuyts committed Sep 19, 2020
1 parent b941518 commit fb6336e
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/conditional/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ mod if_modified_since;
mod if_unmodified_since;
mod last_modified;
mod match_directive;
mod vary;
mod vary_directive;

pub mod if_match;
pub mod if_none_match;

pub use etag::ETag;
pub use match_directive::MatchDirective;
pub use vary::Vary;
pub use vary_directive::VaryDirective;

#[doc(inline)]
pub use if_match::IfMatch;
Expand Down
245 changes: 245 additions & 0 deletions src/conditional/vary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
//! Apply the HTTP method if the ETag matches.

use crate::conditional::VaryDirective;
use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, VARY};

use std::convert::TryInto;
use std::fmt::{self, Debug, Write};
use std::iter::Iterator;
use std::option;
use std::slice;
use std::str::FromStr;

/// Apply the HTTP method if the ETag matches.
///
/// # Specifications
///
/// - [RFC 7231, section 7.1.4: Vary](https://tools.ietf.org/html/rfc7231#section-7.1.4)
///
/// # Examples
///
/// ```
/// # fn main() -> http_types::Result<()> {
/// #
/// use http_types::Response;
/// use http_types::conditional::Vary;
///
/// let mut entries = Vary::new();
/// entries.push("User-Agent")?;
/// entries.push("Accept-Encoding")?;
///
/// let mut res = Response::new(200);
/// entries.apply(&mut res);
///
/// let entries = Vary::from_headers(res)?.unwrap();
/// let mut entries = entries.iter();
/// assert_eq!(entries.next().unwrap(), "User-Agent");
/// assert_eq!(entries.next().unwrap(), "Accept-Encoding");
/// #
/// # Ok(()) }
/// ```
pub struct Vary {
entries: Vec<VaryDirective>,
}

impl Vary {
/// Create a new instance of `Vary`.
pub fn new() -> Self {
Self { entries: vec![] }
}

/// Create a new instance from headers.
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
let mut entries = vec![];
let headers = match headers.as_ref().get(VARY) {
Some(headers) => headers,
None => return Ok(None),
};

for value in headers {
for part in value.as_str().trim().split(',') {
let entry = VaryDirective::from_str(part)?;
entries.push(entry);
}
}

Ok(Some(Self { entries }))
}

/// Sets the `If-Match` header.
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
headers.as_mut().insert(VARY, self.value());
}

/// Get the `HeaderName`.
pub fn name(&self) -> HeaderName {
VARY
}

/// Get the `HeaderValue`.
pub fn value(&self) -> HeaderValue {
let mut output = String::new();
for (n, directive) in self.entries.iter().enumerate() {
let directive: HeaderValue = directive.clone().into();
match n {
0 => write!(output, "{}", directive).unwrap(),
_ => write!(output, ", {}", directive).unwrap(),
};
}

// SAFETY: the internal string is validated to be ASCII.
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
}

/// Push a directive into the list of entries.
pub fn push(
&mut self,
directive: impl TryInto<VaryDirective, Error = crate::Error>,
) -> crate::Result<()> {
self.entries.push(directive.try_into()?);
Ok(())
}

/// An iterator visiting all server entries.
pub fn iter(&self) -> Iter<'_> {
Iter {
inner: self.entries.iter(),
}
}

/// An iterator visiting all server entries.
pub fn iter_mut(&mut self) -> IterMut<'_> {
IterMut {
inner: self.entries.iter_mut(),
}
}
}

impl IntoIterator for Vary {
type Item = VaryDirective;
type IntoIter = IntoIter;

#[inline]
fn into_iter(self) -> Self::IntoIter {
IntoIter {
inner: self.entries.into_iter(),
}
}
}

impl<'a> IntoIterator for &'a Vary {
type Item = &'a VaryDirective;
type IntoIter = Iter<'a>;

#[inline]
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}

impl<'a> IntoIterator for &'a mut Vary {
type Item = &'a mut VaryDirective;
type IntoIter = IterMut<'a>;

#[inline]
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}

/// A borrowing iterator over entries in `Vary`.
#[derive(Debug)]
pub struct IntoIter {
inner: std::vec::IntoIter<VaryDirective>,
}

impl Iterator for IntoIter {
type Item = VaryDirective;

fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}

/// A lending iterator over entries in `Vary`.
#[derive(Debug)]
pub struct Iter<'a> {
inner: slice::Iter<'a, VaryDirective>,
}

impl<'a> Iterator for Iter<'a> {
type Item = &'a VaryDirective;

fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}

/// A mutable iterator over entries in `Vary`.
#[derive(Debug)]
pub struct IterMut<'a> {
inner: slice::IterMut<'a, VaryDirective>,
}

impl<'a> Iterator for IterMut<'a> {
type Item = &'a mut VaryDirective;

fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}

impl ToHeaderValues for Vary {
type Iter = option::IntoIter<HeaderValue>;
fn to_header_values(&self) -> crate::Result<Self::Iter> {
// A HeaderValue will always convert into itself.
Ok(self.value().to_header_values().unwrap())
}
}

impl Debug for Vary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
for directive in &self.entries {
list.entry(directive);
}
list.finish()
}
}

#[cfg(test)]
mod test {
use crate::conditional::Vary;
use crate::Response;

#[test]
fn smoke() -> crate::Result<()> {
let mut entries = Vary::new();
entries.push("User-Agent")?;
entries.push("Accept-Encoding")?;

let mut res = Response::new(200);
entries.apply(&mut res);

let entries = Vary::from_headers(res)?.unwrap();
let mut entries = entries.iter();
assert_eq!(entries.next().unwrap(), "User-Agent");
assert_eq!(entries.next().unwrap(), "Accept-Encoding");
Ok(())
}
}
105 changes: 105 additions & 0 deletions src/conditional/vary_directive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::headers::{HeaderName, HeaderValue};
use std::convert::TryFrom;
use std::str::FromStr;

/// An HeaderName-based match directive.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VaryDirective {
/// An HeaderName.
HeaderName(HeaderName),
/// Vary any resource.
Wildcard,
}

// impl VaryDirective {
// /// Create an instance from a string slice.
// //
// // This is a private method rather than a trait because we assume the
// // input string is a single-value only. This is upheld by the calling
// // function, but we cannot guarantee this to be true in the general
// // sense.
// pub(crate) fn from_str(s: &str) -> crate::Result<Option<Self>> {
// let s = s.trim();

// match s {
// "*" => Ok(Some(VaryDirective::Wildcard)),
// s => {
// HeaderName::from_string(s.into()).map(|name| Some(VaryDirective::HeaderName(name)))
// }
// }
// }
// }

impl From<HeaderName> for VaryDirective {
fn from(name: HeaderName) -> Self {
Self::HeaderName(name)
}
}

impl PartialEq<HeaderName> for VaryDirective {
fn eq(&self, other: &HeaderName) -> bool {
match self {
Self::HeaderName(name) => name.eq(other),
Self::Wildcard => false,
}
}
}

impl<'a> PartialEq<HeaderName> for &'a VaryDirective {
fn eq(&self, other: &HeaderName) -> bool {
match self {
VaryDirective::HeaderName(name) => name.eq(other),
VaryDirective::Wildcard => false,
}
}
}

impl From<VaryDirective> for HeaderValue {
fn from(directive: VaryDirective) -> Self {
match directive {
VaryDirective::HeaderName(name) => unsafe {
HeaderValue::from_bytes_unchecked(name.to_string().into_bytes())
},
VaryDirective::Wildcard => unsafe {
HeaderValue::from_bytes_unchecked("*".to_string().into_bytes())
},
}
}
}

impl FromStr for VaryDirective {
type Err = crate::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"*" => Ok(VaryDirective::Wildcard),
s => Ok(VaryDirective::HeaderName(s.parse()?)),
}
}
}

impl<'a> TryFrom<&'a str> for VaryDirective {
type Error = crate::Error;

fn try_from(value: &'a str) -> Result<Self, Self::Error> {
VaryDirective::from_str(value)
}
}

impl PartialEq<str> for VaryDirective {
fn eq(&self, other: &str) -> bool {
match self {
VaryDirective::Wildcard => "*" == other,
VaryDirective::HeaderName(s) => s == other,
}
}
}

impl<'a> PartialEq<&'a str> for VaryDirective {
fn eq(&self, other: &&'a str) -> bool {
match self {
VaryDirective::Wildcard => &"*" == other,
VaryDirective::HeaderName(s) => s == other,
}
}
}
9 changes: 9 additions & 0 deletions src/headers/header_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ impl HeaderName {
Ok(HeaderName(Cow::Owned(string)))
}

/// Create a new `HeaderName` from an ASCII string.
///
/// # Error
///
/// This function will error if the string is not valid ASCII.
pub fn from_string(s: String) -> Result<Self, Error> {
Self::from_bytes(s.into_bytes())
}

/// Returns the header name as a `&str`.
pub fn as_str(&self) -> &'_ str {
&self.0
Expand Down

0 comments on commit fb6336e

Please sign in to comment.