1use 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
14pub 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#[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 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 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 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}