diff --git a/src/agent.rs b/src/agent.rs index ca088a5..8354dbe 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1,12 +1,12 @@ use cookie::{Cookie, CookieJar}; -use std::str::FromStr; +use error::Error; +use response::{self, Response}; use std::sync::Mutex; -use header::{add_header, get_header, get_all_headers, has_header, Header}; +use header::{add_header, get_all_headers, get_header, has_header, Header}; // to get to share private fields include!("request.rs"); -include!("response.rs"); include!("conn.rs"); include!("stream.rs"); include!("unit.rs"); diff --git a/src/conn.rs b/src/conn.rs index deb9b7d..d09ff7f 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -1,5 +1,6 @@ use std::io::Write; use url::Url; +use chunked_transfer; const CHUNK_SIZE: usize = 1024 * 1024; diff --git a/src/lib.rs b/src/lib.rs index bca428b..7d61c4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,6 +110,7 @@ mod agent; mod error; mod header; mod macros; +mod response; #[cfg(feature = "json")] mod serde_macros; @@ -117,9 +118,10 @@ mod serde_macros; #[cfg(test)] mod test; -pub use agent::{Agent, Request, Response}; +pub use agent::{Agent, Request}; pub use error::Error; pub use header::Header; +pub use response::Response; // re-export pub use cookie::Cookie; diff --git a/src/request.rs b/src/request.rs index 8909c0e..2e056f0 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,13 +1,22 @@ use qstring::QString; use std::io::empty; use std::io::Cursor; +use std::io::Read; use std::sync::Arc; + #[cfg(feature = "json")] use super::SerdeValue; #[cfg(feature = "json")] use serde_json; +#[cfg(feature = "charset")] +use encoding::label::encoding_from_whatwg_label; +#[cfg(feature = "charset")] +use encoding::EncoderTrap; +#[cfg(feature = "charset")] +use response::DEFAULT_CHARACTER_SET; + lazy_static! { static ref URL_BASE: Url = { Url::parse("http://localhost/").expect("Failed to parse URL_BASE") }; } @@ -202,7 +211,7 @@ impl Request { S: Into, { let text = data.into(); - let charset = charset_from_content_type(self.header("content-type")).to_string(); + let charset = response::charset_from_content_type(self.header("content-type")).to_string(); self.do_call(Payload::Text(text, charset)) } diff --git a/src/response.rs b/src/response.rs index b712d75..fb2976c 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,19 +1,26 @@ +use agent::Stream; use ascii::AsciiString; use chunked_transfer; +use header::Header; +use std::io::Cursor; use std::io::Error as IoError; use std::io::ErrorKind; use std::io::Read; use std::io::Result as IoResult; +use std::str::FromStr; + +#[cfg(feature = "json")] +use serde_json; #[cfg(feature = "charset")] use encoding::label::encoding_from_whatwg_label; #[cfg(feature = "charset")] -use encoding::{DecoderTrap, EncoderTrap}; +use encoding::DecoderTrap; use error::Error; -const DEFAULT_CONTENT_TYPE: &'static str = "text/plain"; -const DEFAULT_CHARACTER_SET: &'static str = "utf-8"; +pub const DEFAULT_CONTENT_TYPE: &'static str = "text/plain"; +pub const DEFAULT_CHARACTER_SET: &'static str = "utf-8"; /// Response instances are created as results of firing off requests. /// @@ -243,7 +250,6 @@ impl Response { /// assert_eq!(bytes.len(), len); /// ``` pub fn into_reader(self) -> impl Read { - let is_chunked = self.header("transfer-encoding") .map(|enc| enc.len() > 0) // whatever it says, do chunked .unwrap_or(false); @@ -388,11 +394,6 @@ impl Response { }) } - fn set_stream(&mut self, stream: Stream, is_head: bool) { - self.is_head = is_head; - self.stream = Some(stream); - } - #[cfg(test)] pub fn to_write_vec(&self) -> Vec { self.stream.as_ref().unwrap().to_write_vec() @@ -431,7 +432,7 @@ impl FromStr for Response { let bytes = s.as_bytes().to_owned(); let mut cursor = Cursor::new(bytes); let mut resp = Self::do_from_read(&mut cursor)?; - resp.set_stream(Stream::Cursor(cursor), false); + set_stream(&mut resp, Stream::Cursor(cursor), false); Ok(resp) } } @@ -447,6 +448,11 @@ impl Into for Error { } } +pub fn set_stream(resp: &mut Response, stream: Stream, is_head: bool) { + resp.is_head = is_head; + resp.stream = Some(stream); +} + // application/x-www-form-urlencoded, application/json, and multipart/form-data fn read_next_line(reader: &mut R) -> IoResult { @@ -510,7 +516,7 @@ impl Read for LimitedRead { } } -fn charset_from_content_type(header: Option<&str>) -> &str { +pub fn charset_from_content_type(header: Option<&str>) -> &str { header .and_then(|header| { header.find(";").and_then(|semi| { @@ -521,3 +527,76 @@ fn charset_from_content_type(header: Option<&str>) -> &str { }) .unwrap_or(DEFAULT_CHARACTER_SET) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn content_type_without_charset() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("application/json", resp.content_type()); + } + + #[test] + fn content_type_with_charset() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=iso-8859-4\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("application/json", resp.content_type()); + } + + #[test] + fn content_type_default() { + let s = "HTTP/1.1 200 OK\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("text/plain", resp.content_type()); + } + + #[test] + fn charset() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=iso-8859-4\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("iso-8859-4", resp.charset()); + } + + #[test] + fn charset_default() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("utf-8", resp.charset()); + } + + #[test] + fn chunked_transfer() { + let s = "HTTP/1.1 200 OK\r\nTransfer-Encoding: Chunked\r\n\r\n3\r\nhel\r\nb\r\nlo world!!!\r\n0\r\n\r\n"; + let resp = s.parse::().unwrap(); + assert_eq!("hello world!!!", resp.into_string().unwrap()); + } + + #[test] + #[cfg(feature = "json")] + fn parse_simple_json() { + let s = format!("HTTP/1.1 200 OK\r\n\r\n{{\"hello\":\"world\"}}"); + let resp = s.parse::().unwrap(); + let v = resp.into_json().unwrap(); + assert_eq!( + v, + "{\"hello\":\"world\"}" + .parse::() + .unwrap() + ); + } + + #[test] + fn parse_borked_header() { + let s = format!("HTTP/1.1 BORKED\r\n"); + let resp: Response = s.parse::().unwrap_err().into(); + assert_eq!(resp.http_version(), "HTTP/1.1"); + assert_eq!(*resp.status(), 500); + assert_eq!(resp.status_text(), "Bad Status"); + assert_eq!(resp.content_type(), "text/plain"); + let v = resp.into_string().unwrap(); + assert_eq!(v, "Bad Status\n"); + } +} diff --git a/src/stream.rs b/src/stream.rs index 6695de6..2dff88f 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -2,6 +2,7 @@ use std::net::SocketAddr; use std::net::TcpStream; use std::net::ToSocketAddrs; use std::time::Duration; +use std::io::Result as IoResult; #[cfg(feature = "tls")] use native_tls::TlsConnector; diff --git a/src/test/mod.rs b/src/test/mod.rs index 423ac7f..fed5cdd 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,5 +1,5 @@ -use agent::Unit; use agent::Stream; +use agent::Unit; use error::Error; use header::Header; use std::collections::HashMap; diff --git a/src/unit.rs b/src/unit.rs index c0af75b..1d9600b 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -153,7 +153,7 @@ impl Unit { .map_err(|_| Error::BadUrl(format!("Bad redirection: {}", location)))?; // perform the redirect differently depending on 3xx code. - return match resp.status { + return match resp.status() { 301 | 302 | 303 => { send_body(body, self.is_chunked, &mut stream)?; let empty = Payload::Empty.into_read(); @@ -168,7 +168,7 @@ impl Unit { send_body(body, self.is_chunked, &mut stream)?; // since it is not a redirect, give away the incoming stream to the response object - resp.set_stream(stream, self.is_head); + response::set_stream(&mut resp, stream, self.is_head); // release the response Ok(resp)