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;
|
#[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/<name>/<age> instead.</p>",
|
/hello/<name>/<age> 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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/<name>/<age> instead.</p>";
|
/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"
|
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"
|
||||||
|
|
||||||
|
|
|
@ -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, ()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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): "plain text", "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",
|
||||||
|
|
|
@ -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 {
|
|
||||||
params.push((
|
|
||||||
IndexedStr::Concrete(key.into()),
|
IndexedStr::Concrete(key.into()),
|
||||||
IndexedStr::Concrete(val.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()
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>> {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
if m.supports_payload() {
|
||||||
req.replace_header(mt_str.parse::<ContentType>().unwrap());
|
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 {
|
||||||
|
|
Loading…
Reference in New Issue