unistore-http 0.1.0

HTTP client capability for UniStore
Documentation
//! HTTP 客户端配置
//!
//! 职责:
//! - 定义 HTTP 客户端的配置选项
//! - 提供合理的默认值
//! - 支持 Builder 模式构建配置

use std::time::Duration;

/// HTTP 客户端配置
#[derive(Debug, Clone)]
pub struct HttpClientConfig {
    /// 连接超时(默认 10s)
    pub connect_timeout: Duration,

    /// 请求超时(默认 30s)
    pub request_timeout: Duration,

    /// 用户代理
    pub user_agent: String,

    /// 最大重试次数(默认 3)
    pub max_retries: u32,

    /// 重试间隔策略
    pub retry_strategy: RetryStrategy,

    /// 代理配置
    pub proxy: Option<ProxyConfig>,

    /// 是否跟随重定向(默认 true)
    pub follow_redirects: bool,

    /// 最大重定向次数(默认 10)
    pub max_redirects: u32,

    /// 是否接受无效证书(默认 false,生产环境勿启用)
    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), // 5 分钟
            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 {
    /// 计算第 n 次重试的延迟时间
    ///
    /// # Arguments
    /// * `attempt` - 当前重试次数(从 1 开始)
    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 {
    /// HTTP 代理地址
    pub http: Option<String>,

    /// HTTPS 代理地址
    pub https: Option<String>,

    /// 不使用代理的地址列表
    pub no_proxy: Vec<String>,
}

impl ProxyConfig {
    /// 创建 HTTP 代理配置
    pub fn http(url: impl Into<String>) -> Self {
        Self {
            http: Some(url.into()),
            https: None,
            no_proxy: Vec::new(),
        }
    }

    /// 创建 HTTPS 代理配置
    pub fn https(url: impl Into<String>) -> Self {
        Self {
            http: None,
            https: Some(url.into()),
            no_proxy: Vec::new(),
        }
    }

    /// 创建同时用于 HTTP 和 HTTPS 的代理配置
    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
    }
}

/// HTTP 客户端配置构建器
#[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
    }

    /// 设置 HTTP 代理
    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
    }

    /// 设置 HTTPS 代理
    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);
    }
}