Skip to main content

rithmic_rs/
config.rs

1//! Configuration for Rithmic connections.
2//!
3//! This module provides the primary interface for configuring Rithmic connections.
4//! [`RithmicConfig`] contains connection and login details, while [`RithmicAccount`]
5//! models a concrete trading account identity for order and PnL requests.
6//!
7//! # Example
8//! ```no_run
9//! use rithmic_rs::config::{RithmicConfig, RithmicEnv};
10//! use rithmic_rs::RithmicAccount;
11//!
12//! // Simple one-line configuration from environment variables
13//! let config = RithmicConfig::from_env(RithmicEnv::Demo)?;
14//!
15//! // Or build manually if needed
16//! let config = RithmicConfig::builder(RithmicEnv::Demo)
17//!     .user("my_user")
18//!     .password("my_password")
19//!     .app_name("my_app")
20//!     .app_version("1")
21//!     .build()?;
22//!
23//! let account = RithmicAccount::new("my_fcm", "my_ib", "my_account");
24//! # Ok::<(), Box<dyn std::error::Error>>(())
25//! ```
26
27use std::{env, fmt, str::FromStr};
28
29/// Trading environment selector.
30///
31/// Determines which Rithmic environment to connect to.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
35pub enum RithmicEnv {
36    /// Rithmic Paper Trading (demo/development) environment.
37    #[default]
38    Demo,
39    /// Rithmic 01 (live/production) environment.
40    Live,
41    /// Rithmic Test environment.
42    Test,
43}
44
45impl fmt::Display for RithmicEnv {
46    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47        match self {
48            RithmicEnv::Demo => write!(f, "demo"),
49            RithmicEnv::Live => write!(f, "live"),
50            RithmicEnv::Test => write!(f, "test"),
51        }
52    }
53}
54
55impl FromStr for RithmicEnv {
56    type Err = ConfigError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        match s {
60            "demo" | "development" => Ok(RithmicEnv::Demo),
61            "live" | "production" => Ok(RithmicEnv::Live),
62            "test" => Ok(RithmicEnv::Test),
63            _ => Err(ConfigError::InvalidEnvironment(s.to_string())),
64        }
65    }
66}
67
68/// Configuration error types.
69#[derive(Debug, Clone)]
70#[non_exhaustive]
71pub enum ConfigError {
72    /// The environment string could not be parsed.
73    InvalidEnvironment(String),
74    /// A configuration value was present but invalid.
75    InvalidValue {
76        /// The variable or field name.
77        var: String,
78        /// Why the value was rejected.
79        reason: String,
80    },
81    /// A required environment variable was not set.
82    MissingEnvVar(String),
83    /// A required builder field was not provided.
84    MissingField(String),
85}
86
87impl fmt::Display for ConfigError {
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        match self {
90            ConfigError::MissingEnvVar(var) => {
91                write!(f, "Missing environment variable: {}", var)
92            }
93            ConfigError::InvalidEnvironment(env) => {
94                write!(f, "Invalid environment: {}", env)
95            }
96            ConfigError::InvalidValue { var, reason } => {
97                write!(f, "Invalid value for {}: {}", var, reason)
98            }
99            ConfigError::MissingField(field) => {
100                write!(f, "Missing required field: {}", field)
101            }
102        }
103    }
104}
105
106impl std::error::Error for ConfigError {}
107
108/// Trading account identity for order and PnL requests.
109///
110/// This type is separate from [`RithmicConfig`] because Rithmic authenticates
111/// per user session while order and PnL operations are account-scoped.
112///
113/// # Example
114///
115/// ```ignore
116/// use rithmic_rs::RithmicAccount;
117///
118/// let account = RithmicAccount::new("FCM_ID", "IB_ID", "ACCOUNT_ID");
119/// ```
120#[derive(Clone, Debug, PartialEq, Eq)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122pub struct RithmicAccount {
123    /// Trading account identifier.
124    pub account_id: String,
125    /// Futures Commission Merchant identifier.
126    pub fcm_id: String,
127    /// Introducing Broker identifier.
128    pub ib_id: String,
129}
130
131impl RithmicAccount {
132    /// Create a typed account identity directly.
133    pub fn new(
134        fcm_id: impl Into<String>,
135        ib_id: impl Into<String>,
136        account_id: impl Into<String>,
137    ) -> Self {
138        Self {
139            account_id: account_id.into(),
140            fcm_id: fcm_id.into(),
141            ib_id: ib_id.into(),
142        }
143    }
144
145    /// Create an account identity by loading values from environment variables.
146    ///
147    /// See [`examples/.env.blank`](https://github.com/pbeets/rithmic-rs/blob/main/examples/.env.blank)
148    /// for a template of all required environment variables.
149    pub fn from_env(env: RithmicEnv) -> Result<Self, ConfigError> {
150        let (account_id, fcm_id, ib_id) = match &env {
151            RithmicEnv::Demo => (
152                env::var("RITHMIC_DEMO_ACCOUNT_ID").map_err(|_| {
153                    ConfigError::MissingEnvVar("RITHMIC_DEMO_ACCOUNT_ID".to_string())
154                })?,
155                env::var("RITHMIC_DEMO_FCM_ID")
156                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_FCM_ID".to_string()))?,
157                env::var("RITHMIC_DEMO_IB_ID")
158                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_IB_ID".to_string()))?,
159            ),
160            RithmicEnv::Live => (
161                env::var("RITHMIC_LIVE_ACCOUNT_ID").map_err(|_| {
162                    ConfigError::MissingEnvVar("RITHMIC_LIVE_ACCOUNT_ID".to_string())
163                })?,
164                env::var("RITHMIC_LIVE_FCM_ID")
165                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_FCM_ID".to_string()))?,
166                env::var("RITHMIC_LIVE_IB_ID")
167                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_IB_ID".to_string()))?,
168            ),
169            RithmicEnv::Test => (
170                env::var("RITHMIC_TEST_ACCOUNT_ID").map_err(|_| {
171                    ConfigError::MissingEnvVar("RITHMIC_TEST_ACCOUNT_ID".to_string())
172                })?,
173                env::var("RITHMIC_TEST_FCM_ID")
174                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_FCM_ID".to_string()))?,
175                env::var("RITHMIC_TEST_IB_ID")
176                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_IB_ID".to_string()))?,
177            ),
178        };
179
180        Ok(Self {
181            account_id,
182            fcm_id,
183            ib_id,
184        })
185    }
186}
187
188/// Configuration for Rithmic connections.
189///
190/// This struct contains session-level connection and login details.
191#[derive(Clone)]
192pub struct RithmicConfig {
193    /// Primary WebSocket URL.
194    pub url: String,
195    /// Alternative/beta WebSocket URL used by [`ConnectStrategy::AlternateWithRetry`](crate::ConnectStrategy::AlternateWithRetry).
196    pub beta_url: String,
197    /// Login username.
198    pub user: String,
199    /// Login password.
200    pub password: String,
201    /// Rithmic system name (e.g. "Rithmic Paper Trading").
202    pub system_name: String,
203    /// Target trading environment.
204    pub env: RithmicEnv,
205    /// Application name registered with Rithmic.
206    pub app_name: String,
207    /// Application version string.
208    pub app_version: String,
209}
210
211impl fmt::Debug for RithmicConfig {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        f.debug_struct("RithmicConfig")
214            .field("url", &self.url)
215            .field("beta_url", &self.beta_url)
216            .field("user", &self.user)
217            .field("password", &"[REDACTED]")
218            .field("system_name", &self.system_name)
219            .field("env", &self.env)
220            .field("app_name", &self.app_name)
221            .field("app_version", &self.app_version)
222            .finish()
223    }
224}
225
226impl RithmicConfig {
227    /// Create a configuration by loading values from environment variables.
228    ///
229    /// See [`examples/.env.blank`](https://github.com/pbeets/rithmic-rs/blob/main/examples/.env.blank)
230    /// for a template of all required environment variables.
231    ///
232    /// # Required environment variables
233    ///
234    /// For Demo environment:
235    /// - `RITHMIC_DEMO_USER`: Demo username
236    /// - `RITHMIC_DEMO_PW`: Demo password
237    /// - `RITHMIC_DEMO_URL`: Demo WebSocket URL
238    /// - `RITHMIC_DEMO_ALT_URL`: Demo alternative/beta WebSocket URL
239    ///
240    /// For Live environment:
241    /// - `RITHMIC_LIVE_USER`: Live username
242    /// - `RITHMIC_LIVE_PW`: Live password
243    /// - `RITHMIC_LIVE_URL`: Live WebSocket URL
244    /// - `RITHMIC_LIVE_ALT_URL`: Live alternative/beta WebSocket URL
245    ///
246    /// For Test environment:
247    /// - `RITHMIC_TEST_USER`: Test username
248    /// - `RITHMIC_TEST_PW`: Test password
249    /// - `RITHMIC_TEST_URL`: Test WebSocket URL
250    /// - `RITHMIC_TEST_ALT_URL`: Test alternative/beta WebSocket URL
251    ///
252    /// Shared (all environments):
253    /// - `RITHMIC_APP_NAME` (required): Application name registered with Rithmic
254    /// - `RITHMIC_APP_VERSION` (required): Application version
255    ///
256    /// # Example
257    /// ```no_run
258    /// use rithmic_rs::config::{RithmicConfig, RithmicEnv};
259    /// use rithmic_rs::RithmicAccount;
260    ///
261    /// // Load from environment variables
262    /// let config = RithmicConfig::from_env(RithmicEnv::Demo)?;
263    /// let account = RithmicAccount::from_env(RithmicEnv::Demo)?;
264    /// # Ok::<(), Box<dyn std::error::Error>>(())
265    /// ```
266    pub fn from_env(env: RithmicEnv) -> Result<Self, ConfigError> {
267        let (url, beta_url, user, password, system_name) = match &env {
268            RithmicEnv::Demo => (
269                env::var("RITHMIC_DEMO_URL")
270                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_URL".to_string()))?,
271                env::var("RITHMIC_DEMO_ALT_URL")
272                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_ALT_URL".to_string()))?,
273                env::var("RITHMIC_DEMO_USER")
274                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_USER".to_string()))?,
275                env::var("RITHMIC_DEMO_PW")
276                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_DEMO_PW".to_string()))?,
277                "Rithmic Paper Trading".to_string(),
278            ),
279            RithmicEnv::Live => (
280                env::var("RITHMIC_LIVE_URL")
281                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_URL".to_string()))?,
282                env::var("RITHMIC_LIVE_ALT_URL")
283                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_ALT_URL".to_string()))?,
284                env::var("RITHMIC_LIVE_USER")
285                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_USER".to_string()))?,
286                env::var("RITHMIC_LIVE_PW")
287                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_LIVE_PW".to_string()))?,
288                "Rithmic 01".to_string(),
289            ),
290            RithmicEnv::Test => (
291                env::var("RITHMIC_TEST_URL")
292                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_URL".to_string()))?,
293                env::var("RITHMIC_TEST_ALT_URL")
294                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_ALT_URL".to_string()))?,
295                env::var("RITHMIC_TEST_USER")
296                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_USER".to_string()))?,
297                env::var("RITHMIC_TEST_PW")
298                    .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_TEST_PW".to_string()))?,
299                "Rithmic Test".to_string(),
300            ),
301        };
302
303        let app_name = env::var("RITHMIC_APP_NAME")
304            .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_APP_NAME".to_string()))?;
305
306        let app_version = env::var("RITHMIC_APP_VERSION")
307            .map_err(|_| ConfigError::MissingEnvVar("RITHMIC_APP_VERSION".to_string()))?;
308
309        Ok(Self {
310            url,
311            beta_url,
312            user,
313            password,
314            system_name,
315            env,
316            app_name,
317            app_version,
318        })
319    }
320
321    /// Create a builder for programmatic configuration.
322    ///
323    /// Use this to set configuration values directly in code.
324    ///
325    /// # Example
326    /// ```no_run
327    /// use rithmic_rs::config::{RithmicConfig, RithmicEnv};
328    ///
329    /// let config = RithmicConfig::builder(RithmicEnv::Demo)
330    ///     .user("my_user")
331    ///     .password("my_password")
332    ///     .app_name("my_app")
333    ///     .app_version("1")
334    ///     .build()?;
335    /// # Ok::<(), Box<dyn std::error::Error>>(())
336    /// ```
337    pub fn builder(env: RithmicEnv) -> RithmicConfigBuilder {
338        RithmicConfigBuilder::new(env)
339    }
340}
341
342/// Builder for constructing a RithmicConfig with custom values.
343pub struct RithmicConfigBuilder {
344    env: Option<RithmicEnv>,
345    url: Option<String>,
346    beta_url: Option<String>,
347    user: Option<String>,
348    password: Option<String>,
349    system_name: Option<String>,
350    app_name: Option<String>,
351    app_version: Option<String>,
352}
353
354impl RithmicConfigBuilder {
355    /// Create a new builder for the specified environment.
356    pub fn new(env: RithmicEnv) -> Self {
357        // Set system name default based on environment
358        let system_name = match &env {
359            RithmicEnv::Demo => "Rithmic Paper Trading".to_string(),
360            RithmicEnv::Live => "Rithmic 01".to_string(),
361            RithmicEnv::Test => "Rithmic Test".to_string(),
362        };
363
364        Self {
365            env: Some(env),
366            url: None,
367            beta_url: None,
368            user: None,
369            password: None,
370            system_name: Some(system_name),
371            app_name: None,
372            app_version: None,
373        }
374    }
375
376    /// Set the WebSocket URL.
377    pub fn url(mut self, url: impl Into<String>) -> Self {
378        self.url = Some(url.into());
379        self
380    }
381
382    /// Set the beta WebSocket URL.
383    pub fn beta_url(mut self, beta_url: impl Into<String>) -> Self {
384        self.beta_url = Some(beta_url.into());
385        self
386    }
387
388    /// Set the username.
389    pub fn user(mut self, user: impl Into<String>) -> Self {
390        self.user = Some(user.into());
391        self
392    }
393
394    /// Set the password.
395    pub fn password(mut self, password: impl Into<String>) -> Self {
396        self.password = Some(password.into());
397        self
398    }
399
400    /// Set the system name.
401    pub fn system_name(mut self, system_name: impl Into<String>) -> Self {
402        self.system_name = Some(system_name.into());
403        self
404    }
405
406    /// Set the application name registered with Rithmic.
407    pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
408        self.app_name = Some(app_name.into());
409        self
410    }
411
412    /// Set the application version string.
413    pub fn app_version(mut self, app_version: impl Into<String>) -> Self {
414        self.app_version = Some(app_version.into());
415        self
416    }
417
418    /// Build the configuration.
419    ///
420    /// Returns an error if any required fields are missing.
421    pub fn build(self) -> Result<RithmicConfig, ConfigError> {
422        Ok(RithmicConfig {
423            env: self
424                .env
425                .ok_or_else(|| ConfigError::MissingField("env".to_string()))?,
426            url: self
427                .url
428                .ok_or_else(|| ConfigError::MissingField("url".to_string()))?,
429            beta_url: self
430                .beta_url
431                .ok_or_else(|| ConfigError::MissingField("beta_url".to_string()))?,
432            user: self
433                .user
434                .ok_or_else(|| ConfigError::MissingField("user".to_string()))?,
435            password: self
436                .password
437                .ok_or_else(|| ConfigError::MissingField("password".to_string()))?,
438            system_name: self
439                .system_name
440                .ok_or_else(|| ConfigError::MissingField("system_name".to_string()))?,
441            app_name: self
442                .app_name
443                .ok_or_else(|| ConfigError::MissingField("app_name".to_string()))?,
444            app_version: self
445                .app_version
446                .ok_or_else(|| ConfigError::MissingField("app_version".to_string()))?,
447        })
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    fn demo_env_vars() -> Vec<(&'static str, Option<&'static str>)> {
456        vec![
457            ("RITHMIC_DEMO_ACCOUNT_ID", Some("test_account")),
458            ("RITHMIC_DEMO_FCM_ID", Some("test_fcm")),
459            ("RITHMIC_DEMO_IB_ID", Some("test_ib")),
460            ("RITHMIC_DEMO_USER", Some("demo_user")),
461            ("RITHMIC_DEMO_PW", Some("demo_password")),
462            ("RITHMIC_DEMO_URL", Some("wss://test-demo.example.com:443")),
463            (
464                "RITHMIC_DEMO_ALT_URL",
465                Some("wss://test-demo-alt.example.com:443"),
466            ),
467            ("RITHMIC_APP_NAME", Some("test_app")),
468            ("RITHMIC_APP_VERSION", Some("1")),
469        ]
470    }
471
472    fn live_env_vars() -> Vec<(&'static str, Option<&'static str>)> {
473        vec![
474            ("RITHMIC_LIVE_ACCOUNT_ID", Some("test_account")),
475            ("RITHMIC_LIVE_FCM_ID", Some("test_fcm")),
476            ("RITHMIC_LIVE_IB_ID", Some("test_ib")),
477            ("RITHMIC_LIVE_USER", Some("live_user")),
478            ("RITHMIC_LIVE_PW", Some("live_password")),
479            ("RITHMIC_LIVE_URL", Some("wss://test-live.example.com:443")),
480            (
481                "RITHMIC_LIVE_ALT_URL",
482                Some("wss://test-live-alt.example.com:443"),
483            ),
484            ("RITHMIC_APP_NAME", Some("test_app")),
485            ("RITHMIC_APP_VERSION", Some("1")),
486        ]
487    }
488
489    #[test]
490    fn test_rithmic_env_display() {
491        assert_eq!(RithmicEnv::Demo.to_string(), "demo");
492        assert_eq!(RithmicEnv::Live.to_string(), "live");
493        assert_eq!(RithmicEnv::Test.to_string(), "test");
494    }
495
496    #[test]
497    fn test_rithmic_env_from_str() {
498        assert_eq!("demo".parse::<RithmicEnv>().unwrap(), RithmicEnv::Demo);
499        assert_eq!(
500            "development".parse::<RithmicEnv>().unwrap(),
501            RithmicEnv::Demo
502        );
503        assert_eq!("live".parse::<RithmicEnv>().unwrap(), RithmicEnv::Live);
504        assert_eq!(
505            "production".parse::<RithmicEnv>().unwrap(),
506            RithmicEnv::Live
507        );
508        assert_eq!("test".parse::<RithmicEnv>().unwrap(), RithmicEnv::Test);
509
510        // Test invalid input
511        let result = "invalid".parse::<RithmicEnv>();
512        assert!(result.is_err());
513        if let Err(ConfigError::InvalidEnvironment(env)) = result {
514            assert_eq!(env, "invalid");
515        } else {
516            panic!("Expected InvalidEnvironment error");
517        }
518    }
519
520    #[test]
521    fn test_config_error_display() {
522        let err = ConfigError::MissingEnvVar("TEST_VAR".to_string());
523        assert_eq!(err.to_string(), "Missing environment variable: TEST_VAR");
524
525        let err = ConfigError::InvalidEnvironment("bad_env".to_string());
526        assert_eq!(err.to_string(), "Invalid environment: bad_env");
527
528        let err = ConfigError::InvalidValue {
529            var: "TEST".to_string(),
530            reason: "too short".to_string(),
531        };
532        assert_eq!(err.to_string(), "Invalid value for TEST: too short");
533
534        let err = ConfigError::MissingField("field".to_string());
535        assert_eq!(err.to_string(), "Missing required field: field");
536    }
537
538    #[test]
539    fn test_account_from_env_demo_success() {
540        temp_env::with_vars(demo_env_vars(), || {
541            let account = RithmicAccount::from_env(RithmicEnv::Demo).unwrap();
542
543            assert_eq!(account.account_id, "test_account");
544            assert_eq!(account.fcm_id, "test_fcm");
545            assert_eq!(account.ib_id, "test_ib");
546        });
547    }
548
549    #[test]
550    fn test_from_env_demo_success() {
551        temp_env::with_vars(demo_env_vars(), || {
552            let config = RithmicConfig::from_env(RithmicEnv::Demo).unwrap();
553
554            assert_eq!(config.user, "demo_user");
555            assert_eq!(config.password, "demo_password");
556            assert_eq!(config.url, "wss://test-demo.example.com:443");
557            assert_eq!(config.beta_url, "wss://test-demo-alt.example.com:443");
558            assert_eq!(config.system_name, "Rithmic Paper Trading");
559            assert_eq!(config.env, RithmicEnv::Demo);
560        });
561    }
562
563    #[test]
564    fn test_from_env_live_success() {
565        temp_env::with_vars(live_env_vars(), || {
566            let config = RithmicConfig::from_env(RithmicEnv::Live).unwrap();
567
568            assert_eq!(config.user, "live_user");
569            assert_eq!(config.password, "live_password");
570            assert_eq!(config.system_name, "Rithmic 01");
571            assert_eq!(config.env, RithmicEnv::Live);
572        });
573    }
574
575    #[test]
576    fn test_account_from_env_missing_account_id() {
577        temp_env::with_vars(
578            vec![
579                ("RITHMIC_DEMO_ACCOUNT_ID", None::<&str>),
580                ("RITHMIC_DEMO_FCM_ID", Some("test_fcm")),
581                ("RITHMIC_DEMO_IB_ID", Some("test_ib")),
582                ("RITHMIC_DEMO_USER", Some("demo_user")),
583                ("RITHMIC_DEMO_PW", Some("demo_password")),
584                ("RITHMIC_DEMO_URL", Some("wss://test-demo.example.com:443")),
585                (
586                    "RITHMIC_DEMO_ALT_URL",
587                    Some("wss://test-demo-alt.example.com:443"),
588                ),
589            ],
590            || {
591                let result = RithmicAccount::from_env(RithmicEnv::Demo);
592                assert!(result.is_err());
593
594                if let Err(ConfigError::MissingEnvVar(var)) = result {
595                    assert_eq!(var, "RITHMIC_DEMO_ACCOUNT_ID");
596                } else {
597                    panic!("Expected MissingEnvVar error");
598                }
599            },
600        );
601    }
602
603    #[test]
604    fn test_from_env_missing_credentials() {
605        temp_env::with_vars(
606            vec![
607                ("RITHMIC_DEMO_USER", None::<&str>),
608                ("RITHMIC_DEMO_PW", None),
609                ("RITHMIC_DEMO_URL", Some("wss://test-demo.example.com:443")),
610                (
611                    "RITHMIC_DEMO_ALT_URL",
612                    Some("wss://test-demo-alt.example.com:443"),
613                ),
614            ],
615            || {
616                let result = RithmicConfig::from_env(RithmicEnv::Demo);
617                assert!(result.is_err());
618
619                if let Err(ConfigError::MissingEnvVar(var)) = result {
620                    assert_eq!(var, "RITHMIC_DEMO_USER");
621                } else {
622                    panic!("Expected MissingEnvVar error");
623                }
624            },
625        );
626    }
627
628    #[test]
629    fn test_from_env_missing_url() {
630        temp_env::with_vars(
631            vec![
632                ("RITHMIC_DEMO_USER", Some("demo_user")),
633                ("RITHMIC_DEMO_PW", Some("demo_password")),
634                ("RITHMIC_DEMO_URL", None::<&str>),
635                ("RITHMIC_DEMO_ALT_URL", None),
636            ],
637            || {
638                let result = RithmicConfig::from_env(RithmicEnv::Demo);
639                assert!(result.is_err());
640
641                if let Err(ConfigError::MissingEnvVar(var)) = result {
642                    assert_eq!(var, "RITHMIC_DEMO_URL");
643                } else {
644                    panic!("Expected MissingEnvVar error");
645                }
646            },
647        );
648    }
649
650    #[test]
651    fn test_account_new_complete() {
652        let account = RithmicAccount::new("my_fcm", "my_ib", "my_account");
653
654        assert_eq!(account.account_id, "my_account");
655        assert_eq!(account.fcm_id, "my_fcm");
656        assert_eq!(account.ib_id, "my_ib");
657    }
658
659    #[test]
660    fn test_builder_complete() {
661        let config = RithmicConfig::builder(RithmicEnv::Demo)
662            .user("my_user")
663            .password("my_password")
664            .url("wss://test.example.com:443")
665            .beta_url("wss://test-alt.example.com:443")
666            .app_name("test_app")
667            .app_version("1")
668            .build()
669            .unwrap();
670
671        assert_eq!(config.user, "my_user");
672        assert_eq!(config.password, "my_password");
673        assert_eq!(config.env, RithmicEnv::Demo);
674        assert_eq!(config.url, "wss://test.example.com:443");
675        assert_eq!(config.beta_url, "wss://test-alt.example.com:443");
676        // Builder should set system_name default
677        assert_eq!(config.system_name, "Rithmic Paper Trading");
678    }
679
680    #[test]
681    fn test_builder_custom_urls() {
682        let config = RithmicConfig::builder(RithmicEnv::Demo)
683            .user("my_user")
684            .password("my_password")
685            .url("wss://custom.example.com:443")
686            .beta_url("wss://custom-beta.example.com:443")
687            .system_name("Custom System")
688            .app_name("test_app")
689            .app_version("1")
690            .build()
691            .unwrap();
692
693        assert_eq!(config.url, "wss://custom.example.com:443");
694        assert_eq!(config.beta_url, "wss://custom-beta.example.com:443");
695        assert_eq!(config.system_name, "Custom System");
696    }
697
698    #[test]
699    fn test_builder_missing_user() {
700        let result = RithmicConfig::builder(RithmicEnv::Demo)
701            .password("my_password")
702            .url("wss://test.example.com:443")
703            .beta_url("wss://test-alt.example.com:443")
704            .build();
705
706        assert!(result.is_err());
707        if let Err(ConfigError::MissingField(field)) = result {
708            assert_eq!(field, "user");
709        } else {
710            panic!("Expected MissingField error");
711        }
712    }
713
714    #[test]
715    fn test_builder_demo_defaults() {
716        let builder = RithmicConfigBuilder::new(RithmicEnv::Demo);
717        let config = builder
718            .user("test")
719            .password("test")
720            .url("wss://test.example.com:443")
721            .beta_url("wss://test-alt.example.com:443")
722            .app_name("test_app")
723            .app_version("1")
724            .build()
725            .unwrap();
726
727        // Builder should set system_name default
728        assert_eq!(config.system_name, "Rithmic Paper Trading");
729    }
730
731    #[test]
732    fn test_builder_live_defaults() {
733        let builder = RithmicConfigBuilder::new(RithmicEnv::Live);
734        let config = builder
735            .user("test")
736            .password("test")
737            .url("wss://test.example.com:443")
738            .beta_url("wss://test-alt.example.com:443")
739            .app_name("test_app")
740            .app_version("1")
741            .build()
742            .unwrap();
743
744        // Builder should set system_name default
745        assert_eq!(config.system_name, "Rithmic 01");
746    }
747
748    #[test]
749    fn test_builder_test_defaults() {
750        let builder = RithmicConfigBuilder::new(RithmicEnv::Test);
751        let config = builder
752            .user("test")
753            .password("test")
754            .url("wss://test.example.com:443")
755            .beta_url("wss://test-alt.example.com:443")
756            .app_name("test_app")
757            .app_version("1")
758            .build()
759            .unwrap();
760
761        // Builder should set system_name default
762        assert_eq!(config.system_name, "Rithmic Test");
763    }
764
765    #[test]
766    fn test_builder_into_string_conversions() {
767        // Test that Into<String> works for builder methods
768        let config = RithmicConfig::builder(RithmicEnv::Demo)
769            .user(String::from("my_user"))
770            .password(String::from("my_password"))
771            .url(String::from("wss://test.example.com:443"))
772            .beta_url(String::from("wss://test-alt.example.com:443"))
773            .app_name("test_app")
774            .app_version("1")
775            .build()
776            .unwrap();
777
778        assert_eq!(config.user, "my_user");
779    }
780
781    #[test]
782    fn test_debug_redacts_password() {
783        let config = RithmicConfig::builder(RithmicEnv::Demo)
784            .user("my_user")
785            .password("super_secret_password")
786            .url("wss://test.example.com:443")
787            .beta_url("wss://test-alt.example.com:443")
788            .app_name("test_app")
789            .app_version("1")
790            .build()
791            .unwrap();
792
793        let debug_output = format!("{:?}", config);
794        assert!(
795            !debug_output.contains("super_secret_password"),
796            "Debug output should not contain the actual password"
797        );
798        assert!(
799            debug_output.contains("[REDACTED]"),
800            "Debug output should contain [REDACTED] for the password"
801        );
802        // Other fields should still be visible
803        assert!(debug_output.contains("my_user"));
804    }
805}