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#[derive(Debug, Deserialize, Clone)]
12/// Authentication credentials for the IG Markets API
13pub struct Credentials {
14    /// Username for the IG Markets account
15    pub username: String,
16    /// Password for the IG Markets account
17    pub password: String,
18    /// Account ID for the IG Markets account
19    pub account_id: String,
20    /// API key for the IG Markets API
21    pub api_key: String,
22    /// Client token for the IG Markets API
23    pub client_token: Option<String>,
24    /// Account token for the IG Markets API
25    pub account_token: Option<String>,
26}
27
28#[derive(Debug, Deserialize, Clone)]
29/// Main configuration for the IG Markets API client
30pub struct Config {
31    /// Authentication credentials
32    pub credentials: Credentials,
33    /// REST API configuration
34    pub rest_api: RestApiConfig,
35    /// WebSocket API configuration
36    pub websocket: WebSocketConfig,
37    /// Database configuration for data persistence
38    pub database: DatabaseConfig,
39}
40
41#[derive(Debug, Deserialize, Clone)]
42/// Configuration for the REST API
43pub struct RestApiConfig {
44    /// Base URL for the IG Markets REST API
45    pub base_url: String,
46    /// Timeout in seconds for REST API requests
47    pub timeout: u64,
48}
49
50#[derive(Debug, Deserialize, Clone)]
51/// Configuration for the WebSocket API
52pub struct WebSocketConfig {
53    /// URL for the IG Markets WebSocket API
54    pub url: String,
55    /// Reconnect interval in seconds for WebSocket connections
56    pub reconnect_interval: u64,
57}
58
59impl fmt::Display for Credentials {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(
62            f,
63            "{{\"username\":\"{}\",\"password\":\"[REDACTED]\",\"account_id\":\"[REDACTED]\",\"api_key\":\"[REDACTED]\",\"client_token\":{},\"account_token\":{}}}",
64            self.username,
65            self.client_token
66                .as_ref()
67                .map_or("null".to_string(), |_| "\"[REDACTED]\"".to_string()),
68            self.account_token
69                .as_ref()
70                .map_or("null".to_string(), |_| "\"[REDACTED]\"".to_string())
71        )
72    }
73}
74
75impl fmt::Display for Config {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(
78            f,
79            "{{\"credentials\":{},\"rest_api\":{},\"websocket\":{},\"database\":{}}}",
80            self.credentials, self.rest_api, self.websocket, self.database
81        )
82    }
83}
84
85impl fmt::Display for RestApiConfig {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(
88            f,
89            "{{\"base_url\":\"{}\",\"timeout\":{}}}",
90            self.base_url, self.timeout
91        )
92    }
93}
94
95impl fmt::Display for WebSocketConfig {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(
98            f,
99            "{{\"url\":\"{}\",\"reconnect_interval\":{}}}",
100            self.url, self.reconnect_interval
101        )
102    }
103}
104
105/// Gets an environment variable or returns a default value if not found or cannot be parsed
106///
107/// # Arguments
108///
109/// * `env_var` - The name of the environment variable
110/// * `default` - The default value to use if the environment variable is not found or cannot be parsed
111///
112/// # Returns
113///
114/// The parsed value of the environment variable or the default value
115pub fn get_env_or_default<T: FromStr>(env_var: &str, default: T) -> T
116where
117    <T as FromStr>::Err: Debug,
118{
119    match env::var(env_var) {
120        Ok(val) => val.parse::<T>().unwrap_or_else(|_| {
121            error!("Failed to parse {}: {}, using default", env_var, val);
122            default
123        }),
124        Err(_) => default,
125    }
126}
127
128impl Default for Config {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl Config {
135    /// Creates a new configuration instance from environment variables
136    ///
137    /// Loads configuration from environment variables or .env file.
138    /// Uses default values if environment variables are not found.
139    ///
140    /// # Returns
141    ///
142    /// A new `Config` instance
143    pub fn new() -> Self {
144        // Explicitly load the .env file
145        match dotenv() {
146            Ok(_) => info!("Successfully loaded .env file"),
147            Err(e) => warn!("Failed to load .env file: {}", e),
148        }
149
150        // Check if environment variables are configured
151        let username = get_env_or_default("IG_USERNAME", String::from("default_username"));
152        let password = get_env_or_default("IG_PASSWORD", String::from("default_password"));
153        let api_key = get_env_or_default("IG_API_KEY", String::from("default_api_key"));
154
155        // Check if we are using default values
156        if username == "default_username" {
157            error!("IG_USERNAME not found in environment variables or .env file");
158        }
159        if password == "default_password" {
160            error!("IG_PASSWORD not found in environment variables or .env file");
161        }
162        if api_key == "default_api_key" {
163            error!("IG_API_KEY not found in environment variables or .env file");
164        }
165
166        // Print information about loaded environment variables
167        info!("Environment variables loaded:");
168        info!(
169            "  IG_USERNAME: {}",
170            if username == "default_username" {
171                "Not set"
172            } else {
173                "Set"
174            }
175        );
176        info!(
177            "  IG_PASSWORD: {}",
178            if password == "default_password" {
179                "Not set"
180            } else {
181                "Set"
182            }
183        );
184        info!(
185            "  IG_API_KEY: {}",
186            if api_key == "default_api_key" {
187                "Not set"
188            } else {
189                "Set"
190            }
191        );
192
193        Config {
194            credentials: Credentials {
195                username,
196                password,
197                account_id: get_env_or_default("IG_ACCOUNT_ID", String::from("default_account_id")),
198                api_key,
199                client_token: None,
200                account_token: None,
201            },
202            rest_api: RestApiConfig {
203                base_url: get_env_or_default(
204                    "IG_REST_BASE_URL",
205                    String::from("https://demo-api.ig.com/gateway/deal"),
206                ),
207                timeout: get_env_or_default("IG_REST_TIMEOUT", 30),
208            },
209            websocket: WebSocketConfig {
210                url: get_env_or_default(
211                    "IG_WS_URL",
212                    String::from("wss://demo-apd.marketdatasystems.com"),
213                ),
214                reconnect_interval: get_env_or_default("IG_WS_RECONNECT_INTERVAL", 5),
215            },
216            database: DatabaseConfig {
217                url: get_env_or_default(
218                    "DATABASE_URL",
219                    String::from("postgres://postgres:postgres@localhost/ig"),
220                ),
221                max_connections: get_env_or_default("DATABASE_MAX_CONNECTIONS", 5),
222            },
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": "[REDACTED]",
260            "account_id": "[REDACTED]",
261            "api_key": "[REDACTED]",
262            "client_token": "[REDACTED]",
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        };
334
335        let display_output = config.to_string();
336        let expected_json = json!({
337            "credentials": {
338                "username": "user123",
339                "password": "[REDACTED]",
340                "account_id": "[REDACTED]",
341                "api_key": "[REDACTED]",
342                "client_token": "[REDACTED]",
343                "account_token": null
344            },
345            "rest_api": {
346                "base_url": "https://api.example.com",
347                "timeout": 30
348            },
349            "websocket": {
350                "url": "wss://ws.example.com",
351                "reconnect_interval": 5
352            },
353            "database": {
354                "url": "postgres://user:pass@localhost/ig_db",
355                "max_connections": 5
356            }
357        });
358
359        assert_json_eq!(
360            serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
361            expected_json
362        );
363    }
364}