mirror of https://github.com/rwf2/Rocket.git
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:
parent
fb29b37f30
commit
c58ca894b7
|
@ -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/<name>/<age> 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();
|
||||
}
|
||||
|
|
|
@ -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/<name>/<age> instead.</p>";
|
||||
test("/unknown", ContentType::JSON, Status::NotFound, body.to_string());
|
||||
test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string());
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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, ()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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((
|
||||
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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
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() {
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue