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