From eb922a4e41995d495f0ff28f6e3f3786bd286f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Pereira?= Date: Mon, 11 Mar 2024 17:12:31 -0400 Subject: [PATCH] feat: added async client --- src/{ => async_bin}/client.rs | 341 +++++++++++++++------------------- src/async_bin/mod.rs | 7 + src/{ => async_bin}/source.rs | 267 ++++++++++++-------------- src/async_bin/tinify.rs | 67 +++++++ src/error.rs | 24 +-- src/lib.rs | 83 +-------- 6 files changed, 360 insertions(+), 429 deletions(-) rename src/{ => async_bin}/client.rs (54%) create mode 100644 src/async_bin/mod.rs rename src/{ => async_bin}/source.rs (55%) create mode 100644 src/async_bin/tinify.rs diff --git a/src/client.rs b/src/async_bin/client.rs similarity index 54% rename from src/client.rs rename to src/async_bin/client.rs index eed86a1..978eada 100644 --- a/src/client.rs +++ b/src/async_bin/client.rs @@ -1,55 +1,49 @@ +use crate::async_bin::source::Source; use crate::error::TinifyError; -use crate::source::Source; use std::path::Path; /// The Tinify Client. pub struct Client { - key: String, + source: Source, } impl Client { pub(crate) fn new(key: K) -> Self where - K: AsRef + Into, + K: AsRef, { - Self { - key: key.into(), + Self { + source: Source::new(None, Some(key.as_ref())), } } - fn get_source(&self) -> Source { - Source::new(None, Some(self.key.as_str())) - } - /// Choose a file to compress. /// /// # Examples /// /// ``` - /// use tinify::Tinify; - /// use tinify::TinifyError; - /// - /// fn main() -> Result<(), TinifyError> { + /// use tinify::async_bin::Tinify as AsyncTinify; + /// use tinify::error::TinifyError; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), TinifyError> { /// let key = "tinify api key"; - /// let tinify = Tinify::new().set_key(key); - /// let client = tinify.get_client()?; - /// let _ = client - /// .from_file("./unoptimized.jpg")? + /// let tinify = AsyncTinify::new().set_key(key); + /// let client = tinify.get_async_client()?; + /// client + /// .from_file("./unoptimized.jpg") + /// .await? /// .to_file("./optimized.jpg")?; - /// + /// /// Ok(()) /// } /// ``` - pub fn from_file

( - &self, - path: P, - ) -> Result + #[allow(clippy::wrong_self_convention)] + pub async fn from_file

(self, path: P) -> Result where P: AsRef, { - self - .get_source() - .from_file(path) + self.source.from_file(path).await } /// Choose a buffer to compress. @@ -57,29 +51,27 @@ impl Client { /// # Examples /// /// ``` - /// use tinify::Tinify; - /// use tinify::TinifyError; + /// use tinify::async_bin::Tinify as AsyncTinify; + /// use tinify::error::TinifyError; /// use std::fs; - /// - /// fn main() -> Result<(), TinifyError> { + /// + /// #[tokio::main] + /// async fn main() -> Result<(), TinifyError> { /// let key = "tinify api key"; - /// let tinify = Tinify::new().set_key(key); - /// let client = tinify.get_client()?; + /// let tinify = AsyncTinify::new().set_key(key); /// let bytes = fs::read("./unoptimized.jpg")?; - /// let _ = client - /// .from_buffer(&bytes)? + /// let client = tinify.get_async_client()?; + /// client + /// .from_buffer(&bytes) + /// .await? /// .to_file("./optimized.jpg")?; - /// + /// /// Ok(()) /// } /// ``` - pub fn from_buffer( - &self, - buffer: &[u8], - ) -> Result { - self - .get_source() - .from_buffer(buffer) + #[allow(clippy::wrong_self_convention)] + pub async fn from_buffer(self, buffer: &[u8]) -> Result { + self.source.from_buffer(buffer).await } /// Choose an url image to compress. @@ -87,76 +79,72 @@ impl Client { /// # Examples /// /// ``` - /// use tinify::Tinify; - /// use tinify::TinifyError; - /// - /// fn main() -> Result<(), TinifyError> { + /// use tinify::async_bin::Tinify as AsyncTinify; + /// use tinify::error::TinifyError; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), TinifyError> { /// let key = "tinify api key"; - /// let tinify = Tinify::new().set_key(key); - /// let client = tinify.get_client()?; - /// let _ = client - /// .from_url("https://tinypng.com/images/panda-happy.png")? - /// .to_file("./optimized.png")?; - /// + /// let tinify = AsyncTinify::new().set_key(key); + /// let client = tinify.get_async_client()?; + /// client + /// .from_url("https://tinypng.com/images/panda-happy.png") + /// .await? + /// .to_file("./optimized.jpg")?; + /// /// Ok(()) /// } /// ``` - pub fn from_url

( - &self, - url: P, - ) -> Result + #[allow(clippy::wrong_self_convention)] + pub async fn from_url

(self, url: P) -> Result where P: AsRef, { - self - .get_source() - .from_url(url) + self.source.from_url(url).await } } #[cfg(test)] +#[cfg(feature = "async")] mod tests { use super::*; use crate::convert::Color; + use crate::convert::Type; use crate::resize::Method; use crate::resize::Resize; - use crate::convert::Type; - use crate::TinifyError; - use reqwest::blocking::Client as ReqwestClient; use assert_matches::assert_matches; - use imagesize::size; use dotenv::dotenv; - use std::ffi::OsStr; + use imagesize::size; + use reqwest::Client as ReqwestClient; use std::env; + use std::ffi::OsStr; use std::fs; fn get_key() -> String { dotenv().ok(); - let key = match env::var("KEY") { + match env::var("KEY") { Ok(key) => key, Err(_err) => panic!("No such file or directory."), - }; - - key + } } - #[test] - fn test_invalid_key() { + #[tokio::test] + async fn test_invalid_key() { let client = Client::new("invalid"); let request = client .from_url("https://tinypng.com/images/panda-happy.png") + .await .unwrap_err(); assert_matches!(request, TinifyError::ClientError); } - #[test] - fn test_compress_from_file() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_compress_from_file() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./optimized.jpg"); let tmp_image = Path::new("./tmp_image.jpg"); - let client = Client::new(key); - let _ = client.from_file(tmp_image)?.to_file(output)?; + let _ = Client::new(key).from_file(tmp_image).await?.to_file(output); let actual = fs::metadata(tmp_image)?.len(); let expected = fs::metadata(output)?.len(); @@ -170,14 +158,13 @@ mod tests { Ok(()) } - #[test] - fn test_compress_from_buffer() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_compress_from_buffer() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./optimized.jpg"); let tmp_image = Path::new("./tmp_image.jpg"); let buffer = fs::read(tmp_image).unwrap(); - let client = Client::new(key); - let _ = client.from_buffer(&buffer)?.to_file(output)?; + let _ = Client::new(key).from_buffer(&buffer).await?.to_file(output); let actual = fs::metadata(tmp_image)?.len(); let expected = fs::metadata(output)?.len(); @@ -191,18 +178,18 @@ mod tests { Ok(()) } - #[test] - fn test_compress_from_url() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_compress_from_url() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./optimized.jpg"); let remote_image = "https://tinypng.com/images/panda-happy.png"; - let client = Client::new(key); - let _ = client.from_url(remote_image)?.to_file(output)?; + let _ = Client::new(key) + .from_url(remote_image) + .await? + .to_file(output); let expected = fs::metadata(output)?.len(); - let actual = ReqwestClient::new() - .get(remote_image) - .send()?; + let actual = ReqwestClient::new().get(remote_image).send().await?; if let Some(content_length) = actual.content_length() { assert_eq!(content_length, 30734); @@ -217,13 +204,12 @@ mod tests { Ok(()) } - #[test] - fn test_save_to_file() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_save_to_file() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./optimized.jpg"); let tmp_image = Path::new("./tmp_image.jpg"); - let client = Client::new(key); - let _ = client.from_file(tmp_image)?.to_file(output)?; + let _ = Client::new(key).from_file(tmp_image).await?.to_file(output); assert!(output.exists()); @@ -234,13 +220,13 @@ mod tests { Ok(()) } - #[test] - fn test_save_to_bufffer() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_save_to_bufffer() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./optimized.jpg"); let tmp_image = Path::new("./tmp_image.jpg"); let client = Client::new(key); - let buffer = client.from_file(tmp_image)?.to_buffer(); + let buffer = client.from_file(tmp_image).await?.to_buffer(); assert_eq!(buffer.capacity(), 102051); @@ -251,14 +237,16 @@ mod tests { Ok(()) } - #[test] - fn test_resize_scale_width() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_resize_scale_width() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./tmp_resized.jpg"); let _ = Client::new(key) - .from_file("./tmp_image.jpg".to_string())? - .resize(Resize::new(Method::SCALE, Some(400), None))? - .to_file(output)?; + .from_file("./tmp_image.jpg") + .await? + .resize(Resize::new(Method::SCALE, Some(400), None)) + .await? + .to_file(output); let (width, height) = match size(output) { Ok(dim) => (dim.width, dim.height), @@ -274,14 +262,16 @@ mod tests { Ok(()) } - #[test] - fn test_resize_scale_height() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_resize_scale_height() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./tmp_resized.jpg"); let _ = Client::new(key) - .from_file("./tmp_image.jpg".to_string())? - .resize(Resize::new(Method::SCALE, None, Some(400)))? - .to_file(output)?; + .from_file("./tmp_image.jpg") + .await? + .resize(Resize::new(Method::SCALE, None, Some(400))) + .await? + .to_file(output); let (width, height) = match size(output) { Ok(dim) => (dim.width, dim.height), @@ -297,14 +287,16 @@ mod tests { Ok(()) } - #[test] - fn test_resize_fit() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_resize_fit() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./tmp_resized.jpg"); let _ = Client::new(key) - .from_file("./tmp_image.jpg".to_string())? - .resize(Resize::new(Method::FIT, Some(400), Some(200)))? - .to_file(output)?; + .from_file("./tmp_image.jpg") + .await? + .resize(Resize::new(Method::FIT, Some(400), Some(200))) + .await? + .to_file(output); let (width, height) = match size(output) { Ok(dim) => (dim.width, dim.height), @@ -320,14 +312,16 @@ mod tests { Ok(()) } - #[test] - fn test_resize_cover() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_resize_cover() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./tmp_resized.jpg"); let _ = Client::new(key) - .from_file("./tmp_image.jpg".to_string())? - .resize(Resize::new(Method::COVER, Some(400), Some(200)))? - .to_file(output)?; + .from_file("./tmp_image.jpg") + .await? + .resize(Resize::new(Method::COVER, Some(400), Some(200))) + .await? + .to_file(output); let (width, height) = match size(output) { Ok(dim) => (dim.width, dim.height), @@ -343,14 +337,16 @@ mod tests { Ok(()) } - #[test] - fn test_resize_thumb() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_resize_thumb() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./tmp_resized.jpg"); let _ = Client::new(key) - .from_file("./tmp_image.jpg".to_string())? - .resize(Resize::new(Method::THUMB, Some(400), Some(200)))? - .to_file(output)?; + .from_file("./tmp_image.jpg") + .await? + .resize(Resize::new(Method::THUMB, Some(400), Some(200))) + .await? + .to_file(output); let (width, height) = match size(output) { Ok(dim) => (dim.width, dim.height), @@ -366,42 +362,33 @@ mod tests { Ok(()) } - #[test] - fn test_error_transparent_png_to_jpeg() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_error_transparent_png_to_jpeg() -> Result<(), TinifyError> { let key = get_key(); let request = Client::new(key) - .from_url("https://tinypng.com/images/panda-happy.png")? - .convert(( - Some(Type::JPEG), - None, - None, - ), - None, - ) - .unwrap_err(); + .from_url("https://tinypng.com/images/panda-happy.png") + .await? + .convert((Some(Type::JPEG), None, None), None) + .await + .unwrap_err(); assert_matches!(request, TinifyError::ClientError { .. }); Ok(()) } - #[test] - fn test_transparent_png_to_jpeg() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_transparent_png_to_jpeg() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./panda-sticker.jpg"); let _ = Client::new(key) - .from_url("https://tinypng.com/images/panda-happy.png")? - .convert(( - Some(Type::JPEG), - None, - None, - ), - Some(Color("#000000")), - )? + .from_url("https://tinypng.com/images/panda-happy.png") + .await? + .convert((Some(Type::JPEG), None, None), Some(Color("#000000"))) + .await? .to_file(output); - let extension = - output.extension().and_then(OsStr::to_str).unwrap(); + let extension = output.extension().and_then(OsStr::to_str).unwrap(); assert_eq!(extension, "jpg"); @@ -412,23 +399,18 @@ mod tests { Ok(()) } - #[test] - fn test_convert_from_jpg_to_png() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_convert_from_jpg_to_png() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./tmp_converted.png"); let _ = Client::new(key) - .from_file(Path::new("./tmp_image.jpg"))? - .convert(( - Some(Type::PNG), - None, - None, - ), - None, - )? + .from_file(Path::new("./tmp_image.jpg")) + .await? + .convert((Some(Type::PNG), None, None), None) + .await? .to_file(output); - let extension = - output.extension().and_then(OsStr::to_str).unwrap(); + let extension = output.extension().and_then(OsStr::to_str).unwrap(); assert_eq!(extension, "png"); @@ -439,23 +421,18 @@ mod tests { Ok(()) } - #[test] - fn test_convert_from_jpg_to_webp() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_convert_from_jpg_to_webp() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./panda-sticker.webp"); let _ = Client::new(key) - .from_url("https://tinypng.com/images/panda-happy.png")? - .convert(( - Some(Type::WEBP), - None, - None, - ), - None, - )? + .from_url("https://tinypng.com/images/panda-happy.png") + .await? + .convert((Some(Type::WEBP), None, None), None) + .await? .to_file(output); - let extension = - output.extension().and_then(OsStr::to_str).unwrap(); + let extension = output.extension().and_then(OsStr::to_str).unwrap(); assert_eq!(extension, "webp"); @@ -466,23 +443,18 @@ mod tests { Ok(()) } - #[test] - fn test_convert_smallest_type() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_convert_smallest_type() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./panda-sticker.webp"); let _ = Client::new(key) - .from_url("https://tinypng.com/images/panda-happy.png")? - .convert(( - Some(Type::PNG), - Some(Type::WEBP), - Some(Type::JPEG), - ), - None, - )? + .from_url("https://tinypng.com/images/panda-happy.png") + .await? + .convert((Some(Type::PNG), Some(Type::WEBP), Some(Type::JPEG)), None) + .await? .to_file(output); - let extension = - output.extension().and_then(OsStr::to_str).unwrap(); + let extension = output.extension().and_then(OsStr::to_str).unwrap(); assert_eq!(extension, "webp"); @@ -493,23 +465,18 @@ mod tests { Ok(()) } - #[test] - fn test_convert_smallest_wildcard_type() -> Result<(), TinifyError> { + #[tokio::test] + async fn test_convert_smallest_wildcard_type() -> Result<(), TinifyError> { let key = get_key(); let output = Path::new("./panda-sticker.webp"); let _ = Client::new(key) - .from_url("https://tinypng.com/images/panda-happy.png")? - .convert(( - Some(Type::WILDCARD), - None, - None, - ), - None, - )? + .from_url("https://tinypng.com/images/panda-happy.png") + .await? + .convert((Some(Type::WILDCARD), None, None), None) + .await? .to_file(output); - let extension = - output.extension().and_then(OsStr::to_str).unwrap(); + let extension = output.extension().and_then(OsStr::to_str).unwrap(); assert_eq!(extension, "webp"); diff --git a/src/async_bin/mod.rs b/src/async_bin/mod.rs new file mode 100644 index 0000000..eb57fbb --- /dev/null +++ b/src/async_bin/mod.rs @@ -0,0 +1,7 @@ +mod client; +mod source; +mod tinify; + +pub use self::client::Client; +pub use self::source::Source; +pub use self::tinify::Tinify; diff --git a/src/source.rs b/src/async_bin/source.rs similarity index 55% rename from src/source.rs rename to src/async_bin/source.rs index e6f3c0d..4bd7607 100644 --- a/src/source.rs +++ b/src/async_bin/source.rs @@ -1,213 +1,191 @@ -use crate::resize; -use crate::convert; use crate::convert::Color; use crate::convert::Convert; +use crate::convert::JsonData; use crate::convert::Transform; use crate::error::TinifyError; -use reqwest::blocking::Client as ReqwestClient; -use reqwest::blocking::Response; +use crate::resize; +use crate::API_ENDPOINT; use reqwest::header::HeaderValue; use reqwest::header::CONTENT_TYPE; -use reqwest::StatusCode; +use reqwest::Client as ReqwestClient; use reqwest::Method; -use std::time::Duration; +use reqwest::Response; +use reqwest::StatusCode; +use std::fs::File; use std::io::BufReader; use std::io::BufWriter; use std::io::Read; use std::io::Write; use std::path::Path; -use std::fs::File; use std::str; - -const API_ENDPOINT: &str = "https://api.tinify.com"; +use std::time::Duration; #[derive(Debug)] pub struct Source { url: Option, key: Option, buffer: Option>, + request_client: ReqwestClient, } impl Source { - pub(crate) fn new( - url: Option<&str>, - key: Option<&str>, - ) -> Self { + pub(crate) fn new(url: Option<&str>, key: Option<&str>) -> Self { let url = url.map(|val| val.into()); let key = key.map(|val| val.into()); + let request_client = ReqwestClient::new(); Self { url, key, buffer: None, + request_client, } } - fn request( + pub(crate) async fn request( &self, - method: Method, url: U, + method: Method, buffer: Option<&[u8]>, ) -> Result where U: AsRef, { - let full_url = - format!("{}{}", API_ENDPOINT, url.as_ref()); - let reqwest_client = ReqwestClient::new(); + let full_url = format!("{}{}", API_ENDPOINT, url.as_ref()); let response = match method { Method::POST => { - reqwest_client + self + .request_client .post(full_url) .body(buffer.unwrap().to_owned()) .basic_auth("api", self.key.as_ref()) .timeout(Duration::from_secs(300)) - .send()? - }, + .send() + .await? + } Method::GET => { - reqwest_client + self + .request_client .get(url.as_ref()) .timeout(Duration::from_secs(300)) - .send()? - }, + .send() + .await? + } _ => unreachable!(), }; match response.status() { - StatusCode::UNAUTHORIZED => { - return Err(TinifyError::ClientError); - }, - StatusCode::UNSUPPORTED_MEDIA_TYPE => { - return Err(TinifyError::ClientError); - }, - StatusCode::SERVICE_UNAVAILABLE => { - return Err(TinifyError::ServerError); - }, - _ => {}, - }; - - Ok(response) + StatusCode::UNAUTHORIZED => Err(TinifyError::ClientError), + StatusCode::UNSUPPORTED_MEDIA_TYPE => Err(TinifyError::ClientError), + StatusCode::SERVICE_UNAVAILABLE => Err(TinifyError::ServerError), + _ => Ok(response), + } } - pub(crate) fn from_file

( - self, - path: P, - ) -> Result + #[allow(clippy::wrong_self_convention)] + pub(crate) async fn from_file

(self, path: P) -> Result where P: AsRef, { - let file = File::open(path) - .map_err(|source| TinifyError::ReadError { source })?; + let file = + File::open(path).map_err(|source| TinifyError::ReadError { source })?; let mut reader = BufReader::new(file); let mut buffer = Vec::with_capacity(reader.capacity()); reader.read_to_end(&mut buffer)?; - self.from_buffer(&buffer) + self.from_buffer(&buffer).await } - pub(crate) fn from_buffer( + #[allow(clippy::wrong_self_convention)] + pub(crate) async fn from_buffer( self, buffer: &[u8], ) -> Result { - let response = - self.request(Method::POST, "/shrink", Some(buffer))?; + let response = self.request("/shrink", Method::POST, Some(buffer)).await?; - self.get_source_from_response(response) + self.get_source_from_response(response).await } - pub(crate) fn from_url( - self, - url: U, - ) -> Result + #[allow(clippy::wrong_self_convention)] + pub(crate) async fn from_url(self, url: U) -> Result where U: AsRef, { - let get_request = self.request(Method::GET, url, None); - let buffer = get_request?.bytes()?; + let get_request = self.request(url, Method::GET, None).await?; + let buffer = get_request.bytes().await?; let post_request = - self.request(Method::POST, "/shrink", Some(&buffer))?; + self.request("/shrink", Method::POST, Some(&buffer)).await?; - self.get_source_from_response(post_request) + self.get_source_from_response(post_request).await } - + /// Resize the current compressed image. /// /// # Examples /// /// ``` - /// use tinify::Tinify; - /// use tinify::Client; - /// use tinify::TinifyError; + /// use tinify::async_bin::Tinify as AsyncTinify; + /// use tinify::error::TinifyError; /// use tinify::resize::Method; /// use tinify::resize::Resize; - /// - /// fn get_client() -> Result { - /// let key = "tinify api key"; - /// let tinify = Tinify::new(); /// - /// tinify - /// .set_key(key) - /// .get_client() - /// } - /// - /// fn main() -> Result<(), TinifyError> { - /// let client = get_client()?; - /// let _ = client - /// .from_file("./unoptimized.jpg")? + /// #[tokio::main] + /// async fn main() -> Result<(), TinifyError> { + /// let key = "l96rSTt3HV242TQWyG5DhRwfLRJzkrBg"; + /// let tinify = AsyncTinify::new().set_key(key); + /// let client = tinify.get_async_client()?; + /// client + /// .from_file("./unoptimized.jpg") + /// .await? /// .resize(Resize::new( /// Method::FIT, /// Some(400), - /// Some(200)), - /// )? - /// .to_file("./resized.jpg")?; + /// Some(200), + /// )) + /// .await? + /// .to_file("./optimized.jpg")?; /// /// Ok(()) /// } /// ``` - pub fn resize( + pub async fn resize( self, resize: resize::Resize, ) -> Result { let json_data = resize::JsonData::new(resize); - let mut json_string = - serde_json::to_string(&json_data).unwrap(); + let mut json_string = serde_json::to_string(&json_data).unwrap(); let width = json_data.resize.width; let height = json_data.resize.height; json_string = match ( (width.is_some(), height.is_none()), (height.is_some(), width.is_none()), ) { - ((true, true), (_, _)) => - json_string.replace(",\"height\":null", ""), - ((_, _), (true, true)) => - json_string.replace(",\"width\":null", ""), + ((true, true), (_, _)) => json_string.replace(",\"height\":null", ""), + ((_, _), (true, true)) => json_string.replace(",\"width\":null", ""), _ => json_string, }; - let reqwest_client = ReqwestClient::new(); - let response = reqwest_client + let response = self + .request_client .post(self.url.as_ref().unwrap()) - .header( - CONTENT_TYPE, - HeaderValue::from_static("application/json"), - ) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) .body(json_string) .basic_auth("api", self.key.as_ref()) .timeout(Duration::from_secs(300)) - .send()?; + .send() + .await?; - if response.status() == StatusCode::BAD_REQUEST { - return Err(TinifyError::ClientError); + match response.status() { + StatusCode::BAD_REQUEST => Err(TinifyError::ClientError), + _ => self.get_source_from_response(response).await, } - - self.get_source_from_response(response) } /// The following options are available as a type: /// One image type, specified as a string `"image/webp"` - /// + /// /// Multiple image types, specified as a tuple (`"image/webp"`, `"image/png"`). /// The smallest of the provided image types will be returned. - /// + /// /// The transform object specifies the stylistic transformations /// that will be applied to the image. /// @@ -215,45 +193,42 @@ impl Source { /// /// Specify a background color to convert an image with a transparent background /// to an image type which does not support transparency (like JPEG). - /// + /// /// # Examples /// /// ``` - /// use tinify::Tinify; + /// use tinify::async_bin::Tinify as AsyncTinify; + /// use tinify::error::TinifyError; /// use tinify::convert::Color; /// use tinify::convert::Type; - /// use tinify::TinifyError; /// - /// fn main() -> Result<(), TinifyError> { - /// let _ = Tinify::new() - /// .set_key("api key") - /// .get_client()? - /// .from_url("https://tinypng.com/images/panda-happy.png")? - /// .convert(( - /// Some(Type::JPEG), - /// None, - /// None, - /// ), + /// #[tokio::main] + /// async fn main() -> Result<(), TinifyError> { + /// let key = "l96rSTt3HV242TQWyG5DhRwfLRJzkrBg"; + /// let tinify = AsyncTinify::new().set_key(key); + /// let client = tinify.get_async_client()?; + /// client + /// .from_url("https://tinypng.com/images/panda-happy.png") + /// .await? + /// .convert( + /// (Some(Type::JPEG), None, None), /// Some(Color("#FF5733")), - /// )? - /// .to_file("./converted.webp"); + /// ) + /// .await? + /// .to_file("./optimized.jpg")?; /// /// Ok(()) /// } /// ``` - pub fn convert( + pub async fn convert( self, convert_type: (Option, Option, Option), transform: Option, ) -> Result where - T: AsRef + Into + Copy, + T: Into + Copy, { - let types = &[ - &convert_type.0, - &convert_type.1, - &convert_type.2, - ]; + let types = &[&convert_type.0, &convert_type.1, &convert_type.2]; let count: Vec = types .iter() .filter_map(|&val| val.and_then(|x| Some(x.into()))) @@ -264,12 +239,9 @@ impl Source { _ => count.first().unwrap().to_string(), }; let template = if let Some(color) = transform { - convert::JsonData::new( - Convert::new(parse_type), - Some(Transform::new(color.0)), - ) + JsonData::new(Convert::new(parse_type), Some(Transform::new(color.0))) } else { - convert::JsonData::new(Convert::new(parse_type), None) + JsonData::new(Convert::new(parse_type), None) }; // Using replace to avoid invalid JSON string. @@ -280,31 +252,25 @@ impl Source { .replace("\"[", "[") .replace("]\"", "]") .replace("\\\"", "\""); - let reqwest_client = ReqwestClient::new(); - let response = reqwest_client + let response = self + .request_client .post(self.url.as_ref().unwrap()) - .header( - CONTENT_TYPE, - HeaderValue::from_static("application/json"), - ) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) .body(json_string) .basic_auth("api", self.key.as_ref()) .timeout(Duration::from_secs(300)) - .send()?; + .send() + .await?; if response.status() == StatusCode::BAD_REQUEST { return Err(TinifyError::ClientError); } - - self.get_source_from_response(response) + self.get_source_from_response(response).await } - + /// Save the compressed image to a file. - pub fn to_file

( - &self, - path: P, - ) -> Result<(), TinifyError> + pub fn to_file

(&self, path: P) -> Result<(), TinifyError> where P: AsRef, { @@ -316,13 +282,13 @@ impl Source { Ok(()) } - + /// Convert the compressed image to a buffer. pub fn to_buffer(&self) -> Vec { self.buffer.as_ref().unwrap().to_vec() } - fn get_source_from_response( + pub(crate) async fn get_source_from_response( mut self, response: Response, ) -> Result { @@ -330,35 +296,34 @@ impl Source { let mut url = String::new(); if !location.is_empty() { - let slice = - str::from_utf8(location.as_bytes()).unwrap(); + let slice = str::from_utf8(location.as_bytes()).unwrap(); url.push_str(slice); } - - let get_request = self.request(Method::GET, &url, None); - let bytes = get_request?.bytes()?.to_vec(); + + let get_request = self.request(&url, Method::GET, None).await?; + let bytes = get_request.bytes().await?.to_vec(); self.buffer = Some(bytes); self.url = Some(url); } else { - let bytes = response.bytes()?.to_vec(); + let bytes = response.bytes().await?.to_vec(); self.buffer = Some(bytes); - self.url = None; + self.url = None; } - Ok(self) + Ok(self) } } #[cfg(test)] +#[cfg(feature = "async")] mod tests { use super::*; - use crate::TinifyError; use assert_matches::assert_matches; - #[test] - fn test_request_error() { + #[tokio::test] + async fn test_request_error() { let source = Source::new(None, None); - let request = source.request(Method::GET, "", None).unwrap_err(); + let request = source.request("", Method::GET, None).await.unwrap_err(); assert_matches!(request, TinifyError::ReqwestError { .. }); } diff --git a/src/async_bin/tinify.rs b/src/async_bin/tinify.rs new file mode 100644 index 0000000..1f50409 --- /dev/null +++ b/src/async_bin/tinify.rs @@ -0,0 +1,67 @@ +use crate::async_bin::client::Client; +use crate::error::TinifyError; + +/// Use the API to create a new client. +#[derive(Default)] +pub struct Tinify { + pub key: String, +} + +impl Tinify { + /// Create a new Tinify Object. + pub fn new() -> Self { + Self { key: String::new() } + } + + /// Set a Tinify Key. + pub fn set_key(mut self, key: K) -> Self + where + K: Into, + { + self.key = key.into(); + self + } + + /// Get a new Tinify Client. + /// + /// # Examples + /// + /// ``` + /// use tinify::async_bin::Tinify as AsyncTinify; + /// use tinify::error::TinifyError; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), TinifyError> { + /// let key = "tinify api key"; + /// let tinify = AsyncTinify::new().set_key(key); + /// let client = tinify.get_async_client()?; + /// + /// Ok(()) + /// } + /// ``` + pub fn get_async_client(&self) -> Result { + let client = Client::new(&self.key); + + Ok(client) + } +} + +#[cfg(test)] +#[cfg(feature = "async")] +mod tests { + use super::*; + use dotenv::dotenv; + use std::env; + + #[test] + fn test_get_async_client() -> Result<(), TinifyError> { + dotenv().ok(); + let key = match env::var("KEY") { + Ok(key) => key, + Err(_err) => panic!("No such file or directory."), + }; + let _ = Tinify::new().set_key(key).get_async_client()?; + + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index 84396c4..dc70014 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,11 @@ -use std::io; -use std::fmt; use std::error; +use std::fmt; +use std::io; /// The Tinify API uses HTTP status codes to indicate success or failure. -/// +/// /// Status codes in the 4xx range indicate there was a problem with `Client` request. -/// +/// /// Status codes in the 5xx indicate a temporary problem with the Tinify API `Server`. #[derive(Debug)] pub enum TinifyError { @@ -35,22 +35,18 @@ impl fmt::Display for TinifyError { match *self { TinifyError::ClientError => { write!(f, "There was a problem with the request.") - }, + } TinifyError::ServerError => { write!(f, "There is a temporary problem with the Tinify API.") - }, + } TinifyError::ReadError { .. } => { write!(f, "Read error") - }, + } TinifyError::WriteError { .. } => { write!(f, "Write error") - }, - TinifyError::IOError(ref err) => { - err.fmt(f) - }, - TinifyError::ReqwestError(ref err) => { - err.fmt(f) - }, + } + TinifyError::IOError(ref err) => err.fmt(f), + TinifyError::ReqwestError(ref err) => err.fmt(f), } } } diff --git a/src/lib.rs b/src/lib.rs index 929af0b..1fab544 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,82 +6,11 @@ //! Read more at `https://tinify.com` // --snip-- -mod error; -mod client; -mod source; -pub mod resize; +#[cfg(feature = "async")] +pub mod async_bin; pub mod convert; +pub mod error; +pub mod resize; +pub mod sync; -pub use crate::client::Client; -pub use crate::source::Source; -pub use crate::error::TinifyError; - -/// Use the API to create a new client. -#[derive(Default)] -pub struct Tinify { - pub key: String, -} - -impl Tinify { - /// Create a new Tinify Object. - pub fn new() -> Self { - Self { - key: String::new(), - } - } - - /// Set a Tinify Key. - pub fn set_key( - mut self, - key: K, - ) -> Self - where - K: AsRef + Into, - { - self.key = key.into(); - self - } - - /// Get a new Tinify Client. - /// - /// # Examples - /// - /// ``` - /// use tinify::Tinify; - /// use tinify::TinifyError; - /// - /// fn main() -> Result<(), TinifyError> { - /// let key = "tinify api key"; - /// let tinify = Tinify::new().set_key(key); - /// let client = tinify.get_client()?; - /// - /// Ok(()) - /// } - /// ``` - pub fn get_client(&self) -> Result { - let client = Client::new(self.key.as_str()); - - Ok(client) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use dotenv::dotenv; - use std::env; - - #[test] - fn test_get_client() -> Result<(), TinifyError> { - dotenv().ok(); - let key = match env::var("KEY") { - Ok(key) => key, - Err(_err) => panic!("No such file or directory."), - }; - let _ = Tinify::new() - .set_key(&key) - .get_client()?; - - Ok(()) - } -} +pub (crate) const API_ENDPOINT: &str = "https://api.tinify.com";