diff --git a/Cargo.lock b/Cargo.lock index f2a38a8..ad2cc96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,7 +635,7 @@ dependencies = [ [[package]] name = "reqwew" -version = "0.2.5" +version = "0.3.0" dependencies = [ "bytes", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index f507440..59a43b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "GPL-3.0" name = "reqwew" readme = "README.md" repository = "https://github.com/hack-ink/reqwew" -version = "0.2.5" +version = "0.3.0" [profile.ci-dev] incremental = false diff --git a/src/error.rs b/src/error.rs index fcd9309..9275cc2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,6 @@ pub enum Error { #[error(transparent)] SerdeJson(#[from] serde_json::Error), - #[error("[api] max retries exceeded after {retries} attempts")] - ExceededMaxRetries { retries: u32 }, + #[error("[reqwew] max retries exceeded after {0} attempts")] + ExceededMaxRetries(u32), } diff --git a/src/lib.rs b/src/lib.rs index c9ab9cf..1bcb95e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,55 +15,125 @@ use std::{future::Future, time::Duration}; // crates.io use bytes::Bytes; use once_cell::sync::Lazy; -use reqwest::{Body, Client as RClient, IntoUrl}; +use reqwest::{Body, Client as RClient, IntoUrl, Method as RMethod}; use serde::de::DeserializeOwned; use tokio::time; +/// HTTP methods. +#[derive(Clone, Copy, Debug)] +pub enum Method { + /// GET method. + Get, + /// POST method. + Post, + /// PUT method. + Put, + /// DELETE method. + Delete, + /// HEAD method. + Head, + /// OPTIONS method. + Options, + /// CONNECT method. + Connect, + /// PATCH method. + Patch, + /// TRACE method. + Trace, +} +impl From for RMethod { + fn from(method: Method) -> Self { + match method { + Method::Get => RMethod::GET, + Method::Post => RMethod::POST, + Method::Put => RMethod::PUT, + Method::Delete => RMethod::DELETE, + Method::Head => RMethod::HEAD, + Method::Options => RMethod::OPTIONS, + Method::Connect => RMethod::CONNECT, + Method::Patch => RMethod::PATCH, + Method::Trace => RMethod::TRACE, + } + } +} + /// Basic HTTP client functionality. pub trait Http where Self: Send + Sync, { - /// Perform a GET request. - fn get(&self, uri: U) -> impl Future> + Send + /// Perform a generic request. + fn request( + &self, + uri: U, + method: Method, + body: Option, + ) -> impl Future> + Send where - U: Send + IntoUrl; + U: Send + IntoUrl, + B: Send + Into; - /// Perform a GET request with retries. - fn get_with_retries( + /// Perform a generic request with retries. + fn request_with_retries( &self, uri: U, + method: Method, + body: Option, retries: u32, retry_delay_ms: u64, ) -> impl Future> + Send where U: Send + IntoUrl, + B: Send + Clone + Into, { async move { let u = uri.as_str(); + tracing::debug!("{method:?} {u}"); + for i in 1..=retries { - match self.get(u).await { + match self.request(u, method, body.clone()).await { Ok(r) => return Ok(r), Err(e) => { - tracing::error!( - "attempt {i}/{retries} failed for {u}: {e:?}, \ - retrying in {retry_delay_ms}ms" - ); + tracing::error!("attempt {i}/{retries} failed for {u}: {e:?}, retrying in {retry_delay_ms}ms"); time::sleep(Duration::from_millis(retry_delay_ms)).await; }, } } - Err(Error::ExceededMaxRetries { retries })? + Err(Error::ExceededMaxRetries(retries))? } } + /// Perform a GET request. + fn get(&self, uri: U) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request(uri, Method::Get, None::<&[u8]>) + } + + /// Perform a GET request with retries. + fn get_with_retries( + &self, + uri: U, + retries: u32, + retry_delay_ms: u64, + ) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request_with_retries(uri, Method::Get, None::<&[u8]>, retries, retry_delay_ms) + } + /// Perform a POST request. fn post(&self, uri: U, body: B) -> impl Future> + Send where U: Send + IntoUrl, - B: Send + Into; + B: Send + Into, + { + self.request(uri, Method::Post, Some(body)) + } /// Perform a POST request with retries. fn post_with_retries( @@ -77,34 +147,113 @@ where U: Send + IntoUrl, B: Send + Clone + Into, { - async move { - let u = uri.as_str(); - - for i in 1..=retries { - match self.post(u, body.clone()).await { - Ok(r) => return Ok(r), - Err(e) => { - tracing::error!( - "attempt {i}/{retries} failed for {u}: {e:?}, \ - retrying in {retry_delay_ms}ms" - ); - time::sleep(Duration::from_millis(retry_delay_ms)).await; - }, - } - } - - Err(Error::ExceededMaxRetries { retries })? - } + self.request_with_retries(uri, Method::Post, Some(body), retries, retry_delay_ms) } /// Perform a PUT request. fn put(&self, uri: U, body: B) -> impl Future> + Send where U: Send + IntoUrl, - B: Send + Into; + B: Send + Into, + { + self.request(uri, Method::Put, Some(body)) + } + + /// Perform a DELETE request. + fn delete(&self, uri: U) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request(uri, Method::Delete, None::<&[u8]>) + } + + /// Perform a DELETE request with retries. + fn delete_with_retries( + &self, + uri: U, + retries: u32, + retry_delay_ms: u64, + ) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request_with_retries(uri, Method::Delete, None::<&[u8]>, retries, retry_delay_ms) + } + + /// Perform a HEAD request. + fn head(&self, uri: U) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request(uri, Method::Head, None::<&[u8]>) + } + + /// Perform a HEAD request with retries. + fn head_with_retries( + &self, + uri: U, + retries: u32, + retry_delay_ms: u64, + ) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request_with_retries(uri, Method::Head, None::<&[u8]>, retries, retry_delay_ms) + } - /// Perform a PUT request with retries. - fn put_with_retries( + /// Perform an OPTIONS request. + fn options(&self, uri: U) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request(uri, Method::Options, None::<&[u8]>) + } + + /// Perform an OPTIONS request with retries. + fn options_with_retries( + &self, + uri: U, + retries: u32, + retry_delay_ms: u64, + ) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request_with_retries(uri, Method::Options, None::<&[u8]>, retries, retry_delay_ms) + } + + /// Perform a CONNECT request. + fn connect(&self, uri: U) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request(uri, Method::Connect, None::<&[u8]>) + } + + /// Perform a CONNECT request with retries. + fn connect_with_retries( + &self, + uri: U, + retries: u32, + retry_delay_ms: u64, + ) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request_with_retries(uri, Method::Connect, None::<&[u8]>, retries, retry_delay_ms) + } + + /// Perform a PATCH request. + fn patch(&self, uri: U, body: B) -> impl Future> + Send + where + U: Send + IntoUrl, + B: Send + Into, + { + self.request(uri, Method::Patch, Some(body)) + } + + /// Perform a PATCH request with retries. + fn patch_with_retries( &self, uri: U, body: B, @@ -115,24 +264,28 @@ where U: Send + IntoUrl, B: Send + Clone + Into, { - async move { - let u = uri.as_str(); + self.request_with_retries(uri, Method::Patch, Some(body), retries, retry_delay_ms) + } - for i in 1..=retries { - match self.put(u, body.clone()).await { - Ok(r) => return Ok(r), - Err(e) => { - tracing::error!( - "attempt {i}/{retries} failed for {u}: {e:?}, \ - retrying in {retry_delay_ms}ms" - ); - time::sleep(Duration::from_millis(retry_delay_ms)).await; - }, - } - } + /// Perform a TRACE request. + fn trace(&self, uri: U) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request(uri, Method::Trace, None::<&[u8]>) + } - Err(Error::ExceededMaxRetries { retries })? - } + /// Perform a TRACE request with retries. + fn trace_with_retries( + &self, + uri: U, + retries: u32, + retry_delay_ms: u64, + ) -> impl Future> + Send + where + U: Send + IntoUrl, + { + self.request_with_retries(uri, Method::Trace, None::<&[u8]>, retries, retry_delay_ms) } } @@ -182,39 +335,27 @@ impl From<&RClient> for Client { } } impl Http for Client { - fn get(&self, uri: U) -> impl Future> + Send - where - U: Send + IntoUrl, - { - let u = uri.as_str(); - - tracing::debug!("GET {u}"); - - async move { Ok(self.0.get(uri).send().await?.bytes().await?) } - } - - fn post(&self, uri: U, body: B) -> impl Future> + Send - where - U: Send + IntoUrl, - B: Send + Into, - { - let u = uri.as_str(); - - tracing::debug!("POST {u}"); - - async move { Ok(self.0.post(uri).body(body).send().await?.bytes().await?) } - } - - fn put(&self, uri: U, body: B) -> impl Future> + Send + fn request( + &self, + uri: U, + method: Method, + body: Option, + ) -> impl Future> + Send where U: Send + IntoUrl, B: Send + Into, { let u = uri.as_str(); - tracing::debug!("PUT {u}"); + tracing::debug!("{method:?} {u}"); - async move { Ok(self.0.put(uri).body(body).send().await?.bytes().await?) } + async move { + Ok(if let Some(body) = body { + self.0.request(method.into(), uri).body(body).send().await?.bytes().await? + } else { + self.0.request(method.into(), uri).send().await?.bytes().await? + }) + } } } @@ -224,7 +365,9 @@ impl Http for Client { /// /// # Example /// ```rust -/// pub static CLIENT: Client = reqwew::static(|| reqwest::Client::new()); +/// use reqwew::{once_cell::sync::Lazy, Client}; +/// +/// pub static CLIENT: Lazy = reqwew::lazy(|| Client::default()); /// ``` pub const fn lazy(f: F) -> Lazy { Lazy::new(f) diff --git a/src/test.rs b/src/test.rs index 5fac3d3..33809a9 100644 --- a/src/test.rs +++ b/src/test.rs @@ -7,7 +7,7 @@ static CLIENT: Lazy = lazy(Default::default); #[tokio::test] async fn http_and_response_should_work() { - let response = CLIENT.get_with_reties("https://httpbin.org/get", 3, 500).await.unwrap(); + let response = CLIENT.get_with_retries("https://httpbin.org/get", 3, 500).await.unwrap(); assert!(response.clone().text().contains("httpbin.org")); assert_eq!( @@ -15,8 +15,7 @@ async fn http_and_response_should_work() { "httpbin.org" ); - let response = - CLIENT.post_with_retries("https://httpbin.org/post", "hello", 3, 500).await.unwrap(); + let response = CLIENT.post_with_retries("https://httpbin.org/post", "hello", 3, 500).await.unwrap(); assert!(response.clone().text().contains("https://httpbin.org/post")); assert_eq!(