Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conditional::Vary #225

Merged
merged 1 commit into from
Sep 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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