From c58ca894b78eacce484316bd4856f7efca4efb8d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 29 Mar 2017 04:08:53 -0700 Subject: [PATCH] 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. --- examples/content_types/src/main.rs | 32 +++++---- examples/content_types/src/tests.rs | 33 +++++---- lib/Cargo.toml | 2 +- lib/src/data/from_data.rs | 19 ++++- lib/src/http/accept.rs | 105 ++++++++++++++++++++++------ lib/src/http/content_type.rs | 16 ++++- lib/src/http/known_media_types.rs | 4 +- lib/src/http/media_type.rs | 21 +++--- lib/src/http/mod.rs | 45 ++++++++++++ lib/src/http/parse/accept.rs | 6 +- lib/src/request/request.rs | 19 ++++- lib/src/router/collider.rs | 74 +++++++++++++------- 12 files changed, 277 insertions(+), 99 deletions(-) diff --git a/examples/content_types/src/main.rs b/examples/content_types/src/main.rs index c8cd7d90..d9f76d27 100644 --- a/examples/content_types/src/main.rs +++ b/examples/content_types/src/main.rs @@ -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("//", format = "application/json")] -fn hello(content_type: ContentType, name: String, age: i8) -> content::JSON { - let person = Person { - name: name, - age: age, - }; +fn get_hello(name: String, age: u8) -> content::JSON { + // 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("/", format = "text/plain", data = "")] +fn post_hello(age: u8, name: String) -> content::JSON { + let person = Person { name: name, age: age, }; content::JSON(serde_json::to_string(&person).unwrap()) } #[error(404)] fn not_found(request: &Request) -> content::HTML { - let html = match request.content_type() { - Some(ref ct) if !ct.is_json() => { - format!("

This server only supports JSON requests, not '{}'.

", ct) + let html = match request.format() { + Some(ref mt) if !mt.is_json() && !mt.is_plain() => { + format!("

'{}' requests are not supported.

", mt) } _ => format!("

Sorry, '{}' is an invalid path! Try \ /hello/<name>/<age> instead.

", @@ -47,7 +51,7 @@ fn not_found(request: &Request) -> content::HTML { fn main() { rocket::ignite() - .mount("/hello", routes![hello]) + .mount("/hello", routes![get_hello, post_hello]) .catch(errors![not_found]) .launch(); } diff --git a/examples/content_types/src/tests.rs b/examples/content_types/src/tests.rs index b7b0f254..5e9f5e85 100644 --- a/examples/content_types/src/tests.rs +++ b/examples/content_types/src/tests.rs @@ -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(method: Method, uri: &str, header: H, status: Status, body: String) + where H: Into> +{ 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!("

This server only supports JSON requests, not '{}'.

", - ContentType::HTML); - test("/hello/Michael/80", ContentType::HTML, Status::NotFound, body); + let b = format!("

'{}' requests are not supported.

", 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 = "

Sorry, '/unknown' is an invalid path! Try \ /hello/<name>/<age> instead.

"; - test("/unknown", ContentType::JSON, Status::NotFound, body.to_string()); + test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string()); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 3d55342f..093fb60c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -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" diff --git a/lib/src/data/from_data.rs b/lib/src/data/from_data.rs index 329378bc..ee756269 100644 --- a/lib/src/data/from_data.rs +++ b/lib/src/data/from_data.rs @@ -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 for Result { /// // 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 FromData for Option { } } } + +impl FromData for String { + type Error = (); + + // FIXME: Doc. + fn from_data(_: &Request, data: Data) -> Outcome { + let mut string = String::new(); + match data.open().read_to_string(&mut string) { + Ok(_) => Success(string), + Err(_) => Failure((Status::UnprocessableEntity, ())) + } + } +} diff --git a/lib/src/http/accept.rs b/lib/src/http/accept.rs index feb221b1..3d669126 100644 --- a/lib/src/http/accept.rs +++ b/lib/src/http/accept.rs @@ -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); impl WeightedMediaType { - #[inline(always)] - pub fn media_type(&self) -> &MediaType { - &self.0 - } - #[inline(always)] pub fn weight(&self) -> Option { 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 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); +// 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 "] + #[doc=$str] #[doc=": "] + #[doc=$t] #[doc="/"] #[doc=$s] + #[doc=""] + #[allow(non_upper_case_globals)] + pub const $name: Accept = Accept( + AcceptParams::Static(&[WeightedMediaType(MediaType::$name, None)]) + ); + )+ + }; +} + +impl> From for Accept { + #[inline(always)] + fn from(items: T) -> Accept { + Accept(AcceptParams::Dynamic(items.mapped(|item| item.into()))) + } +} impl Accept { + #[inline(always)] + pub fn new>(items: T) -> Accept { + Accept(AcceptParams::Dynamic(items.into_collection())) + } + + // FIXME: IMPLEMENT THIS. + // #[inline(always)] + // pub fn add>(&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 + '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 + '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> for Accept { + #[inline(always)] + fn into(self) -> Header<'static> { + Header::new("Accept", self.to_string()) + } +} + #[cfg(test)] mod test { use http::{Accept, MediaType}; diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index 8c00ab80..276dc2c0 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -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(top: T, sub: S, ps: P) -> ContentType where T: Into>, S: Into>, K: Into>, V: Into>, - P: IntoIterator + 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); } diff --git a/lib/src/http/known_media_types.rs b/lib/src/http/known_media_types.rs index 1eb7706e..b28a7799 100644 --- a/lib/src/http/known_media_types.rs +++ b/lib/src/http/known_media_types.rs @@ -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", diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index 53e09a31..07a8b7f4 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -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(top: T, sub: S, ps: P) -> MediaType where T: Into>, S: Into>, K: Into>, V: Into>, - P: IntoIterator + P: IntoCollection<(K, V)> { - let mut params = SmallVec::new(); - for (key, val) in ps { - params.push(( - IndexedStr::Concrete(key.into()), - IndexedStr::Concrete(val.into()) - )) - } + 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() diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 1d256db0..94999693 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -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 { + fn into_collection>(self) -> SmallVec; + fn mapped U, A: Array>(self, f: F) -> SmallVec; +} + +impl IntoCollection for T { + #[inline] + fn into_collection>(self) -> SmallVec { + let mut vec = SmallVec::new(); + vec.push(self); + vec + } + + #[inline(always)] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + f(self).into_collection() + } +} + +impl IntoCollection for Vec { + #[inline(always)] + fn into_collection>(self) -> SmallVec { + SmallVec::from_vec(self) + } + + #[inline] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + self.into_iter().map(|item| f(item)).collect() + } +} + +impl<'a, T: Clone> IntoCollection for &'a [T] { + #[inline(always)] + fn into_collection>(self) -> SmallVec { + self.iter().cloned().collect() + } + + #[inline] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + self.iter().cloned().map(|item| f(item)).collect() + } +} diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs index 2733f0fe..f40c77ad 100644 --- a/lib/src/http/parse/accept.rs +++ b/lib/src/http/parse/accept.rs @@ -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> { +fn q<'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"), @@ -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> { diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 2c9b7312..6d607463 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -323,6 +323,8 @@ impl<'r> Request<'r> { /// ``` #[inline(always)] pub fn content_type(&self) -> Option { + // 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 { + 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(&self) -> Option<&'r T> { + pub fn get_state(&self) -> Option<&'r T> { self.preset().managed_state.try_get() } diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index f08b2534..a38b408c 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -113,12 +113,13 @@ impl<'r> Collider> 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(m1: Method, mt1: S1, m2: Method, mt2: S2) -> bool + fn req_route_mt_collide(m: Method, mt1: S1, mt2: S2) -> bool where S1: Into>, S2: Into> { - let mut req = Request::new(m1, "/"); + let mut req = Request::new(m, "/"); if let Some(mt_str) = mt1.into() { - req.replace_header(mt_str.parse::().unwrap()); + if m.supports_payload() { + req.replace_header(mt_str.parse::().unwrap()); + } else { + req.replace_header(mt_str.parse::().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::().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 {