Initial implementation of content negotiation via `Accept`.

This is a breaking change.

This commit changes the meaning of the `format` route attribute when
used on non-payload carrying requests (GET, HEAD, CONNECT, TRACE, and
OPTIONS) so that it matches against the preferred media type in the
`Accept` header of the request. The preferred media type is computed
according to the HTTP 1.1 RFC, barring a few specificty rules to come.
This commit is contained in:
Sergio Benitez 2017-03-29 04:08:53 -07:00
parent fb29b37f30
commit c58ca894b7
12 changed files with 277 additions and 99 deletions

View File

@ -9,33 +9,37 @@ extern crate serde_derive;
#[cfg(test)] mod tests;
use rocket::Request;
use rocket::http::ContentType;
use rocket::response::content;
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: i8,
age: u8,
}
// This shows how to manually serialize some JSON, but in a real application,
// we'd use the JSON contrib type.
// In a `GET` request and all other non-payload supporting request types, the
// preferred media type in the Accept header is matched against the `format` in
// the route attribute.
#[get("/<name>/<age>", format = "application/json")]
fn hello(content_type: ContentType, name: String, age: i8) -> content::JSON<String> {
let person = Person {
name: name,
age: age,
};
fn get_hello(name: String, age: u8) -> content::JSON<String> {
// In a real application, we'd use the JSON contrib type.
let person = Person { name: name, age: age, };
content::JSON(serde_json::to_string(&person).unwrap())
}
println!("ContentType: {}", content_type);
// In a `POST` request and all other payload supporting request types, the
// content type is matched against the `format` in the route attribute.
#[post("/<age>", format = "text/plain", data = "<name>")]
fn post_hello(age: u8, name: String) -> content::JSON<String> {
let person = Person { name: name, age: age, };
content::JSON(serde_json::to_string(&person).unwrap())
}
#[error(404)]
fn not_found(request: &Request) -> content::HTML<String> {
let html = match request.content_type() {
Some(ref ct) if !ct.is_json() => {
format!("<p>This server only supports JSON requests, not '{}'.</p>", ct)
let html = match request.format() {
Some(ref mt) if !mt.is_json() && !mt.is_plain() => {
format!("<p>'{}' requests are not supported.</p>", mt)
}
_ => format!("<p>Sorry, '{}' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
@ -47,7 +51,7 @@ fn not_found(request: &Request) -> content::HTML<String> {
fn main() {
rocket::ignite()
.mount("/hello", routes![hello])
.mount("/hello", routes![get_hello, post_hello])
.catch(errors![not_found])
.launch();
}

View File

@ -1,14 +1,16 @@
use super::rocket;
use super::serde_json;
use super::Person;
use rocket::http::{ContentType, Method, Status};
use rocket::http::{Accept, ContentType, Header, MediaType, Method, Status};
use rocket::testing::MockRequest;
fn test(uri: &str, content_type: ContentType, status: Status, body: String) {
fn test<H>(method: Method, uri: &str, header: H, status: Status, body: String)
where H: Into<Header<'static>>
{
let rocket = rocket::ignite()
.mount("/hello", routes![super::hello])
.mount("/hello", routes![super::get_hello, super::post_hello])
.catch(errors![super::not_found]);
let mut request = MockRequest::new(Method::Get, uri).header(content_type);
let mut request = MockRequest::new(method, uri).header(header);
let mut response = request.dispatch_with(&rocket);
assert_eq!(response.status(), status);
@ -17,24 +19,29 @@ fn test(uri: &str, content_type: ContentType, status: Status, body: String) {
#[test]
fn test_hello() {
let person = Person {
name: "Michael".to_string(),
age: 80,
};
let person = Person { name: "Michael".to_string(), age: 80, };
let body = serde_json::to_string(&person).unwrap();
test("/hello/Michael/80", ContentType::JSON, Status::Ok, body);
test(Method::Get, "/hello/Michael/80", Accept::JSON, Status::Ok, body.clone());
test(Method::Get, "/hello/Michael/80", Accept::Any, Status::Ok, body.clone());
// No `Accept` header is an implicit */*.
test(Method::Get, "/hello/Michael/80", ContentType::XML, Status::Ok, body);
let person = Person { name: "".to_string(), age: 99, };
let body = serde_json::to_string(&person).unwrap();
test(Method::Post, "/hello/99", ContentType::Plain, Status::Ok, body);
}
#[test]
fn test_hello_invalid_content_type() {
let body = format!("<p>This server only supports JSON requests, not '{}'.</p>",
ContentType::HTML);
test("/hello/Michael/80", ContentType::HTML, Status::NotFound, body);
let b = format!("<p>'{}' requests are not supported.</p>", MediaType::HTML);
test(Method::Get, "/hello/Michael/80", Accept::HTML, Status::NotFound, b.clone());
test(Method::Post, "/hello/80", ContentType::HTML, Status::NotFound, b);
}
#[test]
fn test_404() {
let body = "<p>Sorry, '/unknown' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>";
test("/unknown", ContentType::JSON, Status::NotFound, body.to_string());
test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string());
}

View File

@ -28,7 +28,7 @@ state = "0.2"
time = "0.1"
memchr = "1"
base64 = "0.4"
smallvec = "0.3"
smallvec = { git = "https://github.com/SergioBenitez/rust-smallvec" }
pear = "0.0.8"
pear_codegen = "0.0.8"

View File

@ -1,3 +1,5 @@
use std::io::Read;
use outcome::{self, IntoOutcome};
use outcome::Outcome::*;
use http::Status;
@ -115,13 +117,13 @@ impl<'a, S, E> IntoOutcome<S, (Status, E), Data> for Result<S, E> {
/// // Split the string into two pieces at ':'.
/// let (name, age) = match string.find(':') {
/// Some(i) => (&string[..i], &string[(i + 1)..]),
/// None => return Failure((Status::BadRequest, "Missing ':'.".into()))
/// None => return Failure((Status::UnprocessableEntity, "':'".into()))
/// };
///
/// // Parse the age.
/// let age: u16 = match age.parse() {
/// Ok(age) => age,
/// Err(_) => return Failure((Status::BadRequest, "Bad age.".into()))
/// Err(_) => return Failure((Status::UnprocessableEntity, "Age".into()))
/// };
///
/// // Return successfully.
@ -180,3 +182,16 @@ impl<T: FromData> FromData for Option<T> {
}
}
}
impl FromData for String {
type Error = ();
// FIXME: Doc.
fn from_data(_: &Request, data: Data) -> Outcome<Self, Self::Error> {
let mut string = String::new();
match data.open().read_to_string(&mut string) {
Ok(_) => Success(string),
Err(_) => Failure((Status::UnprocessableEntity, ()))
}
}
}

View File

@ -1,19 +1,16 @@
use http::MediaType;
use http::parse::parse_accept;
use std::ops::Deref;
use std::str::FromStr;
use std::fmt;
#[derive(Debug, PartialEq)]
use smallvec::SmallVec;
use http::{Header, IntoCollection, MediaType};
use http::parse::parse_accept;
#[derive(Debug, Clone, 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
@ -25,11 +22,23 @@ impl WeightedMediaType {
}
#[inline(always)]
pub fn into_inner(self) -> MediaType {
pub fn media_type(&self) -> &MediaType {
&self.0
}
#[inline(always)]
pub fn into_media_type(self) -> MediaType {
self.0
}
}
impl From<MediaType> for WeightedMediaType {
#[inline(always)]
fn from(media_type: MediaType) -> WeightedMediaType {
WeightedMediaType(media_type, None)
}
}
impl Deref for WeightedMediaType {
type Target = MediaType;
@ -39,14 +48,55 @@ impl Deref for WeightedMediaType {
}
}
/// The HTTP Accept header.
#[derive(Debug, PartialEq)]
pub struct Accept(pub Vec<WeightedMediaType>);
// FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`.
#[derive(Debug, PartialEq, Clone)]
pub enum AcceptParams {
Static(&'static [WeightedMediaType]),
Dynamic(SmallVec<[WeightedMediaType; 1]>)
}
static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None);
/// The HTTP Accept header.
#[derive(Debug, Clone, PartialEq)]
pub struct Accept(AcceptParams);
macro_rules! accept_constructor {
($($name:ident ($check:ident): $str:expr, $t:expr,
$s:expr $(; $k:expr => $v:expr)*),+) => {
$(
#[doc="An `Accept` header with the single media type for <b>"]
#[doc=$str] #[doc="</b>: <i>"]
#[doc=$t] #[doc="/"] #[doc=$s]
#[doc="</i>"]
#[allow(non_upper_case_globals)]
pub const $name: Accept = Accept(
AcceptParams::Static(&[WeightedMediaType(MediaType::$name, None)])
);
)+
};
}
impl<T: IntoCollection<MediaType>> From<T> for Accept {
#[inline(always)]
fn from(items: T) -> Accept {
Accept(AcceptParams::Dynamic(items.mapped(|item| item.into())))
}
}
impl Accept {
#[inline(always)]
pub fn new<T: IntoCollection<WeightedMediaType>>(items: T) -> Accept {
Accept(AcceptParams::Dynamic(items.into_collection()))
}
// FIXME: IMPLEMENT THIS.
// #[inline(always)]
// pub fn add<M: Into<WeightedMediaType>>(&mut self, media_type: M) {
// self.0.push(media_type.into());
// }
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);
@ -55,6 +105,7 @@ impl Accept {
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();
@ -71,22 +122,25 @@ impl Accept {
#[inline(always)]
pub fn first(&self) -> Option<&WeightedMediaType> {
if self.0.len() > 0 {
Some(&self.0[0])
} else {
None
}
self.iter().next()
}
#[inline(always)]
pub fn iter<'a>(&'a self) -> impl Iterator<Item=&'a WeightedMediaType> + 'a {
self.0.iter()
let slice = match self.0 {
AcceptParams::Static(slice) => slice,
AcceptParams::Dynamic(ref vec) => &vec[..],
};
slice.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())
self.iter().map(|weighted_mt| weighted_mt.media_type())
}
known_media_types!(accept_constructor);
}
impl fmt::Display for Accept {
@ -110,6 +164,15 @@ impl FromStr for Accept {
}
}
/// Creates a new `Header` with name `Accept` and the value set to the HTTP
/// rendering of this `Accept` header.
impl Into<Header<'static>> for Accept {
#[inline(always)]
fn into(self) -> Header<'static> {
Header::new("Accept", self.to_string())
}
}
#[cfg(test)]
mod test {
use http::{Accept, MediaType};

View File

@ -3,7 +3,7 @@ use std::ops::Deref;
use std::str::FromStr;
use std::fmt;
use http::{Header, MediaType};
use http::{IntoCollection, Header, MediaType};
use http::hyper::mime::Mime;
/// Representation of HTTP Content-Types.
@ -136,7 +136,7 @@ impl ContentType {
/// ```rust
/// use rocket::http::ContentType;
///
/// let id = ContentType::with_params("application", "x-id", Some(("id", "1")));
/// let id = ContentType::with_params("application", "x-id", ("id", "1"));
/// assert_eq!(id.to_string(), "application/x-id; id=1".to_string());
/// ```
///
@ -153,11 +153,21 @@ impl ContentType {
pub fn with_params<T, S, K, V, P>(top: T, sub: S, ps: P) -> ContentType
where T: Into<Cow<'static, str>>, S: Into<Cow<'static, str>>,
K: Into<Cow<'static, str>>, V: Into<Cow<'static, str>>,
P: IntoIterator<Item=(K, V)>
P: IntoCollection<(K, V)>
{
ContentType(MediaType::with_params(top, sub, ps))
}
#[inline(always)]
pub fn media_type(&self) -> &MediaType {
&self.0
}
#[inline(always)]
pub fn into_media_type(self) -> MediaType {
self.0
}
known_media_types!(content_types);
}

View File

@ -1,8 +1,8 @@
macro_rules! known_media_types {
($cont:ident) => ($cont! {
Any (is_any): "any Content-Type", "*", "*",
Any (is_any): "any media type", "*", "*",
HTML (is_html): "HTML", "text", "html" ; "charset" => "utf-8",
Plain (is_plain): "plaintext", "text", "plain" ; "charset" => "utf-8",
Plain (is_plain): "plain text", "text", "plain" ; "charset" => "utf-8",
JSON (is_json): "JSON", "application", "json",
MsgPack (is_msgpack): "MessagePack", "application", "msgpack",
Form (is_form): "forms", "application", "x-www-form-urlencoded",

View File

@ -3,6 +3,7 @@ use std::str::FromStr;
use std::fmt;
use std::hash::{Hash, Hasher};
use http::IntoCollection;
use http::ascii::{uncased_eq, UncasedAsciiRef};
use http::parse::{IndexedStr, parse_media_type};
@ -17,7 +18,6 @@ struct MediaParam {
// FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`.
#[derive(Debug, Clone)]
pub enum MediaParams {
Empty,
Static(&'static [(IndexedStr, IndexedStr)]),
Dynamic(SmallVec<[(IndexedStr, IndexedStr); 2]>)
}
@ -142,7 +142,7 @@ impl MediaType {
source: None,
top: IndexedStr::Concrete(top.into()),
sub: IndexedStr::Concrete(sub.into()),
params: MediaParams::Empty,
params: MediaParams::Static(&[]),
}
}
@ -157,7 +157,7 @@ impl MediaType {
/// ```rust
/// use rocket::http::MediaType;
///
/// let id = MediaType::with_params("application", "x-id", Some(("id", "1")));
/// let id = MediaType::with_params("application", "x-id", ("id", "1"));
/// assert_eq!(id.to_string(), "application/x-id; id=1".to_string());
/// ```
///
@ -174,15 +174,13 @@ impl MediaType {
pub fn with_params<T, S, K, V, P>(top: T, sub: S, ps: P) -> MediaType
where T: Into<Cow<'static, str>>, S: Into<Cow<'static, str>>,
K: Into<Cow<'static, str>>, V: Into<Cow<'static, str>>,
P: IntoIterator<Item=(K, V)>
P: IntoCollection<(K, V)>
{
let mut params = SmallVec::new();
for (key, val) in ps {
params.push((
let params = ps.mapped(|(key, val)| (
IndexedStr::Concrete(key.into()),
IndexedStr::Concrete(val.into())
))
}
));
MediaType {
source: None,
@ -259,7 +257,6 @@ impl MediaType {
let param_slice = match self.params {
MediaParams::Static(slice) => slice,
MediaParams::Dynamic(ref vec) => &vec[..],
MediaParams::Empty => &[]
};
param_slice.iter()

View File

@ -36,3 +36,48 @@ pub use self::header::{Header, HeaderMap};
pub use self::media_type::MediaType;
pub use self::cookies::*;
pub use self::session::*;
use smallvec::{Array, SmallVec};
pub trait IntoCollection<T> {
fn into_collection<A: Array<Item=T>>(self) -> SmallVec<A>;
fn mapped<U, F: FnMut(T) -> U, A: Array<Item=U>>(self, f: F) -> SmallVec<A>;
}
impl<T> IntoCollection<T> for T {
#[inline]
fn into_collection<A: Array<Item=T>>(self) -> SmallVec<A> {
let mut vec = SmallVec::new();
vec.push(self);
vec
}
#[inline(always)]
fn mapped<U, F: FnMut(T) -> U, A: Array<Item=U>>(self, mut f: F) -> SmallVec<A> {
f(self).into_collection()
}
}
impl<T> IntoCollection<T> for Vec<T> {
#[inline(always)]
fn into_collection<A: Array<Item=T>>(self) -> SmallVec<A> {
SmallVec::from_vec(self)
}
#[inline]
fn mapped<U, F: FnMut(T) -> U, A: Array<Item=U>>(self, mut f: F) -> SmallVec<A> {
self.into_iter().map(|item| f(item)).collect()
}
}
impl<'a, T: Clone> IntoCollection<T> for &'a [T] {
#[inline(always)]
fn into_collection<A: Array<Item=T>>(self) -> SmallVec<A> {
self.iter().cloned().collect()
}
#[inline]
fn mapped<U, F: FnMut(T) -> U, A: Array<Item=U>>(self, mut f: F) -> SmallVec<A> {
self.iter().cloned().map(|item| f(item)).collect()
}
}

View File

@ -5,7 +5,7 @@ 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>> {
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"),
@ -22,11 +22,11 @@ fn accept<'a>(input: &mut &'a str) -> ParseResult<&'a str, Accept> {
repeat_while!(eat(','), {
skip_while(is_whitespace);
let media_type = media_type();
let weight = q_value(&media_type);
let weight = q(&media_type);
media_types.push(WeightedMediaType(media_type, weight));
});
Accept(media_types)
Accept::new(media_types)
}
pub fn parse_accept(mut input: &str) -> Result<Accept, ParseError<&str>> {

View File

@ -323,6 +323,8 @@ impl<'r> Request<'r> {
/// ```
#[inline(always)]
pub fn content_type(&self) -> Option<ContentType> {
// FIXME: Don't reparse each time! Use RC? Smarter than that?
// Use state::Storage!
self.headers().get_one("Content-Type").and_then(|value| value.parse().ok())
}
@ -336,6 +338,20 @@ impl<'r> Request<'r> {
self.headers().get_one("Accept").and_then(|mut v| media_type(&mut v).ok())
}
#[inline(always)]
pub fn format(&self) -> Option<MediaType> {
if self.method.supports_payload() {
self.content_type().map(|ct| ct.into_media_type())
} else {
// FIXME: Should we be using `accept_first` or `preferred`? Or
// should we be checking neither and instead pass things through
// where the client accepts the thing at all?
self.accept()
.map(|accept| accept.preferred().media_type().clone())
.or(Some(MediaType::Any))
}
}
/// Retrieves and parses into `T` the 0-indexed `n`th dynamic parameter from
/// the request. Returns `Error::NoKey` if `n` is greater than the number of
/// params. Returns `Error::BadParse` if the parameter type `T` can't be
@ -438,9 +454,8 @@ impl<'r> Request<'r> {
}
/// Get the managed state container, if it exists. For internal use only!
/// FIXME: Expose?
#[inline(always)]
pub(crate) fn get_state<T: Send + Sync + 'static>(&self) -> Option<&'r T> {
pub fn get_state<T: Send + Sync + 'static>(&self) -> Option<&'r T> {
self.preset().managed_state.try_get()
}

View File

@ -113,12 +113,13 @@ impl<'r> Collider<Request<'r>> for Route {
self.method == req.method()
&& self.path.collides_with(req.uri())
&& self.path.query().map_or(true, |_| req.uri().query().is_some())
// FIXME: On payload requests, check Content-Type, else Accept.
&& match (req.content_type().as_ref(), self.format.as_ref()) {
(Some(mt_a), Some(mt_b)) => mt_a.collides_with(mt_b),
(Some(_), None) => true,
(None, Some(_)) => false,
(None, None) => true
// FIXME: Avoid calling `format` is `self.format` == None.
&& match self.format.as_ref() {
Some(mt_a) => match req.format().as_ref() {
Some(mt_b) => mt_a.collides_with(mt_b),
None => false
},
None => true
}
}
}
@ -132,7 +133,7 @@ mod tests {
use data::Data;
use handler::Outcome;
use router::route::Route;
use http::{Method, MediaType, ContentType};
use http::{Method, MediaType, ContentType, Accept};
use http::uri::URI;
use http::Method::*;
@ -362,15 +363,19 @@ mod tests {
assert!(!r_mt_mt_collide(Get, "text/html", Get, "text/css"));
}
fn req_route_mt_collide<S1, S2>(m1: Method, mt1: S1, m2: Method, mt2: S2) -> bool
fn req_route_mt_collide<S1, S2>(m: Method, mt1: S1, mt2: S2) -> bool
where S1: Into<Option<&'static str>>, S2: Into<Option<&'static str>>
{
let mut req = Request::new(m1, "/");
let mut req = Request::new(m, "/");
if let Some(mt_str) = mt1.into() {
if m.supports_payload() {
req.replace_header(mt_str.parse::<ContentType>().unwrap());
} else {
req.replace_header(mt_str.parse::<Accept>().unwrap());
}
}
let mut route = Route::new(m2, "/", dummy_handler);
let mut route = Route::new(m, "/", dummy_handler);
if let Some(mt_str) = mt2.into() {
route.format = Some(mt_str.parse::<MediaType>().unwrap());
}
@ -380,24 +385,41 @@ mod tests {
#[test]
fn test_req_route_mt_collisions() {
assert!(req_route_mt_collide(Get, "application/json", Get, "application/json"));
assert!(req_route_mt_collide(Get, "application/json", Get, "application/*"));
assert!(req_route_mt_collide(Get, "application/json", Get, "*/json"));
assert!(req_route_mt_collide(Get, "text/html", Get, "text/html"));
assert!(req_route_mt_collide(Get, "text/html", Get, "*/*"));
assert!(req_route_mt_collide(Post, "application/json", "application/json"));
assert!(req_route_mt_collide(Post, "application/json", "application/*"));
assert!(req_route_mt_collide(Post, "application/json", "*/json"));
assert!(req_route_mt_collide(Post, "text/html", "*/*"));
assert!(req_route_mt_collide(Get, "text/html", Get, None));
assert!(req_route_mt_collide(Get, None, Get, None));
assert!(req_route_mt_collide(Get, "application/json", Get, None));
assert!(req_route_mt_collide(Get, "x-custom/anything", Get, None));
assert!(req_route_mt_collide(Get, "application/json", "application/json"));
assert!(req_route_mt_collide(Get, "text/html", "text/html"));
assert!(req_route_mt_collide(Get, "text/html", "*/*"));
assert!(req_route_mt_collide(Get, None, "text/html"));
assert!(req_route_mt_collide(Get, None, "*/*"));
assert!(req_route_mt_collide(Get, None, "application/json"));
assert!(!req_route_mt_collide(Get, "application/json", Get, "text/html"));
assert!(!req_route_mt_collide(Get, "application/json", Get, "text/*"));
assert!(!req_route_mt_collide(Get, "application/json", Get, "*/xml"));
assert!(req_route_mt_collide(Post, "text/html", None));
assert!(req_route_mt_collide(Post, "application/json", None));
assert!(req_route_mt_collide(Post, "x-custom/anything", None));
assert!(req_route_mt_collide(Post, None, None));
assert!(!req_route_mt_collide(Get, None, Get, "text/html"));
assert!(!req_route_mt_collide(Get, None, Get, "*/*"));
assert!(!req_route_mt_collide(Get, None, Get, "application/json"));
assert!(req_route_mt_collide(Get, "text/html", None));
assert!(req_route_mt_collide(Get, "application/json", None));
assert!(req_route_mt_collide(Get, "x-custom/anything", None));
assert!(req_route_mt_collide(Get, None, None));
assert!(req_route_mt_collide(Get, "text/html, text/plain", "text/html"));
assert!(req_route_mt_collide(Get, "text/html; q=0.5, text/xml", "text/xml"));
assert!(!req_route_mt_collide(Post, "application/json", "text/html"));
assert!(!req_route_mt_collide(Post, "application/json", "text/*"));
assert!(!req_route_mt_collide(Post, "application/json", "*/xml"));
assert!(!req_route_mt_collide(Get, "application/json", "text/html"));
assert!(!req_route_mt_collide(Get, "application/json", "text/*"));
assert!(!req_route_mt_collide(Get, "application/json", "*/xml"));
assert!(!req_route_mt_collide(Post, None, "text/html"));
assert!(!req_route_mt_collide(Post, None, "*/*"));
assert!(!req_route_mt_collide(Post, None, "application/json"));
}
fn req_route_path_collide(a: &'static str, b: &'static str) -> bool {