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 rocket::testing::MockRequest;
use rocket::http::Method::*;
use rocket::http::ContentType;
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 result = MockRequest::new(Post, "/login")
.headers(&[("Content-Type", "application/x-www-form-urlencoded")])
.header(ContentType::Form)
.body(&format!("username={}&password={}&age={}", username, password, age))
.dispatch_with(&rocket)
.unwrap_or("".to_string());

View File

@ -37,14 +37,20 @@ mod test {
use super::rocket;
use rocket::testing::MockRequest;
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!
let rocket = rocket::ignite().mount("/", routes![super::header_count]);
let mut req = MockRequest::new(Get, "/").headers(headers);
let result = req.dispatch_with(&rocket);
assert_eq!(result.unwrap(),
format!("Your request contained {} headers!", headers.len()));
format!("Your request contained {} headers!", num_headers));
}
#[test]
@ -53,14 +59,10 @@ mod test {
let mut headers = vec![];
for j in 0..i {
let string = format!("{}", j);
headers.push((string.clone(), string));
headers.push(Header::new(string.clone(), string));
}
let h_strs: Vec<_> = headers.iter()
.map(|&(ref a, ref b)| (a.as_str(), b.as_str()))
.collect();
test_header_count(&h_strs);
test_header_count(headers);
}
}
}

View File

@ -46,19 +46,44 @@ impl<'h> HeaderMap<'h> {
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> {
self.headers.get(name).into_iter().flat_map(|values| {
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)]
pub fn replace<'p: 'h, H: Into<Header<'p>>>(&mut self, header: H) -> bool {
let header = header.into();
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)]
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>>
@ -72,6 +97,13 @@ impl<'h> HeaderMap<'h> {
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)]
pub fn add_all<'n, H>(&mut self, name: H, values: &mut Vec<Cow<'h, str>>)
where 'n:'h, H: Into<Cow<'n, str>>
@ -85,6 +117,11 @@ impl<'h> HeaderMap<'h> {
}
#[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>> {
self.headers.iter().flat_map(|(key, values)| {
values.iter().map(move |val| {
@ -94,7 +131,20 @@ impl<'h> HeaderMap<'h> {
}
#[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>>)> {
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
/// 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`
/// implementation for that type in the `senstive` handler:
/// implementation for that type. The `APIKey` type is then used in the
/// `senstive` handler.
///
/// ```rust
/// # #![feature(plugin)]
@ -90,20 +91,19 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
///
/// impl<'r> FromRequest<'r> for APIKey {
/// type Error = ();
///
/// fn from_request(request: &'r Request) -> request::Outcome<APIKey, ()> {
/// if let Some(keys) = request.headers().get_raw("x-api-key") {
/// let keys: Vec<_> = request.headers().get("x-api-key").collect();
/// if keys.len() != 1 {
/// return Outcome::Failure((Status::BadRequest, ()));
/// }
///
/// if let Ok(key) = String::from_utf8(keys[0].clone()) {
/// if is_valid(&key) {
/// return Outcome::Success(APIKey(key));
/// }
/// }
/// let key = keys[0];
/// if !is_valid(keys[0]) {
/// return Outcome::Forward(());
/// }
///
/// Outcome::Forward(())
/// return Outcome::Success(APIKey(key.to_string()));
/// }
/// }
///

View File

@ -9,8 +9,9 @@ use super::{FromParam, FromSegments};
use router::Route;
use http::uri::{URI, URIBuf, Segments};
use http::hyper::{self, header};
use http::{Method, ContentType, Cookies};
use http::{Method, ContentType, Header, HeaderMap, Cookies};
use http::hyper;
/// The type of an incoming web request.
///
@ -25,7 +26,8 @@ pub struct Request {
uri: URIBuf, // FIXME: Should be URI (without hyper).
params: RefCell<Vec<&'static str>>,
cookies: Cookies,
headers: header::Headers, // Don't use hyper's headers.
// TODO: Allow non-static here.
headers: HeaderMap<'static>,
}
impl Request {
@ -78,7 +80,8 @@ impl Request {
/// For example, if the request URI is `"/hello/there/i/am/here"`, then
/// `request.get_segments::<T>(1)` will attempt to parse the segments
/// `"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)?;
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)]
pub fn mock(method: Method, uri: &str) -> Request {
Request {
@ -106,7 +109,7 @@ impl Request {
method: method,
cookies: Cookies::new(&[]),
uri: URIBuf::from(uri),
headers: header::Headers::new(),
headers: HeaderMap::new(),
}
}
@ -120,8 +123,7 @@ impl Request {
///
/// Returns the headers in this request.
#[inline(always)]
pub fn headers(&self) -> &header::Headers {
// FIXME: Get rid of Hyper.
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
@ -139,29 +141,9 @@ impl Request {
/// Content-Type of [any](struct.ContentType.html#method.any) is returned.
#[inline(always)]
pub fn content_type(&self) -> ContentType {
let hyp_ct = self.headers().get::<header::ContentType>();
hyp_ct.map_or(ContentType::Any, |ct| ContentType::from(&ct.0))
}
/// <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());
}
})
self.headers().get_one("Content-Type")
.and_then(|value| value.parse().ok())
.unwrap_or(ContentType::Any)
}
/// 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
// 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.
// 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;
*self.params.borrow_mut() = unsafe {
transmute(route.get_params(self.uri.as_uri()))
@ -188,20 +172,13 @@ impl Request {
#[doc(hidden)]
#[inline(always)]
pub fn set_headers(&mut self, h_headers: header::Headers) {
let cookies = match h_headers.get::<header::Cookie>() {
// TODO: Retrieve key from config.
Some(cookie) => cookie.to_cookie_jar(&[]),
None => Cookies::new(&[]),
};
self.headers = h_headers;
self.cookies = cookies;
pub fn add_header(&mut self, header: Header<'static>) {
self.headers.add(header);
}
#[doc(hidden)]
pub fn new(h_method: hyper::Method,
h_headers: header::Headers,
h_headers: hyper::header::Headers,
h_uri: hyper::RequestUri)
-> Result<Request, String> {
let uri = match h_uri {
@ -214,18 +191,23 @@ impl Request {
_ => 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.
Some(cookie) => cookie.to_cookie_jar(&[]),
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 {
params: RefCell::new(vec![]),
method: method,
cookies: cookies,
uri: uri,
headers: h_headers,
headers: headers,
};
Ok(request)

View File

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

View File

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