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)]
12pub struct Credentials {
14 pub username: String,
16 pub password: String,
18 pub account_id: String,
20 pub api_key: String,
22 pub client_token: Option<String>,
24 pub account_token: Option<String>,
26}
27
28#[derive(Debug, Deserialize, Clone)]
29pub struct Config {
31 pub credentials: Credentials,
33 pub rest_api: RestApiConfig,
35 pub websocket: WebSocketConfig,
37 pub database: DatabaseConfig,
39}
40
41#[derive(Debug, Deserialize, Clone)]
42pub struct RestApiConfig {
44 pub base_url: String,
46 pub timeout: u64,
48}
49
50#[derive(Debug, Deserialize, Clone)]
51pub struct WebSocketConfig {
53 pub url: String,
55 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
105pub 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 pub fn new() -> Self {
144 match dotenv() {
146 Ok(_) => info!("Successfully loaded .env file"),
147 Err(e) => warn!("Failed to load .env file: {}", e),
148 }
149
150 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 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 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 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}