about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2019-02-26T10·20+0100
committerVincent Ambo <mail@tazj.in>2019-02-26T10·20+0100
commitb28f6e748d911b23a6c5c2c6badb33e079989517 (patch)
tree87762ec6a974d4300712ad47e4bcd55fc164f50f
parente95a82ca4ccf3d217fe4b4566b5b41498acd7fd0 (diff)
feat: Initial check-in of working client layer
-rw-r--r--.gitignore3
-rw-r--r--Cargo.toml7
-rw-r--r--README.org9
-rw-r--r--src/lib.rs162
4 files changed, 181 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000000..693699042b1a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target
+**/*.rs.bk
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 000000000000..076a3ff1962a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "crimp"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+
+[dependencies]
+curl = "0.4"
diff --git a/README.org b/README.org
new file mode 100644
index 000000000000..73e371512415
--- /dev/null
+++ b/README.org
@@ -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
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 000000000000..2280f9686b1e
--- /dev/null
+++ b/src/lib.rs
@@ -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,
+        })
+    }
+}