about summary refs log blame commit diff
path: root/src/lib.rs
blob: 417eebfd005bc1b2518b6ad33c7e8d5b48e5079f (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14













                                                                    

                        

                                        

                                



                                           


            






                                            


                        









                              

























                                                               
                               
















                                                                         



                                                                                   
 




                                                                       









                                                                 










                                                                                          






                                                                          









                                                                                    
                                                      




                                                     























































                                                                            










                                                                                             
 
//! # crimp
//!
//! This library provides a simplified API over the [cURL Rust
//! bindings][] that resemble that of higher-level libraries such as
//! [reqwest][].
//!
//! `crimp` is intended to be used in situations where HTTP client
//! functionality is desired without adding a significant number of
//! dependencies or sacrificing too much usability.
//!
//! [cURL Rust bindings]: https://docs.rs/curl
//! [reqwest]: https://docs.rs/reqwest

extern crate curl;
extern crate serde;
extern crate serde_json;

use curl::easy::{Easy, List, ReadError};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::io::Write;
use std::string::{FromUtf8Error, ToString};

#[cfg(test)]
mod tests;

type CurlResult<T> = Result<T, curl::Error>;

/// HTTP method to use for the request.
pub enum Method {
    Get, Post, Put, Patch, Delete
}

pub struct Request<'a> {
    handle: Easy,
    headers: List,
    body: Body<'a>,
}

enum Body<'a> {
    NoBody,
    Json(Vec<u8>),
    Bytes {
        content_type: &'a str,
        data: &'a [u8],
    }
}

#[derive(Debug)]
pub struct CurlResponse<T> {
    pub status: u32,
    pub headers: HashMap<String, String>,
    pub body: T,
}

impl <'a> Request<'a> {
    /// Initiate an HTTP request with the given method and URL.
    pub fn new(method: Method, url: &str) -> CurlResult<Self> {
        let mut handle = Easy::new();
        handle.url(url)?;

        match method {
            Method::Get    => handle.get(true)?,
            Method::Post   => handle.post(true)?,
            Method::Put    => handle.put(true)?,
            Method::Patch  => handle.custom_request("PATCH")?,
            Method::Delete => handle.custom_request("DELETE")?,
        }

        Ok(Request {
            handle,
            headers: List::new(),
            body: Body::NoBody,
        })
    }

    /// Add a header to a request.
    pub fn header(&mut self, k: &str, v: &str) -> CurlResult<&mut Self> {
        self.headers.append(&format!("{}: {}", k, v))?;
        Ok(self)
    }

    /// Set the User-Agent for this request.
    pub fn user_agent(&mut self, agent: &str) -> CurlResult<&mut Self> {
        self.handle.useragent(agent)?;
        Ok(self)
    }

    /// Add a byte-array body to a request using the specified
    /// Content-Type.
    pub fn body(&'a mut self, content_type: &'a str, data: &'a [u8]) -> &mut Self {
        self.body = Body::Bytes { data, content_type };
        self
    }

    /// Add a JSON-encoded body from a serializable type.
    pub fn json<T: Serialize>(&'a mut self, body: &T)
                              -> Result<&mut Self, serde_json::Error> {
        let json = serde_json::to_vec(body)?;
        self.body = Body::Json(json);
        Ok(self)
    }

    /// Send the HTTP request and return a response structure
    /// containing the raw body.
    pub fn send(mut self) -> CurlResult<CurlResponse<Vec<u8>>> {
        // Create structures in which to store the response data:
        let mut headers = HashMap::new();
        let mut body = vec![];

        // Optionally set content type if a body payload is
        // configured.
        match self.body {
            Body::Json(..) => self.header("Content-Type", "application/json"),
            Body::Bytes { content_type, .. } => self.header("Content-Type", content_type),
            Body::NoBody => Ok(&mut self),
        }?;

        // Configure headers on the request:
        self.handle.http_headers(self.headers)?;

        {
            // Take a scoped transfer from the Easy handle. This makes it
            // possible to write data into the above local buffers without
            // fighting the borrow-checker:
            let mut transfer = self.handle.transfer();

            // Write the payload if it exists:
            match self.body {
                Body::Bytes { data, .. } => transfer.read_function(move |mut into| {
                    into.write_all(data)
                        .map(|_| data.len())
                        .map_err(|_| ReadError::Abort)
                })?,

                Body::Json(json) => transfer.read_function(move |mut into| {
                    into.write_all(&json)
                        .map(|_| json.len())
                        .map_err(|_| ReadError::Abort)
                })?,

                // Do nothing if there is no body ...
                Body::NoBody => {},
            };

            // Read one header per invocation. Request processing is
            // terminated if any header is malformed:
            transfer.header_function(|header| {
                // Headers are expected to be valid UTF-8 data. If they
                // are not, the conversion is lossy.
                //
                // Technically it is legal for HTTP requests to use
                // different encodings, but we don't interface with such
                // services for hygienic reasons.
                let header = String::from_utf8_lossy(header);
                let split = header.splitn(2, ':').collect::<Vec<_>>();

                // "Malformed" headers are skipped. In most cases this
                // will only be the HTTP version statement.
                if split.len() != 2 {
                    return true;
                }

                headers.insert(
                    split[0].trim().to_string(), split[1].trim().to_string()
                );
                true
            })?;

            // Read the body to the allocated buffer.
            transfer.write_function(|data| {
                let len = data.len();
                body.write_all(data)
                    .map(|_| len)
                    .map_err(|err| panic!("{:?}", err))
            })?;

            transfer.perform()?;
        }

        Ok(CurlResponse {
            status: self.handle.response_code()?,
            headers,
            body
        })
    }
}

impl CurlResponse<Vec<u8>> {
    /// Attempt to parse the HTTP response body as a UTF-8 encoded
    /// string.
    pub fn as_string(self) -> Result<CurlResponse<String>, FromUtf8Error> {
        let body = String::from_utf8(self.body)?;

        Ok(CurlResponse {
            body,
            status: self.status,
            headers: self.headers,
        })
    }

    /// Attempt to deserialize the HTTP response body from JSON.
    pub fn as_json<T: DeserializeOwned>(self) -> Result<CurlResponse<T>, serde_json::Error> {
        let deserialized = serde_json::from_slice(&self.body)?;

        Ok(CurlResponse {
            body: deserialized,
            status: self.status,
            headers: self.headers,
        })
    }
}