feat: Initial check-in of working client layer
This commit is contained in:
		
							parent
							
								
									e95a82ca4c
								
							
						
					
					
						commit
						b28f6e748d
					
				
					 4 changed files with 181 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
/target
 | 
			
		||||
**/*.rs.bk
 | 
			
		||||
Cargo.lock
 | 
			
		||||
							
								
								
									
										7
									
								
								Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "crimp"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
authors = ["Vincent Ambo <mail@tazj.in>"]
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
curl = "0.4"
 | 
			
		||||
							
								
								
									
										9
									
								
								README.org
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								README.org
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
crimp
 | 
			
		||||
=====
 | 
			
		||||
 | 
			
		||||
Crimp is an HTTP client interface on top of the [Rust bindings][] to
 | 
			
		||||
cURL.
 | 
			
		||||
 | 
			
		||||
Please see the module documentation for details on why this exists.
 | 
			
		||||
 | 
			
		||||
[Rust bindings]: https://docs.rs/curl
 | 
			
		||||
							
								
								
									
										162
									
								
								src/lib.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/lib.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,162 @@
 | 
			
		|||
//! # 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;
 | 
			
		||||
 | 
			
		||||
use curl::easy::{Easy, List, ReadError};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::io::Write;
 | 
			
		||||
use std::string::{FromUtf8Error, ToString};
 | 
			
		||||
 | 
			
		||||
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: Option<&'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: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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: &str, body: &'a [u8])
 | 
			
		||||
                    -> CurlResult<&mut Self> {
 | 
			
		||||
        self.header("Content-Type", content_type)?;
 | 
			
		||||
        self.body = Some(body);
 | 
			
		||||
 | 
			
		||||
        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![];
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            // 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:
 | 
			
		||||
            if let Some(body) = self.body {
 | 
			
		||||
                transfer.read_function(move |mut into| {
 | 
			
		||||
                    into.write_all(body)
 | 
			
		||||
                        .map(|_| body.len())
 | 
			
		||||
                        .map_err(|_| ReadError::Abort)
 | 
			
		||||
                })?;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 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,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue