initial commit

This commit is contained in:
Martin Algesten
2017-12-12 04:40:30 +01:00
parent f1b9318994
commit 4df9876c24
15 changed files with 2475 additions and 5 deletions

315
src/agent.rs Normal file
View File

@@ -0,0 +1,315 @@
use std::str::FromStr;
use std::sync::Mutex;
use header::Header;
use util::*;
// to get to share private fields
include!("request.rs");
include!("response.rs");
include!("conn.rs");
#[derive(Debug, Default, Clone)]
pub struct Agent {
pub headers: Vec<Header>,
pub auth: Option<(String, String)>,
pub pool: Arc<Mutex<Option<ConnectionPool>>>,
}
impl Agent {
pub fn new() -> Agent {
Default::default()
}
/// Create a new agent after treating it as a builder.
/// This actually clones the internal state to a new one and instantiates
/// a new connection pool that is reused between connects.
pub fn build(&self) -> Self {
Agent {
headers: self.headers.clone(),
auth: self.auth.clone(),
pool: Arc::new(Mutex::new(Some(ConnectionPool::new()))),
}
}
/// Set a header field that will be present in all requests using the agent.
///
/// ```
/// let agent = ureq::agent()
/// .set("X-API-Key", "foobar")
/// .set("Accept", "application/json")
/// .build();
///
/// let r = agent
/// .get("/my-page")
/// .call();
///
/// if r.ok() {
/// println!("yay got {}", r.into_json().unwrap());
/// } else {
/// println!("Oh no error!");
/// }
/// ```
pub fn set<K, V>(&mut self, header: K, value: V) -> &mut Agent
where
K: Into<String>,
V: Into<String>,
{
add_agent_header(self, header.into(), value.into());
self
}
/// Set many headers that will be present in all requests using the agent.
///
/// ```
/// #[macro_use]
/// extern crate ureq;
///
/// fn main() {
/// let agent = ureq::agent()
/// .set_map(map!{
/// "X-API-Key" => "foobar",
/// "Accept" => "application/json"
/// })
/// .build();
///
/// let r = agent
/// .get("/my_page")
/// .call();
///
/// if r.ok() {
/// println!("yay got {}", r.into_json().unwrap());
/// }
/// }
/// ```
pub fn set_map<K, V, I>(&mut self, headers: I) -> &mut Agent
where
K: Into<String>,
V: Into<String>,
I: IntoIterator<Item = (K, V)>,
{
for (k, v) in headers.into_iter() {
add_agent_header(self, k.into(), v.into());
}
self
}
/// Basic auth that will be present in all requests using the agent.
///
/// ```
/// let agent = ureq::agent()
/// .auth("martin", "rubbermashgum")
/// .build();
///
/// let r = agent
/// .get("/my_page")
/// .call();
/// println!("{:?}", r);
/// ```
pub fn auth<S, T>(&mut self, user: S, pass: T) -> &mut Agent
where
S: Into<String>,
T: Into<String>,
{
let u = user.into();
let p = pass.into();
let pass = basic_auth(&u, &p);
self.auth_kind("Basic", pass)
}
/// Auth of other kinds such as `Digest`, `Token` etc, that will be present
/// in all requests using the agent.
///
/// ```
/// let agent = ureq::agent()
/// .auth_kind("token", "secret")
/// .build();
///
/// let r = agent
/// .get("/my_page")
/// .call();
/// ```
pub fn auth_kind<S, T>(&mut self, kind: S, pass: T) -> &mut Agent
where
S: Into<String>,
T: Into<String>,
{
self.auth = Some((kind.into(), pass.into()));
self
}
/// Request by providing the HTTP verb such as `GET`, `POST`...
///
/// ```
/// let agent = ureq::agent();
///
/// let r = agent
/// .request("GET", "/my_page")
/// .call();
/// println!("{:?}", r);
/// ```
pub fn request<M, S>(&self, method: M, path: S) -> Request
where
M: Into<String>,
S: Into<String>,
{
Request::new(&self, method.into(), path.into())
}
pub fn get<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("GET", path)
}
pub fn head<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("HEAD", path)
}
pub fn post<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("POST", path)
}
pub fn put<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("PUT", path)
}
pub fn delete<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("DELETE", path)
}
pub fn trace<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("TRACE", path)
}
pub fn options<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("OPTIONS", path)
}
pub fn connect<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("CONNECT", path)
}
pub fn patch<S>(&self, path: S) -> Request
where
S: Into<String>,
{
self.request("PATCH", path)
}
}
fn add_agent_header(agent: &mut Agent, k: String, v: String) {
if let Ok(h) = Header::from_str(&format!("{}: {}", k, v)) {
agent.headers.push(h);
}
}
#[cfg(test)]
mod tests {
use super::*;
///////////////////// AGENT TESTS //////////////////////////////
#[test]
fn agent_implements_send() {
let mut agent = Agent::new();
::std::thread::spawn(move || {
agent.set("Foo", "Bar");
});
}
//////////////////// REQUEST TESTS /////////////////////////////
#[test]
fn request_implements_send() {
let agent = Agent::new();
let mut request = Request::new(&agent, "GET".to_string(), "/foo".to_string());
::std::thread::spawn(move || {
request.set("Foo", "Bar");
});
}
//////////////////// RESPONSE TESTS /////////////////////////////
#[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::<Response>().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::<Response>().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::<Response>().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::<Response>().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::<Response>().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::<Response>().unwrap();
assert_eq!("hello world!!!", resp.into_string().unwrap());
}
#[test]
fn parse_simple_json() {
let s = format!("HTTP/1.1 200 OK\r\n\r\n{{\"hello\":\"world\"}}");
let resp = s.parse::<Response>().unwrap();
let v = resp.into_json().unwrap();
assert_eq!(
v,
"{\"hello\":\"world\"}"
.parse::<serde_json::Value>()
.unwrap()
);
}
#[test]
fn parse_borked_header() {
let s = format!("HTTP/1.1 BORKED\r\n");
let resp: Response = s.parse::<Response>().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");
}
}

193
src/conn.rs Normal file
View File

@@ -0,0 +1,193 @@
use dns_lookup;
use rustls;
use std::io::Write;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::net::TcpStream;
use std::time::Duration;
use stream::Stream;
use url::Url;
use webpki;
use webpki_roots;
const CHUNK_SIZE: usize = 1024 * 1024;
#[derive(Debug, Default, Clone)]
pub struct ConnectionPool {}
impl ConnectionPool {
fn new() -> Self {
ConnectionPool {}
}
fn connect(
&mut self,
request: &Request,
method: &str,
url: &Url,
redirects: u32,
payload: Payload,
) -> Result<Response, Error> {
//
// open connection
let mut stream = match url.scheme() {
"http" => connect_http(request, &url),
"https" => connect_https(request, &url),
_ => Err(Error::UnknownScheme(url.scheme().to_string())),
}?;
// send the request start + headers
let mut prelude: Vec<u8> = vec![];
write!(prelude, "{} {} HTTP/1.1\r\n", method, url.path())?;
if !request.has("host") {
write!(prelude, "Host: {}\r\n", url.host().unwrap())?;
}
for header in request.headers.iter() {
write!(prelude, "{}: {}\r\n", header.name(), header.value())?;
}
write!(prelude, "\r\n")?;
stream.write_all(&mut prelude[..])?;
// start reading the response to check it it's a redirect
let mut resp = Response::from_read(&mut stream);
// handle redirects
if resp.redirect() {
if redirects == 0 {
return Err(Error::TooManyRedirects);
}
// the location header
let location = resp.get("location");
if let Some(location) = location {
// join location header to current url in case it it relative
let new_url = url.join(location)
.map_err(|_| Error::BadUrl(format!("Bad redirection: {}", location)))?;
// perform the redirect differently depending on 3xx code.
return match resp.status {
301 | 302 | 303 => {
send_payload(&request, payload, &mut stream)?;
self.connect(request, "GET", &new_url, redirects - 1, Payload::Empty)
}
307 | 308 | _ => {
self.connect(request, method, &new_url, redirects - 1, payload)
}
};
}
}
// send the payload (which can be empty now depending on redirects)
send_payload(&request, payload, &mut stream)?;
// since it is not a redirect, give away the incoming stream to the response object
resp.set_reader(stream);
// release the response
Ok(resp)
}
}
fn connect_http(request: &Request, url: &Url) -> Result<Stream, Error> {
//
let hostname = url.host_str().unwrap();
let port = url.port().unwrap_or(80);
connect_host(request, hostname, port).map(|tcp| Stream::Http(tcp))
}
fn connect_https(request: &Request, url: &Url) -> Result<Stream, Error> {
//
let hostname = url.host_str().unwrap();
let port = url.port().unwrap_or(443);
// TODO let user override TLS roots.
let mut config = rustls::ClientConfig::new();
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
let rc_config = Arc::new(config);
let socket = connect_host(request, hostname, port)?;
webpki::DNSNameRef::try_from_ascii_str(&hostname)
.map_err(|_| Error::ConnectionFailed(format!("Invalid TLS name: {}", hostname)))
.map(|webpki| rustls::ClientSession::new(&rc_config, webpki))
.map(|client| Stream::Https(client, socket))
}
fn connect_host(request: &Request, hostname: &str, port: u16) -> Result<TcpStream, Error> {
//
let ips: Vec<IpAddr> =
dns_lookup::lookup_host(hostname).map_err(|e| Error::DnsFailed(format!("{}", e)))?;
if ips.len() == 0 {
return Err(Error::DnsFailed(format!("No ip address for {}", hostname)));
}
// pick first ip, or should we randomize?
let sock_addr = SocketAddr::new(ips[0], port);
// connect with a configured timeout.
let stream = match request.timeout {
0 => TcpStream::connect(&sock_addr),
_ => TcpStream::connect_timeout(&sock_addr, Duration::from_millis(request.timeout as u64)),
}.map_err(|err| Error::ConnectionFailed(format!("{}", err)))?;
// rust's absurd api returns Err if we set 0.
if request.timeout_read > 0 {
stream
.set_read_timeout(Some(Duration::from_millis(request.timeout_read as u64)))
.ok();
}
if request.timeout_write > 0 {
stream
.set_write_timeout(Some(Duration::from_millis(request.timeout_write as u64)))
.ok();
}
Ok(stream)
}
fn send_payload(request: &Request, payload: Payload, stream: &mut Stream) -> IoResult<()> {
//
let (size, reader) = payload.into_read();
let do_chunk = request.get("transfer-encoding")
// if the user has set an encoding header, obey that.
.map(|enc| enc.eq_ignore_ascii_case("chunked"))
// if the content has a size
.ok_or_else(|| size.
// or if the user set a content-length header
or_else(||
request.get("content-length").map(|len| len.parse::<usize>().unwrap_or(0)))
// and that size is larger than 1MB, chunk,
.map(|size| size > CHUNK_SIZE))
// otherwise, assume chunking since it can be really big.
.unwrap_or(true);
if do_chunk {
pipe(reader, chunked_transfer::Encoder::new(stream))?;
} else {
pipe(reader, stream)?;
}
Ok(())
}
fn pipe<R, W>(mut reader: R, mut writer: W) -> IoResult<()>
where
R: Read,
W: Write,
{
let mut buf = [0_u8; CHUNK_SIZE];
loop {
let len = reader.read(&mut buf)?;
if len == 0 {
break;
}
writer.write_all(&buf[0..len])?;
}
Ok(())
}

61
src/error.rs Normal file
View File

@@ -0,0 +1,61 @@
use std::io::Error as IoError;
#[derive(Debug)]
pub enum Error {
BadUrl(String),
UnknownScheme(String),
DnsFailed(String),
ConnectionFailed(String),
TooManyRedirects,
BadStatus,
BadHeader,
Io(IoError),
}
impl Error {
pub fn status(&self) -> u16 {
match self {
Error::BadUrl(_) => 400,
Error::UnknownScheme(_) => 400,
Error::DnsFailed(_) => 400,
Error::ConnectionFailed(_) => 500,
Error::TooManyRedirects => 400,
Error::BadStatus => 500,
Error::BadHeader => 500,
Error::Io(_) => 500,
}
}
pub fn status_text(&self) -> &str {
match self {
Error::BadUrl(e) => {
println!("{}", e);
"Bad URL"
},
Error::UnknownScheme(_) => "Unknown Scheme",
Error::DnsFailed(_) => "Dns Failed",
Error::ConnectionFailed(_) => "Connection Failed",
Error::TooManyRedirects => "Too Many Redirects",
Error::BadStatus => "Bad Status",
Error::BadHeader => "Bad Header",
Error::Io(_) => "Network Error",
}
}
pub fn body_text(&self) -> String {
match self {
Error::BadUrl(url) => format!("Bad URL: {}", url),
Error::UnknownScheme(scheme) => format!("Unknown Scheme: {}", scheme),
Error::DnsFailed(err) => format!("Dns Failed: {}", err),
Error::ConnectionFailed(err) => format!("Connection Failed: {}", err),
Error::TooManyRedirects => "Too Many Redirects".to_string(),
Error::BadStatus => "Bad Status".to_string(),
Error::BadHeader => "Bad Header".to_string(),
Error::Io(ioe) => format!("Network Error: {}", ioe),
}
}
}
impl From<IoError> for Error {
fn from(err: IoError) -> Error {
Error::Io(err)
}
}

58
src/header.rs Normal file
View File

@@ -0,0 +1,58 @@
use ascii::AsciiString;
use error::Error;
use std::str::FromStr;
#[derive(Debug, Clone)]
/// Wrapper type for a header line.
pub struct Header {
line: AsciiString,
index: usize,
}
impl Header {
/// The header name.
///
/// ```
/// let header = "X-Forwarded-For: 127.0.0.1".parse::<ureq::Header>().unwrap();
/// assert_eq!("X-Forwarded-For", header.name());
/// ```
pub fn name(&self) -> &str {
&self.line.as_str()[0..self.index]
}
/// The header value.
///
/// ```
/// let header = "X-Forwarded-For: 127.0.0.1".parse::<ureq::Header>().unwrap();
/// assert_eq!("127.0.0.1", header.value());
/// ```
pub fn value(&self) -> &str {
&self.line.as_str()[self.index + 1..].trim()
}
/// Compares the given str to the header name ignoring case.
///
/// ```
/// let header = "X-Forwarded-For: 127.0.0.1".parse::<ureq::Header>().unwrap();
/// assert!(header.is_name("x-forwarded-for"));
/// ```
pub fn is_name(&self, other: &str) -> bool {
self.name().eq_ignore_ascii_case(other)
}
}
impl FromStr for Header {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
//
let line = AsciiString::from_str(s).map_err(|_| Error::BadHeader)?;
let index = s.find(":").ok_or_else(|| Error::BadHeader)?;
// no value?
if index >= s.len() {
return Err(Error::BadHeader);
}
Ok(Header { line, index })
}
}

View File

@@ -1,7 +1,144 @@
extern crate ascii;
extern crate base64;
extern crate chrono;
extern crate chunked_transfer;
extern crate dns_lookup;
extern crate encoding;
#[macro_use]
extern crate lazy_static;
extern crate mime_guess;
extern crate qstring;
extern crate rustls;
extern crate serde_json;
extern crate url;
extern crate webpki;
extern crate webpki_roots;
#[macro_use]
mod agent;
mod error;
mod header;
mod stream;
mod util;
pub use agent::{Agent, Request, Response};
pub use header::Header;
// re-export
pub use serde_json::{to_value, Map, Value};
/// Agents keep state between requests.
///
/// By default, no state, such as cookies, is kept between requests.
/// But by creating an agent as entry point for the request, we
/// can keep state.
///
/// ```
/// let agent = ureq::agent();
///
/// let auth = agent
/// .post("/login")
/// .auth("martin", "rubbermashgum")
/// .call(); // blocks. puts auth cookies in agent.
///
/// if !auth.ok() {
/// println!("Noes!");
/// }
///
/// let secret = agent
/// .get("/my-protected-page")
/// .call(); // blocks and waits for request.
///
/// if !secret.ok() {
/// println!("Wot?!");
/// }
///
/// println!("Secret is: {}", secret.into_string().unwrap());
/// ```
pub fn agent() -> Agent {
Agent::new()
}
pub fn request<M, S>(method: M, path: S) -> Request
where
M: Into<String>,
S: Into<String>,
{
Agent::new().request(method, path)
}
pub fn get<S>(path: S) -> Request
where
S: Into<String>,
{
request("GET", path)
}
pub fn head<S>(path: S) -> Request
where
S: Into<String>,
{
request("HEAD", path)
}
pub fn post<S>(path: S) -> Request
where
S: Into<String>,
{
request("POST", path)
}
pub fn put<S>(path: S) -> Request
where
S: Into<String>,
{
request("PUT", path)
}
pub fn delete<S>(path: S) -> Request
where
S: Into<String>,
{
request("DELETE", path)
}
pub fn trace<S>(path: S) -> Request
where
S: Into<String>,
{
request("TRACE", path)
}
pub fn options<S>(path: S) -> Request
where
S: Into<String>,
{
request("OPTIONS", path)
}
pub fn connect<S>(path: S) -> Request
where
S: Into<String>,
{
request("CONNECT", path)
}
pub fn patch<S>(path: S) -> Request
where
S: Into<String>,
{
request("PATCH", path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
fn connect_http_google() {
let resp = get("http://www.google.com/").call();
println!("{:?}", resp);
assert_eq!("text/html; charset=ISO-8859-1", resp.get("content-type").unwrap());
assert_eq!("text/html", resp.content_type());
}
#[test]
fn connect_https_google() {
let resp = get("https://www.google.com/").call();
println!("{:?}", resp);
assert_eq!("text/html; charset=ISO-8859-1", resp.get("content-type").unwrap());
assert_eq!("text/html", resp.content_type());
}
}

513
src/request.rs Normal file
View File

@@ -0,0 +1,513 @@
use qstring::QString;
use serde_json;
use std::sync::Arc;
lazy_static! {
static ref URL_BASE: Url = { Url::parse("http://localhost/").expect("Failed to parse URL_BASE") };
}
#[derive(Clone, Default)]
pub struct Request {
pool: Arc<Mutex<Option<ConnectionPool>>>,
// via agent
method: String,
path: String,
// from request itself
headers: Vec<Header>,
auth: Option<(String, String)>,
query: QString,
timeout: u32,
timeout_read: u32,
timeout_write: u32,
redirects: u32,
}
enum Payload {
Empty,
Text(String),
JSON(serde_json::Value),
Reader(Box<Read + 'static>),
}
impl Default for Payload {
fn default() -> Payload {
Payload::Empty
}
}
impl Payload {
fn into_read(self) -> (Option<usize>, Box<Read + 'static>) {
match self {
Payload::Empty => (Some(0), Box::new(VecRead::from_str(""))),
Payload::Text(s) => {
let read = VecRead::from_str(&s);
(Some(read.len()), Box::new(read))
}
Payload::JSON(v) => {
let vec = serde_json::to_vec(&v).expect("Bad JSON in payload");
let read = VecRead::from_vec(vec);
(Some(read.len()), Box::new(read))
}
Payload::Reader(read) => (None, read),
}
}
}
impl Request {
fn new(agent: &Agent, method: String, path: String) -> Request {
Request {
pool: Arc::clone(&agent.pool),
method,
path,
headers: agent.headers.clone(),
auth: agent.auth.clone(),
redirects: 5,
..Default::default()
}
}
/// "Builds" this request which is effectively the same as cloning.
/// This is needed when we use a chain of request builders, but
/// don't want to send the request at the end of the chain.
///
/// ```
/// let r = ureq::get("/my_page")
/// .set("X-Foo-Bar", "Baz")
/// .build();
/// ```
pub fn build(&self) -> Request {
self.clone()
}
/// Executes the request and blocks the caller until done.
///
/// Use `.timeout()` and `.timeout_read()` to avoid blocking forever.
///
/// ```
/// let r = ureq::get("/my_page")
/// .timeout(10_000) // max 10 seconds
/// .call();
///
/// println!("{:?}", r);
/// ```
pub fn call(&self) -> Response {
self.do_call(Payload::Empty)
}
fn do_call(&self, payload: Payload) -> Response {
let mut lock = self.pool.lock().unwrap();
self.to_url()
.and_then(|url| {
if lock.is_none() {
// create a one off pool.
ConnectionPool::new().connect(self, &self.method, &url, self.redirects, payload)
} else {
// reuse connection pool.
lock.as_mut().unwrap().connect(
self,
&self.method,
&url,
self.redirects,
payload,
)
}
})
.unwrap_or_else(|e| e.into())
}
/// Send data a json value.
///
/// ```
/// #[macro_use]
/// extern crate ureq;
///
/// fn main() {
/// let r = ureq::post("/my_page")
/// .send_json(json!({ "name": "martin", "rust": true }));
/// println!("{:?}", r);
/// }
/// ```
pub fn send_json(&self, data: serde_json::Value) -> Response {
self.do_call(Payload::JSON(data))
}
/// Send data as a string.
///
/// ```
/// let r = ureq::post("/my_page")
/// .content_type("text/plain")
/// .send_str("Hello World!");
/// println!("{:?}", r);
/// ```
pub fn send_str<S>(&self, data: S) -> Response
where
S: Into<String>,
{
let text = data.into();
self.do_call(Payload::Text(text))
}
/// Send data from a reader.
///
///
///
pub fn send<R>(&self, reader: R) -> Response
where
R: Read + Send + 'static,
{
self.do_call(Payload::Reader(Box::new(reader)))
}
/// Set a header field.
///
/// ```
/// let r = ureq::get("/my_page")
/// .set("X-API-Key", "foobar")
/// .set("Accept", "application/json")
/// .call();
///
/// if r.ok() {
/// println!("yay got {}", r.into_json().unwrap());
/// } else {
/// println!("Oh no error!");
/// }
/// ```
pub fn set<K, V>(&mut self, header: K, value: V) -> &mut Request
where
K: Into<String>,
V: Into<String>,
{
add_request_header(self, header.into(), value.into());
self
}
/// Returns the value for a set header.
///
/// ```
/// let req = ureq::get("/my_page")
/// .set("X-API-Key", "foobar")
/// .build();
/// assert_eq!("foobar", req.get("x-api-Key").unwrap());
/// ```
pub fn get<'a>(&self, name: &'a str) -> Option<&str> {
self.headers
.iter()
.find(|h| h.is_name(name))
.map(|h| h.value())
}
/// Tells if the header has been set.
///
/// ```
/// let req = ureq::get("/my_page")
/// .set("X-API-Key", "foobar")
/// .build();
/// assert_eq!(true, req.has("x-api-Key"));
/// ```
pub fn has<'a>(&self, name: &'a str) -> bool {
self.get(name).is_some()
}
/// Set many headers.
///
/// ```
/// #[macro_use]
/// extern crate ureq;
///
/// fn main() {
/// let r = ureq::get("/my_page")
/// .set_map(map!{
/// "X-API-Key" => "foobar",
/// "Accept" => "application/json"
/// })
/// .call();
///
/// if r.ok() {
/// println!("yay got {}", r.into_json().unwrap());
/// }
/// }
/// ```
pub fn set_map<K, V, I>(&mut self, headers: I) -> &mut Request
where
K: Into<String>,
V: Into<String>,
I: IntoIterator<Item = (K, V)>,
{
for (k, v) in headers.into_iter() {
add_request_header(self, k.into(), v.into());
}
self
}
/// Set a query parameter.
///
/// For example, to set `?format=json&dest=/login`
///
/// ```
/// let r = ureq::get("/my_page")
/// .query("format", "json")
/// .query("dest", "/login")
/// .call();
///
/// println!("{:?}", r);
/// ```
pub fn query<K, V>(&mut self, param: K, value: V) -> &mut Request
where
K: Into<String>,
V: Into<String>,
{
self.query.add_pair((param.into(), value.into()));
self
}
/// Set many query parameters.
///
/// For example, to set `?format=json&dest=/login`
///
/// ```
/// #[macro_use]
/// extern crate ureq;
///
/// fn main() {
/// let r = ureq::get("/my_page")
/// .query_map(map!{
/// "format" => "json",
/// "dest" => "/login"
/// })
/// .call();
///
/// println!("{:?}", r);
/// }
/// ```
pub fn query_map<K, V, I>(&mut self, params: I) -> &mut Request
where
K: Into<String>,
V: Into<String>,
I: IntoIterator<Item = (K, V)>,
{
for (k, v) in params.into_iter() {
self.query.add_pair((k.into(), v.into()));
}
self
}
/// Set query parameters as a string.
///
/// For example, to set `?format=json&dest=/login`
///
/// ```
/// let r = ureq::get("/my_page")
/// .query_str("?format=json&dest=/login")
/// .call();
/// println!("{:?}", r);
/// ```
pub fn query_str<S>(&mut self, query: S) -> &mut Request
where
S: Into<String>,
{
let s = query.into();
self.query.add_str(&s);
self
}
/// Set the `Content-Type` header.
///
/// The default is `application/json`.
///
/// As a short-hand the `.content_type()` method accepts the
/// canonicalized MIME type name complete with
/// type/subtype, or simply the extension name such as
/// "xml", "json", "png".
///
/// These are all the same.
///
/// ```
/// ureq::post("/my_page")
/// .set("Content-Type", "text/plain")
/// .call();
///
/// ureq::post("/my_page")
/// .content_type("text/plain")
/// .call();
///
/// ureq::post("/my_page")
/// .content_type("txt")
/// .call();
/// ```
pub fn content_type<S>(&mut self, c: S) -> &mut Request
where
S: Into<String>,
{
self.set("Content-Type", mime_of(c))
}
/// Sets the `Accept` header in the same way as `content_type()`.
///
/// The short-hand `.accept()` method accepts the
/// canonicalized MIME type name complete with
/// type/subtype, or simply the extension name such as
/// "xml", "json", "png".
///
/// These are all the same.
///
/// ```
/// ureq::get("/my_page")
/// .set("Accept", "text/plain")
/// .call();
///
/// ureq::get("/my_page")
/// .accept("text/plain")
/// .call();
///
/// ureq::get("/my_page")
/// .accept("txt")
/// .call();
/// ```
pub fn accept<S>(&mut self, accept: S) -> &mut Request
where
S: Into<String>,
{
self.set("Accept", mime_of(accept))
}
/// Timeout for the socket connection to be successful.
///
/// The default is `0`, which means a request can block forever.
///
/// ```
/// let r = ureq::get("/my_page")
/// .timeout(1_000) // wait max 1 second to connect
/// .call();
/// println!("{:?}", r);
/// ```
pub fn timeout(&mut self, millis: u32) -> &mut Request {
self.timeout = millis;
self
}
/// Timeout for the individual reads of the socket.
///
/// The default is `0`, which means it can block forever.
///
/// ```
/// let r = ureq::get("/my_page")
/// .timeout_read(1_000) // wait max 1 second for the read
/// .call();
/// println!("{:?}", r);
/// ```
pub fn timeout_read(&mut self, millis: u32) -> &mut Request {
self.timeout_read = millis;
self
}
/// Timeout for the individual writes to the socket.
///
/// The default is `0`, which means it can block forever.
///
/// ```
/// let r = ureq::get("/my_page")
/// .timeout_write(1_000) // wait max 1 second for sending.
/// .call();
/// println!("{:?}", r);
/// ```
pub fn timeout_write(&mut self, millis: u32) -> &mut Request {
self.timeout_write = millis;
self
}
/// Basic auth.
///
/// These are the same
///
/// ```
/// let r1 = ureq::get("http://localhost/my_page")
/// .auth("martin", "rubbermashgum")
/// .call();
/// println!("{:?}", r1);
///
/// let r2 = ureq::get("http://martin:rubbermashgum@localhost/my_page").call();
/// println!("{:?}", r2);
/// ```
pub fn auth<S, T>(&mut self, user: S, pass: T) -> &mut Request
where
S: Into<String>,
T: Into<String>,
{
let u = user.into();
let p = pass.into();
let pass = basic_auth(&u, &p);
self.auth_kind("Basic", pass)
}
/// Auth of other kinds such as `Digest`, `Token` etc.
///
/// ```
/// let r = ureq::get("http://localhost/my_page")
/// .auth_kind("token", "secret")
/// .call();
/// println!("{:?}", r);
/// ```
pub fn auth_kind<S, T>(&mut self, kind: S, pass: T) -> &mut Request
where
S: Into<String>,
T: Into<String>,
{
self.auth = Some((kind.into(), pass.into()));
self
}
/// How many redirects to follow.
///
/// Defaults to `5`.
///
/// ```
/// let r = ureq::get("/my_page")
/// .redirects(10)
/// .call();
/// println!("{:?}", r);
/// ```
pub fn redirects(&mut self, n: u32) -> &mut Request {
self.redirects = n;
self
}
// pub fn retry(&self, times: u16) -> Request {
// unimplemented!()
// }
// pub fn sortQuery(&self) -> Request {
// unimplemented!()
// }
// pub fn sortQueryBy(&self, by: Box<Fn(&str, &str) -> usize>) -> Request {
// unimplemented!()
// }
// pub fn ca<S>(&self, accept: S) -> Request
// where S: Into<String> {
// unimplemented!()
// }
// pub fn cert<S>(&self, accept: S) -> Request
// where S: Into<String> {
// unimplemented!()
// }
// pub fn key<S>(&self, accept: S) -> Request
// where S: Into<String> {
// unimplemented!()
// }
// pub fn pfx<S>(&self, accept: S) -> Request // TODO what type? u8?
// where S: Into<String> {
// unimplemented!()
// }
fn to_url(&self) -> Result<Url, Error> {
URL_BASE
.join(&self.path)
.map_err(|e| Error::BadUrl(format!("{}", e)))
}
}
fn add_request_header(request: &mut Request, k: String, v: String) {
if let Ok(h) = Header::from_str(&format!("{}: {}", k, v)) {
request.headers.push(h)
}
}

277
src/response.rs Normal file
View File

@@ -0,0 +1,277 @@
use ascii::AsciiString;
use chunked_transfer;
use encoding::label::encoding_from_whatwg_label;
use encoding::DecoderTrap;
use std::io::Error as IoError;
use std::io::ErrorKind;
use std::io::Read;
use std::io::Result as IoResult;
use error::Error;
const DEFAULT_CONTENT_TYPE: &'static str = "text/plain";
const DEFAULT_CHARACTER_SET: &'static str = "utf-8";
// buffered
// "text/", "/json", or "x-www-form-urlencoded"
pub struct Response {
status_line: AsciiString,
index: (usize, usize), // index into status_line where we split: HTTP/1.1 200 OK
status: u16,
headers: Vec<Header>,
reader: Option<Box<Read + Send + 'static>>,
}
impl ::std::fmt::Debug for Response {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
write!(
f,
"Response[status: {}, status_text: {}]",
self.status(),
self.status_text()
)
}
}
impl Response {
/// The entire status line like: HTTP/1.1 200 OK
pub fn status_line(&self) -> &str {
self.status_line.as_str()
}
/// The http version: HTTP/1.1
pub fn http_version(&self) -> &str {
&self.status_line.as_str()[0..self.index.0]
}
/// The status as a u16: 200
pub fn status(&self) -> &u16 {
&self.status
}
/// The status text: OK
pub fn status_text(&self) -> &str {
&self.status_line.as_str()[self.index.1 + 1..].trim()
}
/// The header corresponding header value for the give name, if any.
pub fn get<'a>(&self, name: &'a str) -> Option<&str> {
self.headers
.iter()
.find(|h| h.is_name(name))
.map(|h| h.value())
}
/// Tells if the response has the named header.
pub fn has<'a>(&self, name: &'a str) -> bool {
self.get(name).is_some()
}
/// All headers corresponding values for the give name, or empty vector.
pub fn get_all<'a>(&self, name: &'a str) -> Vec<&str> {
self.headers
.iter()
.filter(|h| h.is_name(name))
.map(|h| h.value())
.collect()
}
/// Whether the response status is: 200 <= status <= 299
pub fn ok(&self) -> bool {
self.status >= 200 && self.status <= 299
}
pub fn redirect(&self) -> bool {
self.status >= 300 && self.status <= 399
}
/// Whether the response status is: 400 <= status <= 499
pub fn client_error(&self) -> bool {
self.status >= 400 && self.status <= 499
}
/// Whether the response status is: 500 <= status <= 599
pub fn server_error(&self) -> bool {
self.status >= 500 && self.status <= 599
}
/// Whether the response status is: 400 <= status <= 599
pub fn error(&self) -> bool {
self.client_error() || self.server_error()
}
/// The content type part of the "Content-Type" header without
/// the charset.
///
/// Example:
///
/// ```
/// let resp = ureq::get("https://www.google.com/").call();
/// assert_eq!("text/html; charset=ISO-8859-1", resp.get("content-type").unwrap());
/// assert_eq!("text/html", resp.content_type());
/// ```
pub fn content_type(&self) -> &str {
self.get("content-type")
.map(|header| {
header
.find(";")
.map(|index| &header[0..index])
.unwrap_or(header)
})
.unwrap_or(DEFAULT_CONTENT_TYPE)
}
pub fn charset(&self) -> &str {
self.get("content-type")
.and_then(|header| {
header.find(";").and_then(|semi| {
(&header[semi + 1..])
.find("=")
.map(|equal| (&header[semi + equal + 2..]).trim())
})
})
.unwrap_or(DEFAULT_CHARACTER_SET)
}
pub fn into_reader(self) -> impl Read {
let is_chunked = self.get("transfer-encoding")
.map(|enc| enc.eq_ignore_ascii_case("chunked"))
.unwrap_or(false);
let reader = self.reader.expect("No reader in response?!");
match is_chunked {
true => Box::new(chunked_transfer::Decoder::new(reader)),
false => reader,
}
}
pub fn into_string(self) -> IoResult<String> {
let encoding = encoding_from_whatwg_label(self.charset())
.or_else(|| encoding_from_whatwg_label(DEFAULT_CHARACTER_SET))
.unwrap();
let mut buf: Vec<u8> = vec![];
self.into_reader().read_to_end(&mut buf)?;
Ok(encoding.decode(&buf, DecoderTrap::Replace).unwrap())
}
pub fn into_json(self) -> IoResult<serde_json::Value> {
let reader = self.into_reader();
serde_json::from_reader(reader).map_err(|e| {
IoError::new(
ErrorKind::InvalidData,
format!("Failed to read JSON: {}", e),
)
})
}
pub fn new(status: u16, status_text: &str, body: &str) -> Self {
let r = format!("HTTP/1.1 {} {}\r\n\r\n{}\n", status, status_text, body);
(r.as_ref() as &str)
.parse::<Response>()
.unwrap_or_else(|e| e.into())
}
pub fn from_read(reader: impl Read) -> Self
{
Self::do_from_read(reader).unwrap_or_else(|e| e.into())
}
fn do_from_read(mut reader: impl Read) -> Result<Response, Error>
{
//
// HTTP/1.1 200 OK\r\n
let status_line = read_next_line(&mut reader).map_err(|_| Error::BadStatus)?;
let (index, status) = parse_status_line(status_line.as_str())?;
let mut headers: Vec<Header> = Vec::new();
loop {
let line = read_next_line(&mut reader).map_err(|_| Error::BadHeader)?;
if line.len() == 0 {
break;
}
if let Ok(header) = line.as_str().parse::<Header>() {
headers.push(header);
}
}
Ok(Response {
status_line,
index,
status,
headers,
reader: None,
})
}
fn set_reader<R>(&mut self, reader: R) where R: Read + Send + 'static {
self.reader = Some(Box::new(reader));
}
}
fn parse_status_line(line: &str) -> Result<((usize, usize), u16), Error> {
// HTTP/1.1 200 OK\r\n
let mut split = line.splitn(3, ' ');
let http_version = split.next().ok_or_else(|| Error::BadStatus)?;
if http_version.len() < 5 {
return Err(Error::BadStatus);
}
let index1 = http_version.len();
let status = split.next().ok_or_else(|| Error::BadStatus)?;
if status.len() < 3 {
return Err(Error::BadStatus);
}
let index2 = index1 + status.len();
let status = status.parse::<u16>().map_err(|_| Error::BadStatus)?;
let status_text = split.next().ok_or_else(|| Error::BadStatus)?;
if status_text.len() == 0 {
return Err(Error::BadStatus);
}
Ok(((index1, index2), status))
}
impl FromStr for Response {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut read = VecRead::from_str(s);
let mut resp = Self::do_from_read(&mut read)?;
resp.set_reader(read);
Ok(resp)
}
}
impl Into<Response> for Error {
fn into(self) -> Response {
Response::new(self.status(), self.status_text(), &self.body_text())
}
}
// application/x-www-form-urlencoded, application/json, and multipart/form-data
fn read_next_line<R: Read>(reader: &mut R) -> IoResult<AsciiString> {
let mut buf = Vec::new();
let mut prev_byte_was_cr = false;
loop {
let byte = reader.bytes().next();
let byte = match byte {
Some(b) => try!(b),
None => return Err(IoError::new(ErrorKind::ConnectionAborted, "Unexpected EOF")),
};
if byte == b'\n' && prev_byte_was_cr {
buf.pop(); // removing the '\r'
return AsciiString::from_ascii(buf)
.map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
}
prev_byte_was_cr = byte == b'\r';
buf.push(byte);
}
}

34
src/stream.rs Normal file
View File

@@ -0,0 +1,34 @@
use rustls;
use std::io::Read;
use std::io::Result;
use std::io::Write;
use std::net::TcpStream;
pub enum Stream {
Http(TcpStream),
Https(rustls::ClientSession, TcpStream),
}
impl Read for Stream {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match self {
Stream::Http(sock) => sock.read(buf),
Stream::Https(sess, sock) => rustls::Stream::new(sess, sock).read(buf),
}
}
}
impl Write for Stream {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
match self {
Stream::Http(sock) => sock.write(buf),
Stream::Https(sess, sock) => rustls::Stream::new(sess, sock).write(buf),
}
}
fn flush(&mut self) -> Result<()> {
match self {
Stream::Http(sock) => sock.flush(),
Stream::Https(sess, sock) => rustls::Stream::new(sess, sock).flush(),
}
}
}

10
src/util/macros.rs Normal file
View File

@@ -0,0 +1,10 @@
#[macro_export]
macro_rules! map(
{ $($key:expr => $value:expr),* } => {
{
let mut m = ::std::collections::HashMap::new();
$(m.insert($key, $value);)+
m
}
};
);

29
src/util/mod.rs Normal file
View File

@@ -0,0 +1,29 @@
#[allow(dead_code)]
mod macros;
mod serde_macros;
mod vecread;
use base64;
use mime_guess::get_mime_type_str;
pub use util::vecread::VecRead;
pub fn basic_auth(user: &str, pass: &str) -> String {
let safe = match user.find(":") {
Some(idx) => &user[..idx],
None => user,
};
base64::encode(&format!("{}:{}", safe, pass))
}
pub fn mime_of<S: Into<String>>(s: S) -> String {
let s = s.into();
match &s[..] {
"json" => "application/json",
"form" => "application/x-www-form-urlencoded",
_ => match get_mime_type_str(&s) {
Some(mime) => mime,
None => "foo",
},
}.to_string()
}

286
src/util/serde_macros.rs Normal file
View File

@@ -0,0 +1,286 @@
// this file is borrowed in its entirety until macro_reexport stabilizes.
// https://github.com/rust-lang/rust/issues/29638
// Copyright 2017 Serde Developers
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
/// Construct a `serde_json::Value` from a JSON literal.
///
/// ```rust
/// # #[macro_use]
/// # extern crate serde_json;
/// #
/// # fn main() {
/// let value = json!({
/// "code": 200,
/// "success": true,
/// "payload": {
/// "features": [
/// "serde",
/// "json"
/// ]
/// }
/// });
/// # }
/// ```
///
/// Variables or expressions can be interpolated into the JSON literal. Any type
/// interpolated into an array element or object value must implement Serde's
/// `Serialize` trait, while any type interpolated into a object key must
/// implement `Into<String>`. If the `Serialize` implementation of the
/// interpolated type decides to fail, or if the interpolated type contains a
/// map with non-string keys, the `json!` macro will panic.
///
/// ```rust
/// # #[macro_use]
/// # extern crate serde_json;
/// #
/// # fn main() {
/// let code = 200;
/// let features = vec!["serde", "json"];
///
/// let value = json!({
/// "code": code,
/// "success": code == 200,
/// "payload": {
/// features[0]: features[1]
/// }
/// });
/// # }
/// ```
///
/// Trailing commas are allowed inside both arrays and objects.
///
/// ```rust
/// # #[macro_use]
/// # extern crate serde_json;
/// #
/// # fn main() {
/// let value = json!([
/// "notice",
/// "the",
/// "trailing",
/// "comma -->",
/// ]);
/// # }
/// ```
#[macro_export]
macro_rules! json {
// Hide distracting implementation details from the generated rustdoc.
($($json:tt)+) => {
json_internal!($($json)+)
};
}
// Rocket relies on this because they export their own `json!` with a different
// doc comment than ours, and various Rust bugs prevent them from calling our
// `json!` from their `json!` so they call `json_internal!` directly. Check with
// @SergioBenitez before making breaking changes to this macro.
//
// Changes are fine as long as `json_internal!` does not call any new helper
// macros and can still be invoked as `json_internal!($($json)+)`.
#[macro_export]
#[doc(hidden)]
macro_rules! json_internal {
//////////////////////////////////////////////////////////////////////////
// TT muncher for parsing the inside of an array [...]. Produces a vec![...]
// of the elements.
//
// Must be invoked as: json_internal!(@array [] $($tt)*)
//////////////////////////////////////////////////////////////////////////
// Done with trailing comma.
(@array [$($elems:expr,)*]) => {
vec![$($elems,)*]
};
// Done without trailing comma.
(@array [$($elems:expr),*]) => {
vec![$($elems),*]
};
// Next element is `null`.
(@array [$($elems:expr,)*] null $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(null)] $($rest)*)
};
// Next element is `true`.
(@array [$($elems:expr,)*] true $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(true)] $($rest)*)
};
// Next element is `false`.
(@array [$($elems:expr,)*] false $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(false)] $($rest)*)
};
// Next element is an array.
(@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!([$($array)*])] $($rest)*)
};
// Next element is a map.
(@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!({$($map)*})] $($rest)*)
};
// Next element is an expression followed by comma.
(@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!($next),] $($rest)*)
};
// Last element is an expression with no trailing comma.
(@array [$($elems:expr,)*] $last:expr) => {
json_internal!(@array [$($elems,)* json_internal!($last)])
};
// Comma after the most recent element.
(@array [$($elems:expr),*] , $($rest:tt)*) => {
json_internal!(@array [$($elems,)*] $($rest)*)
};
//////////////////////////////////////////////////////////////////////////
// TT muncher for parsing the inside of an object {...}. Each entry is
// inserted into the given map variable.
//
// Must be invoked as: json_internal!(@object $map () ($($tt)*) ($($tt)*))
//
// We require two copies of the input tokens so that we can match on one
// copy and trigger errors on the other copy.
//////////////////////////////////////////////////////////////////////////
// Done.
(@object $object:ident () () ()) => {};
// Insert the current entry followed by trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
$object.insert(($($key)+).into(), $value);
json_internal!(@object $object () ($($rest)*) ($($rest)*));
};
// Insert the last entry without trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr)) => {
$object.insert(($($key)+).into(), $value);
};
// Next value is `null`.
(@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(null)) $($rest)*);
};
// Next value is `true`.
(@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(true)) $($rest)*);
};
// Next value is `false`.
(@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(false)) $($rest)*);
};
// Next value is an array.
(@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!([$($array)*])) $($rest)*);
};
// Next value is a map.
(@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!({$($map)*})) $($rest)*);
};
// Next value is an expression followed by comma.
(@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!($value)) , $($rest)*);
};
// Last value is an expression with no trailing comma.
(@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!($value)));
};
// Missing value for last entry. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
// "unexpected end of macro invocation"
json_internal!();
};
// Missing colon and value for last entry. Trigger a reasonable error
// message.
(@object $object:ident ($($key:tt)+) () $copy:tt) => {
// "unexpected end of macro invocation"
json_internal!();
};
// Misplaced colon. Trigger a reasonable error message.
(@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `:`".
unimplemented!($colon);
};
// Found a comma inside a key. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `,`".
unimplemented!($comma);
};
// Key is fully parenthesized. This avoids clippy double_parens false
// positives because the parenthesization may be necessary here.
(@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
};
// Munch a token into the current key.
(@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
};
//////////////////////////////////////////////////////////////////////////
// The main implementation.
//
// Must be invoked as: json_internal!($($json)+)
//////////////////////////////////////////////////////////////////////////
(null) => {
$crate::Value::Null
};
(true) => {
$crate::Value::Bool(true)
};
(false) => {
$crate::Value::Bool(false)
};
([]) => {
$crate::Value::Array(vec![])
};
([ $($tt:tt)+ ]) => {
$crate::Value::Array(json_internal!(@array [] $($tt)+))
};
({}) => {
$crate::Value::Object($crate::Map::new())
};
({ $($tt:tt)+ }) => {
$crate::Value::Object({
let mut object = $crate::Map::new();
json_internal!(@object object () ($($tt)+) ($($tt)+));
object
})
};
// Any Serialize type: numbers, strings, struct literals, variables etc.
// Must be below every other rule.
($other:expr) => {
$crate::to_value(&$other).unwrap()
};
}

34
src/util/vecread.rs Normal file
View File

@@ -0,0 +1,34 @@
use std::io::Read;
use std::io::Result;
pub struct VecRead {
bytes: Vec<u8>,
index: usize,
}
impl VecRead {
pub fn new(bytes: &[u8]) -> Self {
Self::from_vec(bytes.to_owned())
}
pub fn from_vec(bytes: Vec<u8>) -> Self {
VecRead {
bytes,
index: 0,
}
}
pub fn from_str(s: &str) -> Self {
Self::new(s.as_bytes())
}
pub fn len(&self) -> usize {
self.bytes.len()
}
}
impl Read for VecRead {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
let len = buf.len().min(self.bytes.len() - self.index);
(&mut buf[0..len]).copy_from_slice(&self.bytes[self.index..self.index + len]);
self.index += len;
Ok(len)
}
}