ig_client/
config.rs

1use crate::storage::config::DatabaseConfig;
2use dotenv::dotenv;
3use serde::Deserialize;
4use sqlx::postgres::PgPoolOptions;
5use std::env;
6use std::fmt;
7use std::fmt::Debug;
8use std::str::FromStr;
9use tracing::{error, info, warn};
10
11#[allow(dead_code)]
12#[derive(Debug, 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    pub(crate) client_token: Option<String>,
24    pub(crate) account_token: Option<String>,
25}
26
27#[derive(Debug, Deserialize, Clone)]
28/// Main configuration for the IG Markets API client
29pub struct Config {
30    /// Authentication credentials
31    pub credentials: Credentials,
32    /// REST API configuration
33    pub rest_api: RestApiConfig,
34    /// WebSocket API configuration
35    pub websocket: WebSocketConfig,
36    /// Database configuration for data persistence
37    pub database: DatabaseConfig,
38}
39
40#[derive(Debug, Deserialize, Clone)]
41/// Configuration for the REST API
42pub struct RestApiConfig {
43    /// Base URL for the IG Markets REST API
44    pub base_url: String,
45    /// Timeout in seconds for REST API requests
46    pub timeout: u64,
47}
48
49#[derive(Debug, Deserialize, Clone)]
50/// Configuration for the WebSocket API
51pub struct WebSocketConfig {
52    /// URL for the IG Markets WebSocket API
53    pub url: String,
54    /// Reconnect interval in seconds for WebSocket connections
55    pub reconnect_interval: u64,
56}
57
58impl fmt::Display for Credentials {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(
61            f,
62            "{{\"username\":\"{}\",\"password\":\"[REDACTED]\",\"account_id\":\"[REDACTED]\",\"api_key\":\"[REDACTED]\",\"client_token\":{},\"account_token\":{}}}",
63            self.username,
64            self.client_token
65                .as_ref()
66                .map_or("null".to_string(), |_| "\"[REDACTED]\"".to_string()),
67            self.account_token
68                .as_ref()
69                .map_or("null".to_string(), |_| "\"[REDACTED]\"".to_string())
70        )
71    }
72}
73
74impl fmt::Display for Config {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(
77            f,
78            "{{\"credentials\":{},\"rest_api\":{},\"websocket\":{},\"database\":{}}}",
79            self.credentials, self.rest_api, self.websocket, self.database
80        )
81    }
82}
83
84impl fmt::Display for RestApiConfig {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(
87            f,
88            "{{\"base_url\":\"{}\",\"timeout\":{}}}",
89            self.base_url, self.timeout
90        )
91    }
92}
93
94impl fmt::Display for WebSocketConfig {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(
97            f,
98            "{{\"url\":\"{}\",\"reconnect_interval\":{}}}",
99            self.url, self.reconnect_interval
100        )
101    }
102}
103
104/// Gets an environment variable or returns a default value if not found or cannot be parsed
105///
106/// # Arguments
107///
108/// * `env_var` - The name of the environment variable
109/// * `default` - The default value to use if the environment variable is not found or cannot be parsed
110///
111/// # Returns
112///
113/// The parsed value of the environment variable or the default value
114pub fn get_env_or_default<T: FromStr>(env_var: &str, default: T) -> T
115where
116    <T as FromStr>::Err: Debug,
117{
118    match env::var(env_var) {
119        Ok(val) => val.parse::<T>().unwrap_or_else(|_| {
120            error!("Failed to parse {}: {}, using default", env_var, val);
121            default
122        }),
123        Err(_) => default,
124    }
125}
126
127impl Default for Config {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl Config {
134    /// Creates a new configuration instance from environment variables
135    ///
136    /// Loads configuration from environment variables or .env file.
137    /// Uses default values if environment variables are not found.
138    ///
139    /// # Returns
140    ///
141    /// A new `Config` instance
142    pub fn new() -> Self {
143        // Cargar explícitamente el archivo .env
144        match dotenv() {
145            Ok(_) => info!("Successfully loaded .env file"),
146            Err(e) => warn!("Failed to load .env file: {}", e),
147        }
148
149        // Verificar si las variables de entorno están configuradas
150        let username = get_env_or_default("IG_USERNAME", String::from("default_username"));
151        let password = get_env_or_default("IG_PASSWORD", String::from("default_password"));
152        let api_key = get_env_or_default("IG_API_KEY", String::from("default_api_key"));
153
154        // Verificar si estamos usando valores predeterminados
155        if username == "default_username" {
156            error!("IG_USERNAME not found in environment variables or .env file");
157        }
158        if password == "default_password" {
159            error!("IG_PASSWORD not found in environment variables or .env file");
160        }
161        if api_key == "default_api_key" {
162            error!("IG_API_KEY not found in environment variables or .env file");
163        }
164
165        // Imprimir información sobre las variables de entorno cargadas
166        info!("Environment variables loaded:");
167        info!(
168            "  IG_USERNAME: {}",
169            if username == "default_username" {
170                "Not set"
171            } else {
172                "Set"
173            }
174        );
175        info!(
176            "  IG_PASSWORD: {}",
177            if password == "default_password" {
178                "Not set"
179            } else {
180                "Set"
181            }
182        );
183        info!(
184            "  IG_API_KEY: {}",
185            if api_key == "default_api_key" {
186                "Not set"
187            } else {
188                "Set"
189            }
190        );
191
192        Config {
193            credentials: Credentials {
194                username,
195                password,
196                account_id: get_env_or_default("IG_ACCOUNT_ID", String::from("default_account_id")),
197                api_key,
198                client_token: None,
199                account_token: None,
200            },
201            rest_api: RestApiConfig {
202                base_url: get_env_or_default(
203                    "IG_REST_BASE_URL",
204                    String::from("https://demo-api.ig.com/gateway/deal"),
205                ),
206                timeout: get_env_or_default("IG_REST_TIMEOUT", 30),
207            },
208            websocket: WebSocketConfig {
209                url: get_env_or_default(
210                    "IG_WS_URL",
211                    String::from("wss://demo-apd.marketdatasystems.com"),
212                ),
213                reconnect_interval: get_env_or_default("IG_WS_RECONNECT_INTERVAL", 5),
214            },
215            database: DatabaseConfig {
216                url: get_env_or_default(
217                    "DATABASE_URL",
218                    String::from("postgres://postgres:postgres@localhost/ig"),
219                ),
220                max_connections: get_env_or_default("DATABASE_MAX_CONNECTIONS", 5),
221            },
222        }
223    }
224
225    /// Creates a PostgreSQL connection pool using the database configuration
226    ///
227    /// # Returns
228    ///
229    /// A Result containing either a PostgreSQL connection pool or an error
230    pub async fn pg_pool(&self) -> Result<sqlx::Pool<sqlx::Postgres>, sqlx::Error> {
231        PgPoolOptions::new()
232            .max_connections(self.database.max_connections)
233            .connect(&self.database.url)
234            .await
235    }
236}
237
238#[cfg(test)]
239mod tests_config {
240    use super::*;
241    use once_cell::sync::Lazy;
242    use std::sync::Mutex;
243
244    static ENV_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
245
246    fn with_env_vars<F>(vars: Vec<(&str, &str)>, test: F)
247    where
248        F: FnOnce(),
249    {
250        let _lock = ENV_MUTEX.lock().unwrap();
251        let mut old_vars = Vec::new();
252
253        for (key, value) in vars {
254            old_vars.push((key, env::var(key).ok()));
255            unsafe {
256                env::set_var(key, value);
257            }
258        }
259
260        test();
261
262        for (key, value) in old_vars {
263            match value {
264                Some(v) => unsafe { env::set_var(key, v) },
265                None => unsafe { env::remove_var(key) },
266            }
267        }
268    }
269
270    #[test]
271    fn test_config_new() {
272        with_env_vars(
273            vec![
274                ("IG_USERNAME", "test_user"),
275                ("IG_PASSWORD", "test_pass"),
276                ("IG_API_KEY", "test_api_key"),
277                ("IG_REST_BASE_URL", "https://test-api.ig.com"),
278                ("IG_REST_TIMEOUT", "60"),
279                ("IG_WS_URL", "wss://test-ws.ig.com"),
280                ("IG_WS_RECONNECT_INTERVAL", "10"),
281            ],
282            || {
283                let config = Config::new();
284
285                assert_eq!(config.credentials.username, "test_user");
286                assert_eq!(config.credentials.password, "test_pass");
287                assert_eq!(config.credentials.api_key, "test_api_key");
288                assert_eq!(config.rest_api.base_url, "https://test-api.ig.com");
289                assert_eq!(config.rest_api.timeout, 60);
290                assert_eq!(config.websocket.url, "wss://test-ws.ig.com");
291                assert_eq!(config.websocket.reconnect_interval, 10);
292            },
293        );
294    }
295
296    #[test]
297    fn test_default_values() {
298        with_env_vars(vec![], || {
299            let config = Config::new();
300
301            assert_eq!(config.credentials.username, "default_username");
302            assert_eq!(config.credentials.password, "default_password");
303            assert_eq!(config.credentials.api_key, "default_api_key");
304            assert_eq!(
305                config.rest_api.base_url,
306                "https://demo-api.ig.com/gateway/deal"
307            );
308            assert_eq!(config.rest_api.timeout, 30);
309            assert_eq!(config.websocket.url, "wss://demo-apd.marketdatasystems.com");
310            assert_eq!(config.websocket.reconnect_interval, 5);
311        });
312    }
313}
314
315#[cfg(test)]
316mod tests_display {
317    use super::*;
318    use assert_json_diff::assert_json_eq;
319    use serde_json::json;
320
321    #[test]
322    fn test_credentials_display() {
323        let credentials = Credentials {
324            username: "user123".to_string(),
325            password: "pass123".to_string(),
326            account_id: "acc456".to_string(),
327            api_key: "key789".to_string(),
328            client_token: Some("ctoken".to_string()),
329            account_token: None,
330        };
331
332        let display_output = credentials.to_string();
333        let expected_json = json!({
334            "username": "user123",
335            "password": "[REDACTED]",
336            "account_id": "[REDACTED]",
337            "api_key": "[REDACTED]",
338            "client_token": "[REDACTED]",
339            "account_token": null
340        });
341
342        assert_json_eq!(
343            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
344            expected_json
345        );
346    }
347
348    #[test]
349    fn test_rest_api_config_display() {
350        let rest_api_config = RestApiConfig {
351            base_url: "https://api.example.com".to_string(),
352            timeout: 30,
353        };
354
355        let display_output = rest_api_config.to_string();
356        let expected_json = json!({
357            "base_url": "https://api.example.com",
358            "timeout": 30
359        });
360
361        assert_json_eq!(
362            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
363            expected_json
364        );
365    }
366
367    #[test]
368    fn test_websocket_config_display() {
369        let websocket_config = WebSocketConfig {
370            url: "wss://ws.example.com".to_string(),
371            reconnect_interval: 5,
372        };
373
374        let display_output = websocket_config.to_string();
375        let expected_json = json!({
376            "url": "wss://ws.example.com",
377            "reconnect_interval": 5
378        });
379
380        assert_json_eq!(
381            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
382            expected_json
383        );
384    }
385
386    #[test]
387    fn test_config_display() {
388        let config = Config {
389            credentials: Credentials {
390                username: "user123".to_string(),
391                password: "pass123".to_string(),
392                account_id: "acc456".to_string(),
393                api_key: "key789".to_string(),
394                client_token: Some("ctoken".to_string()),
395                account_token: None,
396            },
397            rest_api: RestApiConfig {
398                base_url: "https://api.example.com".to_string(),
399                timeout: 30,
400            },
401            websocket: WebSocketConfig {
402                url: "wss://ws.example.com".to_string(),
403                reconnect_interval: 5,
404            },
405            database: DatabaseConfig {
406                url: "postgres://user:pass@localhost/ig_db".to_string(),
407                max_connections: 5,
408            },
409        };
410
411        let display_output = config.to_string();
412        let expected_json = json!({
413            "credentials": {
414                "username": "user123",
415                "password": "[REDACTED]",
416                "account_id": "[REDACTED]",
417                "api_key": "[REDACTED]",
418                "client_token": "[REDACTED]",
419                "account_token": null
420            },
421            "rest_api": {
422                "base_url": "https://api.example.com",
423                "timeout": 30
424            },
425            "websocket": {
426                "url": "wss://ws.example.com",
427                "reconnect_interval": 5
428            },
429            "database": {
430                "url": "postgres://user:pass@localhost/ig_db",
431                "max_connections": 5
432            }
433        });
434
435        assert_json_eq!(
436            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
437            expected_json
438        );
439    }
440}