1use crate::constants::{DAYS_TO_BACK_LOOK, DEFAULT_PAGE_SIZE, DEFAULT_SLEEP_TIME};
2use crate::impl_json_display;
3use crate::storage::config::DatabaseConfig;
4use dotenv::dotenv;
5use serde::{Deserialize, Serialize};
6use sqlx::postgres::PgPoolOptions;
7use std::env;
8use std::fmt::Debug;
9use std::str::FromStr;
10use tracing::{error, info, warn};
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct Credentials {
15 pub username: String,
17 pub password: String,
19 pub account_id: String,
21 pub api_key: String,
23 pub client_token: Option<String>,
25 pub account_token: Option<String>,
27}
28
29impl_json_display!(Credentials);
30
31#[derive(Debug, Serialize, Deserialize, Clone)]
32pub struct Config {
34 pub credentials: Credentials,
36 pub rest_api: RestApiConfig,
38 pub websocket: WebSocketConfig,
40 pub database: DatabaseConfig,
42
43 pub sleep_hours: u64,
45 pub page_size: u32,
47 pub days_to_look_back: i64,
49}
50
51impl_json_display!(Config);
52
53#[derive(Debug, Serialize, Deserialize, Clone)]
54pub struct RestApiConfig {
56 pub base_url: String,
58 pub timeout: u64,
60}
61
62impl_json_display!(RestApiConfig);
63
64#[derive(Debug, Serialize, Deserialize, Clone)]
65pub struct WebSocketConfig {
67 pub url: String,
69 pub reconnect_interval: u64,
71}
72
73impl_json_display!(WebSocketConfig);
74
75pub fn get_env_or_default<T: FromStr>(env_var: &str, default: T) -> T
86where
87 <T as FromStr>::Err: Debug,
88{
89 match env::var(env_var) {
90 Ok(val) => val.parse::<T>().unwrap_or_else(|_| {
91 error!("Failed to parse {}: {}, using default", env_var, val);
92 default
93 }),
94 Err(_) => default,
95 }
96}
97
98impl Default for Config {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl Config {
105 pub fn new() -> Self {
114 match dotenv() {
116 Ok(_) => info!("Successfully loaded .env file"),
117 Err(e) => warn!("Failed to load .env file: {}", e),
118 }
119
120 let username = get_env_or_default("IG_USERNAME", String::from("default_username"));
122 let password = get_env_or_default("IG_PASSWORD", String::from("default_password"));
123 let api_key = get_env_or_default("IG_API_KEY", String::from("default_api_key"));
124
125 let sleep_hours = get_env_or_default("TX_LOOP_INTERVAL_HOURS", DEFAULT_SLEEP_TIME);
126 let page_size = get_env_or_default("TX_PAGE_SIZE", DEFAULT_PAGE_SIZE);
127 let days_to_look_back = get_env_or_default("TX_DAYS_LOOKBACK", DAYS_TO_BACK_LOOK);
128
129 if username == "default_username" {
131 error!("IG_USERNAME not found in environment variables or .env file");
132 }
133 if password == "default_password" {
134 error!("IG_PASSWORD not found in environment variables or .env file");
135 }
136 if api_key == "default_api_key" {
137 error!("IG_API_KEY not found in environment variables or .env file");
138 }
139
140 info!("Environment variables loaded:");
142 info!(
143 " IG_USERNAME: {}",
144 if username == "default_username" {
145 "Not set"
146 } else {
147 "Set"
148 }
149 );
150 info!(
151 " IG_PASSWORD: {}",
152 if password == "default_password" {
153 "Not set"
154 } else {
155 "Set"
156 }
157 );
158 info!(
159 " IG_API_KEY: {}",
160 if api_key == "default_api_key" {
161 "Not set"
162 } else {
163 "Set"
164 }
165 );
166
167 Config {
168 credentials: Credentials {
169 username,
170 password,
171 account_id: get_env_or_default("IG_ACCOUNT_ID", String::from("default_account_id")),
172 api_key,
173 client_token: None,
174 account_token: None,
175 },
176 rest_api: RestApiConfig {
177 base_url: get_env_or_default(
178 "IG_REST_BASE_URL",
179 String::from("https://demo-api.ig.com/gateway/deal"),
180 ),
181 timeout: get_env_or_default("IG_REST_TIMEOUT", 30),
182 },
183 websocket: WebSocketConfig {
184 url: get_env_or_default(
185 "IG_WS_URL",
186 String::from("wss://demo-apd.marketdatasystems.com"),
187 ),
188 reconnect_interval: get_env_or_default("IG_WS_RECONNECT_INTERVAL", 5),
189 },
190 database: DatabaseConfig {
191 url: get_env_or_default(
192 "DATABASE_URL",
193 String::from("postgres://postgres:postgres@localhost/ig"),
194 ),
195 max_connections: get_env_or_default("DATABASE_MAX_CONNECTIONS", 5),
196 },
197 sleep_hours,
198 page_size,
199 days_to_look_back,
200 }
201 }
202
203 pub async fn pg_pool(&self) -> Result<sqlx::Pool<sqlx::Postgres>, sqlx::Error> {
209 PgPoolOptions::new()
210 .max_connections(self.database.max_connections)
211 .connect(&self.database.url)
212 .await
213 }
214}
215
216#[cfg(test)]
217mod tests_display {
218 use super::*;
219 use assert_json_diff::assert_json_eq;
220 use serde_json::json;
221
222 #[test]
223 fn test_credentials_display() {
224 let credentials = Credentials {
225 username: "user123".to_string(),
226 password: "pass123".to_string(),
227 account_id: "acc456".to_string(),
228 api_key: "key789".to_string(),
229 client_token: Some("ctoken".to_string()),
230 account_token: None,
231 };
232
233 let display_output = credentials.to_string();
234 let expected_json = json!({
235 "username": "user123",
236 "password": "pass123",
237 "account_id": "acc456",
238 "api_key": "key789",
239 "client_token": "ctoken",
240 "account_token": null
241 });
242
243 assert_json_eq!(
244 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
245 expected_json
246 );
247 }
248
249 #[test]
250 fn test_rest_api_config_display() {
251 let rest_api_config = RestApiConfig {
252 base_url: "https://api.example.com".to_string(),
253 timeout: 30,
254 };
255
256 let display_output = rest_api_config.to_string();
257 let expected_json = json!({
258 "base_url": "https://api.example.com",
259 "timeout": 30
260 });
261
262 assert_json_eq!(
263 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
264 expected_json
265 );
266 }
267
268 #[test]
269 fn test_websocket_config_display() {
270 let websocket_config = WebSocketConfig {
271 url: "wss://ws.example.com".to_string(),
272 reconnect_interval: 5,
273 };
274
275 let display_output = websocket_config.to_string();
276 let expected_json = json!({
277 "url": "wss://ws.example.com",
278 "reconnect_interval": 5
279 });
280
281 assert_json_eq!(
282 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
283 expected_json
284 );
285 }
286
287 #[test]
288 fn test_config_display() {
289 let config = Config {
290 credentials: Credentials {
291 username: "user123".to_string(),
292 password: "pass123".to_string(),
293 account_id: "acc456".to_string(),
294 api_key: "key789".to_string(),
295 client_token: Some("ctoken".to_string()),
296 account_token: None,
297 },
298 rest_api: RestApiConfig {
299 base_url: "https://api.example.com".to_string(),
300 timeout: 30,
301 },
302 websocket: WebSocketConfig {
303 url: "wss://ws.example.com".to_string(),
304 reconnect_interval: 5,
305 },
306 database: DatabaseConfig {
307 url: "postgres://user:pass@localhost/ig_db".to_string(),
308 max_connections: 5,
309 },
310 sleep_hours: 0,
311 page_size: 0,
312 days_to_look_back: 0,
313 };
314
315 let display_output = config.to_string();
316 let expected_json = json!({
317 "credentials": {
318 "username": "user123",
319 "password": "pass123",
320 "account_id": "acc456",
321 "api_key": "key789",
322 "client_token": "ctoken",
323 "account_token": null
324 },
325 "rest_api": {
326 "base_url": "https://api.example.com",
327 "timeout": 30
328 },
329 "websocket": {
330 "url": "wss://ws.example.com",
331 "reconnect_interval": 5
332 },
333 "database": {
334 "url": "postgres://user:pass@localhost/ig_db",
335 "max_connections": 5
336 },
337 "sleep_hours": 0,
338 "page_size": 0,
339 "days_to_look_back": 0
340 });
341
342 assert_json_eq!(
343 serde_json::from_str::<serde_json::Value>(&display_output).unwrap(),
344 expected_json
345 );
346 }
347}