From 5fbc252a32dfdf759cadc1abf2889332a69c6863 Mon Sep 17 00:00:00 2001 From: calebmer Date: Thu, 24 Mar 2016 14:11:27 -0400 Subject: [PATCH] feat(header): add prefer and preference applied headers Closes #747 --- src/header/common/mod.rs | 4 + src/header/common/prefer.rs | 201 ++++++++++++++++++++++++ src/header/common/preference_applied.rs | 103 ++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 src/header/common/prefer.rs create mode 100644 src/header/common/preference_applied.rs diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index 090f02370f..d152c86441 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -44,6 +44,8 @@ pub use self::if_range::IfRange; pub use self::last_modified::LastModified; pub use self::location::Location; pub use self::pragma::Pragma; +pub use self::prefer::{Prefer, Preference}; +pub use self::preference_applied::PreferenceApplied; pub use self::range::{Range, ByteRangeSpec}; pub use self::referer::Referer; pub use self::server::Server; @@ -392,6 +394,8 @@ mod if_unmodified_since; mod last_modified; mod location; mod pragma; +mod prefer; +mod preference_applied; mod range; mod referer; mod server; diff --git a/src/header/common/prefer.rs b/src/header/common/prefer.rs new file mode 100644 index 0000000000..f8b9ef1a7f --- /dev/null +++ b/src/header/common/prefer.rs @@ -0,0 +1,201 @@ +use std::fmt; +use std::str::FromStr; +use header::{Header, HeaderFormat}; +use header::parsing::{from_one_comma_delimited, fmt_comma_delimited}; + +/// `Prefer` header, defined in [RFC7240](http://tools.ietf.org/html/rfc7240) +/// +/// The `Prefer` header field is HTTP header field that can be used by a +/// client to request that certain behaviors be employed by a server +/// while processing a request. +/// +/// # ABNF +/// ```plain +/// Prefer = "Prefer" ":" 1#preference +/// preference = token [ BWS "=" BWS word ] +/// *( OWS ";" [ OWS parameter ] ) +/// parameter = token [ BWS "=" BWS word ] +/// ``` +/// +/// # Example values +/// * `respond-async` +/// * `return=minimal` +/// * `wait=30` +/// +/// # Examples +/// ``` +/// use hyper::header::{Headers, Prefer, Preference}; +/// +/// let mut headers = Headers::new(); +/// headers.set( +/// Prefer(vec![Preference::RespondAsync]) +/// ); +/// ``` +/// ``` +/// use hyper::header::{Headers, Prefer, Preference}; +/// +/// let mut headers = Headers::new(); +/// headers.set( +/// Prefer(vec![ +/// Preference::RespondAsync, +/// Preference::ReturnRepresentation, +/// Preference::Wait(10u32), +/// Preference::Extension("foo".to_owned(), +/// "bar".to_owned(), +/// vec![]), +/// ]) +/// ); +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub struct Prefer(pub Vec); + +__hyper__deref!(Prefer => Vec); + +impl Header for Prefer { + fn header_name() -> &'static str { + "Prefer" + } + + fn parse_header(raw: &[Vec]) -> ::Result { + let preferences = raw.iter() + .filter_map(|line| from_one_comma_delimited(&line[..]).ok()) + .collect::>>() + .concat(); + if !preferences.is_empty() { + Ok(Prefer(preferences)) + } else { + Err(::Error::Header) + } + } +} + +impl HeaderFormat for Prefer { + fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt_comma_delimited(f, &self[..]) + } +} + +/// Prefer contains a list of these preferences. +#[derive(PartialEq, Clone, Debug)] +pub enum Preference { + /// "respond-async" + RespondAsync, + /// "return=representation" + ReturnRepresentation, + /// "return=minimal" + ReturnMinimal, + /// "handling=strict" + HandlingStrict, + /// "handling=leniant" + HandlingLeniant, + /// "wait=delta" + Wait(u32), + + /// Extension preferences. Always has a value, if none is specified it is + /// just "". A preference can also have a list of parameters. + Extension(String, String, Vec<(String, String)>) +} + +impl fmt::Display for Preference { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Preference::*; + fmt::Display::fmt(match *self { + RespondAsync => "respond-async", + ReturnRepresentation => "return=representation", + ReturnMinimal => "return=minimal", + HandlingStrict => "handling=strict", + HandlingLeniant => "handling=leniant", + + Wait(secs) => return write!(f, "wait={}", secs), + + Extension(ref name, ref value, ref params) => { + try!(write!(f, "{}", name)); + if value != "" { try!(write!(f, "={}", value)); } + if params.len() > 0 { + for &(ref name, ref value) in params { + try!(write!(f, "; {}", name)); + if value != "" { try!(write!(f, "={}", value)); } + } + } + return Ok(()); + } + }, f) + } +} + +impl FromStr for Preference { + type Err = Option<::Err>; + fn from_str(s: &str) -> Result::Err>> { + use self::Preference::*; + let mut params = s.split(';').map(|p| { + let mut param: Vec<_> = p.splitn(2, '=').collect(); + match (param.remove(0), param.pop()) { + (name, Some(value)) => (name.trim(), value.trim().trim_matches('"')), + (name, None) => (name.trim(), "") + } + }); + match params.nth(0) { + Some(param) => { + let rest: Vec<(String, String)> = params.map(|(l, r)| (l.to_owned(), r.to_owned())).collect(); + match param { + ("respond-async", "") => if rest.len() == 0 { Ok(RespondAsync) } else { Err(None) }, + ("return", "representation") => if rest.len() == 0 { Ok(ReturnRepresentation) } else { Err(None) }, + ("return", "minimal") => if rest.len() == 0 { Ok(ReturnMinimal) } else { Err(None) }, + ("handling", "strict") => if rest.len() == 0 { Ok(HandlingStrict) } else { Err(None) }, + ("handling", "leniant") => if rest.len() == 0 { Ok(HandlingLeniant) } else { Err(None) }, + ("wait", secs) => if rest.len() == 0 { secs.parse().map(Wait).map_err(Some) } else { Err(None) }, + (left, right) => Ok(Extension(left.to_owned(), right.to_owned(), rest)) + } + }, + None => Err(None) + } + } +} + +#[cfg(test)] +mod tests { + use header::Header; + use super::*; + + #[test] + fn test_parse_multiple_headers() { + let prefer = Header::parse_header(&[b"respond-async".to_vec(), b"return=representation".to_vec()]); + assert_eq!(prefer.ok(), Some(Prefer(vec![Preference::RespondAsync, + Preference::ReturnRepresentation]))) + } + + #[test] + fn test_parse_argument() { + let prefer = Header::parse_header(&[b"wait=100, handling=leniant, respond-async".to_vec()]); + assert_eq!(prefer.ok(), Some(Prefer(vec![Preference::Wait(100), + Preference::HandlingLeniant, + Preference::RespondAsync]))) + } + + #[test] + fn test_parse_quote_form() { + let prefer = Header::parse_header(&[b"wait=\"200\", handling=\"strict\"".to_vec()]); + assert_eq!(prefer.ok(), Some(Prefer(vec![Preference::Wait(200), + Preference::HandlingStrict]))) + } + + #[test] + fn test_parse_extension() { + let prefer = Header::parse_header(&[b"foo, bar=baz, baz; foo; bar=baz, bux=\"\"; foo=\"\", buz=\"some parameter\"".to_vec()]); + assert_eq!(prefer.ok(), Some(Prefer(vec![ + Preference::Extension("foo".to_owned(), "".to_owned(), vec![]), + Preference::Extension("bar".to_owned(), "baz".to_owned(), vec![]), + Preference::Extension("baz".to_owned(), "".to_owned(), vec![("foo".to_owned(), "".to_owned()), ("bar".to_owned(), "baz".to_owned())]), + Preference::Extension("bux".to_owned(), "".to_owned(), vec![("foo".to_owned(), "".to_owned())]), + Preference::Extension("buz".to_owned(), "some parameter".to_owned(), vec![])]))) + } + + #[test] + fn test_fail_with_args() { + let prefer: ::Result = Header::parse_header(&[b"respond-async; foo=bar".to_vec()]); + assert_eq!(prefer.ok(), None); + } +} + +bench_header!(normal, + Prefer, { vec![b"respond-async, return=representation".to_vec(), b"wait=100".to_vec()] }); diff --git a/src/header/common/preference_applied.rs b/src/header/common/preference_applied.rs new file mode 100644 index 0000000000..7833b1829e --- /dev/null +++ b/src/header/common/preference_applied.rs @@ -0,0 +1,103 @@ +use std::fmt; +use header::{Header, HeaderFormat, Preference}; +use header::parsing::{from_one_comma_delimited, fmt_comma_delimited}; + +/// `Preference-Applied` header, defined in [RFC7240](http://tools.ietf.org/html/rfc7240) +/// +/// The `Preference-Applied` response header may be included within a +/// response message as an indication as to which `Prefer` header tokens were +/// honored by the server and applied to the processing of a request. +/// +/// # ABNF +/// ```plain +/// Preference-Applied = "Preference-Applied" ":" 1#applied-pref +/// applied-pref = token [ BWS "=" BWS word ] +/// ``` +/// +/// # Example values +/// * `respond-async` +/// * `return=minimal` +/// * `wait=30` +/// +/// # Examples +/// ``` +/// use hyper::header::{Headers, PreferenceApplied, Preference}; +/// +/// let mut headers = Headers::new(); +/// headers.set( +/// PreferenceApplied(vec![Preference::RespondAsync]) +/// ); +/// ``` +/// ``` +/// use hyper::header::{Headers, PreferenceApplied, Preference}; +/// +/// let mut headers = Headers::new(); +/// headers.set( +/// PreferenceApplied(vec![ +/// Preference::RespondAsync, +/// Preference::ReturnRepresentation, +/// Preference::Wait(10u32), +/// Preference::Extension("foo".to_owned(), +/// "bar".to_owned(), +/// vec![]), +/// ]) +/// ); +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub struct PreferenceApplied(pub Vec); + +__hyper__deref!(PreferenceApplied => Vec); + +impl Header for PreferenceApplied { + fn header_name() -> &'static str { + "Preference-Applied" + } + + fn parse_header(raw: &[Vec]) -> ::Result { + let preferences = raw.iter() + .filter_map(|line| from_one_comma_delimited(&line[..]).ok()) + .collect::>>() + .concat(); + if !preferences.is_empty() { + Ok(PreferenceApplied(preferences)) + } else { + Err(::Error::Header) + } + } +} + +impl HeaderFormat for PreferenceApplied { + fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result { + let preferences: Vec<_> = self.0.iter().map(|pref| match pref { + // The spec ignores parameters in `Preferences-Applied` + &Preference::Extension(ref name, ref value, _) => Preference::Extension( + name.to_owned(), + value.to_owned(), + vec![] + ), + preference @ _ => preference.clone() + }).collect(); + fmt_comma_delimited(f, &preferences) + } +} + +#[cfg(test)] +mod tests { + use header::{HeaderFormat, Preference}; + use super::*; + + #[test] + fn test_format_ignore_parameters() { + assert_eq!( + format!("{}", &PreferenceApplied(vec![Preference::Extension( + "foo".to_owned(), + "bar".to_owned(), + vec![("bar".to_owned(), "foo".to_owned()), ("buz".to_owned(), "".to_owned())] + )]) as &(HeaderFormat + Send + Sync)), + "foo=bar".to_owned() + ); + } +} + +bench_header!(normal, + PreferenceApplied, { vec![b"respond-async, return=representation".to_vec(), b"wait=100".to_vec()] });