diff --git a/examples/forms/src/tests.rs b/examples/forms/src/tests.rs index 2151521a..2bca331a 100644 --- a/examples/forms/src/tests.rs +++ b/examples/forms/src/tests.rs @@ -1,11 +1,12 @@ use super::rocket; use rocket::testing::MockRequest; use rocket::http::Method::*; +use rocket::http::ContentType; fn test_login 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()); diff --git a/examples/from_request/src/main.rs b/examples/from_request/src/main.rs index ced097a2..75c142c2 100644 --- a/examples/from_request/src/main.rs +++ b/examples/from_request/src/main.rs @@ -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>) { + 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); } } } diff --git a/lib/src/http/header.rs b/lib/src/http/header.rs index de364c3c..5e694bdf 100644 --- a/lib/src/http/header.rs +++ b/lib/src/http/header.rs @@ -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 { 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>>(&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>, V: Into> + { + self.replace(Header::new(name, value)) + } + #[inline(always)] pub fn replace_all<'n, 'v: 'h, H>(&mut self, name: H, values: Vec>) where 'n: 'h, H: Into> @@ -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>, V: Into> + { + self.add(Header::new(name, value)) + } + #[inline(always)] pub fn add_all<'n, H>(&mut self, name: H, values: &mut Vec>) where 'n:'h, H: Into> @@ -85,6 +117,11 @@ impl<'h> HeaderMap<'h> { } #[inline(always)] + pub fn remove_all(&mut self) -> Vec> { + let old_map = ::std::mem::replace(self, HeaderMap::new()); + old_map.into_iter().collect() + } + pub fn iter<'s>(&'s self) -> impl Iterator> { 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> { + 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, Vec>)> { self.headers.into_iter() } diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 06ca190b..9def3db3 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -70,7 +70,8 @@ impl IntoOutcome for Result { /// 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 IntoOutcome for Result { /// /// impl<'r> FromRequest<'r> for APIKey { /// type Error = (); -/// fn from_request(request: &'r Request) -> request::Outcome { -/// 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()) { -/// if is_valid(&key) { -/// return Outcome::Success(APIKey(key)); -/// } -/// } +/// fn from_request(request: &'r Request) -> request::Outcome { +/// let keys: Vec<_> = request.headers().get("x-api-key").collect(); +/// 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())); /// } /// } /// diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 4867753c..c49a2af8 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -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>, 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::(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 { + pub fn get_segments<'r, T: FromSegments<'r>>(&'r self, i: usize) + -> Result { 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::(); - hyp_ct.map_or(ContentType::Any, |ct| ContentType::from(&ct.0)) - } - - ///
- /// - /// Unstable - /// (#17): - /// The underlying HTTP library/types are likely to change before v1.0. - /// - ///
- /// - /// Returns the first content-type accepted by this request. - pub fn accepts(&self) -> ContentType { - let accept = self.headers().get::(); - 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::() { - // 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 { let uri = match h_uri { @@ -214,18 +191,23 @@ impl Request { _ => return Err(format!("Bad method: {}", h_method)), }; - let cookies = match h_headers.get::() { + let cookies = match h_headers.get::() { // 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) diff --git a/lib/src/response/response.rs b/lib/src/response/response.rs index 41aa8f0a..80549790 100644 --- a/lib/src/response/response.rs +++ b/lib/src/response/response.rs @@ -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); } } diff --git a/lib/src/testing.rs b/lib/src/testing.rs index 6f9f657d..aadc4a01 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -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>(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()); - } - - hyp_headers.set_raw(name.to_string(), vec_fields); - } - - self.request.set_headers(hyp_headers); + #[inline] + pub fn header<'h, H: Into>>(mut self, header: H) -> Self { + self.request.add_header(header.into()); + self + } + /// + /// 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>(mut self, body: S) -> Self { self.data = Data::new(body.as_ref().as_bytes().into()); self