rithmic_rs/
config.rs

1//! Configuration for Rithmic connections.
2//!
3//! This module provides the primary interface for configuring Rithmic connections.
4//! The [`RithmicConfig`] type contains both account information and connection details.
5//!
6//! # Example
7//! ```no_run
8//! use rithmic_rs::config::{RithmicConfig, RithmicEnv};
9//!
10//! // Simple one-line configuration from environment variables
11//! let config = RithmicConfig::from_env(RithmicEnv::Demo)?;
12//!
13//! // Or build manually if needed
14//! let config = RithmicConfig::builder(RithmicEnv::Demo)
15//!     .account_id("my_account")
16//!     .fcm_id("my_fcm")
17//!     .ib_id("my_ib")
18//!     .user("my_user")
19//!     .password("my_password")
20//!     .build()?;
21//! # Ok::<(), Box<dyn std::error::Error>>(())
22//! ```
23
24use std::{env, fmt, str::FromStr};
25
26/// Trading environment selector.
27///
28/// Determines which Rithmic environment to connect to.
29#[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/// Configuration error types.
63#[derive(Debug, Clone)]
64pub enum ConfigError {
65    /// A required environment variable is missing
66    MissingEnvVar(String),
67    /// An invalid environment string was provided
68    InvalidEnvironment(String),
69    /// A configuration value is invalid
70    InvalidValue { var: String, reason: String },
71    /// A required field is missing when building
72    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/// Configuration for Rithmic connections.
97///
98/// This struct contains both account information and connection details.
99///
100/// # Fields
101/// - Account-related: `account_id`, `fcm_id`, `ib_id`
102/// - Connection-related: `url`, `beta_url`, `user`, `password`, `system_name`, `env`
103#[derive(Clone, Debug)]
104pub struct RithmicConfig {
105    // Account fields
106    pub account_id: String,
107    pub fcm_id: String,
108    pub ib_id: String,
109
110    // Connection fields
111    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    /// Create a configuration by loading values from environment variables.
121    ///
122    /// See [`examples/.env.blank`](https://github.com/pbeets/rithmic-rs/blob/main/examples/.env.blank)
123    /// for a template of all required environment variables.
124    ///
125    /// # Required environment variables
126    ///
127    /// For Demo environment:
128    /// - `RITHMIC_DEMO_ACCOUNT_ID`: Demo account ID
129    /// - `RITHMIC_DEMO_FCM_ID`: Demo FCM (Futures Commission Merchant) ID
130    /// - `RITHMIC_DEMO_IB_ID`: Demo IB (Introducing Broker) ID
131    /// - `RITHMIC_DEMO_USER`: Demo username
132    /// - `RITHMIC_DEMO_PW`: Demo password
133    /// - `RITHMIC_DEMO_URL`: Demo WebSocket URL
134    /// - `RITHMIC_DEMO_ALT_URL`: Demo alternative/beta WebSocket URL
135    ///
136    /// For Live environment:
137    /// - `RITHMIC_LIVE_ACCOUNT_ID`: Live account ID
138    /// - `RITHMIC_LIVE_FCM_ID`: Live FCM (Futures Commission Merchant) ID
139    /// - `RITHMIC_LIVE_IB_ID`: Live IB (Introducing Broker) ID
140    /// - `RITHMIC_LIVE_USER`: Live username
141    /// - `RITHMIC_LIVE_PW`: Live password
142    /// - `RITHMIC_LIVE_URL`: Live WebSocket URL
143    /// - `RITHMIC_LIVE_ALT_URL`: Live alternative/beta WebSocket URL
144    ///
145    /// For Test environment:
146    /// - `RITHMIC_TEST_ACCOUNT_ID`: Test account ID
147    /// - `RITHMIC_TEST_FCM_ID`: Test FCM (Futures Commission Merchant) ID
148    /// - `RITHMIC_TEST_IB_ID`: Test IB (Introducing Broker) ID
149    /// - `RITHMIC_TEST_USER`: Test username
150    /// - `RITHMIC_TEST_PW`: Test password
151    /// - `RITHMIC_TEST_URL`: Test WebSocket URL
152    /// - `RITHMIC_TEST_ALT_URL`: Test alternative/beta WebSocket URL
153    ///
154    /// # Example
155    /// ```no_run
156    /// use rithmic_rs::config::{RithmicConfig, RithmicEnv};
157    ///
158    /// // Load from environment variables
159    /// let config = RithmicConfig::from_env(RithmicEnv::Demo)?;
160    /// # Ok::<(), Box<dyn std::error::Error>>(())
161    /// ```
162    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    /// Create a builder for programmatic configuration.
234    ///
235    /// Use this to set configuration values directly in code.
236    ///
237    /// # Example
238    /// ```no_run
239    /// use rithmic_rs::config::{RithmicConfig, RithmicEnv};
240    ///
241    /// let config = RithmicConfig::builder(RithmicEnv::Demo)
242    ///     .account_id("my_account")
243    ///     .fcm_id("my_fcm")
244    ///     .ib_id("my_ib")
245    ///     .user("my_user")
246    ///     .password("my_password")
247    ///     .build()?;
248    /// # Ok::<(), Box<dyn std::error::Error>>(())
249    /// ```
250    pub fn builder(env: RithmicEnv) -> RithmicConfigBuilder {
251        RithmicConfigBuilder::new(env)
252    }
253}
254
255/// Builder for constructing a RithmicConfig with custom values.
256#[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    /// Create a new builder for the specified environment.
271    pub fn new(env: RithmicEnv) -> Self {
272        // Set system name default based on environment
273        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    /// Set the account ID.
287    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    /// Set the FCM ID.
293    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    /// Set the IB ID.
299    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    /// Set the WebSocket URL.
305    pub fn url(mut self, url: impl Into<String>) -> Self {
306        self.url = Some(url.into());
307        self
308    }
309
310    /// Set the beta WebSocket URL.
311    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    /// Set the username.
317    pub fn user(mut self, user: impl Into<String>) -> Self {
318        self.user = Some(user.into());
319        self
320    }
321
322    /// Set the password.
323    pub fn password(mut self, password: impl Into<String>) -> Self {
324        self.password = Some(password.into());
325        self
326    }
327
328    /// Set the system name.
329    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    /// Build the configuration.
335    ///
336    /// Returns an error if any required fields are missing.
337    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    // Helper to set up test environment variables
377    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        // Test invalid input
455        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            // Deliberately not setting URL variables
584        }
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        // Builder should set system_name default
620        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        // Builder should set system_name default
693        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        // Builder should set system_name default
711        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        // Builder should set system_name default
729        assert_eq!(config.system_name, "Rithmic Test");
730    }
731
732    #[test]
733    fn test_builder_into_string_conversions() {
734        // Test that Into<String> works for builder methods
735        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}