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