ig_client/session/
interface.rs

1use crate::config::Config;
2use crate::error::{AppError, AuthError};
3use crate::utils::rate_limiter::{
4    RateLimitType, RateLimiter, RateLimiterStats, app_non_trading_limiter, create_rate_limiter,
5};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use tracing::debug;
9
10/// Session information for IG Markets API authentication
11#[derive(Debug, Clone)]
12pub struct IgSession {
13    /// Client Session Token (CST) used for authentication
14    pub cst: String,
15    /// Security token used for authentication
16    pub token: String,
17    /// Account ID associated with the session
18    pub account_id: String,
19    /// Base URL for API requests
20    pub base_url: String,
21    /// Client ID for API requests
22    pub client_id: String,
23    /// Lightstreamer endpoint for API requests
24    pub lightstreamer_endpoint: String,
25    /// API key for API requests
26    pub api_key: String,
27    /// Rate limiter for controlling request rates
28    pub(crate) rate_limiter: Option<Arc<RateLimiter>>,
29    /// Flag to indicate if the session is being used in a concurrent context
30    pub(crate) concurrent_mode: Arc<AtomicBool>,
31}
32
33impl IgSession {
34    /// Creates a new session with the given credentials
35    ///
36    /// This is a simplified version for tests and basic usage.
37    /// Uses default values for most fields and a default rate limiter.
38    pub fn new(cst: String, token: String, account_id: String) -> Self {
39        Self {
40            base_url: String::new(),
41            cst,
42            token,
43            client_id: String::new(),
44            account_id,
45            lightstreamer_endpoint: String::new(),
46            api_key: String::new(),
47            rate_limiter: Some(create_rate_limiter(
48                RateLimitType::NonTradingAccount,
49                Some(0.8),
50            )),
51            concurrent_mode: Arc::new(AtomicBool::new(false)),
52        }
53    }
54
55    /// Creates a new session with the given parameters
56    ///
57    /// This creates a thread-safe session that can be shared across multiple threads.
58    /// The rate limiter is wrapped in an Arc to ensure proper synchronization.
59    #[allow(clippy::too_many_arguments)]
60    pub fn new_with_config(
61        base_url: String,
62        cst: String,
63        security_token: String,
64        client_id: String,
65        account_id: String,
66        lightstreamer_endpoint: String,
67        api_key: String,
68        rate_limit_type: RateLimitType,
69        rate_limit_safety_margin: f64,
70    ) -> Self {
71        // Create a rate limiter with the specified type and safety margin
72        let rate_limiter = create_rate_limiter(rate_limit_type, Some(rate_limit_safety_margin));
73
74        Self {
75            base_url,
76            cst,
77            token: security_token,
78            client_id,
79            account_id,
80            lightstreamer_endpoint,
81            api_key,
82            rate_limiter: Some(rate_limiter),
83            concurrent_mode: Arc::new(AtomicBool::new(false)),
84        }
85    }
86
87    /// Creates a new session with the given credentials and a rate limiter
88    ///
89    /// This creates a thread-safe session that can be shared across multiple threads.
90    pub fn with_rate_limiter(
91        cst: String,
92        token: String,
93        account_id: String,
94        limit_type: RateLimitType,
95    ) -> Self {
96        Self {
97            cst,
98            token,
99            account_id,
100            base_url: String::new(),
101            client_id: String::new(),
102            lightstreamer_endpoint: String::new(),
103            api_key: String::new(),
104            rate_limiter: Some(create_rate_limiter(limit_type, Some(0.8))),
105            concurrent_mode: Arc::new(AtomicBool::new(false)),
106        }
107    }
108
109    /// Creates a new session with the given credentials and rate limiter configuration from Config
110    pub fn from_config(cst: String, token: String, account_id: String, config: &Config) -> Self {
111        Self {
112            cst,
113            token,
114            account_id,
115            base_url: String::new(),
116            client_id: String::new(),
117            lightstreamer_endpoint: String::new(),
118            api_key: String::new(),
119            rate_limiter: Some(create_rate_limiter(
120                config.rate_limit_type,
121                Some(config.rate_limit_safety_margin),
122            )),
123            concurrent_mode: Arc::new(AtomicBool::new(false)),
124        }
125    }
126
127    /// Waits if necessary to respect rate limits before making a request
128    ///
129    /// This method will always use a rate limiter - either the one configured in the session,
130    /// or a default one if none is configured.
131    ///
132    /// This method is thread-safe and can be called from multiple threads concurrently.
133    ///
134    /// # Returns
135    /// * `Ok(())` - If the rate limit is respected
136    /// * `Err(AppError::RateLimitExceeded)` - If the rate limit has been exceeded and cannot be respected
137    pub async fn respect_rate_limit(&self) -> Result<(), AppError> {
138        // Mark that this session is being used in a concurrent context
139        self.concurrent_mode.store(true, Ordering::SeqCst);
140
141        // Get the rate limiter from the session or use a default one
142        let limiter = match &self.rate_limiter {
143            Some(limiter) => limiter.clone(),
144            None => {
145                // This should never happen since we always initialize with a default limiter,
146                // but just in case, use the global app non-trading limiter
147                debug!("No rate limiter configured in session, using default");
148                app_non_trading_limiter()
149            }
150        };
151
152        // Wait if necessary to respect the rate limit
153        limiter.wait().await;
154        Ok(())
155    }
156
157    /// Gets statistics about the current rate limit usage
158    pub async fn get_rate_limit_stats(&self) -> Option<RateLimiterStats> {
159        match &self.rate_limiter {
160            Some(limiter) => Some(limiter.get_stats().await),
161            None => None,
162        }
163    }
164}
165
166/// Trait for authenticating with the IG Markets API
167#[async_trait::async_trait]
168pub trait IgAuthenticator: Send + Sync {
169    /// Logs in to the IG Markets API and returns a new session
170    async fn login(&self) -> Result<IgSession, AuthError>;
171    /// Refreshes an existing session with the IG Markets API
172    async fn refresh(&self, session: &IgSession) -> Result<IgSession, AuthError>;
173    /// Switches the active account for the current session
174    ///
175    /// # Arguments
176    /// * `session` - The current session
177    /// * `account_id` - The ID of the account to switch to
178    /// * `default_account` - Whether to set this account as the default (optional)
179    ///
180    /// # Returns
181    /// * A new session with the updated account ID
182    async fn switch_account(
183        &self,
184        session: &IgSession,
185        account_id: &str,
186        default_account: Option<bool>,
187    ) -> Result<IgSession, AuthError>;
188}