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

View File

@ -1,14 +1,16 @@
use super::rocket; use super::rocket;
use super::serde_json; use super::serde_json;
use super::Person; use super::Person;
use rocket::http::{ContentType, Method, Status}; use rocket::http::{Accept, ContentType, Header, MediaType, Method, Status};
use rocket::testing::MockRequest; 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() let rocket = rocket::ignite()
.mount("/hello", routes![super::hello]) .mount("/hello", routes![super::get_hello, super::post_hello])
.catch(errors![super::not_found]); .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); let mut response = request.dispatch_with(&rocket);
assert_eq!(response.status(), status); assert_eq!(response.status(), status);
@ -17,24 +19,29 @@ fn test(uri: &str, content_type: ContentType, status: Status, body: String) {
#[test] #[test]
fn test_hello() { fn test_hello() {
let person = Person { let person = Person { name: "Michael".to_string(), age: 80, };
name: "Michael".to_string(),
age: 80,
};
let body = serde_json::to_string(&person).unwrap(); 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] #[test]
fn test_hello_invalid_content_type() { fn test_hello_invalid_content_type() {
let body = format!("<p>This server only supports JSON requests, not '{}'.</p>", let b = format!("<p>'{}' requests are not supported.</p>", MediaType::HTML);
ContentType::HTML); test(Method::Get, "/hello/Michael/80", Accept::HTML, Status::NotFound, b.clone());
test("/hello/Michael/80", ContentType::HTML, Status::NotFound, body); test(Method::Post, "/hello/80", ContentType::HTML, Status::NotFound, b);
} }
#[test] #[test]
fn test_404() { fn test_404() {
let body = "<p>Sorry, '/unknown' is an invalid path! Try \ let body = "<p>Sorry, '/unknown' is an invalid path! Try \
/hello/&lt;name&gt;/&lt;age&gt; instead.</p>"; /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" time = "0.1"
memchr = "1" memchr = "1"
base64 = "0.4" base64 = "0.4"
smallvec = "0.3" smallvec = { git = "https://github.com/SergioBenitez/rust-smallvec" }
pear = "0.0.8" pear = "0.0.8"
pear_codegen = "0.0.8" pear_codegen = "0.0.8"

View File

@ -1,3 +1,5 @@
use std::io::Read;
use outcome::{self, IntoOutcome}; use outcome::{self, IntoOutcome};
use outcome::Outcome::*; use outcome::Outcome::*;
use http::Status; 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 ':'. /// // Split the string into two pieces at ':'.
/// let (name, age) = match string.find(':') { /// let (name, age) = match string.find(':') {
/// Some(i) => (&string[..i], &string[(i + 1)..]), /// Some(i) => (&string[..i], &string[(i + 1)..]),
/// None => return Failure((Status::BadRequest, "Missing ':'.".into())) /// None => return Failure((Status::UnprocessableEntity, "':'".into()))
/// }; /// };
/// ///
/// // Parse the age. /// // Parse the age.
/// let age: u16 = match age.parse() { /// let age: u16 = match age.parse() {
/// Ok(age) => age, /// Ok(age) => age,
/// Err(_) => return Failure((Status::BadRequest, "Bad age.".into())) /// Err(_) => return Failure((Status::UnprocessableEntity, "Age".into()))
/// }; /// };
/// ///
/// // Return successfully. /// // 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::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use std::fmt; 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>); pub struct WeightedMediaType(pub MediaType, pub Option<f32>);
impl WeightedMediaType { impl WeightedMediaType {
#[inline(always)]
pub fn media_type(&self) -> &MediaType {
&self.0
}
#[inline(always)] #[inline(always)]
pub fn weight(&self) -> Option<f32> { pub fn weight(&self) -> Option<f32> {
self.1 self.1
@ -25,11 +22,23 @@ impl WeightedMediaType {
} }
#[inline(always)] #[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 self.0
} }
} }
impl From<MediaType> for WeightedMediaType {
#[inline(always)]
fn from(media_type: MediaType) -> WeightedMediaType {
WeightedMediaType(media_type, None)
}
}
impl Deref for WeightedMediaType { impl Deref for WeightedMediaType {
type Target = MediaType; type Target = MediaType;
@ -39,14 +48,55 @@ impl Deref for WeightedMediaType {
} }
} }
/// The HTTP Accept header. // FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub struct Accept(pub Vec<WeightedMediaType>); 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 { 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 { pub fn preferred(&self) -> &WeightedMediaType {
static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None);
//
// See https://tools.ietf.org/html/rfc7231#section-5.3.2. // See https://tools.ietf.org/html/rfc7231#section-5.3.2.
let mut all = self.iter(); let mut all = self.iter();
let mut preferred = all.next().unwrap_or(&ANY); let mut preferred = all.next().unwrap_or(&ANY);
@ -55,6 +105,7 @@ impl Accept {
preferred = current; preferred = current;
} else if current.weight_or(0.0) > preferred.weight_or(1.0) { } else if current.weight_or(0.0) > preferred.weight_or(1.0) {
preferred = current; preferred = current;
// FIXME: Prefer text/html over text/*, for example.
} else if current.media_type() == preferred.media_type() { } else if current.media_type() == preferred.media_type() {
if current.weight() == preferred.weight() { if current.weight() == preferred.weight() {
let c_count = current.params().filter(|p| p.0 != "q").count(); let c_count = current.params().filter(|p| p.0 != "q").count();
@ -71,22 +122,25 @@ impl Accept {
#[inline(always)] #[inline(always)]
pub fn first(&self) -> Option<&WeightedMediaType> { pub fn first(&self) -> Option<&WeightedMediaType> {
if self.0.len() > 0 { self.iter().next()
Some(&self.0[0])
} else {
None
}
} }
#[inline(always)] #[inline(always)]
pub fn iter<'a>(&'a self) -> impl Iterator<Item=&'a WeightedMediaType> + 'a { 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)] #[inline(always)]
pub fn media_types<'a>(&'a self) -> impl Iterator<Item=&'a MediaType> + 'a { 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 { 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)] #[cfg(test)]
mod test { mod test {
use http::{Accept, MediaType}; use http::{Accept, MediaType};

View File

@ -3,7 +3,7 @@ use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use std::fmt; use std::fmt;
use http::{Header, MediaType}; use http::{IntoCollection, Header, MediaType};
use http::hyper::mime::Mime; use http::hyper::mime::Mime;
/// Representation of HTTP Content-Types. /// Representation of HTTP Content-Types.
@ -136,7 +136,7 @@ impl ContentType {
/// ```rust /// ```rust
/// use rocket::http::ContentType; /// 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()); /// 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 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>>, where T: Into<Cow<'static, str>>, S: Into<Cow<'static, str>>,
K: Into<Cow<'static, str>>, V: 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)) 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); known_media_types!(content_types);
} }

View File

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

View File

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

View File

@ -36,3 +36,48 @@ pub use self::header::{Header, HeaderMap};
pub use self::media_type::MediaType; pub use self::media_type::MediaType;
pub use self::cookies::*; pub use self::cookies::*;
pub use self::session::*; 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::parse::media_type::media_type;
use http::{MediaType, Accept, WeightedMediaType}; 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() { match media_type.params().next() {
Some(("q", value)) if value.len() <= 4 => match value.parse::<f32>().ok() { 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) 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(','), { repeat_while!(eat(','), {
skip_while(is_whitespace); skip_while(is_whitespace);
let media_type = media_type(); let media_type = media_type();
let weight = q_value(&media_type); let weight = q(&media_type);
media_types.push(WeightedMediaType(media_type, weight)); 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>> { pub fn parse_accept(mut input: &str) -> Result<Accept, ParseError<&str>> {

View File

@ -323,6 +323,8 @@ impl<'r> Request<'r> {
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn content_type(&self) -> Option<ContentType> { 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()) 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()) 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 /// 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 /// 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 /// 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! /// Get the managed state container, if it exists. For internal use only!
/// FIXME: Expose?
#[inline(always)] #[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() self.preset().managed_state.try_get()
} }

View File

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