ig_client/
config.rs

1use crate::constants::{DAYS_TO_BACK_LOOK, DEFAULT_PAGE_SIZE, DEFAULT_SLEEP_TIME};
2use crate::impl_json_display;
3use crate::storage::config::DatabaseConfig;
4use crate::utils::rate_limiter::RateLimitType;
5use dotenv::dotenv;
6use serde::{Deserialize, Serialize};
7use sqlx::postgres::PgPoolOptions;
8use std::env;
9use std::fmt::Debug;
10use std::str::FromStr;
11use tracing::{error, info, warn};
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14/// Authentication credentials for the IG Markets API
15pub struct Credentials {
16    /// Username for the IG Markets account
17    pub username: String,
18    /// Password for the IG Markets account
19    pub password: String,
20    /// Account ID for the IG Markets account
21    pub account_id: String,
22    /// API key for the IG Markets API
23    pub api_key: String,
24    /// Client token for the IG Markets API
25    pub client_token: Option<String>,
26    /// Account token for the IG Markets API
27    pub account_token: Option<String>,
28}
29
30impl_json_display!(Credentials);
31
32#[derive(Debug, Serialize, Deserialize, Clone)]
33/// Main configuration for the IG Markets API client
34pub struct Config {
35    /// Authentication credentials
36    pub credentials: Credentials,
37    /// REST API configuration
38    pub rest_api: RestApiConfig,
39    /// WebSocket API configuration
40    pub websocket: WebSocketConfig,
41    /// Database configuration for data persistence
42    pub database: DatabaseConfig,
43    /// Number of hours between transaction fetching operations
44    pub sleep_hours: u64,
45    /// Number of items to retrieve per page in API requests
46    pub page_size: u32,
47    /// Number of days to look back when fetching historical data
48    pub days_to_look_back: i64,
49    /// Rate limit type to use for API requests
50    pub rate_limit_type: RateLimitType,
51    /// Safety margin for rate limiting (0.0-1.0)
52    pub rate_limit_safety_margin: f64,
53}
54
55impl_json_display!(Config);
56
57#[derive(Debug, Serialize, Deserialize, Clone)]
58/// Configuration for the REST API
59pub struct RestApiConfig {
60    /// Base URL for the IG Markets REST API
61    pub base_url: String,
62    /// Timeout in seconds for REST API requests
63    pub timeout: u64,
64}
65
66impl_json_display!(RestApiConfig);
67
68#[derive(Debug, Serialize, Deserialize, Clone)]
69/// Configuration for the WebSocket API
70pub struct WebSocketConfig {
71    /// URL for the IG Markets WebSocket API
72    pub url: String,
73    /// Reconnect interval in seconds for WebSocket connections
74    pub reconnect_interval: u64,
75}
76
77impl_json_display!(WebSocketConfig);
78
79/// Gets an environment variable or returns a default value if not found or cannot be parsed
80///
81/// # Arguments
82///
83/// * `env_var` - The name of the environment variable
84/// * `default` - The default value to use if the environment variable is not found or cannot be parsed
85///
86/// # Returns
87///
88/// The parsed value of the environment variable or the default value
89pub fn get_env_or_default<T: FromStr>(env_var: &str, default: T) -> T
90where
91    <T as FromStr>::Err: Debug,
92{
93    match env::var(env_var) {
94        Ok(val) => val.parse::<T>().unwrap_or_else(|_| {
95            error!("Failed to parse {}: {}, using default", env_var, val);
96            default
97        }),
98        Err(_) => default,
99    }
100}
101
102impl Default for Config {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl Config {
109    /// Creates a new configuration instance from environment variables
110    ///
111    /// Loads configuration from environment variables or .env file.
112    /// Uses default values if environment variables are not found.
113    ///
114    /// # Returns
115    ///
116    /// A new `Config` instance
117    pub fn new() -> Self {
118        Self::with_rate_limit_type(RateLimitType::OnePerSecond, 0.5)
119    }
120
121    /// Creates a new configuration instance with a specific rate limit type
122    ///
123    /// # Arguments
124    ///
125    /// * `rate_limit_type` - The type of rate limit to enforce
126    /// * `safety_margin` - A value between 0.0 and 1.0 representing the percentage of the actual limit to use
127    ///
128    /// # Returns
129    ///
130    /// A new `Config` instance
131    pub fn with_rate_limit_type(rate_limit_type: RateLimitType, safety_margin: f64) -> Self {
132        // Explicitly load the .env file
133        match dotenv() {
134            Ok(_) => info!("Successfully loaded .env file"),
135            Err(e) => warn!("Failed to load .env file: {}", e),
136        }
137
138        // Check if environment variables are configured
139        let username = get_env_or_default("IG_USERNAME", String::from("default_username"));
140        let password = get_env_or_default("IG_PASSWORD", String::from("default_password"));
141        let api_key = get_env_or_default("IG_API_KEY", String::from("default_api_key"));
142
143        let sleep_hours = get_env_or_default("TX_LOOP_INTERVAL_HOURS", DEFAULT_SLEEP_TIME);
144        let page_size = get_env_or_default("TX_PAGE_SIZE", DEFAULT_PAGE_SIZE);
145        let days_to_look_back = get_env_or_default("TX_DAYS_LOOKBACK", DAYS_TO_BACK_LOOK);
146
147        // Check if we are using default values
148        if username == "default_username" {
149            error!("IG_USERNAME not found in environment variables or .env file");
150        }
151        if password == "default_password" {
152            error!("IG_PASSWORD not found in environment variables or .env file");
153        }
154        if api_key == "default_api_key" {
155            error!("IG_API_KEY not found in environment variables or .env file");
156        }
157
158        // Print information about loaded environment variables
159        info!("Environment variables loaded:");
160        info!(
161            "  IG_USERNAME: {}",
162            if username == "default_username" {
163                "Not set"
164            } else {
165                "Set"
166            }
167        );
168        info!(
169            "  IG_PASSWORD: {}",
170            if password == "default_password" {
171                "Not set"
172            } else {
173                "Set"
174            }
175        );
176        info!(
177            "  IG_API_KEY: {}",
178            if api_key == "default_api_key" {
179                "Not set"
180            } else {
181                "Set"
182            }
183        );
184
185        // Ensure safety margin is within valid range
186        let safety_margin = safety_margin.clamp(0.1, 1.0);
187
188        Config {
189            credentials: Credentials {
190                username,
191                password,
192                account_id: get_env_or_default("IG_ACCOUNT_ID", String::from("default_account_id")),
193                api_key,
194                client_token: None,
195                account_token: None,
196            },
197            rest_api: RestApiConfig {
198                base_url: get_env_or_default(
199                    "IG_REST_BASE_URL",
200                    String::from("https://demo-api.ig.com/gateway/deal"),
201                ),
202                timeout: get_env_or_default("IG_REST_TIMEOUT", 30),
203            },
204            websocket: WebSocketConfig {
205                url: get_env_or_default(
206                    "IG_WS_URL",
207                    String::from("wss://demo-apd.marketdatasystems.com"),
208                ),
209                reconnect_interval: get_env_or_default("IG_WS_RECONNECT_INTERVAL", 5),
210            },
211            database: DatabaseConfig {
212                url: get_env_or_default(
213                    "DATABASE_URL",
214                    String::from("postgres://postgres:postgres@localhost/ig"),
215                ),
216                max_connections: get_env_or_default("DATABASE_MAX_CONNECTIONS", 5),
217            },
218            sleep_hours,
219            page_size,
220            days_to_look_back,
221            rate_limit_type,
222            rate_limit_safety_margin: safety_margin,
223        }
224    }
225
226    /// Creates a PostgreSQL connection pool using the database configuration
227    ///
228    /// # Returns
229    ///
230    /// A Result containing either a PostgreSQL connection pool or an error
231    pub async fn pg_pool(&self) -> Result<sqlx::Pool<sqlx::Postgres>, sqlx::Error> {
232        PgPoolOptions::new()
233            .max_connections(self.database.max_connections)
234            .connect(&self.database.url)
235            .await
236    }
237}
238
239#[cfg(test)]
240mod tests_display {
241    use super::*;
242    use assert_json_diff::assert_json_eq;
243    use serde_json::json;
244
245    #[test]
246    fn test_credentials_display() {
247        let credentials = Credentials {
248            username: "user123".to_string(),
249            password: "pass123".to_string(),
250            account_id: "acc456".to_string(),
251            api_key: "key789".to_string(),
252            client_token: Some("ctoken".to_string()),
253            account_token: None,
254        };
255
256        let display_output = credentials.to_string();
257        let expected_json = json!({
258            "username": "user123",
259            "password": "pass123",
260            "account_id": "acc456",
261            "api_key": "key789",
262            "client_token": "ctoken",
263            "account_token": null
264        });
265
266        assert_json_eq!(
267            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
268            expected_json
269        );
270    }
271
272    #[test]
273    fn test_rest_api_config_display() {
274        let rest_api_config = RestApiConfig {
275            base_url: "https://api.example.com".to_string(),
276            timeout: 30,
277        };
278
279        let display_output = rest_api_config.to_string();
280        let expected_json = json!({
281            "base_url": "https://api.example.com",
282            "timeout": 30
283        });
284
285        assert_json_eq!(
286            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
287            expected_json
288        );
289    }
290
291    #[test]
292    fn test_websocket_config_display() {
293        let websocket_config = WebSocketConfig {
294            url: "wss://ws.example.com".to_string(),
295            reconnect_interval: 5,
296        };
297
298        let display_output = websocket_config.to_string();
299        let expected_json = json!({
300            "url": "wss://ws.example.com",
301            "reconnect_interval": 5
302        });
303
304        assert_json_eq!(
305            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
306            expected_json
307        );
308    }
309
310    #[test]
311    fn test_config_display() {
312        let config = Config {
313            credentials: Credentials {
314                username: "user123".to_string(),
315                password: "pass123".to_string(),
316                account_id: "acc456".to_string(),
317                api_key: "key789".to_string(),
318                client_token: Some("ctoken".to_string()),
319                account_token: None,
320            },
321            rest_api: RestApiConfig {
322                base_url: "https://api.example.com".to_string(),
323                timeout: 30,
324            },
325            websocket: WebSocketConfig {
326                url: "wss://ws.example.com".to_string(),
327                reconnect_interval: 5,
328            },
329            database: DatabaseConfig {
330                url: "postgres://user:pass@localhost/ig_db".to_string(),
331                max_connections: 5,
332            },
333            sleep_hours: 0,
334            page_size: 0,
335            days_to_look_back: 0,
336            rate_limit_type: RateLimitType::NonTradingAccount,
337            rate_limit_safety_margin: 0.8,
338        };
339
340        let display_output = config.to_string();
341        let expected_json = json!({
342            "credentials": {
343                "username": "user123",
344                "password": "pass123",
345                "account_id": "acc456",
346                "api_key": "key789",
347                "client_token": "ctoken",
348                "account_token": null
349            },
350            "rest_api": {
351                "base_url": "https://api.example.com",
352                "timeout": 30
353            },
354            "websocket": {
355                "url": "wss://ws.example.com",
356                "reconnect_interval": 5
357            },
358            "database": {
359                "url": "postgres://user:pass@localhost/ig_db",
360                "max_connections": 5
361            },
362            "sleep_hours": 0,
363            "page_size": 0,
364            "days_to_look_back": 0,
365            "rate_limit_type": "NonTradingAccount",
366            "rate_limit_safety_margin": 0.8
367        });
368
369        assert_json_eq!(
370            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
371            expected_json
372        );
373    }
374}