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