1use std::{env, fmt, str::FromStr};
25
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
32pub enum RithmicEnv {
33 #[default]
34 Demo,
35 Live,
36 Test,
37}
38
39impl fmt::Display for RithmicEnv {
40 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41 match self {
42 RithmicEnv::Demo => write!(f, "demo"),
43 RithmicEnv::Live => write!(f, "live"),
44 RithmicEnv::Test => write!(f, "test"),
45 }
46 }
47}
48
49impl FromStr for RithmicEnv {
50 type Err = ConfigError;
51
52 fn from_str(s: &str) -> Result<Self, Self::Err> {
53 match s {
54 "demo" | "development" => Ok(RithmicEnv::Demo),
55 "live" | "production" => Ok(RithmicEnv::Live),
56 "test" => Ok(RithmicEnv::Test),
57 _ => Err(ConfigError::InvalidEnvironment(s.to_string())),
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub enum ConfigError {
65 MissingEnvVar(String),
67 InvalidEnvironment(String),
69 InvalidValue { var: String, reason: String },
71 MissingField(String),
73}
74
75impl fmt::Display for ConfigError {
76 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77 match self {
78 ConfigError::MissingEnvVar(var) => {
79 write!(f, "Missing environment variable: {}", var)
80 }
81 ConfigError::InvalidEnvironment(env) => {
82 write!(f, "Invalid environment: {}", env)
83 }
84 ConfigError::InvalidValue { var, reason } => {
85 write!(f, "Invalid value for {}: {}", var, reason)
86 }
87 ConfigError::MissingField(field) => {
88 write!(f, "Missing required field: {}", field)
89 }
90 }
91 }
92}
93
94impl std::error::Error for ConfigError {}
95
96#[derive(Clone, Debug)]
104pub struct RithmicConfig {
105 pub account_id: String,
107 pub fcm_id: String,
108 pub ib_id: String,
109
110 pub url: String,
112 pub beta_url: String,
113 pub user: String,
114 pub password: String,
115 pub system_name: String,
116 pub env: RithmicEnv,
117}
118
119impl RithmicConfig {
120 pub fn from_env(env: RithmicEnv) -> Result<Self, ConfigError> {
163 let (account_id, fcm_id, ib_id, url, beta_url, user, password, system_name) = match &env {
164 RithmicEnv::Demo => (
165 env::var("RITHMIC_DEMO_ACCOUNT_ID").map_err(|_| {
166 ConfigError::MissingEnvVar("RITHMIC_DEMO_ACCOUNT_ID".to_string())
167 })?,
168 env::var("RITHMIC_DEMO_FCM_ID")
169 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_FCM_ID".to_string()))?,
170 env::var("RITHMIC_DEMO_IB_ID")
171 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_IB_ID".to_string()))?,
172 env::var("RITHMIC_DEMO_URL")
173 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_URL".to_string()))?,
174 env::var("RITHMIC_DEMO_ALT_URL")
175 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_ALT_URL".to_string()))?,
176 env::var("RITHMIC_DEMO_USER")
177 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_USER".to_string()))?,
178 env::var("RITHMIC_DEMO_PW")
179 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_PW".to_string()))?,
180 "Rithmic Paper Trading".to_string(),
181 ),
182 RithmicEnv::Live => (
183 env::var("RITHMIC_LIVE_ACCOUNT_ID").map_err(|_| {
184 ConfigError::MissingEnvVar("RITHMIC_LIVE_ACCOUNT_ID".to_string())
185 })?,
186 env::var("RITHMIC_LIVE_FCM_ID")
187 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_FCM_ID".to_string()))?,
188 env::var("RITHMIC_LIVE_IB_ID")
189 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_IB_ID".to_string()))?,
190 env::var("RITHMIC_LIVE_URL")
191 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_URL".to_string()))?,
192 env::var("RITHMIC_LIVE_ALT_URL")
193 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_ALT_URL".to_string()))?,
194 env::var("RITHMIC_LIVE_USER")
195 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_USER".to_string()))?,
196 env::var("RITHMIC_LIVE_PW")
197 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_PW".to_string()))?,
198 "Rithmic 01".to_string(),
199 ),
200 RithmicEnv::Test => (
201 env::var("RITHMIC_TEST_ACCOUNT_ID").map_err(|_| {
202 ConfigError::MissingEnvVar("RITHMIC_TEST_ACCOUNT_ID".to_string())
203 })?,
204 env::var("RITHMIC_TEST_FCM_ID")
205 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_FCM_ID".to_string()))?,
206 env::var("RITHMIC_TEST_IB_ID")
207 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_IB_ID".to_string()))?,
208 env::var("RITHMIC_TEST_URL")
209 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_URL".to_string()))?,
210 env::var("RITHMIC_TEST_ALT_URL")
211 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_ALT_URL".to_string()))?,
212 env::var("RITHMIC_TEST_USER")
213 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_USER".to_string()))?,
214 env::var("RITHMIC_TEST_PW")
215 .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_PW".to_string()))?,
216 "Rithmic Test".to_string(),
217 ),
218 };
219
220 Ok(Self {
221 account_id,
222 fcm_id,
223 ib_id,
224 url,
225 beta_url,
226 user,
227 password,
228 system_name,
229 env,
230 })
231 }
232
233 pub fn builder(env: RithmicEnv) -> RithmicConfigBuilder {
251 RithmicConfigBuilder::new(env)
252 }
253}
254
255#[derive(Default)]
257pub struct RithmicConfigBuilder {
258 env: Option<RithmicEnv>,
259 account_id: Option<String>,
260 fcm_id: Option<String>,
261 ib_id: Option<String>,
262 url: Option<String>,
263 beta_url: Option<String>,
264 user: Option<String>,
265 password: Option<String>,
266 system_name: Option<String>,
267}
268
269impl RithmicConfigBuilder {
270 pub fn new(env: RithmicEnv) -> Self {
272 let system_name = match &env {
274 RithmicEnv::Demo => "Rithmic Paper Trading".to_string(),
275 RithmicEnv::Live => "Rithmic 01".to_string(),
276 RithmicEnv::Test => "Rithmic Test".to_string(),
277 };
278
279 Self {
280 env: Some(env),
281 system_name: Some(system_name),
282 ..Default::default()
283 }
284 }
285
286 pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
288 self.account_id = Some(account_id.into());
289 self
290 }
291
292 pub fn fcm_id(mut self, fcm_id: impl Into<String>) -> Self {
294 self.fcm_id = Some(fcm_id.into());
295 self
296 }
297
298 pub fn ib_id(mut self, ib_id: impl Into<String>) -> Self {
300 self.ib_id = Some(ib_id.into());
301 self
302 }
303
304 pub fn url(mut self, url: impl Into<String>) -> Self {
306 self.url = Some(url.into());
307 self
308 }
309
310 pub fn beta_url(mut self, beta_url: impl Into<String>) -> Self {
312 self.beta_url = Some(beta_url.into());
313 self
314 }
315
316 pub fn user(mut self, user: impl Into<String>) -> Self {
318 self.user = Some(user.into());
319 self
320 }
321
322 pub fn password(mut self, password: impl Into<String>) -> Self {
324 self.password = Some(password.into());
325 self
326 }
327
328 pub fn system_name(mut self, system_name: impl Into<String>) -> Self {
330 self.system_name = Some(system_name.into());
331 self
332 }
333
334 pub fn build(self) -> Result<RithmicConfig, ConfigError> {
338 Ok(RithmicConfig {
339 env: self
340 .env
341 .ok_or_else(|| ConfigError::MissingField("env".to_string()))?,
342 account_id: self
343 .account_id
344 .ok_or_else(|| ConfigError::MissingField("account_id".to_string()))?,
345 fcm_id: self
346 .fcm_id
347 .ok_or_else(|| ConfigError::MissingField("fcm_id".to_string()))?,
348 ib_id: self
349 .ib_id
350 .ok_or_else(|| ConfigError::MissingField("ib_id".to_string()))?,
351 url: self
352 .url
353 .ok_or_else(|| ConfigError::MissingField("url".to_string()))?,
354 beta_url: self
355 .beta_url
356 .ok_or_else(|| ConfigError::MissingField("beta_url".to_string()))?,
357 user: self
358 .user
359 .ok_or_else(|| ConfigError::MissingField("user".to_string()))?,
360 password: self
361 .password
362 .ok_or_else(|| ConfigError::MissingField("password".to_string()))?,
363 system_name: self
364 .system_name
365 .ok_or_else(|| ConfigError::MissingField("system_name".to_string()))?,
366 })
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use serial_test::serial;
374 use std::env;
375
376 fn setup_demo_env_vars() {
378 unsafe {
379 env::set_var("RITHMIC_DEMO_ACCOUNT_ID", "test_account");
380 env::set_var("RITHMIC_DEMO_FCM_ID", "test_fcm");
381 env::set_var("RITHMIC_DEMO_IB_ID", "test_ib");
382 env::set_var("RITHMIC_DEMO_USER", "demo_user");
383 env::set_var("RITHMIC_DEMO_PW", "demo_password");
384 env::set_var("RITHMIC_DEMO_URL", "wss://test-demo.example.com:443");
385 env::set_var(
386 "RITHMIC_DEMO_ALT_URL",
387 "wss://test-demo-alt.example.com:443",
388 );
389 }
390 }
391
392 fn setup_live_env_vars() {
393 unsafe {
394 env::set_var("RITHMIC_LIVE_ACCOUNT_ID", "test_account");
395 env::set_var("RITHMIC_LIVE_FCM_ID", "test_fcm");
396 env::set_var("RITHMIC_LIVE_IB_ID", "test_ib");
397 env::set_var("RITHMIC_LIVE_USER", "live_user");
398 env::set_var("RITHMIC_LIVE_PW", "live_password");
399 env::set_var("RITHMIC_LIVE_URL", "wss://test-live.example.com:443");
400 env::set_var(
401 "RITHMIC_LIVE_ALT_URL",
402 "wss://test-live-alt.example.com:443",
403 );
404 }
405 }
406
407 fn cleanup_env_vars() {
408 unsafe {
409 env::remove_var("RITHMIC_DEMO_ACCOUNT_ID");
410 env::remove_var("RITHMIC_DEMO_FCM_ID");
411 env::remove_var("RITHMIC_DEMO_IB_ID");
412 env::remove_var("RITHMIC_DEMO_USER");
413 env::remove_var("RITHMIC_DEMO_PW");
414 env::remove_var("RITHMIC_DEMO_URL");
415 env::remove_var("RITHMIC_DEMO_ALT_URL");
416 env::remove_var("RITHMIC_LIVE_ACCOUNT_ID");
417 env::remove_var("RITHMIC_LIVE_FCM_ID");
418 env::remove_var("RITHMIC_LIVE_IB_ID");
419 env::remove_var("RITHMIC_LIVE_USER");
420 env::remove_var("RITHMIC_LIVE_PW");
421 env::remove_var("RITHMIC_LIVE_URL");
422 env::remove_var("RITHMIC_LIVE_ALT_URL");
423 env::remove_var("RITHMIC_TEST_ACCOUNT_ID");
424 env::remove_var("RITHMIC_TEST_FCM_ID");
425 env::remove_var("RITHMIC_TEST_IB_ID");
426 env::remove_var("RITHMIC_TEST_USER");
427 env::remove_var("RITHMIC_TEST_PW");
428 env::remove_var("RITHMIC_TEST_URL");
429 env::remove_var("RITHMIC_TEST_ALT_URL");
430 }
431 }
432
433 #[test]
434 fn test_rithmic_env_display() {
435 assert_eq!(RithmicEnv::Demo.to_string(), "demo");
436 assert_eq!(RithmicEnv::Live.to_string(), "live");
437 assert_eq!(RithmicEnv::Test.to_string(), "test");
438 }
439
440 #[test]
441 fn test_rithmic_env_from_str() {
442 assert_eq!("demo".parse::<RithmicEnv>().unwrap(), RithmicEnv::Demo);
443 assert_eq!(
444 "development".parse::<RithmicEnv>().unwrap(),
445 RithmicEnv::Demo
446 );
447 assert_eq!("live".parse::<RithmicEnv>().unwrap(), RithmicEnv::Live);
448 assert_eq!(
449 "production".parse::<RithmicEnv>().unwrap(),
450 RithmicEnv::Live
451 );
452 assert_eq!("test".parse::<RithmicEnv>().unwrap(), RithmicEnv::Test);
453
454 let result = "invalid".parse::<RithmicEnv>();
456 assert!(result.is_err());
457 if let Err(ConfigError::InvalidEnvironment(env)) = result {
458 assert_eq!(env, "invalid");
459 } else {
460 panic!("Expected InvalidEnvironment error");
461 }
462 }
463
464 #[test]
465 fn test_config_error_display() {
466 let err = ConfigError::MissingEnvVar("TEST_VAR".to_string());
467 assert_eq!(err.to_string(), "Missing environment variable: TEST_VAR");
468
469 let err = ConfigError::InvalidEnvironment("bad_env".to_string());
470 assert_eq!(err.to_string(), "Invalid environment: bad_env");
471
472 let err = ConfigError::InvalidValue {
473 var: "TEST".to_string(),
474 reason: "too short".to_string(),
475 };
476 assert_eq!(err.to_string(), "Invalid value for TEST: too short");
477
478 let err = ConfigError::MissingField("account_id".to_string());
479 assert_eq!(err.to_string(), "Missing required field: account_id");
480 }
481
482 #[test]
483 #[serial]
484 fn test_from_env_demo_success() {
485 setup_demo_env_vars();
486
487 let config = RithmicConfig::from_env(RithmicEnv::Demo).unwrap();
488
489 assert_eq!(config.account_id, "test_account");
490 assert_eq!(config.fcm_id, "test_fcm");
491 assert_eq!(config.ib_id, "test_ib");
492 assert_eq!(config.user, "demo_user");
493 assert_eq!(config.password, "demo_password");
494 assert_eq!(config.url, "wss://test-demo.example.com:443");
495 assert_eq!(config.beta_url, "wss://test-demo-alt.example.com:443");
496 assert_eq!(config.system_name, "Rithmic Paper Trading");
497 assert_eq!(config.env, RithmicEnv::Demo);
498
499 cleanup_env_vars();
500 }
501
502 #[test]
503 #[serial]
504 fn test_from_env_live_success() {
505 setup_live_env_vars();
506
507 let config = RithmicConfig::from_env(RithmicEnv::Live).unwrap();
508
509 assert_eq!(config.account_id, "test_account");
510 assert_eq!(config.user, "live_user");
511 assert_eq!(config.password, "live_password");
512 assert_eq!(config.system_name, "Rithmic 01");
513 assert_eq!(config.env, RithmicEnv::Live);
514
515 cleanup_env_vars();
516 }
517
518 #[test]
519 #[serial]
520 fn test_from_env_missing_account_id() {
521 cleanup_env_vars();
522 unsafe {
523 env::set_var("RITHMIC_DEMO_FCM_ID", "test_fcm");
524 env::set_var("RITHMIC_DEMO_IB_ID", "test_ib");
525 env::set_var("RITHMIC_DEMO_USER", "demo_user");
526 env::set_var("RITHMIC_DEMO_PW", "demo_password");
527 env::set_var("RITHMIC_DEMO_URL", "wss://test-demo.example.com:443");
528 env::set_var(
529 "RITHMIC_DEMO_ALT_URL",
530 "wss://test-demo-alt.example.com:443",
531 );
532 }
533
534 let result = RithmicConfig::from_env(RithmicEnv::Demo);
535 assert!(result.is_err());
536
537 if let Err(ConfigError::MissingEnvVar(var)) = result {
538 assert_eq!(var, "RITHMIC_DEMO_ACCOUNT_ID");
539 } else {
540 panic!("Expected MissingEnvVar error");
541 }
542
543 cleanup_env_vars();
544 }
545
546 #[test]
547 #[serial]
548 fn test_from_env_missing_credentials() {
549 cleanup_env_vars();
550 unsafe {
551 env::set_var("RITHMIC_DEMO_ACCOUNT_ID", "test_account");
552 env::set_var("RITHMIC_DEMO_FCM_ID", "test_fcm");
553 env::set_var("RITHMIC_DEMO_IB_ID", "test_ib");
554 env::set_var("RITHMIC_DEMO_URL", "wss://test-demo.example.com:443");
555 env::set_var(
556 "RITHMIC_DEMO_ALT_URL",
557 "wss://test-demo-alt.example.com:443",
558 );
559 }
560
561 let result = RithmicConfig::from_env(RithmicEnv::Demo);
562 assert!(result.is_err());
563
564 if let Err(ConfigError::MissingEnvVar(var)) = result {
565 assert_eq!(var, "RITHMIC_DEMO_USER");
566 } else {
567 panic!("Expected MissingEnvVar error");
568 }
569
570 cleanup_env_vars();
571 }
572
573 #[test]
574 #[serial]
575 fn test_from_env_missing_url() {
576 cleanup_env_vars();
577 unsafe {
578 env::set_var("RITHMIC_DEMO_ACCOUNT_ID", "test_account");
579 env::set_var("RITHMIC_DEMO_FCM_ID", "test_fcm");
580 env::set_var("RITHMIC_DEMO_IB_ID", "test_ib");
581 env::set_var("RITHMIC_DEMO_USER", "demo_user");
582 env::set_var("RITHMIC_DEMO_PW", "demo_password");
583 }
585
586 let result = RithmicConfig::from_env(RithmicEnv::Demo);
587 assert!(result.is_err());
588
589 if let Err(ConfigError::MissingEnvVar(var)) = result {
590 assert_eq!(var, "RITHMIC_DEMO_URL");
591 } else {
592 panic!("Expected MissingEnvVar error");
593 }
594
595 cleanup_env_vars();
596 }
597
598 #[test]
599 fn test_builder_complete() {
600 let config = RithmicConfig::builder(RithmicEnv::Demo)
601 .account_id("my_account")
602 .fcm_id("my_fcm")
603 .ib_id("my_ib")
604 .user("my_user")
605 .password("my_password")
606 .url("wss://test.example.com:443")
607 .beta_url("wss://test-alt.example.com:443")
608 .build()
609 .unwrap();
610
611 assert_eq!(config.account_id, "my_account");
612 assert_eq!(config.fcm_id, "my_fcm");
613 assert_eq!(config.ib_id, "my_ib");
614 assert_eq!(config.user, "my_user");
615 assert_eq!(config.password, "my_password");
616 assert_eq!(config.env, RithmicEnv::Demo);
617 assert_eq!(config.url, "wss://test.example.com:443");
618 assert_eq!(config.beta_url, "wss://test-alt.example.com:443");
619 assert_eq!(config.system_name, "Rithmic Paper Trading");
621 }
622
623 #[test]
624 fn test_builder_custom_urls() {
625 let config = RithmicConfig::builder(RithmicEnv::Demo)
626 .account_id("my_account")
627 .fcm_id("my_fcm")
628 .ib_id("my_ib")
629 .user("my_user")
630 .password("my_password")
631 .url("wss://custom.example.com:443")
632 .beta_url("wss://custom-beta.example.com:443")
633 .system_name("Custom System")
634 .build()
635 .unwrap();
636
637 assert_eq!(config.url, "wss://custom.example.com:443");
638 assert_eq!(config.beta_url, "wss://custom-beta.example.com:443");
639 assert_eq!(config.system_name, "Custom System");
640 }
641
642 #[test]
643 fn test_builder_missing_account_id() {
644 let result = RithmicConfig::builder(RithmicEnv::Demo)
645 .fcm_id("my_fcm")
646 .ib_id("my_ib")
647 .user("my_user")
648 .password("my_password")
649 .build();
650
651 assert!(result.is_err());
652 if let Err(ConfigError::MissingField(field)) = result {
653 assert_eq!(field, "account_id");
654 } else {
655 panic!("Expected MissingField error");
656 }
657 }
658
659 #[test]
660 fn test_builder_missing_user() {
661 let result = RithmicConfig::builder(RithmicEnv::Demo)
662 .account_id("my_account")
663 .fcm_id("my_fcm")
664 .ib_id("my_ib")
665 .password("my_password")
666 .url("wss://test.example.com:443")
667 .beta_url("wss://test-alt.example.com:443")
668 .build();
669
670 assert!(result.is_err());
671 if let Err(ConfigError::MissingField(field)) = result {
672 assert_eq!(field, "user");
673 } else {
674 panic!("Expected MissingField error");
675 }
676 }
677
678 #[test]
679 fn test_builder_demo_defaults() {
680 let builder = RithmicConfigBuilder::new(RithmicEnv::Demo);
681 let config = builder
682 .account_id("test")
683 .fcm_id("test")
684 .ib_id("test")
685 .user("test")
686 .password("test")
687 .url("wss://test.example.com:443")
688 .beta_url("wss://test-alt.example.com:443")
689 .build()
690 .unwrap();
691
692 assert_eq!(config.system_name, "Rithmic Paper Trading");
694 }
695
696 #[test]
697 fn test_builder_live_defaults() {
698 let builder = RithmicConfigBuilder::new(RithmicEnv::Live);
699 let config = builder
700 .account_id("test")
701 .fcm_id("test")
702 .ib_id("test")
703 .user("test")
704 .password("test")
705 .url("wss://test.example.com:443")
706 .beta_url("wss://test-alt.example.com:443")
707 .build()
708 .unwrap();
709
710 assert_eq!(config.system_name, "Rithmic 01");
712 }
713
714 #[test]
715 fn test_builder_test_defaults() {
716 let builder = RithmicConfigBuilder::new(RithmicEnv::Test);
717 let config = builder
718 .account_id("test")
719 .fcm_id("test")
720 .ib_id("test")
721 .user("test")
722 .password("test")
723 .url("wss://test.example.com:443")
724 .beta_url("wss://test-alt.example.com:443")
725 .build()
726 .unwrap();
727
728 assert_eq!(config.system_name, "Rithmic Test");
730 }
731
732 #[test]
733 fn test_builder_into_string_conversions() {
734 let config = RithmicConfig::builder(RithmicEnv::Demo)
736 .account_id(String::from("my_account"))
737 .fcm_id(String::from("my_fcm"))
738 .ib_id(String::from("my_ib"))
739 .user(String::from("my_user"))
740 .password(String::from("my_password"))
741 .url(String::from("wss://test.example.com:443"))
742 .beta_url(String::from("wss://test-alt.example.com:443"))
743 .build()
744 .unwrap();
745
746 assert_eq!(config.account_id, "my_account");
747 }
748}