1use reqwest::Client;
2use std::time::{Duration, Instant};
3use std::collections::HashMap;
4use crate::error::{TushareError, TushareResult};
5use crate::types::{TushareRequest, TushareResponse, TushareEntityList};
6use crate::api::{Api, serialize_api_name};
7use crate::logging::{LogConfig, LogLevel, Logger};
8use serde::{Serialize};
9use serde_json;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Clone)]
14pub struct HttpClientConfig {
15 pub connect_timeout: Duration,
17 pub timeout: Duration,
19 pub pool_max_idle_per_host: usize,
21 pub pool_idle_timeout: Duration,
23 pub user_agent: Option<String>,
25 pub tcp_nodelay: bool,
27 pub tcp_keepalive: Option<Duration>,
29}
30
31impl Default for HttpClientConfig {
32 fn default() -> Self {
33 Self {
34 connect_timeout: Duration::from_secs(10),
35 timeout: Duration::from_secs(30),
36 pool_max_idle_per_host: 20, pool_idle_timeout: Duration::from_secs(90), user_agent: Some("tushare-api-rust/1.0.0".to_string()),
39 tcp_nodelay: true, tcp_keepalive: Some(Duration::from_secs(60)), }
42 }
43}
44
45impl HttpClientConfig {
46 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
53 self.connect_timeout = timeout;
54 self
55 }
56
57 pub fn with_timeout(mut self, timeout: Duration) -> Self {
59 self.timeout = timeout;
60 self
61 }
62
63 pub fn with_pool_max_idle_per_host(mut self, max_idle: usize) -> Self {
65 self.pool_max_idle_per_host = max_idle;
66 self
67 }
68
69 pub fn with_pool_idle_timeout(mut self, timeout: Duration) -> Self {
71 self.pool_idle_timeout = timeout;
72 self
73 }
74
75 pub fn with_user_agent<S: Into<String>>(mut self, user_agent: S) -> Self {
77 self.user_agent = Some(user_agent.into());
78 self
79 }
80
81 pub fn with_tcp_nodelay(mut self, enabled: bool) -> Self {
83 self.tcp_nodelay = enabled;
84 self
85 }
86
87 pub fn with_tcp_keepalive(mut self, duration: Option<Duration>) -> Self {
89 self.tcp_keepalive = duration;
90 self
91 }
92
93 pub(crate) fn build_client(&self) -> Result<Client, reqwest::Error> {
95 let mut builder = Client::builder()
96 .connect_timeout(self.connect_timeout)
97 .timeout(self.timeout)
98 .pool_max_idle_per_host(self.pool_max_idle_per_host)
99 .pool_idle_timeout(self.pool_idle_timeout)
100 .tcp_nodelay(self.tcp_nodelay);
101
102 if let Some(ref user_agent) = self.user_agent {
103 builder = builder.user_agent(user_agent);
104 }
105
106 if let Some(keepalive) = self.tcp_keepalive {
107 builder = builder.tcp_keepalive(keepalive);
108 }
109
110 builder.build()
111 }
112}
113
114#[derive(Debug, Serialize)]
116struct InternalTushareRequest {
117 #[serde(serialize_with = "serialize_api_name")]
118 api_name: Api,
119 token: String,
120 params: HashMap<String, String>,
121 fields: Vec<String>,
122}
123
124#[derive(Debug)]
126pub struct TushareClient {
127 token: String,
128 client: Client,
129 logger: Logger,
130}
131
132#[derive(Debug)]
134pub struct TushareClientBuilder {
135 token: Option<String>,
136 http_config: HttpClientConfig,
137 log_config: LogConfig,
138}
139
140impl TushareClientBuilder {
141 pub fn new() -> Self {
142 Self {
143 token: None,
144 http_config: HttpClientConfig::default(),
145 log_config: LogConfig::default(),
146 }
147 }
148
149 pub fn with_token(mut self, token: &str) -> Self {
150 self.token = Some(token.to_string());
151 self
152 }
153
154 pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
155 self.http_config = self.http_config.with_connect_timeout(connect_timeout);
156 self
157 }
158
159 pub fn with_timeout(mut self, timeout: Duration) -> Self {
160 self.http_config = self.http_config.with_timeout(timeout);
161 self
162 }
163
164 pub fn with_http_config(mut self, http_config: HttpClientConfig) -> Self {
166 self.http_config = http_config;
167 self
168 }
169
170 pub fn with_pool_max_idle_per_host(mut self, max_idle: usize) -> Self {
172 self.http_config = self.http_config.with_pool_max_idle_per_host(max_idle);
173 self
174 }
175
176 pub fn with_pool_idle_timeout(mut self, timeout: Duration) -> Self {
178 self.http_config = self.http_config.with_pool_idle_timeout(timeout);
179 self
180 }
181
182 pub fn with_log_config(mut self, log_config: LogConfig) -> Self {
183 self.log_config = log_config;
184 self
185 }
186
187 pub fn with_log_level(mut self, level: LogLevel) -> Self {
189 self.log_config.level = level;
190 self
191 }
192
193 pub fn log_requests(mut self, enabled: bool) -> Self {
195 self.log_config.log_requests = enabled;
196 self
197 }
198
199 pub fn log_responses(mut self, enabled: bool) -> Self {
201 self.log_config.log_responses = enabled;
202 self
203 }
204
205 pub fn log_sensitive_data(mut self, enabled: bool) -> Self {
207 self.log_config.log_sensitive_data = enabled;
208 self
209 }
210
211 pub fn log_performance(mut self, enabled: bool) -> Self {
213 self.log_config.log_performance = enabled;
214 self
215 }
216
217 pub fn build(self) -> TushareResult<TushareClient> {
218 let token = self.token.ok_or(TushareError::InvalidToken)?;
219
220 let client = self.http_config.build_client()
221 .map_err(TushareError::HttpError)?;
222
223 Ok(TushareClient {
224 token,
225 client,
226 logger: Logger::new(self.log_config),
227 })
228 }
229}
230
231impl TushareClient {
232 pub fn builder() -> TushareClientBuilder {
234 TushareClientBuilder::new()
235 }
236
237
238
239 pub fn new(token: &str) -> Self {
253 Self::with_timeout(token, Duration::from_secs(10), Duration::from_secs(30))
254 }
255
256 pub fn from_env() -> TushareResult<Self> {
272 let token = std::env::var("TUSHARE_TOKEN")
273 .map_err(|_| TushareError::InvalidToken)?
274 .trim()
275 .to_string();
276
277 if token.is_empty() {
278 return Err(TushareError::InvalidToken);
279 }
280
281 Ok(Self::new(&token))
282 }
283
284 pub fn from_env_with_timeout(connect_timeout: Duration, timeout: Duration) -> TushareResult<Self> {
309 let token = std::env::var("TUSHARE_TOKEN")
310 .map_err(|_| TushareError::InvalidToken)?
311 .trim()
312 .to_string();
313
314 if token.is_empty() {
315 return Err(TushareError::InvalidToken);
316 }
317
318 Ok(Self::with_timeout(&token, connect_timeout, timeout))
319 }
320
321 pub fn with_timeout(token: &str, connect_timeout: Duration, timeout: Duration) -> Self {
342 let http_config = HttpClientConfig::new()
343 .with_connect_timeout(connect_timeout)
344 .with_timeout(timeout);
345
346 let client = http_config.build_client()
347 .expect("Failed to create HTTP client");
348
349 TushareClient {
350 token: token.to_string(),
351 client,
352 logger: Logger::new(LogConfig::default()),
353 }
354 }
355
356 pub async fn call_api(&self, request: TushareRequest) -> TushareResult<TushareResponse> {
387 let request_id = generate_request_id();
388 let start_time = Instant::now();
389
390 self.logger.log_api_start(
392 &request_id,
393 &request.api_name.name(),
394 request.params.len(),
395 request.fields.len()
396 );
397
398 let token_preview_string = if self.logger.config().log_sensitive_data {
400 Some(format!("token: {}***", &self.token[..self.token.len().min(8)]))
401 } else {
402 None
403 };
404
405 self.logger.log_request_details(
406 &request_id,
407 &request.api_name.name(),
408 &format!("{:?}", request.params),
409 &format!("{:?}", request.fields),
410 token_preview_string.as_deref()
411 );
412
413 let internal_request = InternalTushareRequest {
414 api_name: request.api_name,
415 token: self.token.clone(),
416 params: request.params,
417 fields: request.fields,
418 };
419
420 self.logger.log_http_request(&request_id);
421
422 let response = self.client
423 .post("http://api.tushare.pro")
424 .json(&internal_request)
425 .send()
426 .await
427 .map_err(|e| {
428 let elapsed = start_time.elapsed();
429 self.logger.log_http_error(&request_id, elapsed, &e.to_string());
430 e
431 })?;
432
433 let status = response.status();
434 self.logger.log_http_response(&request_id, status.as_u16());
435
436 let response_text = response.text().await
437 .map_err(|e| {
438 let elapsed = start_time.elapsed();
439 self.logger.log_response_read_error(&request_id, elapsed, &e.to_string());
440 e
441 })?;
442
443 self.logger.log_raw_response(&request_id, &response_text);
444
445 let tushare_response: TushareResponse = serde_json::from_str(&response_text)
446 .map_err(|e| {
447 let elapsed = start_time.elapsed();
448 self.logger.log_json_parse_error(&request_id, elapsed, &e.to_string(), &response_text);
449 e
450 })?;
451
452 let elapsed = start_time.elapsed();
453
454 if tushare_response.code != 0 {
455 let message = format!("error code: {}, error msg: {}", tushare_response.code, tushare_response.msg.clone().unwrap_or_default());
456 self.logger.log_api_error(&request_id, elapsed, tushare_response.code, &message);
457 return Err(TushareError::ApiError {
458 code: tushare_response.code,
459 message
460 });
461 }
462
463 self.logger.log_api_success(&request_id, elapsed, tushare_response.data.clone().map(|data| data.items.len()).unwrap_or(0));
465
466 self.logger.log_response_details(
468 &request_id,
469 &tushare_response.request_id,
470 &format!("{:?}", tushare_response.data.as_ref().map(|d| &d.fields))
471 );
472
473 Ok(tushare_response)
474 }
475
476 pub async fn call_api_as<T>(&self, request: TushareRequest) -> TushareResult<TushareEntityList<T>>
538 where
539 T: crate::traits::FromTushareData,
540 {
541 let response = self.call_api(request).await?;
542 TushareEntityList::try_from(response).map_err(Into::into)
543 }
544}
545
546fn generate_request_id() -> String {
548 let timestamp = SystemTime::now()
549 .duration_since(UNIX_EPOCH)
550 .unwrap_or_default()
551 .as_nanos();
552 format!("req_{}", timestamp)
553}