From ebb2eb6c7ae4446d9193c9fced8e60df699085d8 Mon Sep 17 00:00:00 2001 From: Vladimir Burdukov Date: Sat, 16 Nov 2019 22:48:09 +0300 Subject: [PATCH] Sync implementation --- .gitignore | 3 ++ Cargo.toml | 17 +++++++++ src/lib.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++ tests/get.rs | 50 ++++++++++++++++++++++++ tests/http_error.rs | 20 ++++++++++ 5 files changed, 182 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 tests/get.rs create mode 100644 tests/http_error.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /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 0000000..f82660c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "http_client" +version = "0.1.0" +authors = ["Vladimir Burdukov "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.3" + +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" + +curl = "0.4.20" +url = "1.7.2" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8cc9e49 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,92 @@ +use curl::easy::Easy; +use serde::de::DeserializeOwned; +use serde_json; +use std::borrow::Borrow; +use url::{ParseError, Url}; + +#[derive(Debug)] +pub enum Error { + HttpError(u32), + CurlError(curl::Error), + ParseError(serde_json::Error), +} + +impl From for Error { + fn from(error: curl::Error) -> Error { + Error::CurlError(error) + } +} + +impl From for Error { + fn from(error: serde_json::Error) -> Error { + Error::ParseError(error) + } +} + +pub struct HttpClient { + base_url: Url, +} + +impl HttpClient { + // Public API + + pub fn new(base_url: &str) -> Result { + let base_url = Url::parse(base_url)?; + Ok(HttpClient { base_url }) + } + + pub fn get(&self, path: &str) -> Result { + self.do_get(self.prepare_url_with_path(path)) + } + + pub fn get_with_params(&self, path: &str, iter: I) -> Result + where + T: DeserializeOwned, + I: IntoIterator, + I::Item: Borrow<(K, V)>, + K: AsRef, + V: AsRef, + { + let mut url = self.prepare_url_with_path(path); + url.query_pairs_mut().extend_pairs(iter); + self.do_get(url) + } + + // Private API + + fn prepare_url_with_path(&self, path: &str) -> Url { + let mut url = self.base_url.clone(); + url.set_path(path); + url + } + + fn do_get(&self, url: Url) -> Result { + let mut response = Vec::new(); + let mut easy = Easy::new(); + easy.url(url.as_str()).unwrap(); + + { + let mut transfer = easy.transfer(); + transfer + .write_function(|data| { + response.extend_from_slice(data); + Ok(data.len()) + }) + .unwrap(); + transfer.perform().unwrap(); + } + + let code = easy.response_code().unwrap(); + + if code >= 200 && code < 300 { + let response: T = serde_json::from_slice(&response) + .map_err(|err| Error::from(err)) + .unwrap(); + + Ok(response) + } else { + eprintln!("{}", String::from_utf8_lossy(&response)); + Err(Error::HttpError(code)) + } + } +} diff --git a/tests/get.rs b/tests/get.rs new file mode 100644 index 0000000..44a7b1b --- /dev/null +++ b/tests/get.rs @@ -0,0 +1,50 @@ +use http_client::HttpClient; +use serde_derive::Deserialize; + +#[test] +fn test_get() { + #[derive(Deserialize)] + struct Response { + url: String, + } + + let http_client = HttpClient::new("https://httpbin.org/").unwrap(); + + assert_eq!( + http_client.get::("/get").unwrap().url, + "https://httpbin.org/get" + ); +} + +#[test] +fn test_get_with_params() { + use std::collections::HashMap; + + #[derive(Deserialize)] + struct Response { + url: String, + args: HashMap, + } + + let http_client = HttpClient::new("https://httpbin.org/").unwrap(); + + let params = vec![("key1", "value1"), ("key2", "value2")]; + let response = http_client + .get_with_params::("/get", params) + .unwrap(); + + assert_eq!( + response.url, + "https://httpbin.org/get?key1=value1&key2=value2" + ); + assert_eq!( + response.args, + [ + ("key1".to_owned(), "value1".to_owned()), + ("key2".to_owned(), "value2".to_owned()) + ] + .iter() + .cloned() + .collect() + ) +} diff --git a/tests/http_error.rs b/tests/http_error.rs new file mode 100644 index 0000000..e4e658b --- /dev/null +++ b/tests/http_error.rs @@ -0,0 +1,20 @@ +use http_client::{Error, HttpClient}; +use serde_derive::Deserialize; + +#[test] +fn test_404() { + #[derive(Debug, Deserialize)] + struct Response; + + let http_client = HttpClient::new("https://httpbin.org/").unwrap(); + + match http_client.get::("/status/404").unwrap_err() { + Error::HttpError(404) => (), + error => panic!( + r#"assertion failed: +expected: `Error::HttpError(404)` + got: `{:?}`"#, + error + ), + } +}