Remove dependence from Hyper in Request/MockRequest.

This commit is contained in:
Sergio Benitez 2016-12-15 16:34:19 -08:00
parent a73a082153
commit 08f41816d1
7 changed files with 145 additions and 102 deletions

View File

@ -1,11 +1,12 @@
use super::rocket; use super::rocket;
use rocket::testing::MockRequest; use rocket::testing::MockRequest;
use rocket::http::Method::*; use rocket::http::Method::*;
use rocket::http::ContentType;
fn test_login<F: Fn(String) -> bool>(username: &str, password: &str, age: isize, test: F) { fn test_login<F: Fn(String) -> bool>(username: &str, password: &str, age: isize, test: F) {
let rocket = rocket::ignite().mount("/", routes![super::user_page, super::login]); let rocket = rocket::ignite().mount("/", routes![super::user_page, super::login]);
let result = MockRequest::new(Post, "/login") let result = MockRequest::new(Post, "/login")
.headers(&[("Content-Type", "application/x-www-form-urlencoded")]) .header(ContentType::Form)
.body(&format!("username={}&password={}&age={}", username, password, age)) .body(&format!("username={}&password={}&age={}", username, password, age))
.dispatch_with(&rocket) .dispatch_with(&rocket)
.unwrap_or("".to_string()); .unwrap_or("".to_string());

View File

@ -37,14 +37,20 @@ mod test {
use super::rocket; use super::rocket;
use rocket::testing::MockRequest; use rocket::testing::MockRequest;
use rocket::http::Method::*; use rocket::http::Method::*;
use rocket::http::Header;
fn test_header_count<'h>(headers: Vec<Header<'static>>) {
let num_headers = headers.len();
let mut req = MockRequest::new(Get, "/");
for header in headers {
req = req.header(header);
}
fn test_header_count<'h>(headers: &[(&'h str, &'h str)]) {
// FIXME: Should be able to count headers directly! // FIXME: Should be able to count headers directly!
let rocket = rocket::ignite().mount("/", routes![super::header_count]); let rocket = rocket::ignite().mount("/", routes![super::header_count]);
let mut req = MockRequest::new(Get, "/").headers(headers);
let result = req.dispatch_with(&rocket); let result = req.dispatch_with(&rocket);
assert_eq!(result.unwrap(), assert_eq!(result.unwrap(),
format!("Your request contained {} headers!", headers.len())); format!("Your request contained {} headers!", num_headers));
} }
#[test] #[test]
@ -53,14 +59,10 @@ mod test {
let mut headers = vec![]; let mut headers = vec![];
for j in 0..i { for j in 0..i {
let string = format!("{}", j); let string = format!("{}", j);
headers.push((string.clone(), string)); headers.push(Header::new(string.clone(), string));
} }
let h_strs: Vec<_> = headers.iter() test_header_count(headers);
.map(|&(ref a, ref b)| (a.as_str(), b.as_str()))
.collect();
test_header_count(&h_strs);
} }
} }
} }

View File

@ -46,19 +46,44 @@ impl<'h> HeaderMap<'h> {
HeaderMap { headers: HashMap::new() } HeaderMap { headers: HashMap::new() }
} }
#[inline(always)] #[inline]
pub fn contains(&self, name: &str) -> bool {
self.headers.get(name).is_some()
}
#[inline]
pub fn len(&self) -> usize {
self.headers.iter().flat_map(|(_, values)| values.iter()).count()
}
#[inline]
pub fn get<'a>(&'a self, name: &str) -> impl Iterator<Item=&'a str> { pub fn get<'a>(&'a self, name: &str) -> impl Iterator<Item=&'a str> {
self.headers.get(name).into_iter().flat_map(|values| { self.headers.get(name).into_iter().flat_map(|values| {
values.iter().map(|val| val.borrow()) values.iter().map(|val| val.borrow())
}) })
} }
#[inline]
pub fn get_one<'a>(&'a self, name: &str) -> Option<&'a str> {
self.headers.get(name).and_then(|values| {
if values.len() >= 1 { Some(values[0].borrow()) }
else { None }
})
}
#[inline(always)] #[inline(always)]
pub fn replace<'p: 'h, H: Into<Header<'p>>>(&mut self, header: H) -> bool { pub fn replace<'p: 'h, H: Into<Header<'p>>>(&mut self, header: H) -> bool {
let header = header.into(); let header = header.into();
self.headers.insert(header.name, vec![header.value]).is_some() self.headers.insert(header.name, vec![header.value]).is_some()
} }
#[inline(always)]
pub fn replace_raw<'a: 'h, 'b: 'h, N, V>(&mut self, name: N, value: V) -> bool
where N: Into<Cow<'a, str>>, V: Into<Cow<'b, str>>
{
self.replace(Header::new(name, value))
}
#[inline(always)] #[inline(always)]
pub fn replace_all<'n, 'v: 'h, H>(&mut self, name: H, values: Vec<Cow<'v, str>>) pub fn replace_all<'n, 'v: 'h, H>(&mut self, name: H, values: Vec<Cow<'v, str>>)
where 'n: 'h, H: Into<Cow<'n, str>> where 'n: 'h, H: Into<Cow<'n, str>>
@ -72,6 +97,13 @@ impl<'h> HeaderMap<'h> {
self.headers.entry(header.name).or_insert(vec![]).push(header.value); self.headers.entry(header.name).or_insert(vec![]).push(header.value);
} }
#[inline(always)]
pub fn add_raw<'a: 'h, 'b: 'h, N, V>(&mut self, name: N, value: V)
where N: Into<Cow<'a, str>>, V: Into<Cow<'b, str>>
{
self.add(Header::new(name, value))
}
#[inline(always)] #[inline(always)]
pub fn add_all<'n, H>(&mut self, name: H, values: &mut Vec<Cow<'h, str>>) pub fn add_all<'n, H>(&mut self, name: H, values: &mut Vec<Cow<'h, str>>)
where 'n:'h, H: Into<Cow<'n, str>> where 'n:'h, H: Into<Cow<'n, str>>
@ -85,6 +117,11 @@ impl<'h> HeaderMap<'h> {
} }
#[inline(always)] #[inline(always)]
pub fn remove_all(&mut self) -> Vec<Header<'h>> {
let old_map = ::std::mem::replace(self, HeaderMap::new());
old_map.into_iter().collect()
}
pub fn iter<'s>(&'s self) -> impl Iterator<Item=Header<'s>> { pub fn iter<'s>(&'s self) -> impl Iterator<Item=Header<'s>> {
self.headers.iter().flat_map(|(key, values)| { self.headers.iter().flat_map(|(key, values)| {
values.iter().map(move |val| { values.iter().map(move |val| {
@ -94,7 +131,20 @@ impl<'h> HeaderMap<'h> {
} }
#[inline(always)] #[inline(always)]
pub fn into_iter<'s>(self) pub fn into_iter(self) -> impl Iterator<Item=Header<'h>> {
self.headers.into_iter().flat_map(|(name, value)| {
value.into_iter().map(move |value| {
Header {
name: name.clone(),
value: value
}
})
})
}
#[doc(hidden)]
#[inline(always)]
pub fn into_iter_raw<'s>(self)
-> impl Iterator<Item=(Cow<'h, str>, Vec<Cow<'h, str>>)> { -> impl Iterator<Item=(Cow<'h, str>, Vec<Cow<'h, str>>)> {
self.headers.into_iter() self.headers.into_iter()
} }

View File

@ -70,7 +70,8 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// ensure that the handlers corresponding to these requests don't get called /// ensure that the handlers corresponding to these requests don't get called
/// unless there is an API key in the request and the key is valid. The /// unless there is an API key in the request and the key is valid. The
/// following example implements this using an `APIKey` type and a `FromRequest` /// following example implements this using an `APIKey` type and a `FromRequest`
/// implementation for that type in the `senstive` handler: /// implementation for that type. The `APIKey` type is then used in the
/// `senstive` handler.
/// ///
/// ```rust /// ```rust
/// # #![feature(plugin)] /// # #![feature(plugin)]
@ -90,20 +91,19 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// ///
/// impl<'r> FromRequest<'r> for APIKey { /// impl<'r> FromRequest<'r> for APIKey {
/// type Error = (); /// type Error = ();
/// fn from_request(request: &'r Request) -> request::Outcome<APIKey, ()> {
/// if let Some(keys) = request.headers().get_raw("x-api-key") {
/// if keys.len() != 1 {
/// return Outcome::Failure((Status::BadRequest, ()));
/// }
/// ///
/// if let Ok(key) = String::from_utf8(keys[0].clone()) { /// fn from_request(request: &'r Request) -> request::Outcome<APIKey, ()> {
/// if is_valid(&key) { /// let keys: Vec<_> = request.headers().get("x-api-key").collect();
/// return Outcome::Success(APIKey(key)); /// if keys.len() != 1 {
/// } /// return Outcome::Failure((Status::BadRequest, ()));
/// }
/// } /// }
/// ///
/// Outcome::Forward(()) /// let key = keys[0];
/// if !is_valid(keys[0]) {
/// return Outcome::Forward(());
/// }
///
/// return Outcome::Success(APIKey(key.to_string()));
/// } /// }
/// } /// }
/// ///

View File

@ -9,8 +9,9 @@ use super::{FromParam, FromSegments};
use router::Route; use router::Route;
use http::uri::{URI, URIBuf, Segments}; use http::uri::{URI, URIBuf, Segments};
use http::hyper::{self, header}; use http::{Method, ContentType, Header, HeaderMap, Cookies};
use http::{Method, ContentType, Cookies};
use http::hyper;
/// The type of an incoming web request. /// The type of an incoming web request.
/// ///
@ -25,7 +26,8 @@ pub struct Request {
uri: URIBuf, // FIXME: Should be URI (without hyper). uri: URIBuf, // FIXME: Should be URI (without hyper).
params: RefCell<Vec<&'static str>>, params: RefCell<Vec<&'static str>>,
cookies: Cookies, cookies: Cookies,
headers: header::Headers, // Don't use hyper's headers. // TODO: Allow non-static here.
headers: HeaderMap<'static>,
} }
impl Request { impl Request {
@ -78,7 +80,8 @@ impl Request {
/// For example, if the request URI is `"/hello/there/i/am/here"`, then /// For example, if the request URI is `"/hello/there/i/am/here"`, then
/// `request.get_segments::<T>(1)` will attempt to parse the segments /// `request.get_segments::<T>(1)` will attempt to parse the segments
/// `"there/i/am/here"` as type `T`. /// `"there/i/am/here"` as type `T`.
pub fn get_segments<'r, T: FromSegments<'r>>(&'r self, i: usize) -> Result<T, Error> { pub fn get_segments<'r, T: FromSegments<'r>>(&'r self, i: usize)
-> Result<T, Error> {
let segments = self.get_raw_segments(i).ok_or(Error::NoKey)?; let segments = self.get_raw_segments(i).ok_or(Error::NoKey)?;
T::from_segments(segments).map_err(|_| Error::BadParse) T::from_segments(segments).map_err(|_| Error::BadParse)
} }
@ -98,7 +101,7 @@ impl Request {
} }
} }
// FIXME: Implement a testing framework for Rocket. // FIXME: Make this `new`. Make current `new` a `from_hyp` method.
#[doc(hidden)] #[doc(hidden)]
pub fn mock(method: Method, uri: &str) -> Request { pub fn mock(method: Method, uri: &str) -> Request {
Request { Request {
@ -106,7 +109,7 @@ impl Request {
method: method, method: method,
cookies: Cookies::new(&[]), cookies: Cookies::new(&[]),
uri: URIBuf::from(uri), uri: URIBuf::from(uri),
headers: header::Headers::new(), headers: HeaderMap::new(),
} }
} }
@ -120,8 +123,7 @@ impl Request {
/// ///
/// Returns the headers in this request. /// Returns the headers in this request.
#[inline(always)] #[inline(always)]
pub fn headers(&self) -> &header::Headers { pub fn headers(&self) -> &HeaderMap {
// FIXME: Get rid of Hyper.
&self.headers &self.headers
} }
@ -139,29 +141,9 @@ impl Request {
/// Content-Type of [any](struct.ContentType.html#method.any) is returned. /// Content-Type of [any](struct.ContentType.html#method.any) is returned.
#[inline(always)] #[inline(always)]
pub fn content_type(&self) -> ContentType { pub fn content_type(&self) -> ContentType {
let hyp_ct = self.headers().get::<header::ContentType>(); self.headers().get_one("Content-Type")
hyp_ct.map_or(ContentType::Any, |ct| ContentType::from(&ct.0)) .and_then(|value| value.parse().ok())
} .unwrap_or(ContentType::Any)
/// <div class="stability" style="margin-left: 0;">
/// <em class="stab unstable">
/// Unstable
/// (<a href="https://github.com/SergioBenitez/Rocket/issues/17">#17</a>):
/// The underlying HTTP library/types are likely to change before v1.0.
/// </em>
/// </div>
///
/// Returns the first content-type accepted by this request.
pub fn accepts(&self) -> ContentType {
let accept = self.headers().get::<header::Accept>();
accept.map_or(ContentType::Any, |accept| {
let items = &accept.0;
if items.len() < 1 {
return ContentType::Any;
} else {
return ContentType::from(items[0].item.clone());
}
})
} }
/// Retrieves the URI from the request. Rocket only allows absolute URIs, so /// Retrieves the URI from the request. Rocket only allows absolute URIs, so
@ -179,7 +161,9 @@ impl Request {
// in this structure, which is (obviously) guaranteed to live as long as // in this structure, which is (obviously) guaranteed to live as long as
// the structure AS LONG AS it is not moved out or changed. AS A RESULT, // the structure AS LONG AS it is not moved out or changed. AS A RESULT,
// the `uri` fields MUST NEVER be changed once it is set. // the `uri` fields MUST NEVER be changed once it is set.
// TODO: Find a way to enforce these. Look at OwningRef for inspiration. //
// TODO: Find a way to ecapsulate this better. Look at OwningRef/Rental
// for inspiration.
use ::std::mem::transmute; use ::std::mem::transmute;
*self.params.borrow_mut() = unsafe { *self.params.borrow_mut() = unsafe {
transmute(route.get_params(self.uri.as_uri())) transmute(route.get_params(self.uri.as_uri()))
@ -188,20 +172,13 @@ impl Request {
#[doc(hidden)] #[doc(hidden)]
#[inline(always)] #[inline(always)]
pub fn set_headers(&mut self, h_headers: header::Headers) { pub fn add_header(&mut self, header: Header<'static>) {
let cookies = match h_headers.get::<header::Cookie>() { self.headers.add(header);
// TODO: Retrieve key from config.
Some(cookie) => cookie.to_cookie_jar(&[]),
None => Cookies::new(&[]),
};
self.headers = h_headers;
self.cookies = cookies;
} }
#[doc(hidden)] #[doc(hidden)]
pub fn new(h_method: hyper::Method, pub fn new(h_method: hyper::Method,
h_headers: header::Headers, h_headers: hyper::header::Headers,
h_uri: hyper::RequestUri) h_uri: hyper::RequestUri)
-> Result<Request, String> { -> Result<Request, String> {
let uri = match h_uri { let uri = match h_uri {
@ -214,18 +191,23 @@ impl Request {
_ => return Err(format!("Bad method: {}", h_method)), _ => return Err(format!("Bad method: {}", h_method)),
}; };
let cookies = match h_headers.get::<header::Cookie>() { let cookies = match h_headers.get::<hyper::header::Cookie>() {
// TODO: Retrieve key from config. // TODO: Retrieve key from config.
Some(cookie) => cookie.to_cookie_jar(&[]), Some(cookie) => cookie.to_cookie_jar(&[]),
None => Cookies::new(&[]), None => Cookies::new(&[]),
}; };
let mut headers = HeaderMap::new();
for h_header in h_headers.iter() {
headers.add_raw(h_header.name().to_string(), h_header.value_string())
}
let request = Request { let request = Request {
params: RefCell::new(vec![]), params: RefCell::new(vec![]),
method: method, method: method,
cookies: cookies, cookies: cookies,
uri: uri, uri: uri,
headers: h_headers, headers: headers,
}; };
Ok(request) Ok(request)

View File

@ -282,9 +282,9 @@ impl<'r> Response<'r> {
self.body = Some(Body::Chunked(Box::new(body), chunk_size)); self.body = Some(Body::Chunked(Box::new(body), chunk_size));
} }
// Replaces this response's status and body with that of `other`, if they /// Replaces this response's status and body with that of `other`, if they
// exist. Any headers that exist in `other` replace the ones in `self`. Any /// exist in `other`. Any headers that exist in `other` replace the ones in
// in `self` that aren't in `other` remain. /// `self`. Any in `self` that aren't in `other` remain in `self`.
pub fn merge(&mut self, other: Response<'r>) { pub fn merge(&mut self, other: Response<'r>) {
if let Some(status) = other.status { if let Some(status) = other.status {
self.status = Some(status); self.status = Some(status);
@ -294,7 +294,7 @@ impl<'r> Response<'r> {
self.body = Some(body); self.body = Some(body);
} }
for (name, values) in other.headers.into_iter() { for (name, values) in other.headers.into_iter_raw() {
self.headers.replace_all(name, values); self.headers.replace_all(name, values);
} }
} }
@ -311,7 +311,7 @@ impl<'r> Response<'r> {
self.body = other.body; self.body = other.body;
} }
for (name, mut values) in other.headers.into_iter() { for (name, mut values) in other.headers.into_iter_raw() {
self.headers.add_all(name, &mut values); self.headers.add_all(name, &mut values);
} }
} }

View File

@ -53,9 +53,10 @@
//! ```rust //! ```rust
//! # use rocket::http::Method::*; //! # use rocket::http::Method::*;
//! # use rocket::testing::MockRequest; //! # use rocket::testing::MockRequest;
//! # use rocket::http::ContentType;
//! let (username, password, age) = ("user", "password", 32); //! let (username, password, age) = ("user", "password", 32);
//! MockRequest::new(Post, "/login") //! MockRequest::new(Post, "/login")
//! .headers(&[("Content-Type", "application/x-www-form-urlencoded")]) //! .header(ContentType::Form)
//! .body(&format!("username={}&password={}&age={}", username, password, age)); //! .body(&format!("username={}&password={}&age={}", username, password, age));
//! ``` //! ```
//! //!
@ -99,8 +100,8 @@
//! } //! }
//! ``` //! ```
use http::{hyper, Method}; use http::{Method, Header, Cookie};
use {Rocket, Request, Data}; use ::{Rocket, Request, Data};
/// A type for mocking requests for testing Rocket applications. /// A type for mocking requests for testing Rocket applications.
pub struct MockRequest { pub struct MockRequest {
@ -110,6 +111,7 @@ pub struct MockRequest {
impl MockRequest { impl MockRequest {
/// Constructs a new mocked request with the given `method` and `uri`. /// Constructs a new mocked request with the given `method` and `uri`.
#[inline]
pub fn new<S: AsRef<str>>(method: Method, uri: S) -> Self { pub fn new<S: AsRef<str>>(method: Method, uri: S) -> Self {
MockRequest { MockRequest {
request: Request::mock(method, uri.as_ref()), request: Request::mock(method, uri.as_ref()),
@ -117,33 +119,42 @@ impl MockRequest {
} }
} }
/// Sets the headers for this request. /// Add a header to this request.
/// ///
/// # Examples /// # Examples
/// ///
/// Set the Content-Type header: /// Add the Content-Type header:
/// ///
/// ```rust /// ```rust
/// use rocket::http::Method::*; /// use rocket::http::Method::*;
/// use rocket::testing::MockRequest; /// use rocket::testing::MockRequest;
/// use rocket::http::ContentType;
/// ///
/// let req = MockRequest::new(Get, "/").headers(&[ /// let req = MockRequest::new(Get, "/").header(ContentType::JSON);
/// ("Content-Type", "application/json")
/// ]);
/// ``` /// ```
pub fn headers<'h, H: AsRef<[(&'h str, &'h str)]>>(mut self, headers: H) -> Self { #[inline]
let mut hyp_headers = hyper::header::Headers::new(); pub fn header<'h, H: Into<Header<'static>>>(mut self, header: H) -> Self {
self.request.add_header(header.into());
for &(name, fields) in headers.as_ref() { self
let mut vec_fields = vec![]; }
for field in fields.split(";") { ///
vec_fields.push(field.as_bytes().to_vec()); /// Add a cookie to this request.
} ///
/// # Examples
hyp_headers.set_raw(name.to_string(), vec_fields); ///
} /// Add `user_id` cookie:
///
self.request.set_headers(hyp_headers); /// ```rust
/// use rocket::http::Method::*;
/// use rocket::testing::MockRequest;
/// use rocket::http::Cookie;
///
/// let req = MockRequest::new(Get, "/")
/// .cookie(Cookie::new("user_id".into(), "12".into()));
/// ```
#[inline]
pub fn cookie(self, cookie: Cookie) -> Self {
self.request.cookies().add(cookie);
self self
} }
@ -156,16 +167,13 @@ impl MockRequest {
/// ```rust /// ```rust
/// use rocket::http::Method::*; /// use rocket::http::Method::*;
/// use rocket::testing::MockRequest; /// use rocket::testing::MockRequest;
/// use rocket::http::ContentType;
/// ///
/// let req = MockRequest::new(Post, "/").headers(&[ /// let req = MockRequest::new(Post, "/")
/// ("Content-Type", "application/json") /// .header(ContentType::JSON)
/// ]).body(r#" /// .body(r#"{ "key": "value", "array": [1, 2, 3], }"#);
/// {
/// "key": "value",
/// "array": [1, 2, 3],
/// }
/// "#);
/// ``` /// ```
#[inline]
pub fn body<S: AsRef<str>>(mut self, body: S) -> Self { pub fn body<S: AsRef<str>>(mut self, body: S) -> Self {
self.data = Data::new(body.as_ref().as_bytes().into()); self.data = Data::new(body.as_ref().as_bytes().into());
self self