use std::time::Duration;
#[derive(Debug, Clone)]
pub struct HttpClientConfig {
pub connect_timeout: Duration,
pub request_timeout: Duration,
pub user_agent: String,
pub max_retries: u32,
pub retry_strategy: RetryStrategy,
pub proxy: Option<ProxyConfig>,
pub follow_redirects: bool,
pub max_redirects: u32,
pub accept_invalid_certs: bool,
}
impl Default for HttpClientConfig {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(10),
request_timeout: Duration::from_secs(30),
user_agent: format!("unistore-http/{}", env!("CARGO_PKG_VERSION")),
max_retries: 3,
retry_strategy: RetryStrategy::ExponentialBackoff {
initial: Duration::from_millis(100),
max: Duration::from_secs(5),
},
proxy: None,
follow_redirects: true,
max_redirects: 10,
accept_invalid_certs: false,
}
}
}
impl HttpClientConfig {
pub fn new() -> Self {
Self::default()
}
pub fn quick() -> Self {
Self {
connect_timeout: Duration::from_secs(5),
request_timeout: Duration::from_secs(10),
max_retries: 1,
retry_strategy: RetryStrategy::None,
..Self::default()
}
}
pub fn long_running() -> Self {
Self {
connect_timeout: Duration::from_secs(30),
request_timeout: Duration::from_secs(300), max_retries: 5,
..Self::default()
}
}
pub fn no_retry() -> Self {
Self {
max_retries: 0,
retry_strategy: RetryStrategy::None,
..Self::default()
}
}
}
#[derive(Debug, Clone)]
pub enum RetryStrategy {
None,
Fixed(Duration),
ExponentialBackoff {
initial: Duration,
max: Duration,
},
}
impl RetryStrategy {
pub fn delay_for_attempt(&self, attempt: u32) -> Option<Duration> {
match self {
Self::None => None,
Self::Fixed(duration) => Some(*duration),
Self::ExponentialBackoff { initial, max } => {
let delay = initial.saturating_mul(2u32.saturating_pow(attempt.saturating_sub(1)));
Some(delay.min(*max))
}
}
}
}
#[derive(Debug, Clone)]
pub struct ProxyConfig {
pub http: Option<String>,
pub https: Option<String>,
pub no_proxy: Vec<String>,
}
impl ProxyConfig {
pub fn http(url: impl Into<String>) -> Self {
Self {
http: Some(url.into()),
https: None,
no_proxy: Vec::new(),
}
}
pub fn https(url: impl Into<String>) -> Self {
Self {
http: None,
https: Some(url.into()),
no_proxy: Vec::new(),
}
}
pub fn all(url: impl Into<String>) -> Self {
let url = url.into();
Self {
http: Some(url.clone()),
https: Some(url),
no_proxy: Vec::new(),
}
}
pub fn with_no_proxy(mut self, patterns: Vec<String>) -> Self {
self.no_proxy = patterns;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct HttpClientConfigBuilder {
config: HttpClientConfig,
}
impl HttpClientConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.config.connect_timeout = timeout;
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.config.request_timeout = timeout;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.connect_timeout = timeout;
self.config.request_timeout = timeout;
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.config.user_agent = ua.into();
self
}
pub fn max_retries(mut self, retries: u32) -> Self {
self.config.max_retries = retries;
self
}
pub fn retry_strategy(mut self, strategy: RetryStrategy) -> Self {
self.config.retry_strategy = strategy;
self
}
pub fn no_retry(mut self) -> Self {
self.config.max_retries = 0;
self.config.retry_strategy = RetryStrategy::None;
self
}
pub fn proxy_http(mut self, url: impl Into<String>) -> Self {
let proxy = self.config.proxy.get_or_insert_with(|| ProxyConfig {
http: None,
https: None,
no_proxy: Vec::new(),
});
proxy.http = Some(url.into());
self
}
pub fn proxy_https(mut self, url: impl Into<String>) -> Self {
let proxy = self.config.proxy.get_or_insert_with(|| ProxyConfig {
http: None,
https: None,
no_proxy: Vec::new(),
});
proxy.https = Some(url.into());
self
}
pub fn proxy_all(mut self, url: impl Into<String>) -> Self {
let url = url.into();
self.config.proxy = Some(ProxyConfig {
http: Some(url.clone()),
https: Some(url),
no_proxy: Vec::new(),
});
self
}
pub fn follow_redirects(mut self, follow: bool) -> Self {
self.config.follow_redirects = follow;
self
}
pub fn max_redirects(mut self, max: u32) -> Self {
self.config.max_redirects = max;
self
}
pub fn accept_invalid_certs(mut self, accept: bool) -> Self {
self.config.accept_invalid_certs = accept;
self
}
pub fn build(self) -> HttpClientConfig {
self.config
}
pub fn build_client(self) -> Result<super::HttpClient, super::HttpError> {
super::HttpClient::with_config(self.build())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = HttpClientConfig::default();
assert_eq!(config.connect_timeout, Duration::from_secs(10));
assert_eq!(config.request_timeout, Duration::from_secs(30));
assert_eq!(config.max_retries, 3);
assert!(config.follow_redirects);
assert!(!config.accept_invalid_certs);
}
#[test]
fn test_quick_config() {
let config = HttpClientConfig::quick();
assert_eq!(config.connect_timeout, Duration::from_secs(5));
assert_eq!(config.max_retries, 1);
}
#[test]
fn test_retry_strategy_exponential() {
let strategy = RetryStrategy::ExponentialBackoff {
initial: Duration::from_millis(100),
max: Duration::from_secs(5),
};
assert_eq!(
strategy.delay_for_attempt(1),
Some(Duration::from_millis(100))
);
assert_eq!(
strategy.delay_for_attempt(2),
Some(Duration::from_millis(200))
);
assert_eq!(
strategy.delay_for_attempt(3),
Some(Duration::from_millis(400))
);
assert_eq!(
strategy.delay_for_attempt(10),
Some(Duration::from_secs(5))
);
}
#[test]
fn test_retry_strategy_fixed() {
let strategy = RetryStrategy::Fixed(Duration::from_millis(500));
assert_eq!(
strategy.delay_for_attempt(1),
Some(Duration::from_millis(500))
);
assert_eq!(
strategy.delay_for_attempt(5),
Some(Duration::from_millis(500))
);
}
#[test]
fn test_retry_strategy_none() {
let strategy = RetryStrategy::None;
assert_eq!(strategy.delay_for_attempt(1), None);
}
#[test]
fn test_builder() {
let config = HttpClientConfigBuilder::new()
.connect_timeout(Duration::from_secs(5))
.request_timeout(Duration::from_secs(60))
.max_retries(5)
.user_agent("TestAgent/1.0")
.build();
assert_eq!(config.connect_timeout, Duration::from_secs(5));
assert_eq!(config.request_timeout, Duration::from_secs(60));
assert_eq!(config.max_retries, 5);
assert_eq!(config.user_agent, "TestAgent/1.0");
}
#[test]
fn test_proxy_config() {
let proxy = ProxyConfig::all("http://proxy.example.com:8080")
.with_no_proxy(vec!["localhost".into(), "127.0.0.1".into()]);
assert_eq!(
proxy.http,
Some("http://proxy.example.com:8080".to_string())
);
assert_eq!(
proxy.https,
Some("http://proxy.example.com:8080".to_string())
);
assert_eq!(proxy.no_proxy.len(), 2);
}
}