1use crate::constants::{DAYS_TO_BACK_LOOK, DEFAULT_PAGE_SIZE, DEFAULT_SLEEP_TIME};
2use crate::storage::config::DatabaseConfig;
3use crate::utils::rate_limiter::RateLimitType;
4use dotenv::dotenv;
5use pretty_simple_display::DisplaySimple;
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, DisplaySimple, 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
31#[derive(Debug, DisplaySimple, Serialize, Deserialize, Clone)]
32pub struct Config {
34 pub credentials: Credentials,
36 pub rest_api: RestApiConfig,
38 pub websocket: WebSocketConfig,
40 pub database: DatabaseConfig,
42 pub sleep_hours: u64,
44 pub page_size: u32,
46 pub days_to_look_back: i64,
48 pub rate_limit_type: RateLimitType,
50 pub rate_limit_safety_margin: f64,
52 pub api_version: Option<u8>,
54}
55
56#[derive(Debug, DisplaySimple, Serialize, Deserialize, Clone)]
57pub struct RestApiConfig {
59 pub base_url: String,
61 pub timeout: u64,
63}
64
65#[derive(Debug, DisplaySimple, Serialize, Deserialize, Clone)]
66pub struct WebSocketConfig {
68 pub url: String,
70 pub reconnect_interval: u64,
72}
73
74pub fn get_env_or_default<T: FromStr>(env_var: &str, default: T) -> T
85where
86 <T as FromStr>::Err: Debug,
87{
88 match env::var(env_var) {
89 Ok(val) => val.parse::<T>().unwrap_or_else(|_| {
90 error!("Failed to parse {}: {}, using default", env_var, val);
91 default
92 }),
93 Err(_) => default,
94 }
95}
96
97impl Default for Config {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl Config {
104 pub fn new() -> Self {
113 Self::with_rate_limit_type(RateLimitType::OnePerSecond, 0.5)
114 }
115
116 pub fn with_rate_limit_type(rate_limit_type: RateLimitType, safety_margin: f64) -> Self {
127 match dotenv() {
129 Ok(_) => debug!("Successfully loaded .env file"),
130 Err(e) => debug!("Failed to load .env file: {e}"),
131 }
132
133 let username = get_env_or_default("IG_USERNAME", String::from("default_username"));
135 let password = get_env_or_default("IG_PASSWORD", String::from("default_password"));
136 let api_key = get_env_or_default("IG_API_KEY", String::from("default_api_key"));
137
138 let sleep_hours = get_env_or_default("TX_LOOP_INTERVAL_HOURS", DEFAULT_SLEEP_TIME);
139 let page_size = get_env_or_default("TX_PAGE_SIZE", DEFAULT_PAGE_SIZE);
140 let days_to_look_back = get_env_or_default("TX_DAYS_LOOKBACK", DAYS_TO_BACK_LOOK);
141
142 if username == "default_username" {
144 error!("IG_USERNAME not found in environment variables or .env file");
145 }
146 if password == "default_password" {
147 error!("IG_PASSWORD not found in environment variables or .env file");
148 }
149 if api_key == "default_api_key" {
150 error!("IG_API_KEY not found in environment variables or .env file");
151 }
152
153 debug!("Environment variables loaded:");
155 debug!(
156 " IG_USERNAME: {}",
157 if username == "default_username" {
158 "Not set"
159 } else {
160 "Set"
161 }
162 );
163 debug!(
164 " IG_PASSWORD: {}",
165 if password == "default_password" {
166 "Not set"
167 } else {
168 "Set"
169 }
170 );
171 debug!(
172 " IG_API_KEY: {}",
173 if api_key == "default_api_key" {
174 "Not set"
175 } else {
176 "Set"
177 }
178 );
179
180 let safety_margin = safety_margin.clamp(0.1, 1.0);
182
183 Config {
184 credentials: Credentials {
185 username,
186 password,
187 account_id: get_env_or_default("IG_ACCOUNT_ID", String::from("default_account_id")),
188 api_key,
189 client_token: None,
190 account_token: None,
191 },
192 rest_api: RestApiConfig {
193 base_url: get_env_or_default(
194 "IG_REST_BASE_URL",
195 String::from("https://demo-api.ig.com/gateway/deal"),
196 ),
197 timeout: get_env_or_default("IG_REST_TIMEOUT", 30),
198 },
199 websocket: WebSocketConfig {
200 url: get_env_or_default(
201 "IG_WS_URL",
202 String::from("wss://demo-apd.marketdatasystems.com"),
203 ),
204 reconnect_interval: get_env_or_default("IG_WS_RECONNECT_INTERVAL", 5),
205 },
206 database: DatabaseConfig {
207 url: get_env_or_default(
208 "DATABASE_URL",
209 String::from("postgres://postgres:postgres@localhost/ig"),
210 ),
211 max_connections: get_env_or_default("DATABASE_MAX_CONNECTIONS", 5),
212 },
213 sleep_hours,
214 page_size,
215 days_to_look_back,
216 rate_limit_type,
217 rate_limit_safety_margin: safety_margin,
218 api_version: env::var("IG_API_VERSION")
219 .ok()
220 .and_then(|v| v.parse::<u8>().ok())
221 .filter(|&v| v == 2 || v == 3)
222 .or(Some(3)), }
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 api_version: None,
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 "api_version": null
369 });
370
371 assert_json_eq!(
372 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
373 expected_json
374 );
375 }
376}