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, info, warn};
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct Credentials {
16 pub username: String,
18 pub password: String,
20 pub account_id: String,
22 pub api_key: String,
24 pub client_token: Option<String>,
26 pub account_token: Option<String>,
28}
29
30impl_json_display!(Credentials);
31
32#[derive(Debug, Serialize, Deserialize, Clone)]
33pub struct Config {
35 pub credentials: Credentials,
37 pub rest_api: RestApiConfig,
39 pub websocket: WebSocketConfig,
41 pub database: DatabaseConfig,
43 pub sleep_hours: u64,
45 pub page_size: u32,
47 pub days_to_look_back: i64,
49 pub rate_limit_type: RateLimitType,
51 pub rate_limit_safety_margin: f64,
53}
54
55impl_json_display!(Config);
56
57#[derive(Debug, Serialize, Deserialize, Clone)]
58pub struct RestApiConfig {
60 pub base_url: String,
62 pub timeout: u64,
64}
65
66impl_json_display!(RestApiConfig);
67
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct WebSocketConfig {
71 pub url: String,
73 pub reconnect_interval: u64,
75}
76
77impl_json_display!(WebSocketConfig);
78
79pub fn get_env_or_default<T: FromStr>(env_var: &str, default: T) -> T
90where
91 <T as FromStr>::Err: Debug,
92{
93 match env::var(env_var) {
94 Ok(val) => val.parse::<T>().unwrap_or_else(|_| {
95 error!("Failed to parse {}: {}, using default", env_var, val);
96 default
97 }),
98 Err(_) => default,
99 }
100}
101
102impl Default for Config {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl Config {
109 pub fn new() -> Self {
118 Self::with_rate_limit_type(RateLimitType::OnePerSecond, 0.5)
119 }
120
121 pub fn with_rate_limit_type(rate_limit_type: RateLimitType, safety_margin: f64) -> Self {
132 match dotenv() {
134 Ok(_) => info!("Successfully loaded .env file"),
135 Err(e) => warn!("Failed to load .env file: {}", e),
136 }
137
138 let username = get_env_or_default("IG_USERNAME", String::from("default_username"));
140 let password = get_env_or_default("IG_PASSWORD", String::from("default_password"));
141 let api_key = get_env_or_default("IG_API_KEY", String::from("default_api_key"));
142
143 let sleep_hours = get_env_or_default("TX_LOOP_INTERVAL_HOURS", DEFAULT_SLEEP_TIME);
144 let page_size = get_env_or_default("TX_PAGE_SIZE", DEFAULT_PAGE_SIZE);
145 let days_to_look_back = get_env_or_default("TX_DAYS_LOOKBACK", DAYS_TO_BACK_LOOK);
146
147 if username == "default_username" {
149 error!("IG_USERNAME not found in environment variables or .env file");
150 }
151 if password == "default_password" {
152 error!("IG_PASSWORD not found in environment variables or .env file");
153 }
154 if api_key == "default_api_key" {
155 error!("IG_API_KEY not found in environment variables or .env file");
156 }
157
158 info!("Environment variables loaded:");
160 info!(
161 " IG_USERNAME: {}",
162 if username == "default_username" {
163 "Not set"
164 } else {
165 "Set"
166 }
167 );
168 info!(
169 " IG_PASSWORD: {}",
170 if password == "default_password" {
171 "Not set"
172 } else {
173 "Set"
174 }
175 );
176 info!(
177 " IG_API_KEY: {}",
178 if api_key == "default_api_key" {
179 "Not set"
180 } else {
181 "Set"
182 }
183 );
184
185 let safety_margin = safety_margin.clamp(0.1, 1.0);
187
188 Config {
189 credentials: Credentials {
190 username,
191 password,
192 account_id: get_env_or_default("IG_ACCOUNT_ID", String::from("default_account_id")),
193 api_key,
194 client_token: None,
195 account_token: None,
196 },
197 rest_api: RestApiConfig {
198 base_url: get_env_or_default(
199 "IG_REST_BASE_URL",
200 String::from("https://demo-api.ig.com/gateway/deal"),
201 ),
202 timeout: get_env_or_default("IG_REST_TIMEOUT", 30),
203 },
204 websocket: WebSocketConfig {
205 url: get_env_or_default(
206 "IG_WS_URL",
207 String::from("wss://demo-apd.marketdatasystems.com"),
208 ),
209 reconnect_interval: get_env_or_default("IG_WS_RECONNECT_INTERVAL", 5),
210 },
211 database: DatabaseConfig {
212 url: get_env_or_default(
213 "DATABASE_URL",
214 String::from("postgres://postgres:postgres@localhost/ig"),
215 ),
216 max_connections: get_env_or_default("DATABASE_MAX_CONNECTIONS", 5),
217 },
218 sleep_hours,
219 page_size,
220 days_to_look_back,
221 rate_limit_type,
222 rate_limit_safety_margin: safety_margin,
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": "pass123",
260 "account_id": "acc456",
261 "api_key": "key789",
262 "client_token": "ctoken",
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 sleep_hours: 0,
334 page_size: 0,
335 days_to_look_back: 0,
336 rate_limit_type: RateLimitType::NonTradingAccount,
337 rate_limit_safety_margin: 0.8,
338 };
339
340 let display_output = config.to_string();
341 let expected_json = json!({
342 "credentials": {
343 "username": "user123",
344 "password": "pass123",
345 "account_id": "acc456",
346 "api_key": "key789",
347 "client_token": "ctoken",
348 "account_token": null
349 },
350 "rest_api": {
351 "base_url": "https://api.example.com",
352 "timeout": 30
353 },
354 "websocket": {
355 "url": "wss://ws.example.com",
356 "reconnect_interval": 5
357 },
358 "database": {
359 "url": "postgres://user:pass@localhost/ig_db",
360 "max_connections": 5
361 },
362 "sleep_hours": 0,
363 "page_size": 0,
364 "days_to_look_back": 0,
365 "rate_limit_type": "NonTradingAccount",
366 "rate_limit_safety_margin": 0.8
367 });
368
369 assert_json_eq!(
370 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
371 expected_json
372 );
373 }
374}