use reqwest::blocking::Client;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::error::{Error, Result};
#[derive(Clone, Debug)]
pub struct HttpClient {
inner: Arc<Mutex<Option<Client>>>,
pub base_url: String,
timeout: Duration,
}
impl HttpClient {
pub fn new(timeout: Duration, base_url: impl Into<String>) -> Self {
Self {
inner: Arc::new(Mutex::new(None)),
base_url: base_url.into(),
timeout,
}
}
pub fn standard() -> Self {
Self::new(Duration::from_secs(30), "")
}
pub fn client(&self) -> Result<Client> {
let mut guard = self
.inner
.lock()
.map_err(|_| Error::Sync("poisoned".into()))?;
if guard.is_none() {
let client = Client::builder()
.timeout(self.timeout)
.user_agent("animedb/0.1")
.build()
.map_err(|e| Error::Http(e))?;
*guard = Some(client);
}
Ok(guard.as_ref().unwrap().clone())
}
pub fn get(&self, path: &str) -> reqwest::blocking::RequestBuilder {
self.client()
.expect("HttpClient must have a valid client")
.get(format!("{}{}", self.base_url, path))
}
pub fn post(&self, path: &str) -> reqwest::blocking::RequestBuilder {
self.client()
.expect("HttpClient must have a valid client")
.post(format!("{}{}", self.base_url, path))
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
}
pub fn with_retry<F>(
max_retries: u32,
initial_delay: Duration,
mut request_fn: F,
) -> Result<reqwest::blocking::Response>
where
F: FnMut() -> Result<reqwest::blocking::Response>,
{
let mut delay = initial_delay;
for attempt in 0..=max_retries {
let response = request_fn()?;
if response.status() != reqwest::StatusCode::TOO_MANY_REQUESTS {
return Ok(response.error_for_status()?);
}
if attempt == max_retries {
return Ok(response.error_for_status()?);
}
if let Some(retry_after) = response.headers().get(reqwest::header::RETRY_AFTER)
&& let Ok(secs) = retry_after.to_str().unwrap_or("").parse::<u64>()
{
delay = Duration::from_secs(secs + 1);
}
std::thread::sleep(delay);
delay *= 2;
}
unreachable!("loop always returns inside")
}
#[inline]
pub fn clamp_page_size(page_size: usize, max: usize) -> usize {
page_size.clamp(1, max)
}
#[inline]
pub fn page_to_offset(cursor_page: usize, page_size: usize) -> usize {
cursor_page.saturating_sub(1) * page_size
}
#[cfg(test)]
mod regression_tests {
use super::HttpClient;
#[tokio::test]
async fn http_client_construction_inside_tokio_test_does_not_panic() {
let client = HttpClient::new(std::time::Duration::from_secs(30), "https://example.com");
assert_eq!(client.base_url, "https://example.com");
}
#[test]
fn http_client_construction_in_std_test_does_not_panic() {
let client = HttpClient::new(std::time::Duration::from_secs(30), "https://example.com");
assert_eq!(client.base_url, "https://example.com");
}
}