Handle specificity based preferences in Accept. Allow 3 decimals in q parameter.

This commit is contained in:
Sergio Benitez 2017-03-29 18:18:30 -07:00
parent b102a6a497
commit 7d48944080
3 changed files with 105 additions and 17 deletions

View File

@ -96,23 +96,28 @@ impl Accept {
pub fn preferred(&self) -> &WeightedMediaType {
static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None);
//
// 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;
// FIXME: Prefer text/html over text/*, for example.
} 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;
}
for media_type in all {
if media_type.weight().is_none() && preferred.weight().is_some() {
// Media types without a `q` parameter are preferred.
preferred = media_type;
} else if media_type.weight_or(0.0) > preferred.weight_or(1.0) {
// Prefer media types with a greater weight, but if one doesn't
// have a weight, prefer the one we already have.
preferred = media_type;
} else if media_type.specificity() > preferred.specificity() {
// Prefer more specific media types over less specific ones. IE:
// text/html over application/*.
preferred = media_type;
} else if media_type == preferred {
// Finally, all other things being equal, prefer a media type
// with more parameters over one with fewer. IE: text/html; a=b
// over text/html.
if media_type.params().count() > preferred.params().count() {
preferred = media_type;
}
}
}
@ -120,6 +125,8 @@ impl Accept {
preferred
}
// */html, text/plain
#[inline(always)]
pub fn first(&self) -> Option<&WeightedMediaType> {
self.iter().next()
@ -189,13 +196,22 @@ mod test {
#[test]
fn test_preferred() {
assert_preference!("text/*", "text/*");
assert_preference!("text/*, text/html", "text/*");
assert_preference!("text/*, text/html", "text/html");
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/*, text/html", "text/html");
assert_preference!("text/html, text/*; q=1", "text/html");
assert_preference!("text/html; q=1, text/html", "text/html");
assert_preference!("text/html, text/*; q=0.1", "text/html");
assert_preference!("text/html, application/json", "text/html");
assert_preference!("text/html, application/json; q=1", "text/html");
assert_preference!("application/json; q=1, text/html", "text/html");
assert_preference!("text/*, application/json", "application/json");
assert_preference!("*/*, text/*", "text/*");
assert_preference!("*/*, text/*, text/plain", "text/plain");
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");

View File

@ -228,6 +228,78 @@ impl MediaType {
self.sub.to_str(self.source.as_ref()).into()
}
/// Returns a `u8` representing how specific the top-level type and subtype
/// of this media type are.
///
/// The return value is either `0`, `1`, or `2`, where `2` is the most
/// specific. A `0` is returned when both the top and sublevel types are
/// `*`. A `1` is returned when only one of the top or sublevel types is
/// `*`, and a `2` is returned when neither the top or sublevel types are
/// `*`.
///
/// # Example
///
/// ```rust
/// use rocket::http::MediaType;
///
/// let mt = MediaType::Plain;
/// assert_eq!(mt.specificity(), 2);
///
/// let mt = MediaType::new("text", "*");
/// assert_eq!(mt.specificity(), 1);
///
/// let mt = MediaType::Any;
/// assert_eq!(mt.specificity(), 0);
/// ```
#[inline]
pub fn specificity(&self) -> u8 {
(self.top() != "*") as u8 + (self.sub() != "*") as u8
}
/// Compares `self` with `other` and returns `true` if `self` and `other`
/// are exactly equal to eachother, including with respect to their
/// parameters.
///
/// This is different from the `PartialEq` implementation in that it
/// considers parameters. If `PartialEq` returns false, this function is
/// guaranteed to return false. Similarly, if this function returns `true`,
/// `PartialEq` is guaranteed to return true. However, if `PartialEq`
/// returns `true`, this function may or may not return `true`.
///
/// # Example
///
/// ```rust
/// use rocket::http::MediaType;
///
/// let plain = MediaType::Plain;
/// let plain2 = MediaType::with_params("text", "plain", ("charset", "utf-8"));
/// let just_plain = MediaType::new("text", "plain");
///
/// // The `PartialEq` implementation doesn't consider parameters.
/// assert!(plain == just_plain);
/// assert!(just_plain == plain2);
/// assert!(plain == plain2);
///
/// // While `exact_eq` does.
/// assert!(!plain.exact_eq(&just_plain));
/// assert!(!plain2.exact_eq(&just_plain));
/// assert!(plain.exact_eq(&plain2));
/// ```
pub fn exact_eq(&self, other: &MediaType) -> bool {
self == other && {
let (mut a_params, mut b_params) = (self.params(), other.params());
loop {
match (a_params.next(), b_params.next()) {
(Some(a), Some(b)) if a != b => return false,
(Some(_), Some(_)) => continue,
(Some(_), None) => return false,
(None, Some(_)) => return false,
(None, None) => return true
}
}
}
}
/// Returns an iterator over the (key, value) pairs of the media type's
/// parameter list. The iterator will be empty if the media type has no
/// parameters.

View File

@ -7,8 +7,8 @@ use http::{MediaType, Accept, WeightedMediaType};
fn q<'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", value)) if value.len() <= 5 => match value.parse::<f32>().ok() {
Some(q) if q > 1. => ParseError::custom("accept", "q value must be <= 1"),
Some(q) => ParseResult::Done(Some(q)),
None => ParseError::custom("accept", "q value must be float")
},