Add the Accept ContentType structure.

This commit is contained in:
Sergio Benitez 2017-03-27 01:53:45 -07:00
parent 13359d4f50
commit c09644b270
6 changed files with 262 additions and 9 deletions

View File

@ -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"

151
lib/src/http/accept.rs Normal file
View File

@ -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<f32>);
impl WeightedMediaType {
#[inline(always)]
pub fn media_type(&self) -> &MediaType {
&self.0
}
#[inline(always)]
pub fn weight(&self) -> Option<f32> {
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<WeightedMediaType>);
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<Item=&'a WeightedMediaType> + 'a {
self.0.iter()
}
#[inline(always)]
pub fn media_types<'a>(&'a self) -> impl Iterator<Item=&'a MediaType> + '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<Accept, String> {
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");
}
}

View File

@ -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};

View File

@ -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<f32>> {
match media_type.params().next() {
Some(("q", value)) if value.len() <= 4 => match value.parse::<f32>().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<Accept, ParseError<&str>> {
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]);
}
}

View File

@ -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());

View File

@ -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::*;