Skip to main content

yf_common/
client.rs

1//! HTTP client with rate limiting
2
3use crate::error::{Result, YfCommonError};
4use crate::rate_limit::{RateLimitConfig, YfRateLimiter, wait_for_permit};
5use crate::retry::RetryConfig;
6use reqwest::{Client, Response, StatusCode};
7use std::sync::Arc;
8use std::time::Duration;
9use tracing::debug;
10
11pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
12pub const DEFAULT_USER_AGENT: &str = "yf-common/0.1.0";
13
14/// HTTP client for Yahoo Finance API
15pub struct YahooClient {
16    client: Client,
17    rate_limiter: Arc<YfRateLimiter>,
18    retry_config: RetryConfig,
19}
20
21impl YahooClient {
22    pub fn builder() -> YahooClientBuilder {
23        YahooClientBuilder::new()
24    }
25
26    pub async fn get(&self, url: &str) -> Result<String> {
27        wait_for_permit(&self.rate_limiter).await;
28        debug!("GET {}", url);
29        let response = self.client.get(url).send().await?;
30        self.handle_response(response).await
31    }
32
33    async fn handle_response(&self, response: Response) -> Result<String> {
34        let status = response.status();
35        let url = response.url().to_string();
36
37        match status {
38            s if s.is_success() => Ok(response.text().await?),
39            StatusCode::TOO_MANY_REQUESTS => Err(YfCommonError::RateLimitExceeded(url)),
40            s if s.is_server_error() => {
41                let body = response.text().await.unwrap_or_default();
42                Err(YfCommonError::ServerError(s.as_u16(), body))
43            }
44            s if s.is_client_error() => {
45                let body = response.text().await.unwrap_or_default();
46                Err(YfCommonError::ClientError(s.as_u16(), body))
47            }
48            _ => {
49                let body = response.text().await.unwrap_or_default();
50                Err(YfCommonError::DataError(format!("Unexpected status {}: {}", status, body)))
51            }
52        }
53    }
54
55    pub fn inner(&self) -> &Client {
56        &self.client
57    }
58
59    pub fn retry_config(&self) -> &RetryConfig {
60        &self.retry_config
61    }
62}
63
64/// Builder for YahooClient
65#[derive(Default)]
66pub struct YahooClientBuilder {
67    timeout: Option<Duration>,
68    user_agent: Option<String>,
69    rate_limit_config: Option<RateLimitConfig>,
70    retry_config: Option<RetryConfig>,
71}
72
73impl YahooClientBuilder {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    pub fn with_timeout(mut self, timeout: Duration) -> Self {
79        self.timeout = Some(timeout);
80        self
81    }
82
83    pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
84        self.user_agent = Some(user_agent.into());
85        self
86    }
87
88    pub fn with_rate_limit(mut self, requests_per_minute: u32) -> Self {
89        self.rate_limit_config = Some(RateLimitConfig::new(requests_per_minute));
90        self
91    }
92
93    pub fn with_retry(mut self, config: RetryConfig) -> Self {
94        self.retry_config = Some(config);
95        self
96    }
97
98    pub fn build(self) -> Result<YahooClient> {
99        let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
100        let user_agent = self.user_agent.unwrap_or_else(|| DEFAULT_USER_AGENT.to_string());
101        let rate_limit_config = self.rate_limit_config.unwrap_or_default();
102        let retry_config = self.retry_config.unwrap_or_default();
103
104        let client = Client::builder()
105            .timeout(timeout)
106            .user_agent(&user_agent)
107            .build()
108            .map_err(YfCommonError::RequestError)?;
109
110        Ok(YahooClient {
111            client,
112            rate_limiter: rate_limit_config.build_limiter(),
113            retry_config,
114        })
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_builder_default() {
124        let client = YahooClientBuilder::new().build().unwrap();
125        assert!(client.retry_config().max_retries == 3);
126    }
127
128    #[test]
129    fn test_builder_chaining() {
130        let result = YahooClientBuilder::new()
131            .with_timeout(Duration::from_secs(60))
132            .with_user_agent("test-agent")
133            .with_rate_limit(10)
134            .with_retry(RetryConfig::default())
135            .build();
136        assert!(result.is_ok());
137    }
138
139    #[test]
140    fn test_builder_custom_timeout() {
141        let client = YahooClientBuilder::new()
142            .with_timeout(Duration::from_secs(60))
143            .build()
144            .unwrap();
145        // Client created successfully with custom timeout
146        assert_eq!(client.retry_config().max_retries, 3);
147    }
148
149    #[test]
150    fn test_builder_custom_user_agent() {
151        let client = YahooClientBuilder::new()
152            .with_user_agent("custom-agent/1.0")
153            .build()
154            .unwrap();
155        // Client created successfully with custom user agent
156        assert_eq!(client.retry_config().max_retries, 3);
157    }
158
159    #[test]
160    fn test_builder_custom_rate_limit() {
161        let client = YahooClientBuilder::new()
162            .with_rate_limit(10)
163            .build()
164            .unwrap();
165        // Client created successfully with custom rate limit
166        assert_eq!(client.retry_config().max_retries, 3);
167    }
168
169    #[test]
170    fn test_builder_retry_config() {
171        let client = YahooClientBuilder::new()
172            .with_retry(RetryConfig::new(5))
173            .build()
174            .unwrap();
175        assert_eq!(client.retry_config().max_retries, 5);
176    }
177}