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)]
13pub struct Credentials {
15 pub username: String,
17 pub password: String,
19 pub account_id: String,
21 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)]
28pub struct Config {
30 pub credentials: Credentials,
32 pub rest_api: RestApiConfig,
34 pub websocket: WebSocketConfig,
36 pub database: DatabaseConfig,
38}
39
40#[derive(Debug, Deserialize, Clone)]
41pub struct RestApiConfig {
43 pub base_url: String,
45 pub timeout: u64,
47}
48
49#[derive(Debug, Deserialize, Clone)]
50pub struct WebSocketConfig {
52 pub url: String,
54 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
104pub 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 pub fn new() -> Self {
143 match dotenv() {
145 Ok(_) => info!("Successfully loaded .env file"),
146 Err(e) => warn!("Failed to load .env file: {}", e),
147 }
148
149 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 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 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 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}