dceapi_rs/
http.rs

1//! HTTP client for DCE API requests.
2//!
3//! Provides the base HTTP functionality with automatic token handling and retry logic.
4
5use std::sync::Arc;
6
7use reqwest::Client as HttpClient;
8use serde::{de::DeserializeOwned, Serialize};
9
10use crate::config::Config;
11use crate::error::{Error, ErrorCode, Result};
12use crate::models::ApiResponse;
13use crate::token::TokenManager;
14
15/// Request options that can be set per-request.
16#[derive(Debug, Clone)]
17pub struct RequestOptions {
18    /// Trade type override (1 = futures, 2 = options).
19    pub trade_type: Option<i32>,
20    /// Language override.
21    pub lang: Option<String>,
22}
23
24impl Default for RequestOptions {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl RequestOptions {
31    /// Create new request options with defaults.
32    pub fn new() -> Self {
33        RequestOptions {
34            trade_type: None,
35            lang: None,
36        }
37    }
38
39    /// Set trade type.
40    pub fn with_trade_type(mut self, trade_type: i32) -> Self {
41        self.trade_type = Some(trade_type);
42        self
43    }
44
45    /// Set language.
46    pub fn with_lang(mut self, lang: impl Into<String>) -> Self {
47        self.lang = Some(lang.into());
48        self
49    }
50}
51
52/// Base HTTP client for API requests.
53#[derive(Debug, Clone)]
54pub struct BaseClient {
55    config: Arc<Config>,
56    http_client: HttpClient,
57    token_manager: Arc<TokenManager>,
58}
59
60impl BaseClient {
61    /// Create a new base client.
62    pub fn new(config: Config, http_client: HttpClient, token_manager: Arc<TokenManager>) -> Self {
63        BaseClient {
64            config: Arc::new(config),
65            http_client,
66            token_manager,
67        }
68    }
69
70    /// Execute an HTTP request.
71    ///
72    /// Handles token management, serialization, and response parsing.
73    pub async fn do_request<T, R>(
74        &self,
75        method: reqwest::Method,
76        path: &str,
77        body: Option<&T>,
78        opts: Option<RequestOptions>,
79    ) -> Result<R>
80    where
81        T: Serialize,
82        R: DeserializeOwned,
83    {
84        let opts = opts.unwrap_or_default();
85        
86        // First attempt
87        let result = self.execute_request(&method, path, body, &opts).await;
88        
89        // Check if we need to retry due to token expiry
90        if let Err(Error::Api { code, .. }) = &result {
91            if *code == ErrorCode::TokenExpired as i32 {
92                // Refresh token and retry once
93                self.token_manager.refresh().await?;
94                return self.execute_request(&method, path, body, &opts).await;
95            }
96        }
97        
98        result
99    }
100
101    /// Execute a single HTTP request (no retry).
102    async fn execute_request<T, R>(
103        &self,
104        method: &reqwest::Method,
105        path: &str,
106        body: Option<&T>,
107        opts: &RequestOptions,
108    ) -> Result<R>
109    where
110        T: Serialize,
111        R: DeserializeOwned,
112    {
113        // Get token
114        let token = self.token_manager.token().await?;
115
116        // Build URL
117        let url = format!("{}{}", self.config.base_url, path);
118
119        // Build request
120        let mut request = self.http_client.request(method.clone(), &url);
121
122        // Set headers
123        request = request
124            .header("Content-Type", "application/json")
125            .header("Authorization", format!("Bearer {}", token))
126            .header("apikey", &self.config.api_key)
127            .header(
128                "tradeType",
129                opts.trade_type.unwrap_or(self.config.trade_type).to_string(),
130            );
131
132        if let Some(lang) = opts.lang.as_ref().or(Some(&self.config.lang)) {
133            request = request.header("lang", lang);
134        }
135
136        // Set body if present
137        if let Some(body) = body {
138            request = request.json(body);
139        }
140
141        // Send request
142        let response = request.send().await?;
143
144        // Read response body
145        let resp_text = response.text().await?;
146
147        // Handle response
148        self.parse_response(&resp_text)
149    }
150
151    /// Parse API response and handle error codes.
152    fn parse_response<R>(&self, resp_text: &str) -> Result<R>
153    where
154        R: DeserializeOwned,
155    {
156        // Parse API response
157        let api_resp: ApiResponse = serde_json::from_str(resp_text).map_err(|e| {
158            Error::parse(resp_text, format!("failed to parse response: {}", e))
159        })?;
160
161        // Handle response based on code
162        match ErrorCode::from_code(api_resp.code) {
163            Some(ErrorCode::Success) => {
164                // Success - deserialize data
165                serde_json::from_value(api_resp.data).map_err(|e| {
166                    Error::parse(
167                        resp_text,
168                        format!("failed to deserialize response data: {}", e),
169                    )
170                })
171            }
172
173            Some(ErrorCode::ParamError) => {
174                // 400: Parameter error
175                Err(Error::api(ErrorCode::ParamError as i32, api_resp.msg))
176            }
177
178            Some(ErrorCode::NoPermission) => {
179                // 401: Permission denied
180                Err(Error::api(ErrorCode::NoPermission as i32, api_resp.msg))
181            }
182
183            Some(ErrorCode::TokenExpired) => {
184                // 402: Token expired
185                Err(Error::api(ErrorCode::TokenExpired as i32, api_resp.msg))
186            }
187
188            Some(ErrorCode::ServerError) => {
189                // 500: Server error
190                Err(Error::api(ErrorCode::ServerError as i32, api_resp.msg))
191            }
192
193            Some(ErrorCode::RateLimit) => {
194                // 501: Rate limit
195                Err(Error::api(ErrorCode::RateLimit as i32, api_resp.msg))
196            }
197
198            None => {
199                // Unknown error code
200                Err(Error::api(api_resp.code, api_resp.msg))
201            }
202        }
203    }
204
205    /// Convenience method for GET requests.
206    pub async fn do_get<R>(&self, path: &str, opts: Option<RequestOptions>) -> Result<R>
207    where
208        R: DeserializeOwned,
209    {
210        self.do_request::<(), R>(reqwest::Method::GET, path, None, opts)
211            .await
212    }
213
214    /// Convenience method for POST requests.
215    pub async fn do_post<T, R>(
216        &self,
217        path: &str,
218        body: &T,
219        opts: Option<RequestOptions>,
220    ) -> Result<R>
221    where
222        T: Serialize,
223        R: DeserializeOwned,
224    {
225        self.do_request(reqwest::Method::POST, path, Some(body), opts)
226            .await
227    }
228
229    /// Get reference to the config.
230    pub fn config(&self) -> &Config {
231        &self.config
232    }
233
234    /// Get reference to the token manager.
235    pub fn token_manager(&self) -> &TokenManager {
236        &self.token_manager
237    }
238}