1use crate::constants::{DAYS_TO_BACK_LOOK, DEFAULT_PAGE_SIZE, DEFAULT_SLEEP_TIME};
2use crate::impl_json_display;
3use crate::storage::config::DatabaseConfig;
4use crate::utils::rate_limiter::RateLimitType;
5use dotenv::dotenv;
6use serde::{Deserialize, Serialize};
7use sqlx::postgres::PgPoolOptions;
8use std::env;
9use std::fmt::Debug;
10use std::str::FromStr;
11use tracing::error;
12use tracing::log::debug;
13
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct Credentials {
17 pub username: String,
19 pub password: String,
21 pub account_id: String,
23 pub api_key: String,
25 pub client_token: Option<String>,
27 pub account_token: Option<String>,
29}
30
31impl_json_display!(Credentials);
32
33#[derive(Debug, Serialize, Deserialize, Clone)]
34pub struct Config {
36 pub credentials: Credentials,
38 pub rest_api: RestApiConfig,
40 pub websocket: WebSocketConfig,
42 pub database: DatabaseConfig,
44 pub sleep_hours: u64,
46 pub page_size: u32,
48 pub days_to_look_back: i64,
50 pub rate_limit_type: RateLimitType,
52 pub rate_limit_safety_margin: f64,
54 pub api_version: Option<u8>,
56}
57
58impl_json_display!(Config);
59
60#[derive(Debug, Serialize, Deserialize, Clone)]
61pub struct RestApiConfig {
63 pub base_url: String,
65 pub timeout: u64,
67}
68
69impl_json_display!(RestApiConfig);
70
71#[derive(Debug, Serialize, Deserialize, Clone)]
72pub struct WebSocketConfig {
74 pub url: String,
76 pub reconnect_interval: u64,
78}
79
80impl_json_display!(WebSocketConfig);
81
82pub fn get_env_or_default<T: FromStr>(env_var: &str, default: T) -> T
93where
94 <T as FromStr>::Err: Debug,
95{
96 match env::var(env_var) {
97 Ok(val) => val.parse::<T>().unwrap_or_else(|_| {
98 error!("Failed to parse {}: {}, using default", env_var, val);
99 default
100 }),
101 Err(_) => default,
102 }
103}
104
105impl Default for Config {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl Config {
112 pub fn new() -> Self {
121 Self::with_rate_limit_type(RateLimitType::OnePerSecond, 0.5)
122 }
123
124 pub fn with_rate_limit_type(rate_limit_type: RateLimitType, safety_margin: f64) -> Self {
135 match dotenv() {
137 Ok(_) => debug!("Successfully loaded .env file"),
138 Err(e) => debug!("Failed to load .env file: {e}"),
139 }
140
141 let username = get_env_or_default("IG_USERNAME", String::from("default_username"));
143 let password = get_env_or_default("IG_PASSWORD", String::from("default_password"));
144 let api_key = get_env_or_default("IG_API_KEY", String::from("default_api_key"));
145
146 let sleep_hours = get_env_or_default("TX_LOOP_INTERVAL_HOURS", DEFAULT_SLEEP_TIME);
147 let page_size = get_env_or_default("TX_PAGE_SIZE", DEFAULT_PAGE_SIZE);
148 let days_to_look_back = get_env_or_default("TX_DAYS_LOOKBACK", DAYS_TO_BACK_LOOK);
149
150 if username == "default_username" {
152 error!("IG_USERNAME not found in environment variables or .env file");
153 }
154 if password == "default_password" {
155 error!("IG_PASSWORD not found in environment variables or .env file");
156 }
157 if api_key == "default_api_key" {
158 error!("IG_API_KEY not found in environment variables or .env file");
159 }
160
161 debug!("Environment variables loaded:");
163 debug!(
164 " IG_USERNAME: {}",
165 if username == "default_username" {
166 "Not set"
167 } else {
168 "Set"
169 }
170 );
171 debug!(
172 " IG_PASSWORD: {}",
173 if password == "default_password" {
174 "Not set"
175 } else {
176 "Set"
177 }
178 );
179 debug!(
180 " IG_API_KEY: {}",
181 if api_key == "default_api_key" {
182 "Not set"
183 } else {
184 "Set"
185 }
186 );
187
188 let safety_margin = safety_margin.clamp(0.1, 1.0);
190
191 Config {
192 credentials: Credentials {
193 username,
194 password,
195 account_id: get_env_or_default("IG_ACCOUNT_ID", String::from("default_account_id")),
196 api_key,
197 client_token: None,
198 account_token: None,
199 },
200 rest_api: RestApiConfig {
201 base_url: get_env_or_default(
202 "IG_REST_BASE_URL",
203 String::from("https://demo-api.ig.com/gateway/deal"),
204 ),
205 timeout: get_env_or_default("IG_REST_TIMEOUT", 30),
206 },
207 websocket: WebSocketConfig {
208 url: get_env_or_default(
209 "IG_WS_URL",
210 String::from("wss://demo-apd.marketdatasystems.com"),
211 ),
212 reconnect_interval: get_env_or_default("IG_WS_RECONNECT_INTERVAL", 5),
213 },
214 database: DatabaseConfig {
215 url: get_env_or_default(
216 "DATABASE_URL",
217 String::from("postgres://postgres:postgres@localhost/ig"),
218 ),
219 max_connections: get_env_or_default("DATABASE_MAX_CONNECTIONS", 5),
220 },
221 sleep_hours,
222 page_size,
223 days_to_look_back,
224 rate_limit_type,
225 rate_limit_safety_margin: safety_margin,
226 api_version: env::var("IG_API_VERSION")
227 .ok()
228 .and_then(|v| v.parse::<u8>().ok())
229 .filter(|&v| v == 2 || v == 3)
230 .or(Some(3)), }
232 }
233
234 pub async fn pg_pool(&self) -> Result<sqlx::Pool<sqlx::Postgres>, sqlx::Error> {
240 PgPoolOptions::new()
241 .max_connections(self.database.max_connections)
242 .connect(&self.database.url)
243 .await
244 }
245}
246
247#[cfg(test)]
248mod tests_display {
249 use super::*;
250 use assert_json_diff::assert_json_eq;
251 use serde_json::json;
252
253 #[test]
254 fn test_credentials_display() {
255 let credentials = Credentials {
256 username: "user123".to_string(),
257 password: "pass123".to_string(),
258 account_id: "acc456".to_string(),
259 api_key: "key789".to_string(),
260 client_token: Some("ctoken".to_string()),
261 account_token: None,
262 };
263
264 let display_output = credentials.to_string();
265 let expected_json = json!({
266 "username": "user123",
267 "password": "pass123",
268 "account_id": "acc456",
269 "api_key": "key789",
270 "client_token": "ctoken",
271 "account_token": null
272 });
273
274 assert_json_eq!(
275 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
276 expected_json
277 );
278 }
279
280 #[test]
281 fn test_rest_api_config_display() {
282 let rest_api_config = RestApiConfig {
283 base_url: "https://api.example.com".to_string(),
284 timeout: 30,
285 };
286
287 let display_output = rest_api_config.to_string();
288 let expected_json = json!({
289 "base_url": "https://api.example.com",
290 "timeout": 30
291 });
292
293 assert_json_eq!(
294 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
295 expected_json
296 );
297 }
298
299 #[test]
300 fn test_websocket_config_display() {
301 let websocket_config = WebSocketConfig {
302 url: "wss://ws.example.com".to_string(),
303 reconnect_interval: 5,
304 };
305
306 let display_output = websocket_config.to_string();
307 let expected_json = json!({
308 "url": "wss://ws.example.com",
309 "reconnect_interval": 5
310 });
311
312 assert_json_eq!(
313 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
314 expected_json
315 );
316 }
317
318 #[test]
319 fn test_config_display() {
320 let config = Config {
321 credentials: Credentials {
322 username: "user123".to_string(),
323 password: "pass123".to_string(),
324 account_id: "acc456".to_string(),
325 api_key: "key789".to_string(),
326 client_token: Some("ctoken".to_string()),
327 account_token: None,
328 },
329 rest_api: RestApiConfig {
330 base_url: "https://api.example.com".to_string(),
331 timeout: 30,
332 },
333 websocket: WebSocketConfig {
334 url: "wss://ws.example.com".to_string(),
335 reconnect_interval: 5,
336 },
337 database: DatabaseConfig {
338 url: "postgres://user:pass@localhost/ig_db".to_string(),
339 max_connections: 5,
340 },
341 sleep_hours: 0,
342 page_size: 0,
343 days_to_look_back: 0,
344 rate_limit_type: RateLimitType::NonTradingAccount,
345 rate_limit_safety_margin: 0.8,
346 api_version: None,
347 };
348
349 let display_output = config.to_string();
350 let expected_json = json!({
351 "credentials": {
352 "username": "user123",
353 "password": "pass123",
354 "account_id": "acc456",
355 "api_key": "key789",
356 "client_token": "ctoken",
357 "account_token": null
358 },
359 "rest_api": {
360 "base_url": "https://api.example.com",
361 "timeout": 30
362 },
363 "websocket": {
364 "url": "wss://ws.example.com",
365 "reconnect_interval": 5
366 },
367 "database": {
368 "url": "postgres://user:pass@localhost/ig_db",
369 "max_connections": 5
370 },
371 "sleep_hours": 0,
372 "page_size": 0,
373 "days_to_look_back": 0,
374 "rate_limit_type": "NonTradingAccount",
375 "rate_limit_safety_margin": 0.8,
376 "api_version": null
377 });
378
379 assert_json_eq!(
380 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
381 expected_json
382 );
383 }
384}