tushare_api/
client.rs

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/// HTTP client configuration for reqwest::Client
13#[derive(Debug, Clone)]
14pub struct HttpClientConfig {
15    /// Connection timeout duration
16    pub connect_timeout: Duration,
17    /// Request timeout duration
18    pub timeout: Duration,
19    /// Maximum idle connections per host
20    pub pool_max_idle_per_host: usize,
21    /// Pool idle timeout duration
22    pub pool_idle_timeout: Duration,
23    /// User agent string
24    pub user_agent: Option<String>,
25    /// Enable TCP_NODELAY to reduce latency
26    pub tcp_nodelay: bool,
27    /// TCP keep-alive duration
28    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,  // Increased for better performance
37            pool_idle_timeout: Duration::from_secs(90),  // Longer idle timeout
38            user_agent: Some("tushare-api-rust/1.0.0".to_string()),
39            tcp_nodelay: true,  // Reduce latency
40            tcp_keepalive: Some(Duration::from_secs(60)),  // Keep connections alive
41        }
42    }
43}
44
45impl HttpClientConfig {
46    /// Create a new HTTP client configuration with default values
47    pub fn new() -> Self {
48        Self::default()
49    }
50    
51    /// Set connection timeout
52    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
53        self.connect_timeout = timeout;
54        self
55    }
56    
57    /// Set request timeout
58    pub fn with_timeout(mut self, timeout: Duration) -> Self {
59        self.timeout = timeout;
60        self
61    }
62    
63    /// Set maximum idle connections per host
64    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    /// Set pool idle timeout
70    pub fn with_pool_idle_timeout(mut self, timeout: Duration) -> Self {
71        self.pool_idle_timeout = timeout;
72        self
73    }
74    
75    /// Set user agent string
76    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    /// Enable or disable TCP_NODELAY
82    pub fn with_tcp_nodelay(mut self, enabled: bool) -> Self {
83        self.tcp_nodelay = enabled;
84        self
85    }
86    
87    /// Set TCP keep-alive duration
88    pub fn with_tcp_keepalive(mut self, duration: Option<Duration>) -> Self {
89        self.tcp_keepalive = duration;
90        self
91    }
92    
93    /// Build reqwest::Client with this configuration
94    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/// Internal request structure with token included
115#[derive(Debug)]
116struct ApiNameRef<'a>(&'a Api);
117
118impl<'a> Serialize for ApiNameRef<'a> {
119    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
120    where
121        S: serde::Serializer,
122    {
123        serialize_api_name(self.0, serializer)
124    }
125}
126
127#[derive(Debug, Serialize)]
128struct InternalTushareRequest<'a> {
129    api_name: ApiNameRef<'a>,
130    token: &'a str,
131    params: &'a HashMap<String, String>,
132    fields: &'a [String],
133}
134
135/// Tushare API client
136#[derive(Debug)]
137pub struct TushareClient {
138    token: String,
139    client: Client,
140    logger: Logger,
141}
142
143/// Tushare client builder
144#[derive(Debug)]
145pub struct TushareClientBuilder {
146    token: Option<String>,
147    http_config: HttpClientConfig,
148    log_config: LogConfig,
149}
150
151impl TushareClientBuilder {
152    pub fn new() -> Self {
153        Self {
154            token: None,
155            http_config: HttpClientConfig::default(),
156            log_config: LogConfig::default(),
157        }
158    }
159
160    pub fn with_token(mut self, token: &str) -> Self {
161        self.token = Some(token.to_string());
162        self
163    }
164
165    pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
166        self.http_config = self.http_config.with_connect_timeout(connect_timeout);
167        self
168    }
169
170    pub fn with_timeout(mut self, timeout: Duration) -> Self {
171        self.http_config = self.http_config.with_timeout(timeout);
172        self
173    }
174    
175    /// Set HTTP client configuration
176    pub fn with_http_config(mut self, http_config: HttpClientConfig) -> Self {
177        self.http_config = http_config;
178        self
179    }
180    
181    /// Set maximum idle connections per host
182    pub fn with_pool_max_idle_per_host(mut self, max_idle: usize) -> Self {
183        self.http_config = self.http_config.with_pool_max_idle_per_host(max_idle);
184        self
185    }
186    
187    /// Set pool idle timeout
188    pub fn with_pool_idle_timeout(mut self, timeout: Duration) -> Self {
189        self.http_config = self.http_config.with_pool_idle_timeout(timeout);
190        self
191    }
192
193    pub fn with_log_config(mut self, log_config: LogConfig) -> Self {
194        self.log_config = log_config;
195        self
196    }
197
198    /// Set log level
199    pub fn with_log_level(mut self, level: LogLevel) -> Self {
200        self.log_config.level = level;
201        self
202    }
203
204    /// Enable or disable request logging
205    pub fn log_requests(mut self, enabled: bool) -> Self {
206        self.log_config.log_requests = enabled;
207        self
208    }
209
210    /// Enable or disable response logging
211    pub fn log_responses(mut self, enabled: bool) -> Self {
212        self.log_config.log_responses = enabled;
213        self
214    }
215
216    /// Enable or disable sensitive data logging
217    pub fn log_sensitive_data(mut self, enabled: bool) -> Self {
218        self.log_config.log_sensitive_data = enabled;
219        self
220    }
221
222    /// Enable or disable performance metrics logging
223    pub fn log_performance(mut self, enabled: bool) -> Self {
224        self.log_config.log_performance = enabled;
225        self
226    }
227
228    pub fn build(self) -> TushareResult<TushareClient> {
229        let token = self.token.ok_or(TushareError::InvalidToken)?;
230        
231        let client = self.http_config.build_client()
232            .map_err(TushareError::HttpError)?;
233
234        Ok(TushareClient {
235            token,
236            client,
237            logger: Logger::new(self.log_config),
238        })
239    }
240}
241
242impl TushareClient {
243    /// Create client builder
244    pub fn builder() -> TushareClientBuilder {
245        TushareClientBuilder::new()
246    }
247
248
249
250    /// Create a new Tushare client with default timeout settings
251    /// 
252    /// # Arguments
253    /// 
254    /// * `token` - Tushare API Token
255    /// 
256    /// # Example
257    /// 
258    /// ```rust
259    /// use tushare_api::TushareClient;
260    /// 
261    /// let client = TushareClient::new("your_token_here");
262    /// ```
263    pub fn new(token: &str) -> Self {
264        Self::with_timeout(token, Duration::from_secs(10), Duration::from_secs(30))
265    }
266
267    /// Create a new Tushare client from TUSHARE_TOKEN environment variable with default timeout settings
268    /// 
269    /// # Errors
270    /// 
271    /// Returns `TushareError::InvalidToken` if TUSHARE_TOKEN environment variable does not exist or is empty
272    /// 
273    /// # Example
274    /// 
275    /// ```rust,no_run
276    /// use tushare_api::{TushareClient, TushareResult};
277    /// 
278    /// // Requires TUSHARE_TOKEN environment variable to be set
279    /// let client = TushareClient::from_env()?;
280    /// # Ok::<(), tushare_api::TushareError>(())
281    /// ```
282    pub fn from_env() -> TushareResult<Self> {
283        let token = std::env::var("TUSHARE_TOKEN")
284            .map_err(|_| TushareError::InvalidToken)?
285            .trim()
286            .to_string();
287        
288        if token.is_empty() {
289            return Err(TushareError::InvalidToken);
290        }
291        
292        Ok(Self::new(&token))
293    }
294
295    /// Create a new Tushare client from TUSHARE_TOKEN environment variable with custom timeout settings
296    /// 
297    /// # Arguments
298    /// 
299    /// * `connect_timeout` - Connection timeout duration
300    /// * `timeout` - Request timeout duration
301    /// 
302    /// # Errors
303    /// 
304    /// Returns `TushareError::InvalidToken` if TUSHARE_TOKEN environment variable does not exist or is empty
305    /// 
306    /// # Example
307    /// 
308    /// ```rust,no_run
309    /// use tushare_api::{TushareClient, TushareResult};
310    /// use std::time::Duration;
311    /// 
312    /// // Requires TUSHARE_TOKEN environment variable to be set
313    /// let client = TushareClient::from_env_with_timeout(
314    ///     Duration::from_secs(5),  // Connection timeout 5 seconds
315    ///     Duration::from_secs(60)  // Request timeout 60 seconds
316    /// )?;
317    /// # Ok::<(), tushare_api::TushareError>(())
318    /// ```
319    pub fn from_env_with_timeout(connect_timeout: Duration, timeout: Duration) -> TushareResult<Self> {
320        let token = std::env::var("TUSHARE_TOKEN")
321            .map_err(|_| TushareError::InvalidToken)?
322            .trim()
323            .to_string();
324        
325        if token.is_empty() {
326            return Err(TushareError::InvalidToken);
327        }
328        
329        Ok(Self::with_timeout(&token, connect_timeout, timeout))
330    }
331
332    /// Create a new Tushare client with custom timeout settings
333    /// 
334    /// # Arguments
335    /// 
336    /// * `token` - Tushare API Token
337    /// * `connect_timeout` - Connection timeout duration
338    /// * `timeout` - Request timeout duration
339    /// 
340    /// # Example
341    /// 
342    /// ```rust
343    /// use tushare_api::TushareClient;
344    /// use std::time::Duration;
345    /// 
346    /// let client = TushareClient::with_timeout(
347    ///     "your_token_here",
348    ///     Duration::from_secs(5),  // Connection timeout 5 seconds
349    ///     Duration::from_secs(60)  // Request timeout 60 seconds
350    /// );
351    /// ```
352    pub fn with_timeout(token: &str, connect_timeout: Duration, timeout: Duration) -> Self {
353        let http_config = HttpClientConfig::new()
354            .with_connect_timeout(connect_timeout)
355            .with_timeout(timeout);
356            
357        let client = http_config.build_client()
358            .expect("Failed to create HTTP client");
359
360        TushareClient {
361            token: token.to_string(),
362            client,
363            logger: Logger::new(LogConfig::default()),
364        }
365    }
366
367    /// Call Tushare API with flexible string types support
368    /// 
369    /// # Arguments
370    /// 
371    /// * `request` - API request parameters, supports direct use of string literals
372    /// 
373    /// # Returns
374    /// 
375    /// Returns API response result
376    /// 
377    /// # Example
378    /// 
379    /// ```rust
380    /// use tushare_api::{TushareClient, TushareRequest, Api, params, fields, request};
381    /// 
382    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
383    ///     let client = TushareClient::new("your_token_here");
384    ///     
385    ///     // Now you can use string literals directly!
386    ///     let request = request!(Api::StockBasic, {
387    ///         "list_status" => "L"
388    ///     }, [
389    ///         "ts_code", "name"
390    ///     ]);
391    ///     
392    ///     let response = client.call_api(&request).await?;
393    ///     println!("Response: {:?}", response);
394    /// #   Ok(())
395    /// # }
396    /// ```
397    pub async fn call_api<T>(&self, request: &T) -> TushareResult<TushareResponse>
398    where
399        for<'a> &'a T: TryInto<TushareRequest>,
400        for<'a> <&'a T as TryInto<TushareRequest>>::Error: Into<TushareError>,
401    {
402        let request = request
403            .try_into()
404            .map_err(Into::into)?;
405        self.call_api_inner(&request).await
406    }
407
408    pub async fn call_api_request(&self, request: &TushareRequest) -> TushareResult<TushareResponse> {
409        self.call_api_inner(request).await
410    }
411
412    async fn call_api_inner(&self, request: &TushareRequest) -> TushareResult<TushareResponse> {
413        let request_id = generate_request_id();
414        let start_time = Instant::now();
415        // Log API call start
416        self.logger.log_api_start(
417            &request_id,
418            &request.api_name.name(),
419            request.params.len(),
420            request.fields.len()
421        );
422        
423        // Log detailed request information (if enabled)
424        let token_preview_string = if self.logger.config().log_sensitive_data {
425            Some(format!("token: {}***", &self.token[..self.token.len().min(8)]))
426        } else {
427            None
428        };
429        
430        self.logger.log_request_details(
431            &request_id,
432            &request.api_name.name(),
433            &format!("{:?}", request.params),
434            &format!("{:?}", request.fields),
435            token_preview_string.as_deref()
436        );
437        
438        let internal_request = InternalTushareRequest {
439            api_name: ApiNameRef(&request.api_name),
440            token: &self.token,
441            params: &request.params,
442            fields: &request.fields,
443        };
444
445        self.logger.log_http_request(&request_id);
446        
447        let response = self.client
448            .post("http://api.tushare.pro")
449            .json(&internal_request)
450            .send()
451            .await
452            .map_err(|e| {
453                let elapsed = start_time.elapsed();
454                self.logger.log_http_error(&request_id, elapsed, &e.to_string());
455                e
456            })?;
457
458        let status = response.status();
459        self.logger.log_http_response(&request_id, status.as_u16());
460        
461        let response_text = response.text().await
462            .map_err(|e| {
463                let elapsed = start_time.elapsed();
464                self.logger.log_response_read_error(&request_id, elapsed, &e.to_string());
465                e
466            })?;
467        self.logger.log_raw_response(&request_id, &response_text);
468        
469        let tushare_response: TushareResponse = serde_json::from_str(&response_text)
470            .map_err(|e| {
471                let elapsed = start_time.elapsed();
472                self.logger.log_json_parse_error(&request_id, elapsed, &e.to_string(), &response_text);
473                e
474            })?;
475
476        let elapsed = start_time.elapsed();
477        
478        if tushare_response.code != 0 {
479            let message = format!("error code: {}, error msg: {}", tushare_response.code, tushare_response.msg.clone().unwrap_or_default());
480            self.logger.log_api_error(&request_id, elapsed, tushare_response.code, &message);
481            return Err(TushareError::ApiError {
482                code: tushare_response.code,
483                message
484            });
485        }
486
487        // Log success information and performance metrics
488        self.logger.log_api_success(&request_id, elapsed, tushare_response.data.clone().map(|data| data.items.len()).unwrap_or(0));
489        
490        // Log response details (if enabled)
491        self.logger.log_response_details(
492            &request_id,
493            &tushare_response.request_id,
494            &format!("{:?}", tushare_response.data.as_ref().map(|d| &d.fields))
495        );
496
497        Ok(tushare_response)
498    }
499
500    /// 调用 Tushare API,并将响应的 `data.items` 解析为强类型的 [`TushareEntityList<T>`]。
501    ///
502    /// 这是 [`Self::call_api`] 的便捷封装:先执行请求,再把响应转换为实体列表。
503    ///
504    /// # Type Parameters
505    ///
506    /// - `T`: 单行数据对应的实体类型(需要实现 [`crate::traits::FromTushareData`])。
507    /// - `R`: 请求类型(需要实现 `TryInto<TushareRequest>`),通常可由参数自动推导。
508    ///
509    /// # Errors
510    ///
511    /// - 请求构造失败、网络/HTTP 错误、JSON/数据映射失败等都会以 [`TushareError`] 返回。
512    ///
513    /// # Example
514    ///
515    /// ```rust
516    /// # use tushare_api::{TushareClient, TushareRequest, TushareEntityList, Api, request, DeriveFromTushareData, params, fields};
517    /// # #[derive(Debug, Clone, DeriveFromTushareData)]
518    /// # struct Stock { ts_code: String }
519    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
520    /// let client = TushareClient::from_env()?;
521    /// let stocks: TushareEntityList<Stock> = client
522    ///     .call_api_as(request!(Api::StockBasic, {}, ["ts_code"]))
523    ///     .await?;
524    /// # Ok(()) }
525    /// ```
526    pub async fn call_api_as<T, R>(&self, request: R) -> TushareResult<TushareEntityList<T>>
527    where
528        T: crate::traits::FromTushareData,
529        for<'a> &'a R: TryInto<TushareRequest>,
530        for<'a> <&'a R as TryInto<TushareRequest>>::Error: Into<TushareError>,
531    {
532        let response = self.call_api(&request).await?;
533        TushareEntityList::try_from(response).map_err(Into::into)
534    }
535 }
536
537 /// Generate a unique request ID for logging purposes
538 fn generate_request_id() -> String {
539     let timestamp = SystemTime::now()
540         .duration_since(UNIX_EPOCH)
541         .unwrap_or_default()
542         .as_nanos();
543     format!("req_{}", timestamp)
544 }
545
546 mod tests {
547    use crate::{fields, params, Api, TushareClient, TushareRequest};
548
549    #[tokio::test]
550    async fn test() {
551        unsafe { std::env::set_var("TUSHARE_TOKEN", "xxxx"); }
552        let client = TushareClient::from_env().unwrap();
553        let response = client.call_api(&r#"
554                   {
555                        "api_name": "stock_basic",
556                        "params": { "list_stauts": "L"},
557                        "fields": [ "ts_code",
558                                "symbol",
559                                "name",
560                                "area",
561                                "industry",
562                                "list_date",
563                                "exchange",
564                                "market"]
565                    }
566            "#
567        ).await;
568        println!("resposne = {:?}", response);
569        // let parmas = params!(
570        //     "list_status" => "L",
571        //     "limit" => "100"
572        // );
573        // let req = TushareRequest::new(Api::StockBasic, parmas, fields!("ts_code"));
574        // let response = client.call_api(req).await.unwrap();
575        // println!("resposne = {:?}", response);
576    }
577}