From c09644b270236951090a4b48e1c69460d435f12b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 27 Mar 2017 01:53:45 -0700 Subject: [PATCH] Add the Accept ContentType structure. --- lib/Cargo.toml | 4 +- lib/src/http/accept.rs | 151 +++++++++++++++++++++++++++++++ lib/src/http/mod.rs | 4 +- lib/src/http/parse/accept.rs | 102 +++++++++++++++++++++ lib/src/http/parse/media_type.rs | 8 +- lib/src/http/parse/mod.rs | 2 + 6 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 lib/src/http/accept.rs create mode 100644 lib/src/http/parse/accept.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 0482b748..16af9fb5 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -29,8 +29,8 @@ time = "0.1" memchr = "1" base64 = "0.4" smallvec = "0.3" -pear = "0.0.5" -pear_codegen = "0.0.5" +pear = "0.0.7" +pear_codegen = "0.0.7" [dependencies.cookie] version = "0.7.2" diff --git a/lib/src/http/accept.rs b/lib/src/http/accept.rs new file mode 100644 index 00000000..feb221b1 --- /dev/null +++ b/lib/src/http/accept.rs @@ -0,0 +1,151 @@ +use http::MediaType; +use http::parse::parse_accept; + +use std::ops::Deref; +use std::str::FromStr; +use std::fmt; + +#[derive(Debug, PartialEq)] +pub struct WeightedMediaType(pub MediaType, pub Option); + +impl WeightedMediaType { + #[inline(always)] + pub fn media_type(&self) -> &MediaType { + &self.0 + } + + #[inline(always)] + pub fn weight(&self) -> Option { + self.1 + } + + #[inline(always)] + pub fn weight_or(&self, default: f32) -> f32 { + self.1.unwrap_or(default) + } + + #[inline(always)] + pub fn into_inner(self) -> MediaType { + self.0 + } +} + +impl Deref for WeightedMediaType { + type Target = MediaType; + + #[inline(always)] + fn deref(&self) -> &MediaType { + &self.0 + } +} + +/// The HTTP Accept header. +#[derive(Debug, PartialEq)] +pub struct Accept(pub Vec); + +static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None); + +impl Accept { + pub fn preferred(&self) -> &WeightedMediaType { + // See https://tools.ietf.org/html/rfc7231#section-5.3.2. + let mut all = self.iter(); + let mut preferred = all.next().unwrap_or(&ANY); + for current in all { + if current.weight().is_none() && preferred.weight().is_some() { + preferred = current; + } else if current.weight_or(0.0) > preferred.weight_or(1.0) { + preferred = current; + } else if current.media_type() == preferred.media_type() { + if current.weight() == preferred.weight() { + let c_count = current.params().filter(|p| p.0 != "q").count(); + let p_count = preferred.params().filter(|p| p.0 != "q").count(); + if c_count > p_count { + preferred = current; + } + } + } + } + + preferred + } + + #[inline(always)] + pub fn first(&self) -> Option<&WeightedMediaType> { + if self.0.len() > 0 { + Some(&self.0[0]) + } else { + None + } + } + + #[inline(always)] + pub fn iter<'a>(&'a self) -> impl Iterator + 'a { + self.0.iter() + } + + #[inline(always)] + pub fn media_types<'a>(&'a self) -> impl Iterator + 'a { + self.0.iter().map(|weighted_mt| weighted_mt.media_type()) + } +} + +impl fmt::Display for Accept { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (i, media_type) in self.iter().enumerate() { + if i >= 1 { write!(f, ", ")?; } + write!(f, "{}", media_type.0)?; + } + + Ok(()) + } +} + +impl FromStr for Accept { + // Ideally we'd return a `ParseError`, but that requires a lifetime. + type Err = String; + + #[inline] + fn from_str(raw: &str) -> Result { + parse_accept(raw).map_err(|e| e.to_string()) + } +} + +#[cfg(test)] +mod test { + use http::{Accept, MediaType}; + + macro_rules! assert_preference { + ($string:expr, $expect:expr) => ( + let accept: Accept = $string.parse().expect("accept string parse"); + let expected: MediaType = $expect.parse().expect("media type parse"); + let preferred = accept.preferred(); + assert_eq!(preferred.media_type().to_string(), expected.to_string()); + ) + } + + #[test] + fn test_preferred() { + assert_preference!("text/*", "text/*"); + assert_preference!("text/*, text/html", "text/*"); + assert_preference!("text/*; q=0.1, text/html", "text/html"); + assert_preference!("text/*; q=1, text/html", "text/html"); + assert_preference!("text/html, text/*", "text/html"); + assert_preference!("text/html, text/*; q=1", "text/html"); + assert_preference!("text/html, text/*; q=0.1", "text/html"); + assert_preference!("text/html, application/json", "text/html"); + + assert_preference!("a/b; q=0.1, a/b; q=0.2", "a/b; q=0.2"); + assert_preference!("a/b; q=0.1, b/c; q=0.2", "b/c; q=0.2"); + assert_preference!("a/b; q=0.5, b/c; q=0.2", "a/b; q=0.5"); + + assert_preference!("a/b; q=0.5, b/c; q=0.2, c/d", "c/d"); + assert_preference!("a/b; q=0.5; v=1, a/b", "a/b"); + + assert_preference!("a/b; v=1, a/b; v=1; c=2", "a/b; v=1; c=2"); + assert_preference!("a/b; v=1; c=2, a/b; v=1", "a/b; v=1; c=2"); + assert_preference!("a/b; q=0.5; v=1, a/b; q=0.5; v=1; c=2", + "a/b; q=0.5; v=1; c=2"); + assert_preference!("a/b; q=0.6; v=1, a/b; q=0.5; v=1; c=2", + "a/b; q=0.6; v=1"); + } +} diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index f853453d..4332e47b 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -17,9 +17,10 @@ mod media_type; mod content_type; mod status; mod header; +mod accept; mod parse; -// We need to export this for codegen, but otherwise it's unnecessary. +// We need to export these for codegen, but otherwise it's unnecessary. // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) #[doc(hidden)] pub mod ascii; #[doc(hidden)] pub use self::parse::IndexedStr; @@ -27,6 +28,7 @@ mod parse; pub use self::method::Method; pub use self::content_type::ContentType; +pub use self::accept::{Accept, WeightedMediaType}; pub use self::status::{Status, StatusClass}; pub use self::header::{Header, HeaderMap}; diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs new file mode 100644 index 00000000..be6aa0f8 --- /dev/null +++ b/lib/src/http/parse/accept.rs @@ -0,0 +1,102 @@ +use pear::{ParseResult, ParseError}; +use pear::parsers::*; + +use http::parse::checkers::is_whitespace; +use http::parse::media_type::media_type; +use http::{MediaType, Accept, WeightedMediaType}; + +fn q_value<'a>(_: &'a str, media_type: &MediaType) -> ParseResult<&'a str, Option> { + match media_type.params().next() { + Some(("q", value)) if value.len() <= 4 => match value.parse::().ok() { + Some(q) if q > 1.0 => ParseError::custom("accept", "q value must be <= 1.0"), + Some(q) => ParseResult::Done(Some(q)), + None => ParseError::custom("accept", "q value must be float") + }, + _ => ParseResult::Done(None) + } +} + +#[parser] +fn accept<'a>(input: &mut &'a str) -> ParseResult<&'a str, Accept> { + let mut media_types = vec![]; + repeat_while!(eat(','), { + skip_while(is_whitespace); + let media_type = media_type(input); + let weight = q_value(&media_type); + media_types.push(WeightedMediaType(media_type, weight)); + }); + + Accept(media_types) +} + +pub fn parse_accept(mut input: &str) -> Result> { + parse!(&mut input, (accept(), eof()).0).into() +} + +#[cfg(test)] +mod test { + use http::{Accept, MediaType, WeightedMediaType}; + use super::{ParseResult, parse_accept}; + + macro_rules! assert_no_parse { + ($string:expr) => ({ + let result: Result<_, _> = parse_accept($string).into(); + if result.is_ok() { panic!("{:?} parsed unexpectedly.", $string) } + }); + } + + macro_rules! assert_parse { + ($string:expr) => ({ + match parse_accept($string) { + Ok(accept) => accept, + Err(e) => panic!("{:?} failed to parse: {}", $string, e) + } + }); + } + + macro_rules! assert_parse_eq { + ($string:expr, [$($mt:expr),*]) => ({ + let expected = vec![$($mt),*]; + let result = assert_parse!($string); + for (i, wmt) in result.iter().enumerate() { + assert_eq!(wmt.media_type(), &expected[i]); + } + }); + } + + macro_rules! assert_quality_eq { + ($string:expr, [$($mt:expr),*]) => ({ + let expected = vec![$($mt),*]; + let result = assert_parse!($string); + for (i, wmt) in result.iter().enumerate() { + assert_eq!(wmt.media_type(), &expected[i]); + } + }); + } + + #[test] + fn check_does_parse() { + assert_parse!("text/html"); + assert_parse!("*/*, a/b; q=1.0; v=1, application/something, a/b"); + assert_parse!("a/b, b/c"); + assert_parse!("text/*"); + assert_parse!("text/*; q=1"); + assert_parse!("text/*; q=1; level=2"); + assert_parse!("audio/*; q=0.2, audio/basic"); + assert_parse!("text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"); + assert_parse!("text/*, text/html, text/html;level=1, */*"); + assert_parse!("text/*;q=0.3, text/html;q=0.7, text/html;level=1, \ + text/html;level=2;q=0.4, */*;q=0.5"); + } + + #[test] + fn check_parse_eq() { + assert_parse_eq!("text/html", [MediaType::HTML]); + assert_parse_eq!("text/html, application/json", + [MediaType::HTML, MediaType::JSON]); + assert_parse_eq!("text/html; charset=utf-8; v=1, application/json", + [MediaType::HTML, MediaType::JSON]); + assert_parse_eq!("text/html, text/html; q=0.1, text/html; q=0.2", + [MediaType::HTML, MediaType::HTML, MediaType::HTML]); + } +} diff --git a/lib/src/http/parse/media_type.rs b/lib/src/http/parse/media_type.rs index fca83a6c..d471ead0 100644 --- a/lib/src/http/parse/media_type.rs +++ b/lib/src/http/parse/media_type.rs @@ -25,7 +25,7 @@ fn quoted_string<'a>(input: &mut &'a str) -> ParseResult<&'a str, &'a str> { } #[parser] -fn media_type<'a>(input: &mut &'a str, +pub fn media_type<'a>(input: &mut &'a str, source: &'a str) -> ParseResult<&'a str, MediaType> { let top = take_some_while(|c| is_valid_token(c) && c != '/'); eat('/'); @@ -77,8 +77,7 @@ mod test { macro_rules! assert_parse { ($string:expr) => ({ - let result: Result<_, _> = parse_media_type($string).into(); - match result { + match parse_media_type($string) { Ok(media_type) => media_type, Err(e) => panic!("{:?} failed to parse: {}", $string, e) } @@ -90,9 +89,6 @@ mod test { let result = assert_parse!($string); assert_eq!(result, $result); - let result = assert_parse!($string); - assert_eq!(result, $result); - let expected_params: Vec<(&str, &str)> = vec![$(($k, $v)),*]; if expected_params.len() > 0 { assert_eq!(result.params().count(), expected_params.len()); diff --git a/lib/src/http/parse/mod.rs b/lib/src/http/parse/mod.rs index f1913347..7ef09082 100644 --- a/lib/src/http/parse/mod.rs +++ b/lib/src/http/parse/mod.rs @@ -1,6 +1,8 @@ mod media_type; +mod accept; mod indexed_str; mod checkers; pub use self::indexed_str::*; pub use self::media_type::*; +pub use self::accept::*;